Repository: networkx/networkx

Issue:
Multi-line entries in GML files not supported
I am dealing with GML files that contain string entries spread across multiple lines. Networkx cannot parse such entries (Cytoscape for example can).

### Current Behavior

Parsing GML files with multi-line entries fails with  `NetworkXError: cannot tokenize...`.

### Expected Behavior

Networkx should be able to parse those entries

### Steps to Reproduce

Example GML file:

    graph
    [
	    node
	    [
		    id 0
		    label "multiline node"
		    label2 "multiline1
	    multiline2
	    multiline3"
		    alt_name "id 0"
	    ]
    ]

Example code:

    nx.read_gml("multiline.gml")
    # fails with NetworkXError: cannot tokenize...

<!--- Provide a minimal example that reproduces the bug -->

### Environment

<!--- Please provide details about your local environment -->

Python version: 3.9.7 and 3.11.4
NetworkX version: 2.6.3 and latest (git commit ff9c27b40)

### Additional context

Already working on a fix

Discussion:

Patch:
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 2942cd8fe..5e2552789 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -14,10 +14,20 @@ jobs:
       - uses: actions/setup-python@v4
         with:
           python-version: "3.10"
-      - run: |
+      - name: Install Python requirements
+        run: |
           python -m pip install -r requirements/release.txt
