From 6dd1db5fd16ac32e4c45ee818115196b5f73283a Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Sun, 6 Sep 2026 22:13:27 -0400 Subject: [PATCH 1/2] refactor: plot the dependency graphs with pydot pydotplus is a fork of pydot that has not been released since 2014, and is imported when pymake is imported. pydot provides the same interface, is maintained, and is what the networkx dot interface is written against. --- docs/requirements.txt | 2 +- pixi.lock | 37 ++++++++++++++++---------------- pixi.toml | 2 +- pymake/plot/__init__.py | 2 +- pymake/plot/dependency_graphs.py | 2 +- pyproject.toml | 2 +- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 31071f49..f6c73fa9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ requests networkx meson ninja -pydotplus +pydot nbsphinx nbsphinx_link Ipython diff --git a/pixi.lock b/pixi.lock index 145d848c..7dbde807 100644 --- a/pixi.lock +++ b/pixi.lock @@ -224,7 +224,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -442,7 +442,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -507,7 +507,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -682,7 +682,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -858,7 +858,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda @@ -1184,7 +1184,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -1391,7 +1391,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -1452,7 +1452,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -1597,7 +1597,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda @@ -1741,7 +1741,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/py-cpuinfo-9.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda @@ -9146,18 +9146,19 @@ packages: license_family: BSD size: 110100 timestamp: 1733195786147 -- conda: https://conda.anaconda.org/conda-forge/noarch/pydotplus-2.0.2-pyhd8ed1ab_7.conda - sha256: e6681723a8383bf4caaadde22a595289748c6ff909ecbd01c36fbc3b168f41e0 - md5: 26e4ff85fd62f32f1818b57363995bc6 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydot-4.0.1-pyhcf101f3_2.conda + sha256: af7213a8ca077895e7e10c8f33d5de3436b8a26828422e8a113cc59c9277a3e2 + md5: 15f6d0866b0997c5302fc230a566bc72 depends: - - graphviz - - pyparsing >=2.0.1 - - python >=3.9 + - graphviz >=2.38.0 + - pyparsing >=3.1.0 + - python >=3.10 + - python license: MIT license_family: MIT run_exports: {} - size: 26425 - timestamp: 1734535829892 + size: 150656 + timestamp: 1766345630713 - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a md5: 6b6ece66ebcae2d5f326c77ef2c5a066 diff --git a/pixi.toml b/pixi.toml index e049dec9..f199fba7 100644 --- a/pixi.toml +++ b/pixi.toml @@ -20,7 +20,7 @@ networkx = "*" ninja = "*" numpy = ">=1.20.3,<3" pip = "*" -pydotplus = "*" +pydot = "*" pytest = "!=8.1.0" pytest-benchmark = "*" pytest-cov = "*" diff --git a/pymake/plot/__init__.py b/pymake/plot/__init__.py index b1e9bf9a..c4ef9fd6 100644 --- a/pymake/plot/__init__.py +++ b/pymake/plot/__init__.py @@ -1,3 +1,3 @@ """Functions to plot source code dependencies determined using a directed -acyclic graph (DAG). pydotplus is used to plot the DAG. +acyclic graph (DAG). pydot is used to plot the DAG. """ diff --git a/pymake/plot/dependency_graphs.py b/pymake/plot/dependency_graphs.py index cd3d9373..5f0a1f74 100644 --- a/pymake/plot/dependency_graphs.py +++ b/pymake/plot/dependency_graphs.py @@ -21,7 +21,7 @@ from pathlib import Path -import pydotplus.graphviz as pydot +import pydot from ..utils._compiler_language_files import ( _get_ordered_srcfiles, diff --git a/pyproject.toml b/pyproject.toml index 3224e7bf..e17f8313 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "networkx", "meson>=1.3.1", "ninja", - "pydotplus", + "pydot", ] dynamic = ["version"] From 410b724f54f5f19c9c77e8acb41c6065b077d105 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Sun, 6 Sep 2026 22:18:12 -0400 Subject: [PATCH 2/2] refactor: make pydot an optional dependency pydot is only needed to plot dependency graphs, which also needs the graphviz binaries that pip cannot install. The import is guarded so pymake imports without it, and plotting reports what to install. --- pymake/plot/__init__.py | 3 ++- pymake/plot/dependency_graphs.py | 20 ++++++++++++++++++-- pyproject.toml | 6 ++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pymake/plot/__init__.py b/pymake/plot/__init__.py index c4ef9fd6..d8ee85bb 100644 --- a/pymake/plot/__init__.py +++ b/pymake/plot/__init__.py @@ -1,3 +1,4 @@ """Functions to plot source code dependencies determined using a directed -acyclic graph (DAG). pydot is used to plot the DAG. +acyclic graph (DAG). pydot, which is an optional dependency, is used to +plot the DAG. """ diff --git a/pymake/plot/dependency_graphs.py b/pymake/plot/dependency_graphs.py index 5f0a1f74..0c691bd4 100644 --- a/pymake/plot/dependency_graphs.py +++ b/pymake/plot/dependency_graphs.py @@ -21,14 +21,26 @@ from pathlib import Path -import pydot - from ..utils._compiler_language_files import ( _get_ordered_srcfiles, _get_srcfiles, ) from ..utils._dag import _get_f_nodelist +try: + import pydot +except ImportError: + pydot = None + + +def _check_pydot(): + """Raise an error if pydot, which is an optional dependency, is missing.""" + if pydot is None: + raise ImportError( + "pydot is required to plot dependency graphs, install it with " + "'pip install mfpymake[plot]'" + ) + def to_pydot(dag, filename="mygraph.png"): """Create a png file of a Directed Acyclic Graph. @@ -44,6 +56,8 @@ def to_pydot(dag, filename="mygraph.png"): ------- """ + _check_pydot() + # Create the graph graph = pydot.Dot(graph_type="digraph") @@ -156,6 +170,8 @@ def make_plots( ------- """ + _check_pydot() + srcfiles = _get_ordered_srcfiles(_get_srcfiles(srcdir, include_subdir)) nodelist = _get_f_nodelist(srcfiles) for idx, n in enumerate(nodelist): diff --git a/pyproject.toml b/pyproject.toml index e17f8313..b33162d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,17 +32,19 @@ dependencies = [ "networkx", "meson>=1.3.1", "ninja", - "pydot", ] dynamic = ["version"] [project.optional-dependencies] +plot = [ + "pydot", +] lint = [ "cffconvert", "ruff", ] test = [ - "mfpymake[lint]", + "mfpymake[lint,plot]", "coverage!=7.6.5", "flaky", "filelock",