Repository: networkx/networkx

Issue:
edgelist reader removes deliminators if deliminator is white-space and file has empty fields [BUG]
This `line.strip` without an argument, means a white-space delimited file (e.g. tab, which is quite common) with  empty edge attribute(s) in the last column(s) results in a crash with error `Edge data [...] and data_keys [...] are not the same length`. 
   
https://github.com/networkx/networkx/blob/729bec23b10e89b5f6d37175e30484f2278107c4/networkx/algorithms/bipartite/edgelist.py#L222

An example file that would crash:

```
A	B	interaction	2
B	C	interaction	4
C	A	interaction	
```
The last line ends in `\t\n`, and both are striped instead of just the `\n`. This is inconsistent, as the same data in a different order of columns works fine, e.g.:
```
A	B	2	interaction
B	C	4	interaction
C	A		interaction
```


# Fix

    s = line.rstrip("\n").split(delimiter)

is the safe fix, and is cross OS compatible in Python (https://peps.python.org/pep-0278/)

Discussion:
carissableker: Hi @dschult 

As the bug reporter, I have the following comment:


> If I understand the subtleties here, it looks like this will no longer strip anything from the beginning of a line and will only strip a newline character off of the end of the line.

Yes, this is correct. In my opinion this is the simplest and most predictable behaviour. The aim of this line is to prepare the input by removing the *completely expected* newline and split the line into its components by using the *explicit* user-provided delimitator. Doing anything more than this, e.g. attempting to do undocumented whitespace "cleaning" would be obscured, unpredictable and/or become very complex. 

> So, it fixes the problem with a tab delimiter, but breaks an input file that has spaces at the front or end of a line. It looks like all tests pass.

It would not break (crash), it would respect the user input (an attribute/node name with leading or trailing white space). 

> I guess to fix the currently broken cases and leave the current behavior for extra whitespace at start and end, we should strip all white space **except** the delimiter. Something like:
> 
> ```python
>     stripchars = None if delimiter is None else string.whitespace.replace(delimiter, "")
>     ....
>     <other stuff and start loop, etc>
>     s = line.strip(stripchars).split(delimiter)
> ```

This is not consistent though. The following lines, (for an equivalent undirected edge) would have a different result:

```
A\tB\s\n
```

and 

```
B\s\tA\n
```

or alternatively:

```
\s\tB\n
```

and 

```
B\t\sA\n
```

Of course, it may not be the users intent to have a node name of ` A` or `B `,  but it is what was provided to networkx and I don't think it should be networkx's prerogative to attempt to deduce  what the user means and unilaterally "fix" it, as there lies many dragons. 

Compare this behaviour to other software, e.g. Excel or pandas, removing leading/trailing whitespace is usually done on per cell basis (instead of line), and is only done on explicit user direction. The only place where editing of (non newline or delimiter) whitespace in the background may be relevant is perhaps in "fixed width" format. 

## Alternatively
If you do require that node names should not have leading or trailing whitespace, this should be removed per item after `split(deliminator)` (and then be documented). E.g.:

```
s = [x.strip() for x in s]
```
In this case, also the two examples above would be consistent. 



cyschneck: For purposes of an example if it splits on a tab then

```
import string
stripchars = string.whitespace.replace('\t', '')
```

### Current Behavior (some conditions retain white space)

If there a space after the B value and the tab
```
line = 'A\tB \n'
s = line.strip(stripchars).split('\t')
```
Produces a clean value before and after the values A and B
```
['A', 'B']
```
However, if the space is after the B value and before the tab
```
line = 'B \tA\n'
s = line.strip(stripchars).split('\t')
```
Produces an output that still includes the space after B
```
['B ', 'A']
```

### rstrip() Behavior (conditions retain white space)

```
line = 'A\tB \n'
s  = line.rstrip('\n').split('\t')
```
Produces B with a space trailing it
```
['A', 'B ']
```
As well as the if the space precedes the tab

```
line = 'B \tA\n'
s = line.rstrip('\n').split('\t')
```
Where, the B is first in the list, but still includes the space
```
['B ', 'A']
```

### Proposed List Comprehension (no additional white space)

This seems promising since it does fully remove all white space around each element in the list

```
line = 'B \tA\n'
s =[x.strip() for x in line if x.strip() != '']
```
No space around B or A
```
['B', 'A']
```
And 
```
s = 'A\tB \n'
s =[x.strip() for x in line if x.strip() != '']
```
Also with no space around the B or A
```
['A', 'B']
```
dschult: Yes -- when I said "broken" I meant it breaks backward compatibility -- not crashing anything. Sorry for the confusion.

I understand the rationale for either stripping whitespace from every entry or none of the entries. stripping the whitespace on the start of the first and end of the last without doing anything in between is not consistent across entries in a line. I tend to lean toward the simpler approach of not stripping whitespace at either end -- just remove the newline. And that is what is in this PR.

But, I'm worried about backward compatibility: Do we need to deprecate the current behavior? And what idiom can we give those for whom the tools work with their current file to get it to work with the new code. It seems like trouble could only occur for folks with `node_type=None` and `delimiter` set to something. For example: `line = " b,a\n"` or `line="b,a \n"`. I guess we could deprecate with a warning to strip their lines like:  
```python
with open(path, "rb") as file:
    G = nx.parse_edgelist([line.strip() for line in file], delimiter=",")
```
Is there a better way?  Or does someone feel this doesn't need a deprecation?

Or   ;P    maybe we should just tell people to fix the current Issue by doing:
```python
with open(path, "rb") as file:
    G = nx.parse_edgelist([line.replace("\t", "@#$") for line in file], delimiter="@#$")
```


Patch:
diff --git a/networkx/algorithms/bipartite/edgelist.py b/networkx/algorithms/bipartite/edgelist.py
index ebd47b032..db6ef9d8e 100644
--- a/networkx/algorithms/bipartite/edgelist.py
+++ b/networkx/algorithms/bipartite/edgelist.py
@@ -220,7 +220,7 @@ def parse_edgelist(
         if not len(line):
             continue
         # split line, should have 2 or more
-        s = line.strip().split(delimiter)
+        s = line.rstrip("\n").split(delimiter)
         if len(s) < 2:
             continue
         u = s.pop(0)
diff --git a/networkx/readwrite/adjlist.py b/networkx/readwrite/adjlist.py
index 56a1b4d2b..768af5ad7 100644
--- a/networkx/readwrite/adjlist.py
+++ b/networkx/readwrite/adjlist.py
@@ -201,7 +201,7 @@ def parse_adjlist(
             line = line[:p]
         if not len(line):
             continue
-        vlist = line.strip().split(delimiter)
+        vlist = line.rstrip("\n").split(delimiter)
         u = vlist.pop(0)
         # convert types
         if nodetype is not None:
diff --git a/networkx/readwrite/edgelist.py b/networkx/readwrite/edgelist.py
index ed445c80b..393b64ed7 100644
--- a/networkx/readwrite/edgelist.py
+++ b/networkx/readwrite/edgelist.py
@@ -247,7 +247,7 @@ def parse_edgelist(
             if not line:
                 continue
         # split line, should have 2 or more
-        s = line.strip().split(delimiter)
+        s = line.rstrip("\n").split(delimiter)
         if len(s) < 2:
             continue
         u = s.pop(0)
diff --git a/networkx/readwrite/multiline_adjlist.py b/networkx/readwrite/multiline_adjlist.py
index 526b30a9b..808445dbf 100644
--- a/networkx/readwrite/multiline_adjlist.py
+++ b/networkx/readwrite/multiline_adjlist.py
@@ -246,7 +246,7 @@ def parse_multiline_adjlist(
         if not line:
             continue
         try:
-            (u, deg) = line.strip().split(delimiter)
+            (u, deg) = line.rstrip("\n").split(delimiter)
             deg = int(deg)
         except BaseException as err:
             raise TypeError(f"Failed to read node and degree on line ({line})") from err
@@ -270,7 +270,7 @@ def parse_multiline_adjlist(
                     line = line[:p]
                 if line:
                     break
-            vlist = line.strip().split(delimiter)
+            vlist = line.rstrip("\n").split(delimiter)
             numb = len(vlist)
             if numb < 1:
                 continue  # isolated node


Test patch:
diff --git a/networkx/algorithms/bipartite/tests/test_edgelist.py b/networkx/algorithms/bipartite/tests/test_edgelist.py
index 86ffba072..66be8a2f5 100644
--- a/networkx/algorithms/bipartite/tests/test_edgelist.py
+++ b/networkx/algorithms/bipartite/tests/test_edgelist.py
@@ -216,3 +216,25 @@ class TestEdgelist:
             G = bipartite.parse_edgelist(
                 lines, nodetype=int, data=[("weight", int), ("key", int)]
             )
+
+
+def test_bipartite_edgelist_consistent_strip_handling():
+    """See gh-7462
+
+    Input when printed looks like:
+
+        A       B       interaction     2
+        B       C       interaction     4
+        C       A       interaction
+
+    Note the trailing \\t in the last line, which indicates the existence of
+    an empty data field.
+    """
+    lines = io.StringIO(
+        "A\tB\tinteraction\t2\nB\tC\tinteraction\t4\nC\tA\tinteraction\t"
+    )
+    descr = [("type", str), ("weight", str)]
+    # Should not raise
+    G = nx.bipartite.parse_edgelist(lines, delimiter="\t", data=descr)
+    expected = [("A", "B", "2"), ("A", "C", ""), ("B", "C", "4")]
+    assert sorted(G.edges(data="weight")) == expected
diff --git a/networkx/readwrite/tests/test_adjlist.py b/networkx/readwrite/tests/test_adjlist.py
index 6f52c234f..f2218eba1 100644
--- a/networkx/readwrite/tests/test_adjlist.py
+++ b/networkx/readwrite/tests/test_adjlist.py
@@ -239,3 +239,24 @@ class TestMultilineAdjlist:
         H = nx.read_multiline_adjlist(fh, nodetype=int, delimiter=":")
         assert nodes_equal(list(H), list(G))
         assert edges_equal(list(H.edges()), list(G.edges()))
+
+
+@pytest.mark.parametrize(
+    ("lines", "delim"),
+    (
+        (["1 2 5", "2 3 4", "3 5", "4", "5"], None),  # No extra whitespace
+        (["1\t2\t5", "2\t3\t4", "3\t5", "4", "5"], "\t"),  # tab-delimited
+        (
+            ["1\t2\t5", "2\t3\t4", "3\t5\t", "4\t", "5"],
+            "\t",
+        ),  # tab-delimited, extra delims
+        (
+            ["1\t2\t5", "2\t3\t4", "3\t5\t\t\n", "4\t", "5"],
+            "\t",
+        ),  # extra delim+newlines
+    ),
+)
+def test_adjlist_rstrip_parsing(lines, delim):
+    """Regression test related to gh-7465"""
+    expected = nx.Graph([(1, 2), (1, 5), (2, 3), (2, 4), (3, 5)])
+    nx.utils.graphs_equal(nx.parse_adjlist(lines, delimiter=delim), expected)
diff --git a/networkx/readwrite/tests/test_edgelist.py b/networkx/readwrite/tests/test_edgelist.py
index 9751885aa..fe58b3b7d 100644
--- a/networkx/readwrite/tests/test_edgelist.py
+++ b/networkx/readwrite/tests/test_edgelist.py
@@ -295,3 +295,20 @@ class TestEdgelist:
         assert H is not H2  # they should be different graphs
         assert nodes_equal(list(H), list(G))
         assert edges_equal(list(H.edges()), list(G.edges()))
+
+
+def test_edgelist_consistent_strip_handling():
+    """See gh-7462
+
+    Input when printed looks like::
+
+        1       2       3
+        2       3
+        3       4       3.0
+
+    Note the trailing \\t after the `3` in the second row, indicating an empty
+    data value.
+    """
+    s = io.StringIO("1\t2\t3\n2\t3\t\n3\t4\t3.0")
+    G = nx.parse_edgelist(s, delimiter="\t", nodetype=int, data=[("value", str)])
+    assert sorted(G.edges(data="value")) == [(1, 2, "3"), (2, 3, ""), (3, 4, "3.0")]