-          python -m build --sdist --wheel
-      - uses: scientific-python/upload-nightly-action@main
+      - name: Build a wheel and a sdist
+        run: |
+          python -m build .
+      - name: Verify the distribution
+        run: twine check --strict dist/*
+      - name: List contents of sdist
+        run: python -m tarfile --list dist/networkx-*.tar.gz
+      - name: List contents of wheel
+        run: python -m zipfile --list dist/networkx-*.whl
+      - name: Upload nighlty wheel
+        uses: scientific-python/upload-nightly-action@main
         with:
           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_NIGHTLY }}
           artifacts_path: dist/
diff --git a/networkx/algorithms/time_dependent.py b/networkx/algorithms/time_dependent.py
index f458e2ca8..5dfdd63b9 100644
--- a/networkx/algorithms/time_dependent.py
+++ b/networkx/algorithms/time_dependent.py
@@ -10,6 +10,7 @@ __all__ = ["cd_index"]
 
 @not_implemented_for("undirected")
 @not_implemented_for("multigraph")
+@nx._dispatch(node_attrs={"time": None, "weight": 1})
 def cd_index(G, node, *, time_delta=timedelta(days=5 * 365), time="time", weight=None):
     r"""Compute the CD index for `node` within the graph `G`.
 
@@ -39,7 +40,7 @@ def cd_index(G, node, *, time_delta=timedelta(days=5 * 365), time="time", weight
 
     Raises
     ------
-    ValueError
+    NetworkXError
        If not all nodes have a `time` attribute or
        `time_delta` and `time` attribute types are not compatible or
        `n` equals 0.
@@ -88,7 +89,7 @@ def cd_index(G, node, *, time_delta=timedelta(days=5 * 365), time="time", weight
 
     """
     if not all(time in G.nodes[n] for n in G):
-        raise ValueError("Not all nodes have a 'time' attribute.")
+        raise nx.NetworkXError("Not all nodes have a 'time' attribute.")
 
     try:
         # get target_date
@@ -96,7 +97,7 @@ def cd_index(G, node, *, time_delta=timedelta(days=5 * 365), time="time", weight
         # keep the predecessors that existed before the target date
         pred = {i for i in G.pred[node] if G.nodes[i][time] <= target_date}
     except:
-        raise ValueError(
+        raise nx.NetworkXError(
             "Addition and comparison are not supported between 'time_delta' "
             "and 'time' types, default time_delta = datetime.timedelta."
         )
@@ -107,7 +108,7 @@ def cd_index(G, node, *, time_delta=timedelta(days=5 * 365), time="time", weight
     # n is size of the union of the focal node's predecessors and its successors' predecessors
     n = len(pred.union(*(G.pred[s].keys() - {node} for s in G[node])))
     if n == 0:
-        raise ValueError("The cd index cannot be defined.")
+        raise nx.NetworkXError("The cd index cannot be defined.")
 
     # calculate cd index
     if weight is None:
diff --git a/networkx/readwrite/gml.py b/networkx/readwrite/gml.py
index ffa5cb3ff..60a7821b7 100644
--- a/networkx/readwrite/gml.py
+++ b/networkx/readwrite/gml.py
@@ -311,9 +311,34 @@ def parse_gml_lines(lines, label, destringizer):
         ]
         tokens = re.compile("|".join(f"({pattern})" for pattern in patterns))
         lineno = 0
+        multilines = []  # entries spread across multiple lines
         for line in lines:
-            length = len(line)
             pos = 0
+
+            # deal with entries spread across multiple lines
+            #
+            # should we actually have to deal with escaped "s then do it here
+            if multilines:
+                multilines.append(line.strip())
+                if line[-1] == '"':  # closing multiline entry
+                    # multiline entries will be joined by space. cannot
+                    # reintroduce newlines as this will break the tokenizer
+                    line = " ".join(multilines)
+                    multilines = []
+                else:  # continued multiline entry
+                    lineno += 1
+                    continue
+            else:
+                if line.count('"') == 1:  # opening multiline entry
+                    if line.strip()[0] != '"' and line.strip()[-1] != '"':
+                        # since we expect something like key "value", the " should not be found at ends
+                        # otherwise tokenizer will pick up the formatting mistake.
+                        multilines = [line.rstrip()]
+                        lineno += 1
+                        continue
+
+            length = len(line)
+
             while pos < length:
                 match = tokens.match(line, pos)
                 if match is None:


Test patch:
diff --git a/networkx/algorithms/shortest_paths/generic.py b/networkx/algorithms/shortest_paths/generic.py
index 2c61239e6..b29206d8b 100644
--- a/networkx/algorithms/shortest_paths/generic.py
+++ b/networkx/algorithms/shortest_paths/generic.py
@@ -517,6 +517,7 @@ def all_shortest_paths(G, source, target, weight=None, method="dijkstra"):
     return _build_paths_from_predecessors({source}, target, pred)
 
 
+@nx._dispatch(edge_attrs="weight")
 def single_source_all_shortest_paths(G, source, weight=None, method="dijkstra"):
     """Compute all shortest simple paths from the given source in the graph.
 
@@ -592,6 +593,7 @@ def single_source_all_shortest_paths(G, source, weight=None, method="dijkstra"):
             pass
 
 
+@nx._dispatch(edge_attrs="weight")
 def all_pairs_all_shortest_paths(G, weight=None, method="dijkstra"):
     """Compute all shortest paths between all nodes.
 
diff --git a/networkx/algorithms/tests/test_time_dependent.py b/networkx/algorithms/tests/test_time_dependent.py
index e4f8c8b08..82929fd67 100644
--- a/networkx/algorithms/tests/test_time_dependent.py
+++ b/networkx/algorithms/tests/test_time_dependent.py
@@ -279,7 +279,9 @@ class TestCdIndex:
 
         nx.set_node_attributes(G, node_attrs)
 
-        with pytest.raises(ValueError, match="The cd index cannot be defined.") as ve:
+        with pytest.raises(
+            nx.NetworkXError, match="The cd index cannot be defined."
+        ) as ve:
             nx.cd_index(G, 4)
 
     def test_time_timedelta_compatibility(self):
@@ -312,7 +314,7 @@ class TestCdIndex:
         nx.set_node_attributes(G, node_attrs)
 
         with pytest.raises(
-            ValueError,
+            nx.NetworkXError,
             match="Addition and comparison are not supported between 'time_delta' and 'time' types, default time_delta = datetime.timedelta",
         ) as ve:
             nx.cd_index(G, 4)
@@ -349,7 +351,7 @@ class TestCdIndex:
         nx.set_node_attributes(G, node_attrs)
 
         with pytest.raises(
-            ValueError, match="Not all nodes have a 'time' attribute."
+            nx.NetworkXError, match="Not all nodes have a 'time' attribute."
         ) as ve:
             nx.cd_index(G, 4)
 
diff --git a/networkx/readwrite/tests/test_gml.py b/networkx/readwrite/tests/test_gml.py
index 0d0bebf69..420139074 100644
--- a/networkx/readwrite/tests/test_gml.py
+++ b/networkx/readwrite/tests/test_gml.py
@@ -614,6 +614,28 @@ graph
             os.close(fd)
             os.unlink(fname)
 
+    def test_multiline(self):
+        # example from issue #6836
+        multiline_example = """
+graph
+[
+    node
+    [
+	    id 0
+	    label "multiline node"
+	    label2 "multiline1
+    multiline2
+    multiline3"
+	    alt_name "id 0"
+    ]
+]
+"""
+        G = nx.parse_gml(multiline_example)
+        assert G.nodes["multiline node"] == {
+            "label2": "multiline1 multiline2 multiline3",
+            "alt_name": "id 0",
+        }
+
 
 @contextmanager
 def byte_file():