@@ -365,6 +365,119 @@ def test_to_networkx_types_and_node_attributes():
365365 assert pytest .approx (nxG [0 ][1 ]["weight" ]) == G .adj [0 , 1 ]
366366
367367
368+ @pytest .mark .skipif (not HAS_NX , reason = "networkx not installed" )
369+ def test_graphml_roundtrip_undirected_with_meta (tmp_path , S_dense , meta_df ):
370+ """Round-trip via GraphML for undirected, weighted graph with metadata."""
371+ G = Graph .from_dense (
372+ S_dense ,
373+ directed = False ,
374+ weighted = True ,
375+ mode = "similarity" ,
376+ meta = meta_df ,
377+ )
378+ path = tmp_path / "graph_undirected.graphml"
379+
380+ # Export to GraphML
381+ G .to_graphml (path )
382+
383+ # Import back
384+ G2 = Graph .from_graphml (path )
385+
386+ assert G2 .n_nodes == G .n_nodes
387+ assert G2 .directed == G .directed
388+ assert G2 .weighted == G .weighted
389+ assert G2 .mode == G .mode
390+
391+ # Adjacency must be equal
392+ np .testing .assert_array_almost_equal (G2 .adj .toarray (), G .adj .toarray ())
393+
394+ # Metadata should be preserved (content-wise)
395+ assert G2 .meta is not None
396+
397+ # Compare metadata ignoring column order and dtype
398+ meta1 = G2 .meta .reindex (sorted (G2 .meta .columns ), axis = 1 ).reset_index (drop = True )
399+ meta2 = G .meta .reindex (sorted (G .meta .columns ), axis = 1 ).reset_index (drop = True )
400+ pd .testing .assert_frame_equal (meta1 , meta2 , check_dtype = False )
401+
402+
403+ @pytest .mark .skipif (not HAS_NX , reason = "networkx not installed" )
404+ def test_graphml_roundtrip_directed (tmp_path ):
405+ """Round-trip via GraphML for a directed weighted graph."""
406+ A = _csr ([1.0 , 2.0 , 3.0 ], [0 , 1 , 2 ], [1 , 2 , 0 ], 3 )
407+ G = Graph .from_csr (A , directed = True , weighted = True , mode = "distance" )
408+ path = tmp_path / "graph_directed.graphml"
409+
410+ G .to_graphml (path )
411+ G2 = Graph .from_graphml (path )
412+
413+ assert G2 .n_nodes == G .n_nodes
414+ assert G2 .directed is True
415+ assert G2 .weighted is True
416+ assert G2 .mode == "distance"
417+ np .testing .assert_array_almost_equal (G2 .adj .toarray (), G .adj .toarray ())
418+
419+
420+ @pytest .mark .skipif (not HAS_NX , reason = "networkx not installed" )
421+ def test_from_graphml_uses_default_mode_when_missing (tmp_path ):
422+ """
423+ Importing GraphML created directly by networkx without a 'mode' attribute
424+ should fall back to default_mode.
425+ """
426+ import networkx as nx
427+
428+ G_nx = nx .Graph ()
429+ G_nx .add_edge (0 , 1 , weight = 1.5 )
430+ path = tmp_path / "no_mode.graphml"
431+ nx .write_graphml (G_nx , path )
432+
433+ # No 'mode' in graph attributes, so default_mode is used
434+ G = Graph .from_graphml (path , default_mode = "distance" )
435+ assert G .mode == "distance"
436+ assert not G .directed
437+ assert G .weighted
438+ assert G .n_nodes == 2
439+ np .testing .assert_array_almost_equal (
440+ G .adj .toarray (),
441+ np .array ([[0.0 , 1.5 ], [1.5 , 0.0 ]], dtype = float ),
442+ )
443+
444+
445+ @pytest .mark .skipif (not HAS_NX , reason = "networkx not installed" )
446+ def test_from_graphml_builds_metadata_from_node_attributes (tmp_path ):
447+ """
448+ Node attributes in a GraphML file should become meta columns in Graph.
449+ """
450+ import networkx as nx
451+
452+ G_nx = nx .Graph ()
453+ G_nx .add_node (0 , name = "a" , group = 1 )
454+ G_nx .add_node (1 , name = "b" , group = 2 )
455+ G_nx .add_edge (0 , 1 , weight = 2.0 )
456+
457+ path = tmp_path / "with_node_attrs.graphml"
458+ nx .write_graphml (G_nx , path )
459+
460+ G = Graph .from_graphml (path , default_mode = "similarity" )
461+
462+ assert G .n_nodes == 2
463+ assert G .meta is not None
464+ assert list (G .meta .columns ) == ["group" , "name" ] or sorted (G .meta .columns ) == ["group" , "name" ]
465+ # Check content, ignoring column order and dtype
466+ meta_sorted = G .meta .reindex (sorted (G .meta .columns ), axis = 1 )
467+ expected = pd .DataFrame ({"name" : ["a" , "b" ], "group" : [1 , 2 ]})
468+ expected_sorted = expected .reindex (sorted (expected .columns ), axis = 1 )
469+ pd .testing .assert_frame_equal (
470+ meta_sorted .reset_index (drop = True ),
471+ expected_sorted ,
472+ check_dtype = False ,
473+ )
474+ # adjacency matches the edge
475+ np .testing .assert_array_almost_equal (
476+ G .adj .toarray (),
477+ np .array ([[0.0 , 2.0 ], [2.0 , 0.0 ]], dtype = float ),
478+ )
479+
480+
368481@pytest .mark .skipif (not HAS_IG , reason = "python-igraph not installed" )
369482def test_to_igraph_types_and_attributes ():
370483 A = _csr ([0.2 , 0.9 , 0.3 ], [0 , 1 , 2 ], [1 , 2 , 0 ], 3 )
@@ -382,6 +495,7 @@ def test_to_igraph_types_and_attributes():
382495 assert "weight" in igG .es .attributes ()
383496
384497
498+
385499# ----------------- Distance/similarity conversion -----------------
386500def test_convert_mode_distance_to_similarity_and_back_dense (S_dense , meta_df ):
387501 G = Graph .from_dense (
0 commit comments