Contributing#
Contributions to nx-backbone are welcome.
Development setup#
Clone the repository and install in editable mode with all development dependencies:
git clone https://github.com/brianckeegan/nx-backbone.git
cd nx-backbone
pip install -e ".[dev,docs]"
Running tests#
Run the test suite and the docstring examples with pytest:
pytest --doctest-modules nx_backbone tests
Measure coverage:
pytest --cov=nx_backbone --cov-report=term-missing
tests/test_minimal_deps.py runs the pure-Python methods in a subprocess in
which numpy, scipy, and matplotlib cannot be imported. If you add a method that
needs an optional dependency, import it inside the function (not at module
level) so import nx_backbone keeps working with NetworkX alone.
Run the suite under NetworkX’s backend-dispatch test harness, which sends
every dispatchable call through the nx_loopback test backend and checks
the result against plain NetworkX (see Backends):
NETWORKX_TEST_BACKEND=nx_loopback pytest --doctest-modules nx_backbone tests
Continuous integration (.github/workflows/tests.yml) runs lint, mypy, the test
suite on Python 3.12-3.14 (with doctests and coverage on 3.13), a job pinned to
the oldest supported versions, the dispatch tests (tests/test_dispatch.py
and tests/test_api_contract.py, which call every dispatchable function)
under nx_loopback, and the minimal-dependency smoke tests on every push and
pull request.
A test that sweeps a registry (every method, style, scorer or dispatchable function) loops over it inside one test and collects the failures, so that a failure message names every failing case; it is not parametrized into one test per case.
Benchmarks#
benchmarks/ holds an airspeed velocity (asv) suite, as NumPy, SciPy and NetworkX use. It
times the statistical scorers on Les Misérables and on a 2000-node synthetic
weighted graph, sdsm/fdsm/fixedfill/fixedrow/fixedcol on a
400 x 300 bipartite graph, mdl_hypergraph_backbone (edge, node,
auto) at m = 500 and 2000, the statistically validated hypergraph and cores,
and the shortest-path methods (salience, metric backbone, GLAB).
Install asv and compare your branch with main; asv builds each commit in
its own virtual environment, so commit your changes first:
pip install -e ".[benchmark]"
cd benchmarks
asv machine --yes # once per machine
asv continuous main HEAD # flags benchmarks that changed by > 10%
asv continuous -b MDL main HEAD runs only the benchmarks matching a regular
expression. After editing a benchmark, asv run -E existing --quick runs each
one once against the nx-backbone already installed in your environment,
without building anything. Benchmarks are plain Python: a class with setup and time_* methods and optional
params; seeded inputs come from benchmarks/benchmarks/_data.py.
The Benchmarks workflow (.github/workflows/benchmarks.yml) runs the same
comparison on GitHub Actions when started by hand from the Actions tab
(workflow_dispatch); its log lists the benchmarks that got faster or
slower.
benchmarks/benchmarks/les_miserables.py is also the single definition of
the Les Misérables benchmark: the
tests pin the edge count each case keeps, and the docs build renders the table
from it. When a method’s count changes on purpose, update EXPECTED_EDGES in
tests/test_les_miserables.py; the docs follow automatically.
Reference implementations#
tests/test_reference_implementations.py compares p-values and backbones
with R backbone and
netbone on Les
Misérables, the karate club, and Davis Southern Women. It reads JSON
fixtures in tests/fixtures/, so neither R nor netbone is needed to run
the tests, and CI does not install them. Where nx-backbone
intentionally differs from a reference, the test checks the documented
relationship and the method’s docstring Notes explain the difference.
The reference conventions note
lists every such difference, measures its effect on the three datasets, and
records the options and the decision on each (issue #59).
Regenerate the fixtures when a reference implementation releases a new version, or when you add a comparison. Both generators read the same CSV files, which are exported from the datasets that ship with NetworkX:
python scripts/export_reference_inputs.py # tests/fixtures/reference_inputs/*.csv
R backbone (3.0 or later; the fixture records the R and package
versions used):
Rscript -e 'install.packages(c("backbone", "jsonlite"))'
Rscript scripts/make_reference_fixtures.R # tests/fixtures/r_backbone.json
The R script seeds the random number generator, so FDSM results are
reproducible for a given R and backbone version.
netbone pins old dependencies (NumPy 1.26, NetworkX 2.8, SciPy 1.13, JAX 0.4), so install it in its own Python 3.12 environment and run the script with that interpreter from the repository root:
python3.12 -m venv /tmp/netbone-env
/tmp/netbone-env/bin/pip install netbone==0.2.3
/tmp/netbone-env/bin/python -I scripts/make_netbone_fixtures.py # tests/fixtures/netbone.json
netbone’s ECM solver is not bit-for-bit reproducible, so its p-values can
change by about 1e-5 between runs; the test allows for that. After
regenerating, run the reference tests and review every change in the
fixtures with git diff.
Building documentation#
Build the documentation locally, treating warnings as errors as CI does:
cd docs
make html SPHINXOPTS="-W --keep-going -n"
The built documentation will be in docs/_build/html/.
The build validates every rendered docstring with numpydoc: each public
function needs a docstring with a summary, every parameter documented in
signature order, and Returns and Examples sections. The checks are
listed in [tool.numpydoc_validation] in pyproject.toml; run them in
seconds, without building the docs, with:
python -m numpydoc lint nx_backbone/*.py
Code style#
The project follows the conventions of NetworkX and the wider scientific-Python stack:
Lint and format with ruff (configured in
pyproject.toml):ruff check nx_backbone tests scripts docs/conf.py docs/_figures docs/examples ruff format nx_backbone tests scripts docs/conf.py docs/_figures docs/examples
Take a graph
Gas the first argument, return a new graph, and never mutate the input.Decorate a new function that takes a graph and returns a new graph with
nx_backbone._dispatch._dispatchable(edge_attrs="weight"if it reads edge weights), innermost, belownot_implemented_forandpy_random_stateas in NetworkX, and add it totests/test_dispatch.py.Name edge-weight parameters
weightand random-state parametersseed; acceptNone, anint, or a NumPyGenerator/RandomState, as NetworkX does.Raise
networkx.NetworkXErrorfor invalid graph input andnetworkx.NetworkXNotImplemented(vianetworkx.utils.decorators.not_implemented_for()) for unsupported graph types; raiseValueErrorfor invalid non-graph arguments. Functions that also accept a matrix (the bipartite scorers take NumPy, SciPy sparse, or pandas incidence matrices) use the module’s own check, which raises the same exception for graph input, becausenot_implemented_forassumes a graph argument.Annotate public functions (the package ships
py.typed). Start each module withfrom __future__ import annotationsand import NumPy, SciPy, pandas, and Matplotlib types only underif TYPE_CHECKING:, so the core package still imports with only NetworkX installed. Shared aliases (Seed,GraphInput,Hyperedges, …) live innx_backbone._typing; importnot_implemented_forandpy_random_statefrom there too. They are NetworkX’s own decorators, typed so that they keep the decorated function’s signature (_dispatchableis typed the same way, and also acceptsbackend=...). Check with mypy (non-strict, configured inpyproject.toml; thetypingextra pins mypy andtypes-networkx):mypy nx_backbone
Name score attributes
<method>_pvalue,<method>_score, or<method>_keepso they compose with the filters innx_backbone.filters.Use NumPy-style docstrings for all public functions
Include
Parameters,Returns,Raises(if applicable),References, andExamplessections in docstringsUse RST
.. math::directives for mathematical formulasUse
:func:and:mod:roles for cross-references within docstrings
Making a release#
Publishing a GitHub Release triggers three workflows: publish-to-PyPI.yml
(PyPI), publish-to-conda.yml (anaconda.org), and deploy-docs.yml
(GitHub Pages). The PyPI workflow follows the PyPA trusted publishing guide:
https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
The package version comes from the release tag through setuptools-scm:
tag v0.3.0 builds version 0.3.0. Do not set a version in
pyproject.toml or in the conda recipe. (The v0.2.0 tag never reached
PyPI because pyproject.toml still set version = "0.1.0" there.)
One-time setup:
On PyPI, add a trusted publisher for the
nx-backboneproject: ownerbrianckeegan, repositorynx-backbone, workflow filenamepublish-to-PyPI.yml, environment namepypi. Before the first upload the project does not exist yet, so add it as a pending publisher (PyPI account settings, “Publishing”); the first release then creates the project. (Versions 0.2 and earlier were published asnetworkx-backbone, which is not updated.)In the repository settings, create the
pypienvironment. If it has deployment branch or tag rules, allow tags matchingv*: a release run deploys from the tag, not frommain.Do the same for the
github-pagesenvironment, which GitHub creates for Pages with a rule that allows only the default branch: under Deployment branches and tags, add a tag rulev*. Without it the documentation deploy of a release fails with “Tag “vX.Y.Z” is not allowed to deploy to github-pages due to environment protection rules” (as for v0.3.0). To publish the docs after such a failure, run the Deploy Documentation workflow by hand onmainwhilemainis still at the release commit.For anaconda.org uploads, add an
ANACONDA_TOKENrepository secret.
For each release:
Merge everything for the release into
mainwith green CI, and set the release date in the changelog heading (Version X.Y.Z (YYYY-MM-DD)).Draft a GitHub Release whose tag is
vX.Y.Z(the tag can be new; GitHub creates it onmainwhen the release is published) and paste the release notes. Curated notes for each release live indocs/release-notes/.Publish the release. The PyPI workflow checks out the tag with its full history, runs the tests, builds the wheel and sdist, checks them with
twine check --strict, and fails before uploading unless both carry version X.Y.Z. The conda workflow checks the same thing before building.Check https://pypi.org/project/nx-backbone/ and the documentation site. If an upload fails, fix the cause (for example the trusted publisher or the environment rules) and use “Re-run failed jobs” on the release run, which rebuilds from the same tag.
Running either publishing workflow by hand (workflow_dispatch) is a dry
run: it tests and builds, but never uploads. Never publish from a manual run
on main; it would build a development version such as
0.2.0.post1.dev9.
Submitting changes#
Fork the repository
Create a feature branch
Make your changes with tests
Run the test suite to verify
Submit a pull request