diff --git a/.all-contributorsrc b/.all-contributorsrc index 78c931202..6daaa80b3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -531,6 +531,60 @@ "contributions": [ "code" ] + }, + { + "login": "JDPowell648", + "name": "JDPowell648", + "avatar_url": "https://avatars.githubusercontent.com/u/41934552?v=4", + "profile": "https://github.com/JDPowell648", + "contributions": [ + "doc" + ] + }, + { + "login": "Adriankhl", + "name": "k.h.lai", + "avatar_url": "https://avatars.githubusercontent.com/u/16377650?v=4", + "profile": "https://github.com/Adriankhl", + "contributions": [ + "code" + ] + }, + { + "login": "gruebel", + "name": "Anton Grübel", + "avatar_url": "https://avatars.githubusercontent.com/u/33207684?v=4", + "profile": "https://github.com/gruebel", + "contributions": [ + "code" + ] + }, + { + "login": "flange-ipb", + "name": "flange-ipb", + "avatar_url": "https://avatars.githubusercontent.com/u/34936695?v=4", + "profile": "https://github.com/flange-ipb", + "contributions": [ + "code" + ] + }, + { + "login": "pmp-p", + "name": "Paul m. p. Peny", + "avatar_url": "https://avatars.githubusercontent.com/u/16009100?v=4", + "profile": "https://discuss.afpy.org/", + "contributions": [ + "code" + ] + }, + { + "login": "DavidRConnell", + "name": "David R. Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/35470740?v=4", + "profile": "https://davidrconnell.github.io/", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9b7f0666..f047f808a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: python-version: '3.8' - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -39,7 +39,7 @@ jobs: CIBW_TEST_SKIP: "cp310-manylinux_i686 cp311-manylinux_i686 cp312-manylinux_i686" - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_BUILD: "*-musllinux_${{ matrix.wheel_arch }}" @@ -64,7 +64,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (manylinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -89,7 +89,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Build wheels (musllinux) - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip wheel && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -156,7 +156,7 @@ jobs: cmake --install . - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -256,7 +256,7 @@ jobs: shell: cmd - name: Build wheels - uses: pypa/cibuildwheel@v2.16.5 + uses: pypa/cibuildwheel@v2.17.0 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 516ea19ca..2b88275d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,14 @@ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.6.0 hooks: - - id: check-ast - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.275 + rev: v0.3.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - exclude: ^doc/examples_sphinx-gallery/ - language_version: python3 + - id: ruff-format diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ca918b0..f787db23f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # igraph Python interface changelog +## [0.11.5] - 2024-05-07 + +### Added + +- Added a `prefixattr=...` keyword argument to `Graph.write_graphml()` that + allows the user to strip the `g_`, `v_` and `e_` prefixes from GraphML files + written by igraph. + +### Changed + +- `Graph.are_connected()` has now been renamed to `Graph.are_adjacent()`, + following up a similar change in the C core. The old name of the function + is deprecated but will be kept around until at least 0.12.0. + +- The C core of igraph was updated to version 0.10.12. + ## [0.11.4] ### Added @@ -621,7 +637,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[main]: https://github.com/igraph/python-igraph/compare/0.11.4...main +[main]: https://github.com/igraph/python-igraph/compare/0.11.5...main +[0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 [0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 [0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 [0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 37b369f5d..dc02942b4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -81,6 +81,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
szcf-weiya

💻
tristanlatr

💻 +
JDPowell648

📖 +
k.h.lai

💻 +
Anton Grübel

💻 +
flange-ipb

💻 +
Paul m. p. Peny

💻 + + +
David R. Connell

💻 diff --git a/README.md b/README.md index a94f33f5a..45a127ca4 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ Also, when building in MSYS2, you need to set the `SETUPTOOLS_USE_DISTUTILS` environment variable to `stdlib`; this is because MSYS2 uses a patched version of `distutils` that conflicts with `setuptools >= 60.0`. +> [!TIP] +> You need the following packages: +> `$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` + ### Enabling GraphML By default, GraphML is disabled, because `libxml2` is not available on Windows in @@ -158,8 +162,16 @@ the packaged igraph library instead of bringing its own copy. It is also useful on macOS if you want to link to the igraph library installed from Homebrew. -Due to the lack of support of `pkg-config` on Windows, it is currently not -possible to build against an external library on Windows. +Due to the lack of support of `pkg-config` on MSVC, it is currently not +possible to build against an external library on MSVC. + +In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have +[mingw-w64-igraph](https://packages.msys2.org/base/mingw-w64-igraph) installed, +simply type: +``` +IGRAPH_USE_PKG_CONFIG=1 SETUPTOOLS_USE_DISTUTILS=stdlib pip install igraph +``` +to build. **Warning:** the Python interface is guaranteed to work only with the same version of the C core that is vendored inside the `vendor/source/igraph` diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 943f719cc..bb911efad 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -63,7 +63,7 @@ To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :att >>> v1, v2 = e.source, e.target Vice versa, to get the edge if from the source and target vertices, you can use :meth:`Graph.get_eid` or, for multiple pairs of source/targets, -:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_connected`. +:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_adjacent`. To get the edges incident on a vertex, you can use :meth:`Vertex.incident`, :meth:`Vertex.out_edges` and :meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones of course:: diff --git a/pyproject.toml b/pyproject.toml index 0d5932b7b..0ded74ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,5 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.ruff] -ignore = ["B905", "C901", "E402", "E501"] -line-length = 80 -select = ["B", "C", "E", "F", "W"] +lint.ignore = ["B905", "C901", "E402", "E501"] +lint.select = ["B", "C", "E", "F", "W"] diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 4315a6df0..44b617cf4 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1445,10 +1445,10 @@ PyObject *igraphmodule_Graph_is_biconnected(igraphmodule_GraphObject *self, PyOb /** \ingroup python_interface_graph * \brief Decides whether there is an edge from a given vertex to an other one. * \return Py_True if the vertices are directly connected, Py_False otherwise - * \sa igraph_are_connected + * \sa igraph_are_adjacent */ -PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +PyObject *igraphmodule_Graph_are_adjacent(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { static char *kwlist[] = { "v1", "v2", NULL }; PyObject *v1, *v2; @@ -1464,7 +1464,7 @@ PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(v2, &idx2, &self->g)) return NULL; - if (igraph_are_connected(&self->g, idx1, idx2, &res)) + if (igraph_are_adjacent(&self->g, idx1, idx2, &res)) return igraphmodule_handle_igraph_error(); if (res) @@ -9728,18 +9728,19 @@ PyObject *igraphmodule_Graph_write_pajek(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_write_graphml(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *fname = NULL; - static char *kwlist[] = { "f", NULL }; + PyObject *fname = NULL, *prefixattr_o = Py_True; + static char *kwlist[] = { "f", "prefixattr", NULL }; igraphmodule_filehandle_t fobj; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &fname)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &fname, &prefixattr_o)) return NULL; if (igraphmodule_filehandle_init(&fobj, fname, "w")) return NULL; - if (igraph_write_graph_graphml(&self->g, igraphmodule_filehandle_get(&fobj), - /*prefixattr=*/ 1)) { + if (igraph_write_graph_graphml( + &self->g, igraphmodule_filehandle_get(&fobj), PyObject_IsTrue(prefixattr_o) + )) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -14553,10 +14554,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // STRUCTURAL PROPERTIES OF GRAPHS // ///////////////////////////////////// - // interface to igraph_are_connected - {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, + // interface to igraph_are_adjacent + {"are_adjacent", (PyCFunction) igraphmodule_Graph_are_adjacent, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2)\n--\n\n" + "are_adjacent(v1, v2)\n--\n\n" "Decides whether two given vertices are directly connected.\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -15121,6 +15122,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "get_shortest_path(v, to, weights=None, mode=\"out\", output=\"vpath\", algorithm=\"auto\")\n--\n\n" "Calculates the shortest path from a source vertex to a target vertex in a graph.\n\n" + "This function only returns a single shortest path. Consider using L{get_shortest_paths()}\n" + "to find all shortest paths between a source and one or more target vertices.\n\n" "@param v: the source vertex of the path\n" "@param to: the target vertex of the path\n" "@param weights: edge weights in a list or the name of an edge attribute\n" @@ -15138,7 +15141,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " algorithm automatically based on whether the graph has negative weights\n" " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" " uses the Bellman-Ford algorithm. Ignored for unweighted graphs.\n" - "@return: see the documentation of the C{output} parameter.\n"}, + "@return: see the documentation of the C{output} parameter.\n" + "@see: L{get_shortest_paths()}\n"}, /* interface to igraph_get_shortest_paths */ {"get_shortest_paths", (PyCFunction) igraphmodule_Graph_get_shortest_paths, @@ -16930,9 +16934,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_edgelist */ {"write_graphml", (PyCFunction) igraphmodule_Graph_write_graphml, METH_VARARGS | METH_KEYWORDS, - "write_graphml(f)\n--\n\n" + "write_graphml(f, prefixattr=True)\n--\n\n" "Writes the graph to a GraphML file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" + "@param prefixattr: whether attribute names in the written file should be\n" + " prefixed with C{g_}, C{v_} and C{e_} for graph, vertex and edge\n" + " attributes, respectively. This might be needed to ensure the uniqueness\n" + " of attribute identifiers in the written GraphML file.\n" }, /* interface to igraph_write_graph_leda */ {"write_leda", (PyCFunction) igraphmodule_Graph_write_leda, diff --git a/src/_igraph/igraphmodule_api.h b/src/_igraph/igraphmodule_api.h index f7eeb932b..8ab364b50 100644 --- a/src/_igraph/igraphmodule_api.h +++ b/src/_igraph/igraphmodule_api.h @@ -56,25 +56,8 @@ extern "C" { /* Return -1 and set exception on error, 0 on success */ static int import_igraph(void) { - PyObject *c_api_object; - PyObject *module; - - module = PyImport_ImportModule("igraph._igraph"); - if (module == 0) - return -1; - - c_api_object = PyObject_GetAttrString(module, "_C_API"); - if (c_api_object == 0) { - Py_DECREF(module); - return -1; - } - - if (PyCObject_Check(c_api_object)) - PyIGraph_API = (void**)PyCObject_AsVoidPtr(c_api_object); - - Py_DECREF(c_api_object); - Py_DECREF(module); - return 0; + PyIGraph_API = (void **)PyCapsule_Import("igraph._igraph._C_API", 0); + return (PyIGraph_API != NULL) ? 0 : -1; } #endif diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index c0997aa8c..7f0e328b7 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -948,6 +948,14 @@ def Incidence(cls, *args, **kwds): deprecated("Graph.Incidence() is deprecated; use Graph.Biadjacency() instead") return cls.Biadjacency(*args, **kwds) + def are_connected(self, *args, **kwds): + """Deprecated alias to L{Graph.are_adjacent()}.""" + deprecated( + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " + "instead" + ) + return self.are_adjacent(*args, **kwds) + def get_incidence(self, *args, **kwds): """Deprecated alias to L{Graph.get_biadjacency()}.""" deprecated( diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index b45e6d9ce..b1fca9ab4 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1459,6 +1459,9 @@ def _ensure_list(obj): def compare_communities(comm1, comm2, method="vi", remove_none=False): """Compares two community structures using various distance measures. + For measures involving entropies (e.g., the variation of information metric), + igraph uses natural logarithms. + B{References} - Meila M: Comparing clusterings by the variation of information. In: diff --git a/src/igraph/io/adjacency.py b/src/igraph/io/adjacency.py index ff83df5a6..7f09ed167 100644 --- a/src/igraph/io/adjacency.py +++ b/src/igraph/io/adjacency.py @@ -78,6 +78,8 @@ def _construct_graph_from_weighted_adjacency( ): """Generates a graph from its weighted adjacency matrix. + Only edges with a non-zero weight are created. + @param matrix: the adjacency matrix. Possible types are: - a list of lists - a numpy 2D array or matrix (will be converted to list of lists) @@ -85,12 +87,12 @@ def _construct_graph_from_weighted_adjacency( to a dense matrix) @param mode: the mode to be used. Possible values are: - C{"directed"} - the graph will be directed and a matrix element - specifies the number of edges between two vertices. + specifies the weight of the corresponding edge. - C{"undirected"} - the graph will be undirected and a matrix element - specifies the number of edges between two vertices. The matrix must + specifies the weight of the corresponding edge. The matrix must be symmetric. - - C{"max"} - undirected graph will be created and the number of - edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{"max"} - undirected graph will be created and the weight of the + edge between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} - C{"upper"} - undirected graph with the upper right triangle of diff --git a/src/igraph/io/libraries.py b/src/igraph/io/libraries.py index 52e47b8af..f35cc9545 100644 --- a/src/igraph/io/libraries.py +++ b/src/igraph/io/libraries.py @@ -55,7 +55,7 @@ def _export_graph_to_networkx( eattr["_igraph_index"] = i if multigraph and "_nx_multiedge_key" in eattr: - eattr["key"] = eattr.pop["_nx_multiedge_key"] + eattr["key"] = eattr.pop("_nx_multiedge_key") if vertex_attr_hashable in graph.vertex_attributes(): hashable_source = graph.vs[vertex_attr_hashable][edge.source] diff --git a/src/igraph/version.py b/src/igraph/version.py index 4c4a3bf3a..92c6ba6fb 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 11, 4) +__version_info__ = (0, 11, 5) __version__ = ".".join("{0}".format(x) for x in __version_info__) diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4b6683d32..1299cada9 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -112,7 +112,7 @@ def testVertexNameIndexingBug196(self): g.add_vertices([a, b]) g.add_edges([(a, b)]) self.assertEqual(g.ecount(), 1) - self.assertTrue(g.are_connected(a, b)) + self.assertTrue(g.are_adjacent(a, b)) def testInvalidAttributeNames(self): g = Graph.Famous("bull") diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 69657062e..72047dc1a 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -360,6 +360,7 @@ def testGraphML(self): self.assertTrue("name" in g.vertex_attributes()) g.write_graphml(tmpfname) + g.write_graphml(tmpfname, prefixattr=False) def testGraphMLz(self): with temporary_file( diff --git a/tests/test_structural.py b/tests/test_structural.py index 5ad20648c..bfda0f2e5 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -994,7 +994,7 @@ def testGetAllSimplePaths(self): self.assertEqual(15, path[-1]) curr = path[0] for next in path[1:]: - self.assertTrue(g.are_connected(curr, next)) + self.assertTrue(g.are_adjacent(curr, next)) curr = next def testPathLengthHist(self): diff --git a/vendor/source/igraph b/vendor/source/igraph index 2de2c37e3..9e7717014 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 2de2c37e353e15000d0608d2dd2f4b3c3595edb6 +Subproject commit 9e77170146f537ad44e81ca905548738a5a086a0