././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5281236 graphviz-0.21/0000777000000000000000000000000015023511374010221 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979792.0 graphviz-0.21/CHANGES.rst0000666000000000000000000005600315023511220012015 0ustar00Changelog ========= Version 0.21 ------------ Drop Python 3.8 support (end of life 7 Oct 2024). Tag Python 3.13 support. Add support for ``format='svg_inline'``, available since upstream Graphviz 10.0.1. Produces header-less SVG suitable for inlining into HTML (see https://www.graphviz.org/docs/outputs/svg/). Switch project to ``pyproject.toml`` and build to ``python -m build``) (https://build.pypa.io). This changes the source distribution formar from ``.zip`` to PEP 625 compliant ``.tar.gz`` (https://peps.python.org/pep-0625/). Version 0.20.3 -------------- Revert improvements to the internal ``tools.deprecate_positional_args()`` decorator that caused false positive ``PendingDeprecationWarning: The signature of ... will be reduced`` warnings that have been misinterpreted in the ``0.20.2`` release process. Version 0.20.2 -------------- Drop Python 3.7 support (end of life 27 Jun 2023). Tag Python 3.11 and 3.12 support. Add caveat about ``labe`` escaping/quoting to ``.node()`` and ``.render()`` API docs. Document that ``doctest_skip_exe()`` lines in doctest should be ignored. Improve internal ``tools.deprecate_positional_args()`` decorator and fix incorect test assertion. Update GitHub actions. Pin ``pytest`` test dependency to ``<8.1`` as a workaround for ``import file mismatch error`` related to ``conftest.py`` files, in ``pytest`` ``8.1.1``, see https://github.com/pytest-dev/pytest/issues/12123. Version 0.20.1 -------------- Fix documentation building: upgrade to Sphinx 5.0. Fix broken user guide links in API documentation. Version 0.20 ------------ Add keyword-only ``neato_no_op`` argument to ``.render()``, ``.pipe()``, and stand-alone ``graphviz.render()`` and ``graphviz.pipe()``. When building a ``Graph`` or ``Digraph``, warn about an expected DOT syntax error in rendering when passing a string that ends with an odd number of backslashes (e.g. invalid ``dot.node('spam', label='\\')`` instead of correct ``..., label=r'\\'`` for a node labled as a backslash). Increase visibility of ``graphviz.escape()`` in the documentation. Version 0.19.2 -------------- Drop Python 3.6 support (end of life 23 Dec 2021). Fix ``ExecutableNotFound`` and ``CalledProcessError`` in ``graphviz.__all__``. Better document ``0.18`` change of behaviour for the ``body`` argument/attribute (lines need to include their final newline). Version 0.19.1 -------------- Fix undecoded ``CalledProcessError.stdout`` and ``.stderr`` when ``.pipe()`` call with an ``encoding`` different from ``self.encoding`` fails. Fix missing project root ``conftest.py`` in source distribution. Extend ``examples/graphviz-escapes.ipynb``. Improve test coverage. Increase build scripts verbosity. Version 0.19 ------------ Add ``PendingDeprecationWarning`` to calls using positional arguments that will be **deprecated in a later version**. The future API will allow from one to three positional arguments depending on the method or function. Keyword-only arguments where not around when this library was created. This signals dependents and in general users to start updating or pinning to the wanted version (or range). Crucially, this helps new users with a safer API that allows to avoid some common mistakes. Warnings reported in tests. Add keyword-only ``outfile`` argument to ``.render()`` and stand-alone ``graphviz.render()``. Allows to override the rendered output file name: ``.render(filename='spam.gv', outfile='spam.pdf')`` Allows to derive the ``format`` and the ``filename`` from the rendered ``outfile`` name: ``.render(outfile='spam.svg')`` Tries to infer default ``format`` from the ``outfile`` suffix. You can override by setting ``format`` explicitly. Warns with a ``graphviz.FormatSuffixMismatchWarning`` if there is a mismatch between given ``format`` and the inferred format from ``outfile`` suffix. Warns with a ``graphviz.UnknownSuffixWarning`` if ``format`` is given and ``outfile`` uses a suffix that cannot be mapped to a supported format. Add ``graphviz.set_jupyter_format()`` to set the output ``format`` used by the Jupyter visualization of ``graphviz.Graph``, ``graphviz.Digraph``, and ``graphviz.Source`` (supported formats: ``'svg'``, ``'png'``, ``'jpeg'``). Replace ``_repr_svg_()`` internally with ``_repr_mimebundle_(include, exclude)`` returning a mimebundle ``{'image/svg+xml', '`_ Christoph Boeddeker. Add keyword-only ``raise_if_result_exists`` argument to ``.render()`` and stand-alone ``graphviz.render()``. Raises ``graphviz.FileExistsError`` if the rendered file already exists. Add support to for ``.render()`` and stand-alone ``.render()`` to overwrite the input source file with the rendered output when using the ``outfile`` keyword-only argument. This probably only makes sense for text-based Graphviz formats such as ``dot`` or ``plain``. You need to specify ``overwrite_filepath=True`` to enable this. Add ``graphviz.CalledProcessError`` derived from ``subprocess.CalledProcessError`` so users can choose either one in their excepts. Add ``graphviz.FileExistsError`` derived from ``FileExistsError`` so users can choose either one in their excepts. Add ``--only-exe`` flag to ``run-tests.py`` (overrides ``--skip-exe``). Add ``--no-open`` and ``--open`` flags to ``build-docs.py``. Add ``lint-code.py`` and use in build job. Increase doctest coverage. Extend type annotations. Accept path-like objects for ``filename``, ``directory``, and ``filepath``. Extend and improve documentation. Improve build tests. Version 0.18.2 -------------- Fix ``filepath`` fallback to ``name`` of ``Graph/Digraph`` for when filepath is not present (restore ``graphviz.Graph('spam').filename == 'spam.gv'`` broken in 0.18). Fix unintended API docs reference to internal ``backend`` name for ``DOT_BINARY`` and ``UNFLATTEN_BINARY``. Moved to public API as ``graphviz.DOT_BINARY`` and ``graphviz.UNFLATTEN_BINARY``. Fix broken documentation links. Docs: re-render most SVGs and notebooks with upstream Graphviz 2.49.3. Version 0.18.1 -------------- Fix ``TypeError: argument of type 'WindowsPath' is not iterable`` on Windows platform under Python 3.6 and 3.7 (work around https://bugs.python.org/issue41649). Update outdated examples source links. Improve mode structure and separation of concerns. Improve test structure and coverage. Improve output of ``try-examples.py``. Add exit status for CI. Disable `view()`. Add ``build-docs.py`` script for development. Add ``update-help.py`` script for development. Version 0.18 ------------ Change of beaviour: File endings are now normalized so that all DOT source outputs end with a final newline (Unix convention, simplifies concatenation). This includes DOT source files written by ``.render()``, ``.view()``, or ``.save()`` as well was ``.source`` generated or loaded from ``Source`` (or ``Source.from_file()``). Change of behaviour: ``Source`` instances created by ``Source.from_file()`` no nonger write the content read into ``.source`` back into the file. Use ``.save(skip_existing=False)`` before calling ``.render()`` or ``.view()`` if you want to overwrite the file to produce the previous (less safe) behaviour. Change of undocumented behaviour: When iterating over a ``Graph``, ``Digraph``, or ``Source`` instance, the yielded lines now include a final newline (``'\n'``). This mimics iteration over ``file`` object lines in text mode. Change of behaviour: When adding custom DOT statements using the ``body`` argument of ``Graph`` or ``Digraph`` or appending to the ``body`` attribute of an instance, the lines now need to include their final newline (``'\n'``). When passing invalid parameters such as unknown ``engine``, ``format``, etc., ``.render()`` now raises early before writing the file. Call ``.save()`` explicitly to produce the previous (less safe) behaviour. Add optional keyword-only ``encoding`` argument to ``pipe()``. Returns the decoded stdout from the rendering process (e.g. ``format='svg'``). Delegates encoding/decoding to ``subprocess`` in the common case (input and output encoding are the same, e.g. default ``encoding='utf-8'``). Used by the Jupyter notebook integration. Add optional keyword-only ``engine`` argument to ``.pipe()`` and ``.render()``. Add optional keyword-only ``renderer`` and ``formatter`` arguments to ``Graph()``, ``Digraph()``, ``Source()`` and ``Source.from_file()`` to set default renderers and formatters (similar to ``format``). Used by ``.pipe()``, ``.render()``, and ``.view()`` if not given as method-argument. Add ``pipe_string()``, ``pipe_lines()``, and ``pipe_lines_string()``. Pipe ``input_string``, return ``string``. Pipe ``input_lines`` incrementally, return ``bytes``. Pipe ``input_lines`` incrementally, return ``string``. Add ``set_default_engine()`` and ``set_default_format()`` Add ``DOT_BINARY`` and ``UNFLATTEN_BINARY``. Restructure the internal class hierarchy using multiple-inheritance with cooperative ``super()`` calling: ``Graph`` now inherits both from ``Dot`` and from ``Render``, and both of them inherit from ``Base`` which defines their common interface: Lines of DOT source code that ``Dot`` generates (also ``Source``) and rendering iterates over. This might break some undocumented use of subclassing and require adatation (e.g. if the methods don't use cooperative ``super()`` calling convention or if the MRO has conflicts, supposedly rare). Improve test separation. Improve test coverage of running the tests with ``--skip-exe``. Add ``pytype`` checking and ``flake8`` to build workflow. Extend type annotations. Add https://mybinder.org config with head development environment. Add launch badge to code repository. Improve documentation and examples. Add development docs. Document release process. Version 0.17 ------------ Drop Python 2 support. Tag Python 3.10 support. Migrate CI to GitHub actions. Add ``pypy3`` to matrix. Tests: implement ``--skip-exe`` via custom ``pytest`` marker. Documentation: point Anaconda users to ``conda-forge/python-graphviz``. Move type hints from docstrings to type annotations. Improve doctests. Examples: standardize import convention and modernize. Re-render example notebooks with Graphviz 2.46.1. Version 0.16 ------------ Add ``.unflatten()`` method to ``Graph``, ``Digraph``, and ``Source``. Add standalone ``unflatten()``. Make ``Source.__str__()`` return the ``.source`` instead of the ``repr()`` (like ``Graph`` and ``Digraph``). Render with ``dot -K ...`` instead of `` ...`` internally (work around `upstream issue `_). Add documentation hint to archived upstream version for Windows. Re-render most documentation graphs with Graphviz 2.44.1. Version 0.15 ------------ ``Graph`` and ``Digraph`` instances created via the context-manager returned by ``subgraph()`` now (re)use ``directory``, ``format``, ``engine``, and ``encoding`` from the parent instead of using defaults (behavioral change). Note that these attributes are only relevant when rendering the subgraph independently (i.e. as a stand-alone graph) from within the ``with``-block, which was previously underdocumented. PR `#116 `_ BMaxV. To reflect that the DOT language does not allow subgraph statements to specify ``strict`` (i.e. no way to override the setting of the containing graph), instances created via the context-manager are now ``strict=None`` instead of ``False`` (so they continue to render stand-alone as non-strict by default). Drop Python 3.5 support and tag Python 3.9 support. Add documentation link to new upstream installation procedure for Windows. Version 0.14.2 -------------- Adapt ``graphviz.version()`` to support the Graphviz Release version entry format introduced with ``2.44.2`` (``version()`` is needed to run the tests). Version 0.14.1 -------------- Document the colon-separated ``node[:port[:compass]]`` format used for ``tail`` and ``head`` points in the ``edge()``- and ``edges()``-methods. PR `#101 `_ Michał Góral. Version 0.14 ------------ Improve handling of escaped quotes (``\"``). Different from other layout engine escapes sequences such as ``\l`` and ``\N`` (which are passed on as is by default), there is no use case for backslash-escaping a literal quote character because escaping of quotes is done by this library. Therefore, a backslash-escaped quote (e.g. in ``label='\\"'``) is now treated the same as a plain unescaped quote, i.e. both ``label='"'`` and ``label='\\"'`` produce the same DOT source ``[label="\""]`` (a label that renders as a literal quote). Before this change, use of ``'\\"'`` could break the quoting mechanism creating invalid or unintended DOT, possibly leading to syntax errors from the rendering process. Add notebook section to documentation. Add ``sphinx.ext.viewcode`` to docs (note that this currently lacks links for methods, so that not all of the code is linked; use the source repo for reading on). Improve test and doc building config. Version 0.13.2 -------------- Fix missing support for four-part versions in ``graphviz.version()``. Version 0.13.1 -------------- Tag Python 3.8 support. Fix quoting for non-ASCII numerals. Version 0.13 ------------ Add explicit support for layout engine escape sequences such as ``\l`` and ``\N``. These already worked implicitly before but where broken by backslash escaping in ``0.12``, which is reverted by this release. Escaping now resembles the stdlib ``re`` module: literal backslashes need to be escaped (doubled), which is most conveniently done by using raw string literals for strings that use escape sequences (including escaped backslashes), e.g. ``label=r'\\'``. Add ``escape()`` function (resembling ``re.escape()``) for disabling all meta-characters in a string for rendering. Use ``logging`` in example notebook, add notebooks demonstrating layout engines and escape sequence usage, improve tests with parametrization. Version 0.12 ------------ Fix missing escaping of backslashes, e.g. in labels (pull request DNGros). Add ``quiet`` argument to standalone ``view()`` function, and ``quiet_view`` argument on ``.render()`` and ``.view()`` methods. Suppresses the ``stderr`` output of started viewer processes (unavailable on Windows). Add basic debug logging via the stdlib ``logging`` module. Reformatted some examples, improved tests by using autospec for mocks. Version 0.11.1 -------------- Include ``stderr`` in ``str()`` of raised ``subprocess.CalledProcessError``. Version 0.11 ------------ Add ``quiet`` argument to ``.render()`` and ``.pipe()`` methods of ``Graph``, ``Digraph``, and ``Source`` objects, allowing to suppress ``stderr`` of the layout subprocess (parity with stand-alone ``render()`` and ``pipe()`` functions). The rendering process for ``render()`` methods and stand-alone function is now started from the directory of the rendered dot source file. This allows to render graph descriptions that use relative paths inline (e.g. for referring to image files to be included) by using paths relative to the source file location. Previously, such relative paths would need to be given relative to the directory from which ``render()`` was started, so this change is backwards incompatible for code that relied on the previous behaviour. Drop Python 3.4 support. Version 0.10.1 -------------- Fix broken renderer argument in ``pipe()`` method and function. Version 0.10 ------------ Add ``format`` argument to ``Graph/Digraph.render()``. This follows stand-alone ``render()`` function and mirrors the ``Graph/Digraph.pipe()`` method (usually, ``format`` is set on the instance). Add ``renderer`` and ``formatter`` arguments to ``Graph/Digraph.render()`` and ``pipe()`` methods, as well as stand-alone ``render()`` and ``pipe()`` functions. Version 0.9 ----------- Use ``sys.stderr`` to write stderr output from rendering process to stderr (instead of file descriptor inheritance). Ensures stderr is passed in special environments such as IDLE. Suppress rendering process ``stdout`` in ``render()``. Make ``quiet=True`` also suppress ``stderr`` on success of ``render()`` and ``pipe()`` (exit-status ``0``). Include ``stderr`` from rendering process in ``CalledProcessError`` exception. Version 0.8.4 ------------- Tag Python 3.7 support (work around subprocess ``close_fds`` issue on Windows). Version 0.8.3 ------------- Fix compatibility with ``python -OO``. Version 0.8.2 ------------- Add ``nohtml()`` to support labels of the form ``'<...>'`` (disabling their default treatment as HTML strings). Make default ``'utf-8'`` ``encoding`` more visible. Set ``encoding = locale.getpreferredencoding()`` when ``encoding`` argument/property is set to ``None`` explicitly (follow stdlib ``io.open()`` behaviour). Version 0.8.1 ------------- Add ``Source.from_file()``-classmethod (simpler in-line SVG display of ready-made .gv files within Jupyter). Drop Python 3.3 support. Version 0.8 ----------- Add ``clear()``-method for ``Graph`` and ``Digraph``. Add ``grapviz.version()`` function. Drop dot source extra indent for edge statements following dotguide examples. Include LICENSE file in wheel. Version 0.7.1 ------------- Fix ``TypeError`` in ``graphviz.pipe()`` with invalid dot code under Python 3. Add ``copy()``-method for ``Graph``, ``Digraph``, and ``Source``. Add ``graphviz.render(..., quiet=True)``. Fix ``graphivz.view()`` exception on unsupported platform. Raise a dedicated ``RuntimeError`` subclass ``graphviz.ExecutableNotFound`` when the Graphviz executables are not found. Port tests from ``nose/unittest`` to ``pytest``, extend, use mocks. Version 0.7 ----------- Support setting top-level attrs with ``g.attr(key=value)``. Add context manager usage of ``subgraph()`` for adding a subgraph in a with-block. Add json-based output formats to known ``FORMATS`` (Graphviz 2.40+). Drop extra indent level for DOT source with nonempty ``graph/node/edge_attr``. Add a final newline to a saved DOT source file if it does not end with one. Raise ``subprocess.CalledProcessError`` on non-zero exit status from rendering. Raise early when adding a ``subgraph()`` with ``strict=True`` (avoid DOT syntax error). Make undocumented ``quote()``, ``quote_edge()``, and ``attributes()`` methods private. Version 0.6 ----------- Drop Python 2.6 support (use ``graphviz<0.6`` there). Improve tests for ``mkdirs()``. Better document adding custom DOT using the ``body`` attribute. Add ``view()``-support for FreeBSD (pull request Julien Gamba). Version 0.5.2 ------------- Add ``ENGINES`` and ``FORMATS`` to the documented public API. Version 0.5.1 ------------- Fixed PY3 compatibility. Version 0.5 ----------- Add low-level functions ``render()``, ``pipe()``, and ``view()`` for directly working with existing files and strings. Support all ``render()``-arguments in the ``view()``-short-cut-method. Version 0.4.10 -------------- Added ``'patchwork'`` engine. Version 0.4.9 ------------- Add support for ``strict`` graphs and digraphs. Hide ``render/pipe()`` subprocess console window on Windows when invoked from non-console process (e.g. from IDLE). Improve documentation markup/wording. Make ``TestNoent`` more robust. Version 0.4.8 ------------- Make ``_repr_svg_()`` available on ``Source`` (pull request RafalSkolasinski). Version 0.4.7 ------------- Fixed ``view()``-method on Linux under Python 3 (pull request Antony Lee). Version 0.4.6 ------------- Fixed ``view()``-method on Linux and Darwin (pull request Eric L. Frederich). Version 0.4.5 ------------- Added example for HTML-like labels (``structs.py``). Added ``Source`` class for rendering verbatim DOT source code. Added Python 2.6 support (pull request Jim Crist). Version 0.4.4 ------------- Added the ``pipe()``-method directly returning the ``stdout`` of rendering. Added ``_repr_svg_()`` for inline rendering in IPython notebooks. Version 0.4.3 ------------- Added examples generating some of the graphs from the Graphviz Gallery. Added sphinx-based API documentation. Version 0.4.2 ------------- Added support for HTML-like labels. Version 0.4.1 ------------- Added support for less common output formats. Removed dropped formats (``'dia'``, ``'pcl'``). Added ``'osage'`` layout engine. Documented ``format`` and ``engine`` options in the README. The ``view()`` convenience method now returns the result file name (like render()). Version 0.4 ----------- Added ``attr()`` method for inline switching of node/edge attributes. Added ``subgraph()`` method (obsoletes separate ``Subgraph`` class). Add ``cleanup`` option to ``render()``. Replaced ``dry`` option on ``render()`` with separate ``save()`` method. Removed undocumented ``append()`` and ``extend()`` methods (if needed, the ``body`` attribute can be edited directly). Version 0.3.5 ------------- Skip empty ``comment`` when creating DOT source. Document ``graph_attr``, ``node_attr``, and ``edge_attr`` in the README. More informative exception when Graphviz executables cannot be called. Version 0.3.4 ------------- Fixed missing identifier quoting for DOT keywords (thanks to Paulo Urio). Version 0.3.3 ------------- Made ``format`` and ``engine`` case-insensitive. Version 0.3.2 ------------- Indent ``graph_attr``, ``node_attr``, and ``edge_attr`` lines, adapt nodes and edges. Version 0.3.1 ------------- Fixed ``view()`` failing on paths with forward slashes on Windows. Version 0.3 ----------- Added Python 3.3+ support. Made attributes order stable (sorting plain dicts). Fixed edgeop in undirected graphs. Version 0.2.2 ------------- Support pdf opening on Linux. Fixed rendering filenames w/spaces. Version 0.2.1 ------------- Fixed rendering on Mac OS X. Version 0.2 ----------- Added format selection, use ``'PDF``' as default. Added engines selection, use ``'dot'`` as default. Added source encoding, use ``'UTF-8'`` as default. Changed constructor arguments order, removed ``compile()`` and ``save()``-method, reimplemented compilation in ``render()`` method, make interface more similar to gv.3python (backwards incompatible change). Double-quote-sign escaping, attribute list quoting. ``mkdirs()`` now correctly supports current directory filenames. Version 0.1.1 ------------- Removed automatic ``'-'`` to ``'−'`` replacement from labels. Fixed documentation typos. Version 0.1 ----------- First public release. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1748876599.0 graphviz-0.21/LICENSE.txt0000666000000000000000000000212315017336467012055 0ustar00The MIT License (MIT) Copyright (c) 2013-2025 Sebastian Bank Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749759863.0 graphviz-0.21/MANIFEST.in0000666000000000000000000000047015022633567011770 0ustar00include README.rst LICENSE.txt CHANGES.rst include requirements.txt include conftest.py build-docs.py lint-code.py run-tests.py try-examples.py update-help.py recursive-include tests *.py *.png recursive-include examples *.py *.ipynb recursive-include docs *.rst *.txt *.py *.png *.svg prune docs/_build ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5261228 graphviz-0.21/PKG-INFO0000666000000000000000000003002215023511374011313 0ustar00Metadata-Version: 2.4 Name: graphviz Version: 0.21 Summary: Simple Python interface for Graphviz Author-email: Sebastian Bank License-Expression: MIT Project-URL: Homepage, https://github.com/xflr6/graphviz Project-URL: Documentation, https://graphviz.readthedocs.io Project-URL: Changelog, https://graphviz.readthedocs.io/en/latest/changelog.html Project-URL: Issue Tracker, https://github.com/xflr6/graphviz/issues Project-URL: CI, https://github.com/xflr6/graphviz/actions Project-URL: Coverage, https://codecov.io/gh/xflr6/graphviz Keywords: graph,visualization,dot,render Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Scientific/Engineering :: Visualization Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: dev Requires-Dist: build; extra == "dev" Requires-Dist: wheel; extra == "dev" Requires-Dist: twine; extra == "dev" Requires-Dist: flake8; extra == "dev" Requires-Dist: Flake8-pyproject; extra == "dev" Requires-Dist: pep8-naming; extra == "dev" Requires-Dist: tox>=3; extra == "dev" Provides-Extra: test Requires-Dist: pytest<8.1,>=7; extra == "test" Requires-Dist: pytest-mock>=3; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: coverage; extra == "test" Provides-Extra: docs Requires-Dist: sphinx<7,>=5; extra == "docs" Requires-Dist: sphinx-autodoc-typehints; extra == "docs" Requires-Dist: sphinx-rtd-theme>=0.2.5; extra == "docs" Dynamic: license-file Graphviz ======== |PyPI version| |License| |Supported Python| |Downloads| |Build| |Codecov| |Readthedocs-stable| |Readthedocs-latest| |Binder-stable| This package facilitates the creation and rendering of graph descriptions in the DOT_ language of the Graphviz_ graph drawing software (`upstream repo`_) from Python. Create a graph object, assemble the graph by adding nodes and edges, and retrieve its DOT source code string. Save the source code to a file and render it with the Graphviz installation of your system. Use the ``view`` option/method to directly inspect the resulting (PDF, PNG, SVG, etc.) file with its default application. Graphs can also be rendered and displayed within `Jupyter notebooks`_ (formerly known as `IPython notebooks`_, `example `_, `nbviewer `_) as well as the `Jupyter QtConsole`_. Links ----- - GitHub: https://github.com/xflr6/graphviz - PyPI: https://pypi.org/project/graphviz/ - Documentation: https://graphviz.readthedocs.io - Changelog: https://graphviz.readthedocs.io/en/latest/changelog.html - Issue Tracker: https://github.com/xflr6/graphviz/issues - Download: https://pypi.org/project/graphviz/#files - Development documentation: https://graphviz.readthedocs.io/en/latest/development.html - Release process: https://graphviz.readthedocs.io/en/latest/release_process.html Installation ------------ This package runs under Python 3.9+, use pip_ to install: .. code:: bash $ pip install graphviz To render the generated DOT source code, you also need to install Graphviz_ (`download page `_, `archived versions `_, `installation procedure for Windows `_). Make sure that the directory containing the ``dot`` executable is on your systems' ``PATH`` (sometimes done by the installer; setting ``PATH`` on `Linux `_, `Mac `_, and `Windows `_). Anaconda_: see the conda-forge_ package `conda-forge/python-graphviz `_ (`feedstock `_), which should automatically ``conda install`` `conda-forge/graphviz `_ (`feedstock `_) as dependency. Quickstart ---------- Create a graph object: .. code:: python >>> import graphviz # doctest: +NO_EXE >>> dot = graphviz.Digraph(comment='The Round Table') >>> dot #doctest: +ELLIPSIS Add nodes and edges: .. code:: python >>> dot.node('A', 'King Arthur') # doctest: +NO_EXE >>> dot.node('B', 'Sir Bedevere the Wise') >>> dot.node('L', 'Sir Lancelot the Brave') >>> dot.edges(['AB', 'AL']) >>> dot.edge('B', 'L', constraint='false') Check the generated source code: .. code:: python >>> print(dot.source) # doctest: +NORMALIZE_WHITESPACE +NO_EXE // The Round Table digraph { A [label="King Arthur"] B [label="Sir Bedevere the Wise"] L [label="Sir Lancelot the Brave"] A -> B A -> L B -> L [constraint=false] } Save and render the source code (skip/ignore any ``doctest_mark_exe()`` lines): .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv').replace('\\', '/') 'doctest-output/round-table.gv.pdf' Save and render and view the result: .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv', view=True) # doctest: +SKIP 'doctest-output/round-table.gv.pdf' .. image:: https://raw.github.com/xflr6/graphviz/master/docs/_static/round-table.svg :align: center :alt: round-table.svg **Caveat:** Backslash-escapes and strings of the form ``<...>`` have a special meaning in the DOT language. If you need to render arbitrary strings (e.g. from user input), check the details in the `user guide`_. See also -------- - pygraphviz_ |--| full-blown interface wrapping the Graphviz C library with SWIG - graphviz-python_ |--| official Python bindings (`documentation `_) - pydot_ |--| stable pure-Python approach, requires pyparsing License ------- This package is distributed under the `MIT license`_. .. _Graphviz: https://www.graphviz.org .. _DOT: https://www.graphviz.org/doc/info/lang.html .. _upstream repo: https://gitlab.com/graphviz/graphviz/ .. _upstream-download: https://www.graphviz.org/download/ .. _upstream-archived: https://www2.graphviz.org/Archive/stable/ .. _upstream-windows: https://forum.graphviz.org/t/new-simplified-installation-procedure-on-windows/224 .. _set-path-windows: https://www.computerhope.com/issues/ch000549.htm .. _set-path-linux: https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix .. _set-path-darwin: https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently .. _pip: https://pip.pypa.io .. _Jupyter notebooks: https://jupyter.org .. _IPython notebooks: https://ipython.org/notebook.html .. _Jupyter QtConsole: https://qtconsole.readthedocs.io .. _notebook: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _notebook-nbviewer: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _Anaconda: https://docs.anaconda.com/anaconda/install/ .. _conda-forge: https://conda-forge.org .. _conda-forge-python-graphviz: https://anaconda.org/conda-forge/python-graphviz .. _conda-forge-python-graphviz-feedstock: https://github.com/conda-forge/python-graphviz-feedstock .. _conda-forge-graphviz: https://anaconda.org/conda-forge/graphviz .. _conda-forge-graphviz-feedstock: https://github.com/conda-forge/graphviz-feedstock .. _user guide: https://graphviz.readthedocs.io/en/stable/manual.html .. _pygraphviz: https://pypi.org/project/pygraphviz/ .. _graphviz-python: https://pypi.org/project/graphviz-python/ .. _graphviz-python-docs: https://www.graphviz.org/pdf/gv.3python.pdf .. _pydot: https://pypi.org/project/pydot/ .. _MIT license: https://opensource.org/licenses/MIT .. |--| unicode:: U+2013 .. |PyPI version| image:: https://img.shields.io/pypi/v/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Latest PyPI Version .. |License| image:: https://img.shields.io/pypi/l/graphviz.svg :target: https://github.com/xflr6/graphviz/blob/master/LICENSE.txt :alt: License .. |Supported Python| image:: https://img.shields.io/pypi/pyversions/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Supported Python Versions .. |Downloads| image:: https://img.shields.io/pypi/dm/graphviz.svg :target: https://pypistats.org/packages/graphviz :alt: Monthly downloads .. |Build| image:: https://github.com/xflr6/graphviz/actions/workflows/build.yaml/badge.svg?branch=master :target: https://github.com/xflr6/graphviz/actions/workflows/build.yaml?query=branch%3Amaster :alt: Build .. |Codecov| image:: https://codecov.io/gh/xflr6/graphviz/branch/master/graph/badge.svg :target: https://codecov.io/gh/xflr6/graphviz :alt: Codecov .. |Readthedocs-stable| image:: https://readthedocs.org/projects/graphviz/badge/?version=stable :target: https://graphviz.readthedocs.io/en/stable/ :alt: Readthedocs (stable) .. |Readthedocs-latest| image:: https://readthedocs.org/projects/graphviz/badge/?version=latest :target: https://graphviz.readthedocs.io/en/latest/ :alt: Readthedocs (latest) .. |Binder-stable| image:: https://img.shields.io/badge/launch-binder%20(stable)-579ACA.svg?logo=data:image/png;base64,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 :target: https://mybinder.org/v2/gh/xflr6/graphviz/stable :alt: Binder (stable) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1748876599.0 graphviz-0.21/README.rst0000666000000000000000000002410015017336467011720 0ustar00Graphviz ======== |PyPI version| |License| |Supported Python| |Downloads| |Build| |Codecov| |Readthedocs-stable| |Readthedocs-latest| |Binder-stable| This package facilitates the creation and rendering of graph descriptions in the DOT_ language of the Graphviz_ graph drawing software (`upstream repo`_) from Python. Create a graph object, assemble the graph by adding nodes and edges, and retrieve its DOT source code string. Save the source code to a file and render it with the Graphviz installation of your system. Use the ``view`` option/method to directly inspect the resulting (PDF, PNG, SVG, etc.) file with its default application. Graphs can also be rendered and displayed within `Jupyter notebooks`_ (formerly known as `IPython notebooks`_, `example `_, `nbviewer `_) as well as the `Jupyter QtConsole`_. Links ----- - GitHub: https://github.com/xflr6/graphviz - PyPI: https://pypi.org/project/graphviz/ - Documentation: https://graphviz.readthedocs.io - Changelog: https://graphviz.readthedocs.io/en/latest/changelog.html - Issue Tracker: https://github.com/xflr6/graphviz/issues - Download: https://pypi.org/project/graphviz/#files - Development documentation: https://graphviz.readthedocs.io/en/latest/development.html - Release process: https://graphviz.readthedocs.io/en/latest/release_process.html Installation ------------ This package runs under Python 3.9+, use pip_ to install: .. code:: bash $ pip install graphviz To render the generated DOT source code, you also need to install Graphviz_ (`download page `_, `archived versions `_, `installation procedure for Windows `_). Make sure that the directory containing the ``dot`` executable is on your systems' ``PATH`` (sometimes done by the installer; setting ``PATH`` on `Linux `_, `Mac `_, and `Windows `_). Anaconda_: see the conda-forge_ package `conda-forge/python-graphviz `_ (`feedstock `_), which should automatically ``conda install`` `conda-forge/graphviz `_ (`feedstock `_) as dependency. Quickstart ---------- Create a graph object: .. code:: python >>> import graphviz # doctest: +NO_EXE >>> dot = graphviz.Digraph(comment='The Round Table') >>> dot #doctest: +ELLIPSIS Add nodes and edges: .. code:: python >>> dot.node('A', 'King Arthur') # doctest: +NO_EXE >>> dot.node('B', 'Sir Bedevere the Wise') >>> dot.node('L', 'Sir Lancelot the Brave') >>> dot.edges(['AB', 'AL']) >>> dot.edge('B', 'L', constraint='false') Check the generated source code: .. code:: python >>> print(dot.source) # doctest: +NORMALIZE_WHITESPACE +NO_EXE // The Round Table digraph { A [label="King Arthur"] B [label="Sir Bedevere the Wise"] L [label="Sir Lancelot the Brave"] A -> B A -> L B -> L [constraint=false] } Save and render the source code (skip/ignore any ``doctest_mark_exe()`` lines): .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv').replace('\\', '/') 'doctest-output/round-table.gv.pdf' Save and render and view the result: .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv', view=True) # doctest: +SKIP 'doctest-output/round-table.gv.pdf' .. image:: https://raw.github.com/xflr6/graphviz/master/docs/_static/round-table.svg :align: center :alt: round-table.svg **Caveat:** Backslash-escapes and strings of the form ``<...>`` have a special meaning in the DOT language. If you need to render arbitrary strings (e.g. from user input), check the details in the `user guide`_. See also -------- - pygraphviz_ |--| full-blown interface wrapping the Graphviz C library with SWIG - graphviz-python_ |--| official Python bindings (`documentation `_) - pydot_ |--| stable pure-Python approach, requires pyparsing License ------- This package is distributed under the `MIT license`_. .. _Graphviz: https://www.graphviz.org .. _DOT: https://www.graphviz.org/doc/info/lang.html .. _upstream repo: https://gitlab.com/graphviz/graphviz/ .. _upstream-download: https://www.graphviz.org/download/ .. _upstream-archived: https://www2.graphviz.org/Archive/stable/ .. _upstream-windows: https://forum.graphviz.org/t/new-simplified-installation-procedure-on-windows/224 .. _set-path-windows: https://www.computerhope.com/issues/ch000549.htm .. _set-path-linux: https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix .. _set-path-darwin: https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently .. _pip: https://pip.pypa.io .. _Jupyter notebooks: https://jupyter.org .. _IPython notebooks: https://ipython.org/notebook.html .. _Jupyter QtConsole: https://qtconsole.readthedocs.io .. _notebook: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _notebook-nbviewer: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _Anaconda: https://docs.anaconda.com/anaconda/install/ .. _conda-forge: https://conda-forge.org .. _conda-forge-python-graphviz: https://anaconda.org/conda-forge/python-graphviz .. _conda-forge-python-graphviz-feedstock: https://github.com/conda-forge/python-graphviz-feedstock .. _conda-forge-graphviz: https://anaconda.org/conda-forge/graphviz .. _conda-forge-graphviz-feedstock: https://github.com/conda-forge/graphviz-feedstock .. _user guide: https://graphviz.readthedocs.io/en/stable/manual.html .. _pygraphviz: https://pypi.org/project/pygraphviz/ .. _graphviz-python: https://pypi.org/project/graphviz-python/ .. _graphviz-python-docs: https://www.graphviz.org/pdf/gv.3python.pdf .. _pydot: https://pypi.org/project/pydot/ .. _MIT license: https://opensource.org/licenses/MIT .. |--| unicode:: U+2013 .. |PyPI version| image:: https://img.shields.io/pypi/v/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Latest PyPI Version .. |License| image:: https://img.shields.io/pypi/l/graphviz.svg :target: https://github.com/xflr6/graphviz/blob/master/LICENSE.txt :alt: License .. |Supported Python| image:: https://img.shields.io/pypi/pyversions/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Supported Python Versions .. |Downloads| image:: https://img.shields.io/pypi/dm/graphviz.svg :target: https://pypistats.org/packages/graphviz :alt: Monthly downloads .. |Build| image:: https://github.com/xflr6/graphviz/actions/workflows/build.yaml/badge.svg?branch=master :target: https://github.com/xflr6/graphviz/actions/workflows/build.yaml?query=branch%3Amaster :alt: Build .. |Codecov| image:: https://codecov.io/gh/xflr6/graphviz/branch/master/graph/badge.svg :target: https://codecov.io/gh/xflr6/graphviz :alt: Codecov .. |Readthedocs-stable| image:: https://readthedocs.org/projects/graphviz/badge/?version=stable :target: https://graphviz.readthedocs.io/en/stable/ :alt: Readthedocs (stable) .. |Readthedocs-latest| image:: https://readthedocs.org/projects/graphviz/badge/?version=latest :target: https://graphviz.readthedocs.io/en/latest/ :alt: Readthedocs (latest) .. |Binder-stable| image:: https://img.shields.io/badge/launch-binder%20(stable)-579ACA.svg?logo=data:image/png;base64,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 :target: https://mybinder.org/v2/gh/xflr6/graphviz/stable :alt: Binder (stable) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639307241.0 graphviz-0.21/build-docs.py0000666000000000000000000000420114155353751012625 0ustar00#!/usr/bin/env python3 # flake8: noqa """Build the docs with https://www.sphinx-doc.org.""" import functools import pathlib import sys import webbrowser from sphinx.cmd import build SELF = pathlib.Path(__file__) SOURCE = pathlib.Path('docs') TARGET = SOURCE / '_build' RESULT = TARGET / 'index.html' BROWSER_OPEN = '--open' SKIP_OPEN_RESULT = '--no-open' DEFAULT_ARGS = [BROWSER_OPEN, '-W', '-n', '-v', str(SOURCE), str(TARGET)] OPEN_RESULT = BROWSER_OPEN in DEFAULT_ARGS print = functools.partial(print, sep='\n') args = sys.argv[1:] print(f'run {[SELF.name] + args}') if not args: args = DEFAULT_ARGS if SKIP_OPEN_RESULT in args: open_result = None args = [a for a in args for name, value in [a.partition('=')[::2]] if name not in (SKIP_OPEN_RESULT, BROWSER_OPEN)] elif any(a.partition('=')[0] == BROWSER_OPEN for a in args): open_result = RESULT values = {value for a in args for name, value in [a.partition('=')[::2]] if name == BROWSER_OPEN and value} if values: if len(values) != 1: raise ValueError(f'conflicting {BROWSER_OPEN}: {values}') value, = values if value: open_result = open_result.parent / value args = [a for a in args if a.partition('=')[0] != BROWSER_OPEN] if not args: # no pytest args given args = [a for a in DEFAULT_ARGS if a != SKIP_OPEN_RESULT and a.partition('=')[0] != BROWSER_OPEN] if args == ['-b', 'doctest']: args += ['-W', str(SOURCE), str(SOURCE / '_doctest')] print('', f'sphinx.cmd.build.main({args})',) returncode = build.main(args) status = 'FAILED' if returncode else 'PASSED' print('', f'{status} (returncode {returncode!r})', end='') try: if 'doctest' not in args: print('', f'index: {RESULT}', f'assert {RESULT!r}.stat().st_size', end='') assert open_result.stat().st_size, f'non-empty {open_result}' if open_result: print('', f'webbrowser.open({open_result!r})', end='') webbrowser.open(open_result) finally: sys.exit(returncode) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649449412.0 graphviz-0.21/conftest.py0000666000000000000000000000340014224114704012413 0ustar00"""pytest command line options and doctest flag definition/setup.""" import doctest import unittest.mock NO_EXE = doctest.register_optionflag('NO_EXE') class NoExeChecker(doctest.OutputChecker): # noqa: E302 def check_output(self, want, got, optionflags, *args, **kwargs) -> bool: if optionflags & NO_EXE: return True return super().check_output(want, got, optionflags, *args, **kwargs) unittest.mock.patch.object(doctest, 'OutputChecker', new=NoExeChecker).start() # noqa: E305 import pytest # noqa: E402 SKIP_EXE = '--skip-exe' ONLY_EXE = '--only-exe' def pytest_addoption(parser): parser.addoption(SKIP_EXE, action='store_true', help='Skip tests with pytest.mark.exe.' ' Xfail tests with pytest.mark.exe(xfail=True).' ' Skip doctests with doctest_mark_exe().' ' Xfail doctests with doctest_mark_exe(xfail=True).' ' exe marks tests requiring backend.DOT_BINARY.') parser.addoption(ONLY_EXE, action='store_true', help='Skip tests without pytest.mark.exe.' ' Overrides --skip-exe.' ' exe marks tests requiring backend.DOT_BINARY.') @pytest.fixture(autouse=True) def doctests(pytestconfig, doctest_namespace): def doctest_mark_exe(**kwargs): return None if pytestconfig.getoption(SKIP_EXE): def doctest_mark_exe(*, reason=SKIP_EXE, xfail: bool = False, **kwargs): # noqa: F811 return (pytest.xfail(reason=reason, **kwargs) if xfail else pytest.skip(reason, **kwargs)) doctest_namespace.update(doctest_mark_exe=doctest_mark_exe) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.3630855 graphviz-0.21/docs/0000777000000000000000000000000015023511373011150 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/docs/_links.rst0000666000000000000000000001040714226560631013170 0ustar00.. _Graphviz: https://www.graphviz.org .. _graphviz.org gallery: https://www.graphviz.org/gallery/ .. _graphviz.org documentation: https://www.graphviz.org/documentation/ .. _upstream-download: https://www.graphviz.org/download/ .. _upstream-archived: https://www2.graphviz.org/Archive/stable/ .. _upstream-windows: https://forum.graphviz.org/t/new-simplified-installation-procedure-on-windows/224 .. _set-path-windows: https://www.computerhope.com/issues/ch000549.htm .. _set-path-linux: https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix .. _set-path-darwin: https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently .. _Anaconda: https://docs.anaconda.com/anaconda/install/ .. _conda-forge: https://conda-forge.org .. _conda-forge-python-graphviz: https://anaconda.org/conda-forge/python-graphviz .. _conda-forge-python-graphviz-feedstock: https://github.com/conda-forge/python-graphviz-feedstock .. _conda-forge-graphviz: https://anaconda.org/conda-forge/graphviz .. _conda-forge-graphviz-feedstock: https://github.com/conda-forge/graphviz-feedstock .. _pip: https://pip.pypa.io .. _venv: https://docs.python.org/3/library/venv.html#creating-virtual-environments .. _virtualenv: https://virtualenv.pypa.io .. _tox: https://tox.wiki .. _pytype: https://google.github.io/pytype/ .. _pytpe_platforms: https://google.github.io/pytype/#requirements .. _pytype_python_versions: https://google.github.io/pytype/support.html#python-version .. _sphinx: https://www.sphinx-doc.org .. _sphinx-rtd-theme: https://sphinx-rtd-theme.readthedocs.io .. _flake8: https://flake8.pycqa.org .. _twine: https://twine.readthedocs.io .. _DOT: https://www.graphviz.org/doc/info/lang.html .. _DOT command: https://graphviz.org/doc/info/command.html .. _DOT layouts: https://graphviz.org/docs/layouts/ .. _DOT outputs: https://graphviz.org/docs/outputs/ .. _DOT neato: https://graphviz.org/docs/layouts/neato/ .. _neato no-op: https://www.graphviz.org/doc/info/command.html#-n .. _DOT attrs: https://www.graphviz.org/doc/info/attrs.html .. _DOT pos: https://graphviz.org/docs/attrs/pos/ .. _DOT overlap: https://graphviz.org/docs/attrs/overlap/ .. _DOT splines: https://graphviz.org/docs/attrs/splines/ .. _DOT escString: https://www.graphviz.org/docs/attr-types/escString/ .. _DOT shapes: https://graphviz.org/doc/info/shapes.html .. _DOT shapes HTML: https://graphviz.org/doc/info/shapes.html#html .. _DOT manpage_pdf: https://www.graphviz.org/pdf/dot.1.pdf .. _DOT unflatten: https://linux.die.net/man/1/unflatten .. _DOT unflatten_pdf: https://www.graphviz.org/pdf/unflatten.1.pdf .. _Jupyter notebook: https://jupyter.org .. _Jupyter Qt Console: https://qtconsole.readthedocs.io .. _Spyder IDE: https://www.spyder-ide.org .. _Spyder ipythonconsole: https://docs.spyder-ide.org/current/panes/ipythonconsole.html .. _IPython.display: https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#functions .. _examples directory: https://github.com/xflr6/graphviz/tree/master/examples/ .. _GitHub graphviz-notebook.ipynb: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _GitHub graphviz-engines.ipynb: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-engines.ipynb .. _GitHub graphviz-jupyter-format.ipynb: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-jupyter-format.ipynb .. _GitHub graphviz-escapes.ipynb: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-escapes.ipynb .. _nbviewer graphviz-notebook.ipynb: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _nbviewer graphviz-engines.ipynb: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-engines.ipynb .. _nbviewer graphviz-jupyter-format.ipynb: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-jupyter-format.ipynb .. _nbviewer graphviz-escapes.ipynb: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-escapes.ipynb .. _raw string literals: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals .. _live viewer updates: https://superuser.com/questions/599442/pdf-viewer-that-handles-live-updating-of-pdf-doesnt-lock-the-file ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.3950937 graphviz-0.21/docs/_static/0000777000000000000000000000000015023511373012576 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/angles.svg0000666000000000000000000002517614144524371014610 0ustar00 G cluster_1 Linear Angle Variations (white to black gradient) cluster_2 Radial Angle Variations (white to black gradient) n9 n9:360 n8 n8:315 n7 n7:270 n6 n6:225 n5 n5:180 n14 n14:180 n5->n14 n4 n4:135 n3 n3:90 n2 n2:45 n1 n1:0 n18 n18:360 n17 n17:315 n16 n16:270 n15 n15:225 n13 n13:135 n12 n12:90 n11 n11:45 n10 n10:0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/btree.svg0000666000000000000000000002102014144524371014420 0ustar00 g node0 G node1 E node0:f0->node1:f1 node4 R node0:f2->node4:f1 node2 B node1:f0->node2:f1 node3 F node1:f2->node3:f1 node7 A node2:f0->node7:f1 node8 C node2:f2->node8:f1 node5 H node4:f0->node5:f1 node6 Y node4:f2->node6:f1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/cluster.svg0000666000000000000000000001771514144524371015020 0ustar00 G cluster_0 process #1 cluster_1 process #2 a0 a0 a1 a1 a0->a1 a2 a2 a1->a2 b3 b3 a1->b3 a3 a3 a2->a3 a3->a0 end end a3->end b0 b0 b1 b1 b0->b1 b2 b2 b1->b2 b2->a3 b2->b3 b3->end start start start->a0 start->b0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/cluster_edge.svg0000666000000000000000000001357314144524371016002 0ustar00 G cluster0 cluster1 a a b b a->b c c a->c d d b->d f f b->f c->d e e c->e g g c->g d->e h h d->h e->g e->f ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/colors.svg0000666000000000000000000000342414144524371014630 0ustar00 RGB: #40e0d0 RGB: #40e0d0 RGBA: #ff000042 RGBA: #ff000042 HSV: 0.051 0.718 0.627 HSV: 0.051 0.718 0.627 name: deeppink name: deeppink ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/diamond.svg0000666000000000000000000000147614144524371014747 0ustar00 diamond <> ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/er.svg0000666000000000000000000001667514144524371013751 0ustar00 ER Entity Relation Diagram drawn by NEATO course course C-I C-I course--C-I n institute institute name1 name institute--name1 S-I S-I institute--S-I 1 student student name2 name student--name2 grade grade student--grade number number student--number S-C S-C student--S-C m name0 name name0--course code code code--course C-I--institute 1 S-C--course n S-I--student n ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/escapes.svg0000666000000000000000000000251214144524371014747 0ustar00 backslash \ multi_line centered left right ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/fdpclust.svg0000666000000000000000000001013014144524371015143 0ustar00 G clusterB clusterA clusterC e e e--__0:clusterB a a b b a--b C C D D C--D d d d--D f f d--f __1:clusterC--__2:clusterB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/fsm.svg0000666000000000000000000002270514144524371014117 0ustar00 finite_state_machine LR_0 LR_0 LR_2 LR_2 LR_0->LR_2 SS(B) LR_1 LR_1 LR_0->LR_1 SS(S) LR_3 LR_3 LR_4 LR_4 LR_8 LR_8 LR_6 LR_6 LR_8->LR_6 S(b) LR_5 LR_5 LR_8->LR_5 S(a) LR_2->LR_4 S(A) LR_2->LR_6 SS(b) LR_2->LR_5 SS(a) LR_1->LR_3 S($end) LR_6->LR_6 S(b) LR_6->LR_5 S(a) LR_5->LR_5 S(a) LR_7 LR_7 LR_5->LR_7 S(b) LR_7->LR_8 S(b) LR_7->LR_5 S(a) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/g_c_n.svg0000666000000000000000000000376414144524371014403 0ustar00 G agraph cluster1 acluster anode anode ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/hello.svg0000666000000000000000000000262114144524371014430 0ustar00 G Hello Hello World World Hello->World ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/holy-grenade.svg0000666000000000000000000000457514144524371015715 0ustar00 the holy hand grenade 1 1 2 2 1->2 3 3 2->3 lob lob 3->lob ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/html_table.svg0000666000000000000000000000250114144524371015435 0ustar00 html_table tab left right ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/docs/_static/literal_backslash.svg0000666000000000000000000000155014150757034016775 0ustar00 literal_backslash \\ \ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/ni.svg0000666000000000000000000001100014144524371013722 0ustar00 ni 1 Ni! 2 Ni! 1--2 3 Ni! 2--3 4 Ni! 3--4 5 Ni! 4--5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/pet-shop.svg0000666000000000000000000000244614144524371015071 0ustar00 pet-shop parrot parrot dead dead parrot->dead ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636921230.0 graphviz-0.21/docs/_static/process.svg0000666000000000000000000001371414144267616015016 0ustar00 G run run intr intr run--intr kernel kernel run--kernel runbl runbl intr--runbl runbl--run zombie zombie kernel--zombie sleep sleep kernel--sleep runmem runmem kernel--runmem sleep--runmem swap swap sleep--swap runswap runswap swap--runswap runswap--runmem new new runswap--new new--runmem ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626617725.0 graphviz-0.21/docs/_static/qtconsole-source.png0000666000000000000000000003350414075033575016627 0ustar00PNG  IHDRK罬sRGBgAMA a pHYsod6IDATx^yxzE@P6D up|vE@0OaEYdsCTr:M%lA%>>Pu;mqH^{ĩlA%]qzٮ{mvKm7w~j׫r^ju=ԋ_zkvS[lWUSm ]pR{Y7Hp&HݣCnh{쐱f^v[NJ[EJӷP{̚u*p~uW_[9\qq?xx5{e]X՝V/~Oc:fg8i0wW6.*MjMW AԵWJY3] R~:]qgNS '7:Am^myzGy68fݽpm)lfc& Z)f"άi*=C}RK-^Ny =SlK'm'xd֝U9 .N3Yw)̀gpw#읇g$G?0m ] zP]Ѥx PRօє4 FW+,FWfl8v{qꀷ^}3۠6j;nCMCm8C쟶z۶h84{f"S@Ftyxb|"Z7kbF0Dt9sZe2A(nY)%:\=޵Au9 i9dǾo6ƂROrwOV[︛E;'mz3;Xxw+>&xo1xR$_pA`'$`R> J3+9A\il8}V}+ F1G)nfm{;&fhL, FW+,LGx雎`Dj A(HD8ԽwJ˷ߥF pG'IMOZw);&t_q낾p1Wc ﮤUԫ FW+Nߪ3]džNԆiLdeA1)#\=L9?rwƄ+!MKo+[s-[ZwW DpRsj'}X/m?JA]AAcߏ|KɴdS P fd1{I~w  hn@ E AA  AAP  sp@AԹ`8  \0Aup[_%&c 0p,0%ñpw2 H VVԊRkCeQǽj].̓hV(l8Fj11[?txR 9z-&9-W 7EuP{gc*B6\,L3~P1-fVԾ=9?7pgéEAy^*w\ 4!kj:qc2~@A /wGmM5Soz2E]/c|!^G~|wzshy0H*peJ,+pr|\"I]=&&hr5_gMѤ@th84ZU^LV{Y}i2TM6晾IG-ۖL OcK$Jr F.98f4ǔǬ8nGʘ0( 1ad04VHc 20c 20c 0p,4a/,7뮻  :Qi8V  F~7`8g"rF8#4™pDp&"Ga8i?{aVeq]wOD8~lM]MOq\~}:ӣҧ!=7c@#Gt3$2Nz;dM֣}8wf&3Zj4jqbRz&َ$6pwv &#hO{'Ld O/` S/2~yV!헙+pj՗qz"BkxsfQR8zBw8|r2Gnە?4x~uaY˛džm'OσN\7|<`Pm,#Q"Chw g"rԛpwf7w\4DLD7X:8#4rWL,$Tug8q~t_-3n+Pޯ&mF*e*S{=a~GLx >!1b/ 07JI8w`&.b78vuh;8~.և_ 뉍;.'نkLrVx sP?!T/12^L/dvjm[wT_z(Z)#]xh]1?\ʏΟ$>㈗ 3țG2m??\/'asO*zvuh[wi9iop!2E֣:8TInn]FU76R~mG/-{;7!ob}eag6sד}Se<5S EBoU:qc궙0Epws y"%Q:prRmCq<}B>*rAro_qmk*r%'"nyyؠ~5c¸GتufLR݆1Df_Ump|q7n`bC-jco).Qӯ Nwxv}-G* .7uUp rn;S:y)ȃ4pS^NG܁ܶ eBęPW\:Y=񀋡l2Gc.n^|_M(oxcKNMrܛ0mڤI\*O >6~y)^?䯺_ >Yؙ'I>Xl&ŪCXgbbE>jڱalc|TO !1-fۓo-C/rD&`;0QB'`N}d]Y@ee~<ᅵ:P GT'2f7fp~38~))^?9&}H|Oʼ썿8:Xvơw6&8E+KeT,WCٶCǥ>vkdq_e8hFtձ^ݛ$qSr TMo8(y9XؼvpR$K;v8*85^1Qm~uc]޴O`k!7Ka wxsLN,l;xbInQg5,lqy5Ŵ=Gꐮxqo|}͛z=ah8^S5)\O_Ն:}Xܵ@:k7Eq2>9# GQSzpYew 4[7HPh% C֯?8N1Ԏtpm;ZESv6ax~F|$~gJ< [hbod ů/0D\y8~F(?k4'ʃ7vѧD G- Ƿ>DAx 39l[hJ.aԍù6h$Aj]߸;0ye9?u S][DØ3H D`8g$R|47_ᾭ-PmumS Y~w*dR~_`2<+H*p =0j♚If]['otalQm.fuҷ`存[3HdL^F޸m!}jRCOyn+bK>"x~HՔ;iл#:h;Nv'I&IW.Khf"xF .B:Eܧ_xyf.p܅Vw.o xܥ~M /vtsER_|FX O6oY?Oy/8~hHjzA'_Ӟ7 ؿ-R<&~S&B.01}T(7!oR,Lc}^=8 .9HǽqTʅ.y>biOmhHA|yT;~M1AuS>'~SMż}L\GS3뙗u n }X)b~9=x g$Rf8^^ 쁞;=5"`Oj_4_qL"pH㹎Ym.,8笺՘Ds2%p|'IsgpF"UYw8Nh)>vW!L5N!giwTµI*{`/,:V73k屗ڡr8zx|s國GKg&ql<>Lȃ_y^;^qe!o,8bU|ʛ%o@[~ef/CUZ׭'Xx8# G1^ t3ez`z'TPȚ:Rⓣ\f|62%zoܼQ}/Wy;^ԧ׮ܣ.`pŽg)`)3NǚPKpupz.b>f].΢ИAq~!/~._]~zLnr퀥3p_`5܅+s+0Ql;2n͟;f4LUnBWtYj]pCD'cRe,{ UWnf@HCw#.tp2 |DDkVJ#rׅ)>)59w8&4~5fⷦ>X# ж_/Umߘ pxXJ8# wW#c G. c`ʝ( 3E=*7â_vEQs7Bs Dy#*,_:h8,7H G_6q9MIprAݱh2ΔsAu=$ÁDpJ0 S̃o2 .FP3T^W ^&#b5ATO'K2!H*pI(B[~'S;_ެ714.Gphm%@MMY.-wm+en̊ߖW`yDpBH%;}5=L<6Dfc81NpWfd8; 8# H IpF"U0H3@H  0p/Lg"rF85Dp&"G=;ph= }™pDgE39@# LD`8g"rF8#iYaW4Zn<>k{5_.F,jOwoI},N]S7Z /LDpz0̙9R=QunuX.n+,uw:O!LtD q`UIy rڷ-O{gE}vs⧲`Xܵh7_)p/LD,*י2{/ywU\QO-(=&x.+Lz1Qj''HE8_DzQZspV'>D_/R>~7-X?p&"Go8&P7@bPM(Ԏ_E~z엙* o[j+ v$IޯE>.o3'-.'h3~'m&)KPzҺlμ?Iѐ+&(@1_V@]r6RR 4L g"r#` G4 Y*ISLhf'd>,℩JL8&m'r?N!\]&ܷ cJzo(kbI&Xzi<Be${~bI`RW;S엟*Fum%^ģ L@4a= \t%ΑS)LDz63x8)$LAr;5܄ ƯXؑ~A5SnOd\NqM RCfѱ%B蝴XI؟Ͷ900M3ƛP O`ބM+ˣv8r=.`8Jfj #ɘ71~BACOHx}87fboML$6ϸrN&7[kK*'&F]oʏ~y}hph3[chwꞁ'f;IQ]΁m&fg"rd8VW h`kl5`p&"G͆@i%+< p5&L}3殇6t Dp^M0DJ8D(p80cLD39j4!3,`C5@ڐ\/p39J2Qw|D!?D_39@# LD`8g"rF8#4™pDp&"G0h39@# LD`8g"rF8#4™pDp&"G0h39 q׫.L}[Rg}ԧ>?x#zMe/W7p <3™n8nVu#N?^y~{߫^׫ۥ<Qgl&v}? /g=Y; :? gG׽Էm}kW#8˞gp~]* ǟ'sk> \ejAGw\//fmRC‰'hIgM4v\Lesl8﨧?=L{ on,}H9> enA=OKҗ Iqz]/=W7n4#g-[}5Si?eγo8=a3ՠ;t73Ad֓ z$ O~Ҭ eo|Uz]> }>w?ؼw7}}>jM73gD=7_%˜gp|61u_}ߵ%ݱL$HO%]vE=iO3wYh63 QM?ڥŇ ꃜCg{/~@o8N M'/=li}1O z=[+k~@O8N gaD^uQΛ}Ɍ~GδIgh@y39p8IHs1jv0uSon<@`$2ZGuvyg馛wQ{ B~@y`WՕn88gW^y 9眣6nhHTFM7d < @El.20ob̅3r,4™dԙ  L)l`8g" p&"G0h39@# LD`8g"rT1AA]4AA\FgIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626617725.0 graphviz-0.21/docs/_static/qtconsole.png0000666000000000000000000003763614075033575015343 0ustar00PNG  IHDRN"#sRGBgAMA a pHYsod?3IDATx^y5Ey (1GAe&D6 UA48Ack5srӁBqI|~33fLB!&W_};0!g\uUf??L+Oyg#LB-O~b~_F';xkzCt@]:S-!B_3J3'ĘWQ`^כv{:+4;faw]^cWU/yy΋w7~F-!ZʊE[vƻ̛ιI'yG7g\qf뮛,{‚6ov4ŦSpjn0 - ̫ƥѷ!T%hbLZb*tU6oXϸ/~(>n:K+_=ؑc?jg~Ѽl} ^Uono0uOkG?h l~v1ۿ|"3!Ԃpm<2' >n{Kznc}ݑf'nm9O`6MGFy_@y13meʖޗ02SzF^_4x,딶A5@TF)+mi)&G"FC$Tg&^ZpvW?o6((́ݕ ]{+36vxd1OqTjmbnV?SVvBJ]䙴8LP1X[LDm,o̩ 虬9.4LMvWم>zD~]qscqݾ WZ2/BTŢ-;MM`Il>~IاO4ei1_V=&Bh+t3>v8ji`:>1Bi(tL_!Q_Bh&MB!0!0!0!0!0!0!0!``:`"`:`"`:`"`:`"`:`" a:Mon9_Xm3?7oOT3'*Ɩ/l 0V4p d{}L\H^e&EҲ,k%yיK b~ENAsQ+mVna3SWcAqm3N]K:u֔cLj^SMbg:_V|qH}_BˏBU??*HղF 'Zrwrnm3M9ɩL$Y,?A,nτLG 8]Q 6SΥj53 N2`qe:)d13ՠ)ioww^g5cqˑX10Yo:~G`7R O7<XhFB$6pg:{$n*g@i{E>GU6Hn٠ջX |U)37PR~|vBPgqۮ[4ӡCѨ0y(jrE3d[{/G ꉝ7Kh?&%{L^ְedDzqW*MwLGrG`70gGʺk׍P\C:=1 ,Gn,3ls0ڷCS}43tttVN2- f^I޲U2]>6uxVh ӑe(J;(6ğLlx3--F  ? kVghjz~me:Snh+K+mT%+8#f%W6zF#egSv LXjd\Y~~~mq=?CV1[fi<OeKi 6͐vgr6j4!oj{%Tٶ)f롞JJRNїC=kWӁm/[O#~q,f%g(/~d5B?u1Hg:j6Q!;aJ~g !+9hqV6U͍ik29KoN^k[u;g>v>/w+pigQoևw2" d=yK%O+$9_ UYM\/qEGu}-?FVjj{-3T??B׏$ c43Lt\SNFltQL7;DVB}J4}7rpP7\ezNgfme?RύD?&_^7 |8Qfii eQ˩S'5>[ZNIs{b I'8a^LBcY%Oٯ6YHCv?>[Fx0`}Y3.x"j?qfے8L hf"VF3ZR^oG%6Nޛ djē=L~[ ^b+L*t؃X3iZȴ -jdM7Y"{^ } linKYcfJ0~0-43t5/;*wOwqqoc*z㗲bUVY~-Vo9Pz ˸yrۛ^ofWNZC-~va:ۃgPۧ;'^~4]q7%,BWr-mX/mA?/ەOkC3t;_";Jq;9;wJo˯S _ҳn+;.3)jdW-v]?^FO1]B#=m> HmrӖ/Hm^U?[]2ůh@hf"V ~x;,ggNT۹%WMP`,[;X0VuX_~Vr6Pwkodp5,Z!П~~w;Q'TP4_WY-[~J.Us6s~Wp ^xZUgj|vY;8*5vC`Wm@J^[_~tHY >h=KCm 9hx*g(pGSWc4Ro߫m3pLĪ;!;N ;hvQ";iUs(<-q z0PG͵z [(rY xs hw0N!֙ڮFzCٺ8 3JwԟA8a믗~umqiS?P<LĪC!yX/|*獰snUL-I/O;\4C|]~ %"- ߮P4[ϯm[뽥]iވsv#wr? ?ROko`CcΠgq+43LPS5%DM?S}&f&bՉ(zc\"#*R~ê󶗿F3GâXux;D0f&b5=ӡ_1kpzԟBi-XoqϬ7 $@|kqX7'^3]DL*~zm{"h>bE?6NIFIǽ0imSd!L~,i#r(v}9BF3LH߉ʧE\l[VLC(pf.2\-;ƃJkVYa1lպ#zt~s-v u{V nO4?\Kzr2Ԧbc],j:y׷Bk˶h; вZAy4LG}{.;+;M6|Yg6O( 2 Sb?HVmf3$Wi[_ˏ/mԶ&VGA(׵>DW-Z? չ$kfo3O jmOCSfڷCaAr`ɣXug: azNNZ__5?Ckȗ]5cfRM&n{3&i)kx@~/"O}ůcj_)?v;iL?hݶ ˺̵u饕^MKcf{rvVj"f2ϖP%C KLj,g:jdH-C[&w1?xp;;heC:94ȇ(aP9m/00fʩS륤>BZL #F*Q<ۿiCmV#7wuYl^)DTg2t24^y|D0qa:2vb:2M@.r #,3LGiL!Yf&b,LGWk CaKA#0'0tC3\0ٴQ+Seu} j=^\!#L*tQ\+(8H:MG?ouF=Ho12# ƠTi3U~1 _tFe1~Y*)+K [Lj3v..D1)S%d@Whf"V5rIvveLthgXMtC3t@4D0f&bh43ea:R_áXa: LĊ+f&bh43)ufa3|%'5+B3t@4W L hf"VF3t@4DVXm͂(,z/4iG9g撗e+Dy3\C3t,qh(1:2`eƢPo1-bO1A9zKLjYtM?@QnApRl,A-$gjo?MeeÿkmOYUN|YP2)+^f&bLGmiV 堚후')'˟m_!U65~;kibHB3ZQWꃜ28\uAW4Z[lbn U9yv7 ;y,Aӑ2@hV^g? D0idP-0YC`[M;s0j6[sYAbtLjc ^dxOGꙎ2ؖxV,_6FB3Y>?xl6쿜p,˟f?x|}hf"V+tdK7%v?75֍'(x/ {$xʲBn:y:c˟d?8gJ|f&b. :%>8N̋'d43+LGx̵/|3543+LDXa: L hf"V+tW,DG,.7KkLXOD0m}˩;Xtd:Z Y.HHhlCޡfg,J'?ٜ|E hF"Ec1{b-o"ep'\cG3)t\~f62W.RfSir4#NMM7d⋋A6=|,u]G]LqgK!ľ_v -&!!7>IO2{oN;d2 rnhn:},7^veE E3)tMo2'pB1k:BSGocLG[1tIm+H!g7wqG׽uE/z5 }C'>bo/K0|##{9kLL?Y2yCl0bZG'XnhF"Ey سb* 1yc 䩑c9<jw}wf뭷BC |^뮻qy睭yIӾTIGfr4#NL0f^uf:} ,khF"Ea2| f$RԩXni{HS!\~f62W.RA9HQC8s[laԐװ#_W4#AޡfL }qO.RD3)aRN;mN O_6HhC[AsOYC^*^{~W4#A*[LX5tvm]zIx:JVHDLG=cH=μ=1Rߤ^+ьD&j:Jcm2S"Oqg֮][)[ꐺrK[hF"ES1%rW_m8ۏwN ѶTdYVʐM6!u f$R4U"_gm95Yr<"+{5"_dtM2l˒2l HhfL|^.\q}a<~+OˆIyg͚5vHhMG> &f$R(4#"LDa: HQhF"EB3)t@HЌD0f$R(4#"LDa: HQhF"EB3)t@HЌD0f$R(4#"LDa: HQhF"EB3)t@HЌD;.b77~{]wլ[ @3)Zk- !s1摏|dlH?" Hhɘkךh˿4V*rЌDrˆxSZq-)ӱ`ļ?b.HhI[o<Lq-s`hF"EKt+>Hq-9qYg6뭷93T7Hђ3wuWu;,R`hF"E3k:e_7tE]d͜}O6C<#y01̈́鐳\r9S;ۛ_lf3/x +^ 84ᄏ_d䑼̣h3??o2:`x4#np /|` ;l: }\z6BȲR%eJRN;d딺 @ y>?ۗs ￿}ի G}yԣez[afN=Tk:sOqOݏb:RJB3)t?6m]1~3;|;47c|򖷘m;Ϧ r6d 74|e:?S[oο 9Qg=JB3)t|[߲ycXk&/W&My:DِJ#AE//*MRcS҅u],w4#a=DǗ%͓<1K/5}Ck֬;h=h!u`:C3)tzyE{+g3噈}>" QOӂS!igq@qM7嗟#.JD3)tz#pY=H1__m\:)>f8묳%fM7Ed:/7ͩwy?FRXhF"ECaGfwC.˿Kn\>pLYy<>+^i\"e\~ˈQ{Ad: <2 +Djh1闃  V*HЦc_>.x :d4#M0|7yk*|f$R4=@3)tȍGyyK^b ܐ6;I?,W4#M hYNg‹f$Rԉ{V[m,^uWmƞőt4#LGh曛|+R:vᢋ.*R@3)twujnko,3xolf$R4Q"g=ve=~+eo4/-#:HQt\{J_zFNKӼ1Sꖶ{fM6_:w}im1%k׮5el.{Ÿ:.Bs!ؗ|I M `hiJpE򺰰`_ocvK,+eHYR]!up EN7(M?kjEnڼKͩj8;_~^HrDޙ!IyG2ooKb@1`44#6! ӡ!]k͝dɗ%~wqV<#yo&^04#!1tX>hF !t@HQi:.23wua:@E3)*M_n殿>tf$RT+tf$RTܪU0?[fMe:B! y܍7h'B!ƥ_zFeIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/rank_same.svg0000666000000000000000000000613714144524371015273 0ustar00 A A C C A->C B B A->B X X Y Y X->Y D D C->D ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/round-table.svg0000666000000000000000000000427714144524371015552 0ustar00 A King Arthur B Sir Bedevere the Wise A->B L Sir Lancelot the Brave A->L B->L ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/docs/_static/splines.svg0000666000000000000000000000227314226560631015005 0ustar00 splines a b a->b c ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/structs.svg0000666000000000000000000001042314144524371015033 0ustar00 structs struct1 left middle right struct2 one two struct1:f1->struct2:f0 struct3 hello world b g h c d e f struct1:f2->struct3:here ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/structs_revisited.svg0000666000000000000000000000762514144524371017123 0ustar00 structs struct1 left middle right struct2 one two struct1:f1->struct2:f0 struct3 hello world b c d e f g h struct1:f2->struct3:here ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/traffic_lights.svg0000666000000000000000000002317714144524371016326 0ustar00 TrafficLights PetriNet Model TrafficLights Extracted from ConceptBase and layed out by Graphviz gy2 gy2 yellow2 yellow2 gy2->yellow2 yr2 yr2 red2 red2 yr2->red2 safe1 safe1 yr2->safe1 rg2 rg2 green2 green2 rg2->green2 gy1 gy1 yellow1 yellow1 gy1->yellow1 yr1 yr1 safe2 safe2 yr1->safe2 red1 red1 yr1->red1 rg1 rg1 green1 green1 rg1->green1 green2->gy2 yellow2->yr2 red2->rg2 safe2->rg2 green1->gy1 yellow1->yr1 red1->rg1 safe1->rg1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/unix.svg0000666000000000000000000007003714144524371014316 0ustar00 unix 5th Edition 5th Edition 6th Edition 6th Edition 5th Edition->6th Edition PWB 1.0 PWB 1.0 5th Edition->PWB 1.0 LSX LSX 6th Edition->LSX 1 BSD 1 BSD 6th Edition->1 BSD Mini Unix Mini Unix 6th Edition->Mini Unix Wollongong Wollongong 6th Edition->Wollongong Interdata Interdata 6th Edition->Interdata PWB 1.2 PWB 1.2 PWB 1.0->PWB 1.2 USG 1.0 USG 1.0 PWB 1.0->USG 1.0 2 BSD 2 BSD 1 BSD->2 BSD Unix/TS 3.0 Unix/TS 3.0 Interdata->Unix/TS 3.0 PWB 2.0 PWB 2.0 Interdata->PWB 2.0 7th Edition 7th Edition Interdata->7th Edition TS 4.0 TS 4.0 Unix/TS 3.0->TS 4.0 PWB 2.0->Unix/TS 3.0 8th Edition 8th Edition 7th Edition->8th Edition 32V 32V 7th Edition->32V V7M V7M 7th Edition->V7M Ultrix-11 Ultrix-11 7th Edition->Ultrix-11 Xenix Xenix 7th Edition->Xenix UniPlus+ UniPlus+ 7th Edition->UniPlus+ 9th Edition 9th Edition 8th Edition->9th Edition 3 BSD 3 BSD 32V->3 BSD V7M->Ultrix-11 2.8 BSD 2.8 BSD 2 BSD->2.8 BSD 2.8 BSD->Ultrix-11 2.9 BSD 2.9 BSD 2.8 BSD->2.9 BSD 4 BSD 4 BSD 3 BSD->4 BSD 4.1 BSD 4.1 BSD 4 BSD->4.1 BSD 4.1 BSD->8th Edition 4.1 BSD->2.8 BSD 4.2 BSD 4.2 BSD 4.1 BSD->4.2 BSD 4.3 BSD 4.3 BSD 4.2 BSD->4.3 BSD Ultrix-32 Ultrix-32 4.2 BSD->Ultrix-32 PWB 1.2->PWB 2.0 CB Unix 1 CB Unix 1 USG 1.0->CB Unix 1 USG 2.0 USG 2.0 USG 1.0->USG 2.0 CB Unix 2 CB Unix 2 CB Unix 1->CB Unix 2 USG 3.0 USG 3.0 USG 2.0->USG 3.0 CB Unix 3 CB Unix 3 CB Unix 2->CB Unix 3 Unix/TS++ Unix/TS++ CB Unix 3->Unix/TS++ PDP-11 Sys V PDP-11 Sys V CB Unix 3->PDP-11 Sys V CB Unix 3->TS 4.0 Unix/TS++->TS 4.0 USG 3.0->Unix/TS 3.0 Unix/TS 1.0 Unix/TS 1.0 Unix/TS 1.0->Unix/TS 3.0 System V.0 System V.0 TS 4.0->System V.0 System V.2 System V.2 System V.0->System V.2 System V.3 System V.3 System V.2->System V.3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/wide-unflatten-stagger-2.svg0000666000000000000000000001333514144524371020050 0ustar00 0 0 1 1 0->1 2 2 0->2 3 3 0->3 4 4 0->4 5 5 0->5 6 6 0->6 7 7 0->7 8 8 0->8 9 9 0->9 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/wide-unflatten-stagger-3.svg0000666000000000000000000001332714144524371020052 0ustar00 0 0 1 1 0->1 2 2 0->2 3 3 0->3 4 4 0->4 5 5 0->5 6 6 0->6 7 7 0->7 8 8 0->8 9 9 0->9 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637001465.0 graphviz-0.21/docs/_static/wide.svg0000666000000000000000000001312514144524371014256 0ustar00 0 0 1 1 0->1 2 2 0->2 3 3 0->3 4 4 0->4 5 5 0->5 6 6 0->6 7 7 0->7 8 8 0->8 9 9 0->9 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979371.0 graphviz-0.21/docs/api.rst0000666000000000000000000021153515023510353012457 0ustar00API Reference ============= .. autosummary:: :nosignatures: graphviz.Graph graphviz.Digraph graphviz.Source graphviz.escape graphviz.nohtml graphviz.ExecutableNotFound graphviz.CalledProcessError graphviz.RequiredArgumentError graphviz.render graphviz.pipe graphviz.pipe_string graphviz.unflatten graphviz.view graphviz.version .. hint:: The two main classes :class:`.Graph` and :class:`.Digraph` (creating **undirected** vs. **directed** graphs) have exactly the same API. Their division reflects the fact that both graph syntaxes cannot be mixed. Graph ----- .. autoclass:: graphviz.Graph :members: directed, name, comment, filename, directory, format, engine, encoding, renderer, formatter, graph_attr, node_attr, edge_attr, body, strict, __iter__, source, node, edge, edges, attr, subgraph, filepath, save, render, view, pipe, unflatten, _repr_mimebundle_, clear, copy Digraph ------- .. autoclass:: graphviz.Digraph :members: directed, name, comment, filename, directory, format, engine, encoding, renderer, formatter, graph_attr, node_attr, edge_attr, body, strict, __iter__, source, node, edge, edges, attr, subgraph, filepath, save, render, view, pipe, unflatten, _repr_mimebundle_, clear, copy Source ------ .. autoclass:: graphviz.Source :members: from_file, filename, directory, format, engine, encoding, renderer, formatter, __iter__, source, filepath, save, render, view, pipe, unflatten, _repr_mimebundle_, copy Quoting/escaping ---------------- .. autofunction:: graphviz.escape .. autofunction:: graphviz.nohtml Exceptions ---------- .. autoexception:: graphviz.ExecutableNotFound .. autoexception:: graphviz.CalledProcessError .. autoexception:: graphviz.RequiredArgumentError .. autoexception:: graphviz.FileExistsError Warnings -------- .. autoexception:: graphviz.UnknownSuffixWarning .. autoexception:: graphviz.FormatSuffixMismatchWarning .. autoexception:: graphviz.DotSyntaxWarning Low-level functions ------------------- The functions in this section are provided to work directly with existing files and strings instead of using the object-oriented DOT_ creation methods documented above. .. autofunction:: graphviz.render .. autofunction:: graphviz.pipe .. autofunction:: graphviz.pipe_string .. autofunction:: graphviz.pipe_lines .. autofunction:: graphviz.pipe_lines_string .. autofunction:: graphviz.unflatten .. autofunction:: graphviz.view Constants --------- Manually maintained allowlists for Graphviz_ **parameters** (cf. `man dot `_, `outputs `_, and ``dot -T:`` output): .. autodata:: graphviz.ENGINES :annotation: .. autodata:: graphviz.FORMATS :annotation: .. autodata:: graphviz.RENDERERS :annotation: .. autodata:: graphviz.FORMATTERS :annotation: Supported **IPython/Jupyter display formats**: .. autodata:: graphviz.SUPPORTED_JUPYTER_FORMATS :annotation: Names of **upstream binaries**: .. autodata:: graphviz.DOT_BINARY :annotation: .. autodata:: graphviz.UNFLATTEN_BINARY :annotation: Defaults -------- Functions for setting **package-wide defaults** for ``engine`` and ``format``: .. attention:: These functions are provided mainly to simplify testing but may also be used by end-users for convenience in scripts. They **should be avoided in library code**. Prefer passing or setting ``engine`` and ``format`` explicitly if you create a library that depends on this package. .. autofunction:: graphviz.set_default_engine .. autofunction:: graphviz.set_default_format Function for setting the **package-wide default for IPython/Jupyter display format**: .. attention:: This function is provided for end-users. Prefer `IPython.display`_ functions in library code. .. autofunction:: graphviz.set_jupyter_format Other ----- .. autofunction:: graphviz.version .. include:: _links.rst Online ``help()`` (internal) ---------------------------- Results of :func:`help` for :class:`graphviz.Graph`, :class:`graphviz.Digraph`, and :class:`graphviz.Source` for reference. .. attention:: The outputs in this section may contain (some) **internals** (implementation details). They serve to record some current implementation details and their changes. They mainly serve the development process (e.g. checking the MRO). They might be outdated. They **may change at any point** in time. See above for the full (public) API. First shalt thou take out the Holy Pin. Then shalt thou count to three, no more, no less. To **update** :func:`help` outputs below: .. code:: bash $ ./update-help.py To **debug**: remove ``+SKIP`` flags below and check output(s): .. code:: bash $ ./run-tests.py docs --doctest-report none Graph """"" Partially syntax-highlighted: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#graph-1 .. doctest:: >>> import graphviz >>> help(graphviz.Graph) # doctest: +NORMALIZE_WHITESPACE +SKIP Help on class Graph in module graphviz.graphs: class Graph(graphviz.dot.GraphSyntax, BaseGraph) | Graph(name: Optional[str] = None, comment: Optional[str] = None, filename=None, directory=None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, *, renderer: Optional[str] = None, formatter: Optional[str] = None) -> None | | Graph source code in the DOT language. | | Args: | name: Graph name used in the source code. | comment: Comment added to the first line of the source. | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``). | directory: (Sub)directory for source saving and rendering. | format: Rendering output format (``'pdf'``, ``'png'``, ...). | engine: Layout command used (``'dot'``, ``'neato'``, ...). | renderer: Output renderer used (``'cairo'``, ``'gd'``, ...). | formatter: Output formatter used (``'cairo'``, ``'gd'``, ...). | encoding: Encoding for saving the source. | graph_attr: Mapping of ``(attribute, value)`` pairs for the graph. | node_attr: Mapping of ``(attribute, value)`` pairs set for all nodes. | edge_attr: Mapping of ``(attribute, value)`` pairs set for all edges. | body: Iterable of verbatim lines (including their final newline) | to add to the graph ``body``. | strict (bool): Rendering should merge multi-edges. | | Note: | All parameters are `optional` and can be changed under their | corresponding attribute name after instance creation. | | Method resolution order: | Graph | graphviz.dot.GraphSyntax | BaseGraph | graphviz.dot.Dot | graphviz.quoting.Quote | graphviz.rendering.Render | graphviz.saving.Save | graphviz.jupyter_integration.JupyterIntegration | graphviz.piping.Pipe | graphviz.unflattening.Unflatten | graphviz.encoding.Encoding | graphviz.base.Base | graphviz.base.LineIterable | graphviz.backend.mixins.Render | graphviz.backend.mixins.Pipe | graphviz.parameters.mixins.Parameters | graphviz.parameters.engines.Engine | graphviz.parameters.formats.Format | graphviz.parameters.renderers.Renderer | graphviz.parameters.formatters.Formatter | graphviz.parameters.base.ParameterBase | graphviz.copying.CopyBase | graphviz.backend.mixins.View | graphviz.backend.mixins.Unflatten | builtins.object | | Readonly properties defined here: | | directed | ``False`` | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __annotations__ = {} | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.dot.GraphSyntax: | | __dict__ | dictionary for instance variables | | __weakref__ | list of weak references to the object | | ---------------------------------------------------------------------- | Methods inherited from BaseGraph: | | __init__(self, name: Optional[str] = None, comment: Optional[str] = None, filename=None, directory=None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, *, renderer: Optional[str] = None, formatter: Optional[str] = None) -> None | Initialize self. See help(type(self)) for accurate signature. | | ---------------------------------------------------------------------- | Readonly properties inherited from BaseGraph: | | source | The generated DOT source code as string. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.dot.Dot: | | __iter__(self, subgraph: bool = False) -> Iterator[str] | Yield the DOT source code line by line (as graph or subgraph). | | Yields: Line ending with a newline (``'\n'``). | | attr(self, kw: Optional[str] = None, _attributes=None, **attrs) -> None | Add a general or graph/node/edge attribute statement. | | Args: | kw: Attributes target | (``None`` or ``'graph'``, ``'node'``, ``'edge'``). | attrs: Attributes to be set (must be strings, may be empty). | | See the :ref:`usage examples in the User Guide `. | | clear(self, keep_attrs: bool = False) -> None | Reset content to an empty body, clear graph/node/egde_attr mappings. | | Args: | keep_attrs (bool): preserve graph/node/egde_attr mappings | | edge(self, tail_name: str, head_name: str, label: Optional[str] = None, _attributes=None, **attrs) -> None | Create an edge between two nodes. | | Args: | tail_name: Start node identifier | (format: ``node[:port[:compass]]``). | head_name: End node identifier | (format: ``node[:port[:compass]]``). | label: Caption to be displayed near the edge. | attrs: Any additional edge attributes (must be strings). | | Note: | The ``tail_name`` and ``head_name`` strings are separated | by (optional) colon(s) into ``node`` name, ``port`` name, | and ``compass`` (e.g. ``sw``). | See :ref:`details in the User Guide `. | | Attention: | When rendering ``label``, backslash-escapes | and strings of the form ``<...>`` have a special meaning. | See the sections :ref:`backslash-escapes` and | :ref:`quoting-and-html-like-labels` in the user guide for details. | | edges(self, tail_head_iter) -> None | Create a bunch of edges. | | Args: | tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs | (format:``node[:port[:compass]]``). | | | Note: | The ``tail_name`` and ``head_name`` strings are separated | by (optional) colon(s) into ``node`` name, ``port`` name, | and ``compass`` (e.g. ``sw``). | See :ref:`details in the User Guide `. | | node(self, name: str, label: Optional[str] = None, _attributes=None, **attrs) -> None | Create a node. | | Args: | name: Unique identifier for the node inside the source. | label: Caption to be displayed (defaults to the node ``name``). | attrs: Any additional node attributes (must be strings). | | Attention: | When rendering ``label``, backslash-escapes | and strings of the form ``<...>`` have a special meaning. | See the sections :ref:`backslash-escapes` and | :ref:`quoting-and-html-like-labels` in the user guide for details. | | subgraph(self, graph=None, name: Optional[str] = None, comment: Optional[str] = None, graph_attr=None, node_attr=None, edge_attr=None, body=None) | Add the current content of the given sole ``graph`` argument | as subgraph or return a context manager | returning a new graph instance | created with the given (``name``, ``comment``, etc.) arguments | whose content is added as subgraph | when leaving the context manager's ``with``-block. | | Args: | graph: An instance of the same kind | (:class:`.Graph`, :class:`.Digraph`) as the current graph | (sole argument in non-with-block use). | name: Subgraph name (``with``-block use). | comment: Subgraph comment (``with``-block use). | graph_attr: Subgraph-level attribute-value mapping | (``with``-block use). | node_attr: Node-level attribute-value mapping | (``with``-block use). | edge_attr: Edge-level attribute-value mapping | (``with``-block use). | body: Verbatim lines to add to the subgraph ``body`` | (``with``-block use). | | See the :ref:`usage examples in the User Guide `. | | When used as a context manager, the returned new graph instance | uses ``strict=None`` and the parent graph's values | for ``directory``, ``format``, ``engine``, and ``encoding`` by default. | | Note: | If the ``name`` of the subgraph begins with | ``'cluster'`` (all lowercase) | the layout engine will treat it as a special cluster subgraph. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.rendering.Render: | | render(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, view: bool = False, cleanup: bool = False, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, quiet_view: bool = False, *, outfile: Union[os.PathLike, str, NoneType] = None, engine: Optional[str] = None, raise_if_result_exists: bool = False, overwrite_source: bool = False) -> str | Save the source to file and render with the Graphviz engine. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``).s | directory: (Sub)directory for source saving and rendering. | view (bool): Open the rendered result | with the default application. | cleanup (bool): Delete the source file | after successful rendering. | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process | (implies ``view=True``, ineffective on Windows platform). | outfile: Path for the rendered output file. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` | if the result file exists. | overwrite_source: Allow ``dot`` to write to the file it reads from. | Incompatible with ``raise_if_result_exists``. | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | ValueError: If ``outfile`` is the same file as the source file | unless ``overwite_source=True``. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | RuntimeError: If viewer opening is requested but not supported. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> dot = graphviz.Graph(name='spam', directory='doctest-output') | >>> dot.render(format='png').replace('\', '/') | 'doctest-output/spam.gv.png' | >>> dot.render(outfile='spam.svg').replace('\', '/') | 'doctest-output/spam.svg' | | Note: | The layout command is started from the directory of ``filepath``, | so that references to external files | (e.g. ``[image=images/camelot.png]``) | can be given as paths relative to the DOT source file. | | view(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, cleanup: bool = False, quiet: bool = False, quiet_view: bool = False) -> str | Save the source to file, open the rendered result in a viewer. | | Convenience short-cut for running ``.render(view=True)``. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``). | directory: (Sub)directory for source saving and rendering. | cleanup (bool): Delete the source file after successful rendering. | quiet (bool): Suppress ``stderr`` output from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process (ineffective on Windows). | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | graphviz.ExecutableNotFound: If the Graphviz executable | is not found. | graphviz.CalledProcessError: If the exit status is non-zero. | RuntimeError: If opening the viewer is not supported. | | Short-cut method for calling :meth:`.render` with ``view=True``. | | Note: | There is no option to wait for the application to close, | and no way to retrieve the application's exit status. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.saving.Save: | | save(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, *, skip_existing: Optional[bool] = False) -> str | Save the DOT source to file. Ensure the file ends with a newline. | | Args: | filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) | directory: (Sub)directory for source saving and rendering. | skip_existing: Skip write if file exists (default: ``False``). | | Returns: | The (possibly relative) path of the saved source file. | | ---------------------------------------------------------------------- | Readonly properties inherited from graphviz.saving.Save: | | filepath | The target path for saving the DOT source file. | | ---------------------------------------------------------------------- | Data and other attributes inherited from graphviz.saving.Save: | | directory = '' | | ---------------------------------------------------------------------- | Methods inherited from graphviz.piping.Pipe: | | pipe(self, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, *, engine: Optional[str] = None, encoding: Optional[str] = None) -> Union[bytes, str] | Return the source piped through the Graphviz layout command. | | Args: | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | encoding: Encoding for decoding the stdout. | | Returns: | Bytes or if encoding is given decoded string | (stdout of the layout command). | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> source = 'graph { spam }' | >>> graphviz.Source(source, format='svg').pipe()[:14] | b'>> graphviz.Source(source, format='svg').pipe(encoding='ascii')[:14] | '>> graphviz.Source(source, format='svg').pipe(encoding='utf-8')[:14] | ' 'graphviz.Source' | Return a new :class:`.Source` instance with the source | piped through the Graphviz *unflatten* preprocessor. | | Args: | stagger: Stagger the minimum length | of leaf edges between 1 and this small integer. | fanout: Fanout nodes with indegree = outdegree = 1 | when staggering (requires ``stagger``). | chain: Form disconnected nodes into chains | of up to this many nodes. | | Returns: | Prepocessed DOT source code (improved layout aspect ratio). | | Raises: | graphviz.RequiredArgumentError: If ``fanout`` is given | but ``stagger`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``unflatten`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the unflattening 'unflatten' subprocess is non-zero. | | See also: | Upstream documentation: | https://www.graphviz.org/pdf/unflatten.1.pdf | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.encoding.Encoding: | | encoding | The encoding for the saved source file. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.base.Base: | | __str__(self) -> str | The DOT source code as string. | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.engines.Engine: | | engine | The layout engine used for rendering | (``'dot'``, ``'neato'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formats.Format: | | format | The output format used for rendering | (``'pdf'``, ``'png'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.renderers.Renderer: | | renderer | The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formatters.Formatter: | | formatter | The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Methods inherited from graphviz.copying.CopyBase: | | copy(self) | Return a copied instance of the object. | | Returns: | An independent copy of the current object. Digraph """"""" Partially symntax-highlighed: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#digraph-1 .. doctest:: >>> import graphviz >>> help(graphviz.Digraph) # doctest: +NORMALIZE_WHITESPACE +SKIP Help on class Digraph in module graphviz.graphs: class Digraph(graphviz.dot.DigraphSyntax, BaseGraph) | Digraph(name: Optional[str] = None, comment: Optional[str] = None, filename=None, directory=None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, *, renderer: Optional[str] = None, formatter: Optional[str] = None) -> None | | Directed graph source code in the DOT language. | | Args: | name: Graph name used in the source code. | comment: Comment added to the first line of the source. | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``). | directory: (Sub)directory for source saving and rendering. | format: Rendering output format (``'pdf'``, ``'png'``, ...). | engine: Layout command used (``'dot'``, ``'neato'``, ...). | renderer: Output renderer used (``'cairo'``, ``'gd'``, ...). | formatter: Output formatter used (``'cairo'``, ``'gd'``, ...). | encoding: Encoding for saving the source. | graph_attr: Mapping of ``(attribute, value)`` pairs for the graph. | node_attr: Mapping of ``(attribute, value)`` pairs set for all nodes. | edge_attr: Mapping of ``(attribute, value)`` pairs set for all edges. | body: Iterable of verbatim lines (including their final newline) | to add to the graph ``body``. | strict (bool): Rendering should merge multi-edges. | | Note: | All parameters are `optional` and can be changed under their | corresponding attribute name after instance creation. | | Method resolution order: | Digraph | graphviz.dot.DigraphSyntax | BaseGraph | graphviz.dot.Dot | graphviz.quoting.Quote | graphviz.rendering.Render | graphviz.saving.Save | graphviz.jupyter_integration.JupyterIntegration | graphviz.piping.Pipe | graphviz.unflattening.Unflatten | graphviz.encoding.Encoding | graphviz.base.Base | graphviz.base.LineIterable | graphviz.backend.mixins.Render | graphviz.backend.mixins.Pipe | graphviz.parameters.mixins.Parameters | graphviz.parameters.engines.Engine | graphviz.parameters.formats.Format | graphviz.parameters.renderers.Renderer | graphviz.parameters.formatters.Formatter | graphviz.parameters.base.ParameterBase | graphviz.copying.CopyBase | graphviz.backend.mixins.View | graphviz.backend.mixins.Unflatten | builtins.object | | Readonly properties defined here: | | directed | ``True`` | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __annotations__ = {} | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.dot.DigraphSyntax: | | __dict__ | dictionary for instance variables | | __weakref__ | list of weak references to the object | | ---------------------------------------------------------------------- | Methods inherited from BaseGraph: | | __init__(self, name: Optional[str] = None, comment: Optional[str] = None, filename=None, directory=None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, *, renderer: Optional[str] = None, formatter: Optional[str] = None) -> None | Initialize self. See help(type(self)) for accurate signature. | | ---------------------------------------------------------------------- | Readonly properties inherited from BaseGraph: | | source | The generated DOT source code as string. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.dot.Dot: | | __iter__(self, subgraph: bool = False) -> Iterator[str] | Yield the DOT source code line by line (as graph or subgraph). | | Yields: Line ending with a newline (``'\n'``). | | attr(self, kw: Optional[str] = None, _attributes=None, **attrs) -> None | Add a general or graph/node/edge attribute statement. | | Args: | kw: Attributes target | (``None`` or ``'graph'``, ``'node'``, ``'edge'``). | attrs: Attributes to be set (must be strings, may be empty). | | See the :ref:`usage examples in the User Guide `. | | clear(self, keep_attrs: bool = False) -> None | Reset content to an empty body, clear graph/node/egde_attr mappings. | | Args: | keep_attrs (bool): preserve graph/node/egde_attr mappings | | edge(self, tail_name: str, head_name: str, label: Optional[str] = None, _attributes=None, **attrs) -> None | Create an edge between two nodes. | | Args: | tail_name: Start node identifier | (format: ``node[:port[:compass]]``). | head_name: End node identifier | (format: ``node[:port[:compass]]``). | label: Caption to be displayed near the edge. | attrs: Any additional edge attributes (must be strings). | | Note: | The ``tail_name`` and ``head_name`` strings are separated | by (optional) colon(s) into ``node`` name, ``port`` name, | and ``compass`` (e.g. ``sw``). | See :ref:`details in the User Guide `. | | Attention: | When rendering ``label``, backslash-escapes | and strings of the form ``<...>`` have a special meaning. | See the sections :ref:`backslash-escapes` and | :ref:`quoting-and-html-like-labels` in the user guide for details. | | edges(self, tail_head_iter) -> None | Create a bunch of edges. | | Args: | tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs | (format:``node[:port[:compass]]``). | | | Note: | The ``tail_name`` and ``head_name`` strings are separated | by (optional) colon(s) into ``node`` name, ``port`` name, | and ``compass`` (e.g. ``sw``). | See :ref:`details in the User Guide `. | | node(self, name: str, label: Optional[str] = None, _attributes=None, **attrs) -> None | Create a node. | | Args: | name: Unique identifier for the node inside the source. | label: Caption to be displayed (defaults to the node ``name``). | attrs: Any additional node attributes (must be strings). | | Attention: | When rendering ``label``, backslash-escapes | and strings of the form ``<...>`` have a special meaning. | See the sections :ref:`backslash-escapes` and | :ref:`quoting-and-html-like-labels` in the user guide for details. | | subgraph(self, graph=None, name: Optional[str] = None, comment: Optional[str] = None, graph_attr=None, node_attr=None, edge_attr=None, body=None) | Add the current content of the given sole ``graph`` argument | as subgraph or return a context manager | returning a new graph instance | created with the given (``name``, ``comment``, etc.) arguments | whose content is added as subgraph | when leaving the context manager's ``with``-block. | | Args: | graph: An instance of the same kind | (:class:`.Graph`, :class:`.Digraph`) as the current graph | (sole argument in non-with-block use). | name: Subgraph name (``with``-block use). | comment: Subgraph comment (``with``-block use). | graph_attr: Subgraph-level attribute-value mapping | (``with``-block use). | node_attr: Node-level attribute-value mapping | (``with``-block use). | edge_attr: Edge-level attribute-value mapping | (``with``-block use). | body: Verbatim lines to add to the subgraph ``body`` | (``with``-block use). | | See the :ref:`usage examples in the User Guide `. | | When used as a context manager, the returned new graph instance | uses ``strict=None`` and the parent graph's values | for ``directory``, ``format``, ``engine``, and ``encoding`` by default. | | Note: | If the ``name`` of the subgraph begins with | ``'cluster'`` (all lowercase) | the layout engine will treat it as a special cluster subgraph. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.rendering.Render: | | render(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, view: bool = False, cleanup: bool = False, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, quiet_view: bool = False, *, outfile: Union[os.PathLike, str, NoneType] = None, engine: Optional[str] = None, raise_if_result_exists: bool = False, overwrite_source: bool = False) -> str | Save the source to file and render with the Graphviz engine. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``).s | directory: (Sub)directory for source saving and rendering. | view (bool): Open the rendered result | with the default application. | cleanup (bool): Delete the source file | after successful rendering. | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process | (implies ``view=True``, ineffective on Windows platform). | outfile: Path for the rendered output file. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` | if the result file exists. | overwrite_source: Allow ``dot`` to write to the file it reads from. | Incompatible with ``raise_if_result_exists``. | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | ValueError: If ``outfile`` is the same file as the source file | unless ``overwite_source=True``. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | RuntimeError: If viewer opening is requested but not supported. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> dot = graphviz.Graph(name='spam', directory='doctest-output') | >>> dot.render(format='png').replace('\', '/') | 'doctest-output/spam.gv.png' | >>> dot.render(outfile='spam.svg').replace('\', '/') | 'doctest-output/spam.svg' | | Note: | The layout command is started from the directory of ``filepath``, | so that references to external files | (e.g. ``[image=images/camelot.png]``) | can be given as paths relative to the DOT source file. | | view(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, cleanup: bool = False, quiet: bool = False, quiet_view: bool = False) -> str | Save the source to file, open the rendered result in a viewer. | | Convenience short-cut for running ``.render(view=True)``. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``). | directory: (Sub)directory for source saving and rendering. | cleanup (bool): Delete the source file after successful rendering. | quiet (bool): Suppress ``stderr`` output from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process (ineffective on Windows). | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | graphviz.ExecutableNotFound: If the Graphviz executable | is not found. | graphviz.CalledProcessError: If the exit status is non-zero. | RuntimeError: If opening the viewer is not supported. | | Short-cut method for calling :meth:`.render` with ``view=True``. | | Note: | There is no option to wait for the application to close, | and no way to retrieve the application's exit status. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.saving.Save: | | save(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, *, skip_existing: Optional[bool] = False) -> str | Save the DOT source to file. Ensure the file ends with a newline. | | Args: | filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) | directory: (Sub)directory for source saving and rendering. | skip_existing: Skip write if file exists (default: ``False``). | | Returns: | The (possibly relative) path of the saved source file. | | ---------------------------------------------------------------------- | Readonly properties inherited from graphviz.saving.Save: | | filepath | The target path for saving the DOT source file. | | ---------------------------------------------------------------------- | Data and other attributes inherited from graphviz.saving.Save: | | directory = '' | | ---------------------------------------------------------------------- | Methods inherited from graphviz.piping.Pipe: | | pipe(self, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, *, engine: Optional[str] = None, encoding: Optional[str] = None) -> Union[bytes, str] | Return the source piped through the Graphviz layout command. | | Args: | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | encoding: Encoding for decoding the stdout. | | Returns: | Bytes or if encoding is given decoded string | (stdout of the layout command). | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> source = 'graph { spam }' | >>> graphviz.Source(source, format='svg').pipe()[:14] | b'>> graphviz.Source(source, format='svg').pipe(encoding='ascii')[:14] | '>> graphviz.Source(source, format='svg').pipe(encoding='utf-8')[:14] | ' 'graphviz.Source' | Return a new :class:`.Source` instance with the source | piped through the Graphviz *unflatten* preprocessor. | | Args: | stagger: Stagger the minimum length | of leaf edges between 1 and this small integer. | fanout: Fanout nodes with indegree = outdegree = 1 | when staggering (requires ``stagger``). | chain: Form disconnected nodes into chains | of up to this many nodes. | | Returns: | Prepocessed DOT source code (improved layout aspect ratio). | | Raises: | graphviz.RequiredArgumentError: If ``fanout`` is given | but ``stagger`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``unflatten`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the unflattening 'unflatten' subprocess is non-zero. | | See also: | Upstream documentation: | https://www.graphviz.org/pdf/unflatten.1.pdf | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.encoding.Encoding: | | encoding | The encoding for the saved source file. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.base.Base: | | __str__(self) -> str | The DOT source code as string. | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.engines.Engine: | | engine | The layout engine used for rendering | (``'dot'``, ``'neato'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formats.Format: | | format | The output format used for rendering | (``'pdf'``, ``'png'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.renderers.Renderer: | | renderer | The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formatters.Formatter: | | formatter | The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Methods inherited from graphviz.copying.CopyBase: | | copy(self) | Return a copied instance of the object. | | Returns: | An independent copy of the current object. Source """""" Partially syntax-highlighted: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#source-1 .. doctest:: >>> import graphviz >>> help(graphviz.Source) # doctest: +NORMALIZE_WHITESPACE +SKIP Help on class Source in module graphviz.sources: class Source(graphviz.rendering.Render, graphviz.saving.Save, graphviz.jupyter_integration.JupyterIntegration, graphviz.piping.Pipe, graphviz.unflattening.Unflatten) | Source(source: str, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', *, renderer: Optional[str] = None, formatter: Optional[str] = None, loaded_from_path: Optional[os.PathLike] = None) -> None | | Verbatim DOT source code string to be rendered by Graphviz. | | Args: | source: The verbatim DOT source code string. | filename: Filename for saving the source (defaults to ``'Source.gv'``). | directory: (Sub)directory for source saving and rendering. | format: Rendering output format (``'pdf'``, ``'png'``, ...). | engine: Layout engine used (``'dot'``, ``'neato'``, ...). | encoding: Encoding for saving the source. | | Note: | All parameters except ``source`` are optional. All of them | can be changed under their corresponding attribute name | after instance creation. | | Method resolution order: | Source | graphviz.rendering.Render | graphviz.saving.Save | graphviz.jupyter_integration.JupyterIntegration | graphviz.piping.Pipe | graphviz.unflattening.Unflatten | graphviz.encoding.Encoding | graphviz.base.Base | graphviz.base.LineIterable | graphviz.backend.mixins.Render | graphviz.backend.mixins.Pipe | graphviz.parameters.mixins.Parameters | graphviz.parameters.engines.Engine | graphviz.parameters.formats.Format | graphviz.parameters.renderers.Renderer | graphviz.parameters.formatters.Formatter | graphviz.parameters.base.ParameterBase | graphviz.copying.CopyBase | graphviz.backend.mixins.View | graphviz.backend.mixins.Unflatten | builtins.object | | Methods defined here: | | __init__(self, source: str, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', *, renderer: Optional[str] = None, formatter: Optional[str] = None, loaded_from_path: Optional[os.PathLike] = None) -> None | Initialize self. See help(type(self)) for accurate signature. | | __iter__(self) -> Iterator[str] | Yield the DOT source code read from file line by line. | | Yields: Line ending with a newline (``'\n'``). | | save(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, *, skip_existing: Optional[bool] = None) -> str | Save the DOT source to file. Ensure the file ends with a newline. | | Args: | filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) | directory: (Sub)directory for source saving and rendering. | skip_existing: Skip write if file exists (default: ``None``). | By default skips if instance was loaded from the target path: | ``.from_file(self.filepath)``. | | Returns: | The (possibly relative) path of the saved source file. | | ---------------------------------------------------------------------- | Class methods defined here: | | from_file(filename: Union[os.PathLike, str], directory: Union[os.PathLike, str, NoneType] = None, format: Optional[str] = None, engine: Optional[str] = None, encoding: Optional[str] = 'utf-8', renderer: Optional[str] = None, formatter: Optional[str] = None) -> 'Source' | Return an instance with the source string read from the given file. | | Args: | filename: Filename for loading/saving the source. | directory: (Sub)directory for source loading/saving and rendering. | format: Rendering output format (``'pdf'``, ``'png'``, ...). | engine: Layout command used (``'dot'``, ``'neato'``, ...). | encoding: Encoding for loading/saving the source. | | ---------------------------------------------------------------------- | Readonly properties defined here: | | source | The DOT source code as string. | | Normalizes so that the string always ends in a final newline. | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __annotations__ = {'_loaded_from_path': typing.Optional[os.PathLike], ... | | ---------------------------------------------------------------------- | Methods inherited from graphviz.rendering.Render: | | render(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, view: bool = False, cleanup: bool = False, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, quiet_view: bool = False, *, outfile: Union[os.PathLike, str, NoneType] = None, engine: Optional[str] = None, raise_if_result_exists: bool = False, overwrite_source: bool = False) -> str | Save the source to file and render with the Graphviz engine. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``).s | directory: (Sub)directory for source saving and rendering. | view (bool): Open the rendered result | with the default application. | cleanup (bool): Delete the source file | after successful rendering. | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process | (implies ``view=True``, ineffective on Windows platform). | outfile: Path for the rendered output file. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` | if the result file exists. | overwrite_source: Allow ``dot`` to write to the file it reads from. | Incompatible with ``raise_if_result_exists``. | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | ValueError: If ``outfile`` is the same file as the source file | unless ``overwite_source=True``. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | RuntimeError: If viewer opening is requested but not supported. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> dot = graphviz.Graph(name='spam', directory='doctest-output') | >>> dot.render(format='png').replace('\', '/') | 'doctest-output/spam.gv.png' | >>> dot.render(outfile='spam.svg').replace('\', '/') | 'doctest-output/spam.svg' | | Note: | The layout command is started from the directory of ``filepath``, | so that references to external files | (e.g. ``[image=images/camelot.png]``) | can be given as paths relative to the DOT source file. | | view(self, filename: Union[os.PathLike, str, NoneType] = None, directory: Union[os.PathLike, str, NoneType] = None, cleanup: bool = False, quiet: bool = False, quiet_view: bool = False) -> str | Save the source to file, open the rendered result in a viewer. | | Convenience short-cut for running ``.render(view=True)``. | | Args: | filename: Filename for saving the source | (defaults to ``name`` + ``'.gv'``). | directory: (Sub)directory for source saving and rendering. | cleanup (bool): Delete the source file after successful rendering. | quiet (bool): Suppress ``stderr`` output from the layout subprocess. | quiet_view (bool): Suppress ``stderr`` output | from the viewer process (ineffective on Windows). | | Returns: | The (possibly relative) path of the rendered file. | | Raises: | graphviz.ExecutableNotFound: If the Graphviz executable | is not found. | graphviz.CalledProcessError: If the exit status is non-zero. | RuntimeError: If opening the viewer is not supported. | | Short-cut method for calling :meth:`.render` with ``view=True``. | | Note: | There is no option to wait for the application to close, | and no way to retrieve the application's exit status. | | ---------------------------------------------------------------------- | Readonly properties inherited from graphviz.saving.Save: | | filepath | The target path for saving the DOT source file. | | ---------------------------------------------------------------------- | Data and other attributes inherited from graphviz.saving.Save: | | directory = '' | | ---------------------------------------------------------------------- | Methods inherited from graphviz.piping.Pipe: | | pipe(self, format: Optional[str] = None, renderer: Optional[str] = None, formatter: Optional[str] = None, neato_no_op: Union[bool, int, NoneType] = None, quiet: bool = False, *, engine: Optional[str] = None, encoding: Optional[str] = None) -> Union[bytes, str] | Return the source piped through the Graphviz layout command. | | Args: | format: The output format used for rendering | (``'pdf'``, ``'png'``, etc.). | renderer: The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | formatter: The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | neato_no_op: Neato layout engine no-op flag. | quiet (bool): Suppress ``stderr`` output | from the layout subprocess. | engine: Layout engine for rendering | (``'dot'``, ``'neato'``, ...). | encoding: Encoding for decoding the stdout. | | Returns: | Bytes or if encoding is given decoded string | (stdout of the layout command). | | Raises: | ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` | are unknown. | graphviz.RequiredArgumentError: If ``formatter`` is given | but ``renderer`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the rendering ``dot`` subprocess is non-zero. | | Example: | >>> doctest_mark_exe() | >>> import graphviz | >>> source = 'graph { spam }' | >>> graphviz.Source(source, format='svg').pipe()[:14] | b'>> graphviz.Source(source, format='svg').pipe(encoding='ascii')[:14] | '>> graphviz.Source(source, format='svg').pipe(encoding='utf-8')[:14] | ' 'graphviz.Source' | Return a new :class:`.Source` instance with the source | piped through the Graphviz *unflatten* preprocessor. | | Args: | stagger: Stagger the minimum length | of leaf edges between 1 and this small integer. | fanout: Fanout nodes with indegree = outdegree = 1 | when staggering (requires ``stagger``). | chain: Form disconnected nodes into chains | of up to this many nodes. | | Returns: | Prepocessed DOT source code (improved layout aspect ratio). | | Raises: | graphviz.RequiredArgumentError: If ``fanout`` is given | but ``stagger`` is None. | graphviz.ExecutableNotFound: If the Graphviz ``unflatten`` executable | is not found. | graphviz.CalledProcessError: If the returncode (exit status) | of the unflattening 'unflatten' subprocess is non-zero. | | See also: | Upstream documentation: | https://www.graphviz.org/pdf/unflatten.1.pdf | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.encoding.Encoding: | | encoding | The encoding for the saved source file. | | ---------------------------------------------------------------------- | Methods inherited from graphviz.base.Base: | | __str__(self) -> str | The DOT source code as string. | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.base.LineIterable: | | __dict__ | dictionary for instance variables | | __weakref__ | list of weak references to the object | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.engines.Engine: | | engine | The layout engine used for rendering | (``'dot'``, ``'neato'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formats.Format: | | format | The output format used for rendering | (``'pdf'``, ``'png'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.renderers.Renderer: | | renderer | The output renderer used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Data descriptors inherited from graphviz.parameters.formatters.Formatter: | | formatter | The output formatter used for rendering | (``'cairo'``, ``'gd'``, ...). | | ---------------------------------------------------------------------- | Methods inherited from graphviz.copying.CopyBase: | | copy(self) | Return a copied instance of the object. | | Returns: | An independent copy of the current object. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/attributes.rst0000666000000000000000000000302314575646724014112 0ustar00Attributes ---------- To directly add DOT_ ``att_stmt`` attribute statements, call the :meth:`~.Graph.attr` method of the :class:`.Graph` or :class:`.Digraph` instance with the wanted target as first argument and the attributes as keyword args. .. hint:: Attribute statements affect all **later** graphs, nodes, or edges within the same (sub-)graph. .. doctest:: >>> import graphviz >>> ni = graphviz.Graph('ni') # doctest: +NO_EXE >>> ni.attr('node', shape='rarrow') >>> ni.node('1', 'Ni!') >>> ni.node('2', 'Ni!') >>> ni.node('3', 'Ni!', shape='egg') >>> ni.attr('node', shape='star') >>> ni.node('4', 'Ni!') >>> ni.node('5', 'Ni!') If you omit the first :meth:`~.Graph.attr` argument, the method can be used to set arbitrary attributes as key-value pairs targeting the current (sub-)graph (e.g. for ``rankdir``, ``label``, or setting ``rank='same'`` within a subgraph context, :ref:`example `): .. doctest:: >>> ni.attr(rankdir='LR') # doctest: +NO_EXE >>> ni.edges(['12', '23', '34', '45']) >>> print(ni.source) # doctest: +NORMALIZE_WHITESPACE graph ni { node [shape=rarrow] 1 [label="Ni!"] 2 [label="Ni!"] 3 [label="Ni!" shape=egg] node [shape=star] 4 [label="Ni!"] 5 [label="Ni!"] rankdir=LR 1 -- 2 2 -- 3 3 -- 4 4 -- 5 } .. image:: _static/ni.svg :align: center .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/basic_usage.rst0000666000000000000000000000663414575646724014204 0ustar00Basic usage ----------- The :doc:`graphviz ` package provides two main classes: :class:`graphviz.Graph` and :class:`graphviz.Digraph`. They create graph descriptions in the DOT_ language for undirected and directed graphs respectively. They have the same :doc:`API `. .. hint:: :class:`.Graph` and :class:`.Digraph` produce different DOT syntax and have different values for :attr:`~.Graph.directed`. Create a graph by instantiating a new :class:`.Graph` or :class:`.Digraph` object: .. doctest:: >>> import graphviz >>> dot = graphviz.Digraph('round-table', comment='The Round Table') # doctest: +NO_EXE >>> dot # doctest: +ELLIPSIS Their constructors allow to set the graph's :attr:`~.Graph.name` identifier, the :attr:`~.Graph.filename` for the DOT source and the rendered graph, an optional :attr:`~.Graph.comment` for the first source code line, etc. Add nodes and edges to the graph object using its :meth:`~.Graph.node` and :meth:`~.Graph.edge` or :meth:`~.Graph.edges` methods: .. doctest:: >>> dot.node('A', 'King Arthur') # doctest: +NO_EXE >>> dot.node('B', 'Sir Bedevere the Wise') >>> dot.node('L', 'Sir Lancelot the Brave') >>> dot.edges(['AB', 'AL']) >>> dot.edge('B', 'L', constraint='false') The :meth:`~.Graph.node` method takes a ``name`` identifier as first argument and an optional ``label``. The :meth:`~.Graph.edge` method takes the names of start node and end node, while :meth:`~.Graph.edges` takes an iterable of name pairs. Keyword arguments are turned into (node and edge) attributes (see extensive `Graphviz docs on available attributes `_). Check the generated DOT source code: .. doctest:: >>> print(dot.source) # doctest: +NORMALIZE_WHITESPACE +NO_EXE // The Round Table digraph "round-table" { A [label="King Arthur"] B [label="Sir Bedevere the Wise"] L [label="Sir Lancelot the Brave"] A -> B A -> L B -> L [constraint=false] } Use the :meth:`~.Graph.render` method to save the DOT source code and render it with the default ``dot`` `layout engine `_ (see :ref:`below ` for using other layout engines). .. attention:: Skip/ignore any ``doctest_mark_exe()`` lines in documentation code examples. .. doctest:: >>> doctest_mark_exe() # skip this line >>> dot.render(directory='doctest-output').replace('\\', '/') 'doctest-output/round-table.gv.pdf' Passing ``view=True`` will automatically open the resulting (PDF, SVG, PNG, etc.) file with your system's default viewer application for the rendered file type. .. doctest:: >>> doctest_mark_exe() # skip this line >>> dot.render(directory='doctest-output', view=True) # doctest: +SKIP 'doctest-output/round-table.gv.pdf' .. image:: _static/round-table.svg :align: center .. include:: _links.rst .. attention:: Backslash-escapes and strings of the form ``<...>`` have a special meaning in the DOT_ language and are currently passed on as is by this library. If you need to render arbitrary strings literally (e.g. from user input), consider wrapping them with the :func:`graphviz.escape` function first. See the sections on :ref:`backslash-escapes` and :ref:`quoting-and-html-like-labels` below for details. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1635006830.0 graphviz-0.21/docs/changelog.rst0000666000000000000000000000005714135034556013641 0ustar00.. _changelog: .. include:: ../CHANGES.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979442.0 graphviz-0.21/docs/conf.py0000666000000000000000000000562015023510462012450 0ustar00# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath(os.pardir)) import graphviz # -- Project information ----------------------------------------------------- project = 'graphviz' copyright = '2013-2025, Sebastian Bank' author = 'Sebastian Bank' # The full version, including alpha/beta/rc tags release = '0.21' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx_autodoc_typehints', # https://github.com/agronholm/sphinx-autodoc-typehints/issues/15 'sphinx.ext.viewcode', ] doctest_global_setup = '''\ import doctest as _doctest _doctest.register_optionflag('NO_EXE') def doctest_mark_exe(**kwargs): pass ''' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Extension configuration ------------------------------------------------- # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'py': ('https://docs.python.org/3', None), } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/docs/custom_dot.rst0000666000000000000000000000214714150757034014074 0ustar00Custom DOT statements --------------------- To add arbitrary statements to the created DOT_ source, you can use the :attr:`~.Graph.body` attribute of :class:`.Graph` and :class:`.Digraph` objects. It holds the verbatim :class:`list` of (:class:`str`) lines to be written to the source file (including their final newline). Use its ``append()`` or ``extend()`` method: .. doctest:: >>> import graphviz >>> rt = graphviz.Digraph(comment='The Round Table') # doctest: +NO_EXE >>> rt.body.append('\t"King Arthur" -> {\n\t\t"Sir Bedevere", "Sir Lancelot"\n\t}\n') >>> rt.edge('Sir Bedevere', 'Sir Lancelot', constraint='false') >>> print(rt.source) # doctest: +NORMALIZE_WHITESPACE // The Round Table digraph { "King Arthur" -> { "Sir Bedevere", "Sir Lancelot" } "Sir Bedevere" -> "Sir Lancelot" [constraint=false] } .. attention:: Note that you might need to correctly quote/escape identifiers and strings containing whitespace or other special characters when using this method. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1748876599.0 graphviz-0.21/docs/development.rst0000666000000000000000000001617515017336467014252 0ustar00.. _development: Development =========== |PyPI version| |License| |Supported Python| |Wheel| |Downloads| - GitHub: https://github.com/xflr6/graphviz - Changelog: https://graphviz.readthedocs.io/en/latest/changelog.html - Issue Tracker: https://github.com/xflr6/graphviz/issues Installation ------------ |Binder-HEAD| Development environment **binder** : https://mybinder.org/v2/gh/xflr6/graphviz/HEAD Local installation ^^^^^^^^^^^^^^^^^^ Install in a venv_ in development mode (includes all ``extras_require``): .. code:: bash $ git clone https://github.com/xflr6/graphviz.git $ cd graphviz $ python -m venv .venv $ source .venv/bin/activate $ python -m pip install -r requirements.txt .. admonition:: Platform: Windows ``.venv\Script\activate.bat`` to replace ``source .venv/bin/activate`` .. hint:: alternatively: ``pip install -e .[dev,test,docs]`` (same as ``pip install -r requirements.txt``) Tests ----- |Build| |Codecov| - GitHub Actions `Build workflow `_ (Python 3.9 to 3.13, experimental: PyPy 3.9 to 3.10) - Codecov `test coverage `_ (`main branch `_) **Run the tests** (in the current environment): .. code:: bash $ python run-tests.py Run **only tests** that are expected to ``PASS`` or ``XFAIL`` **without Graphviz** executables: .. code:: bash $ python run-tests.py --skip-exe **Run the tests** with tox_ (**installing** into a virtualenv_ or many of them): .. code:: bash $ python -m tox **Run the static type checker** (pytype_, supported `platforms `_ and `Python versions `_): .. code:: bash $ pip install pytype $ pytype **Run the code linter** (flake8_): .. code:: bash $ python lint-code.py Documentation ------------- |Readthedocs-stable| |Readthedocs-latest| - Read the Docs Project Home: https://readthedocs.org/projects/graphviz/ - stable: https://graphviz.readthedocs.io - latest: https://graphviz.readthedocs.io/en/latest/ **Build the documentation** with sphinx_ and sphinx-rtd-theme_ (in the current environment): .. code:: bash $ python build-docs.py Overview -------- Use ``help()`` in the REPL to shows/structure methods and attributes in dependency order: - Introduction: https://graphviz.readthedocs.io/en/latest/api.html#online-help-internal - ``Graph``: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#graph-1 - ``Digraph``: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#digraph-1 - ``Source``: https://github.com/xflr6/graphviz/blob/master/docs/api.rst#source-1 .. tip:: In the above, cooperative multiple inheritance classes reveal their (diamond) MRO structure and methods are shown in **method resolution order** (MRO), which should be an extension of their dependency relation... TLDR; you might find this presentation helps to follow the implementation. .. include:: _links.rst .. |PyPI version| image:: https://img.shields.io/pypi/v/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Latest PyPI Version .. |License| image:: https://img.shields.io/pypi/l/graphviz.svg :target: https://github.com/xflr6/graphviz/blob/master/LICENSE.txt :alt: License .. |Supported Python| image:: https://img.shields.io/pypi/pyversions/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Supported Python Versions .. |Wheel| image:: https://img.shields.io/pypi/wheel/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Wheel format .. |Downloads| image:: https://img.shields.io/pypi/dm/graphviz.svg :target: https://pypi.org/project/graphviz/#files :alt: Monthly downloads .. |Build| image:: https://github.com/xflr6/graphviz/actions/workflows/build.yaml/badge.svg?branch=master :target: https://github.com/xflr6/graphviz/actions/workflows/build.yaml?query=branch%3Amaster :alt: Build .. |Codecov| image:: https://codecov.io/gh/xflr6/graphviz/branch/master/graph/badge.svg :target: https://codecov.io/gh/xflr6/graphviz :alt: Codecov .. |Readthedocs-stable| image:: https://readthedocs.org/projects/graphviz/badge/?version=stable :target: https://graphviz.readthedocs.io/en/stable/ :alt: Readthedocs (stable) .. |Readthedocs-latest| image:: https://readthedocs.org/projects/graphviz/badge/?version=latest :target: https://graphviz.readthedocs.io/en/latest/ :alt: Readthedocs (latest) .. |Binder-HEAD| image:: https://img.shields.io/badge/launch-binder%20(HEAD)-E66581.svg?logo=data:image/png;base64,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 :target: https://mybinder.org/v2/gh/xflr6/graphviz/HEAD :alt: Binder (HEAD) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/docs/engines.rst0000666000000000000000000000100014150757034013327 0ustar00Engines ------- To use a different `layout engine `_ than the default ``dot`` when rendering your graph, you can use the ``engine`` argument on the constructor of :class:`.Graph` or :class:`.Digraph`. .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> g = graphviz.Graph(engine='neato') You can also change the :attr:`~.Graph.engine` attribute on an existing instance: .. doctest:: >>> g.engine = 'circo' # doctest: +NO_EXE .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/escapes.rst0000666000000000000000000000535614575646724013362 0ustar00Backslash escapes ----------------- The Graphviz_ layout `engines `_ support a number of `escape sequences `_ such as ``\n``, ``\l``, ``\r`` (for placement of multi-line labels: *centered*, *left*-justified, *right*-justified) and ``\N``, ``\G``, ``\L`` (expanded to the current *node* name, *graph* name, object *label*). To be able to use them from this library (e.g., for labels), backslashes in strings are (mostly) passed on **as is**. .. attention:: This means that **literal** backslashes need to be **escaped** (doubled) by the user. As the backslash is also special in Python :class:`string ` literals a **second** level of doubling is needed. E.g. ``label='\\\\'`` for a label that is rendered as single literal backlash: ``\``. .. tip:: Doubling of backslashes can be avoided by using `raw string literals`_ (``r'...'``) instead. This is similar to the solution proposed for the stdlib :mod:`re` module. See also https://en.wikipedia.org/wiki/Leaning_toothpick_syndrome. .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> e = graphviz.Digraph('escapes') >>> e.node('backslash', label=r'\\') >>> e.node('multi_line', label=r'centered\nleft\lright\r') >>> print(e.source) # doctest: +NORMALIZE_WHITESPACE digraph escapes { backslash [label="\\"] multi_line [label="centered\nleft\lright\r"] } .. image:: _static/escapes.svg :align: center To disable any special character meaning in a string (e.g. from user input to be rendered literally), use the :func:`graphviz.escape` function (similar to the :func:`re.escape` function): .. doctest:: >>> bs = graphviz.Digraph('literal_backslash') # doctest: +NO_EXE >>> bs.node(graphviz.escape('\\')) >>> print(bs.source) # doctest: +NORMALIZE_WHITESPACE digraph literal_backslash { "\\" } .. doctest:: >>> doctest_mark_exe() # skip this line >>> bs.render(format='svg', directory='doctest-output').replace('\\', '/') 'doctest-output/literal_backslash.gv.svg' .. image:: _static/literal_backslash.svg :align: center .. admonition:: Historical note To prevent breaking the internal quoting mechanism, the special meaning of ``\"`` as a backslash-escaped quote has been disabled since version ``0.14`` of this library. E.g. both ``label='"'`` and ``label='\\"'`` now produce the same DOT source ``[label="\""]`` (a label that renders as a literal quote). See also `examples/graphviz-escapes.ipynb `_ (`nbviewer `_). .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/examples.rst0000666000000000000000000000523614575646724013552 0ustar00.. _examples: Examples ======== .. tip:: The following code examples are included in the ``examples/`` directory of the `source repository/distribution `_. .. note:: Most of them recreate examples from the `graphviz.org gallery`_ or the `graphviz.org documentation`_. .. include:: _links.rst hello.py -------- .. literalinclude:: ../examples/hello.py :lines: 3- .. image:: _static/hello.svg :align: center process.py ---------- .. literalinclude:: ../examples/process.py :lines: 3- .. image:: _static/process.svg :align: center fsm.py ------ .. literalinclude:: ../examples/fsm.py :lines: 3- .. image:: _static/fsm.svg :align: center .. _cluster.py: cluster.py ---------- .. literalinclude:: ../examples/cluster.py :lines: 3- .. image:: _static/cluster.svg :align: center er.py ----- .. literalinclude:: ../examples/er.py :lines: 3- .. image:: _static/er.svg :align: center unix.py ------- .. literalinclude:: ../examples/unix.py :lines: 3- .. image:: _static/unix.svg :align: center structs.py ---------- .. literalinclude:: ../examples/structs.py :lines: 3- .. image:: _static/structs.svg :align: center structs_revisited.py -------------------- .. literalinclude:: ../examples/structs_revisited.py :lines: 3- .. image:: _static/structs_revisited.svg :align: center .. _btree.py: btree.py -------- .. literalinclude:: ../examples/btree.py :lines: 3- .. image:: _static/btree.svg :align: center traffic_lights.py ----------------- .. literalinclude:: ../examples/traffic_lights.py :lines: 3- .. image:: _static/traffic_lights.svg :align: center fdpclust.py ----------- .. literalinclude:: ../examples/fdpclust.py :lines: 3- .. image:: _static/fdpclust.svg :align: center cluster_edge.py --------------- .. literalinclude:: ../examples/cluster_edge.py :lines: 3- .. image:: _static/cluster_edge.svg :align: center g_c_n.py -------- .. literalinclude:: ../examples/g_c_n.py :lines: 3- .. image:: _static/g_c_n.svg :align: center angles.py --------- .. literalinclude:: ../examples/angles.py :lines: 3- .. image:: _static/angles.svg :align: center .. _rank_same.py: rank_same.py ------------ .. literalinclude:: ../examples/rank_same.py :lines: 3- .. image:: _static/rank_same.svg :align: center colors.py --------- .. literalinclude:: ../examples/colors.py :lines: 3- .. image:: _static/colors.svg :align: center ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/existing_files.rst0000666000000000000000000000351414575646724014745 0ustar00Existing files -------------- To directly render an existing DOT_ source file (e.g. created with other tools), you can use the :func:`graphviz.render` function. .. doctest:: >>> doctest_mark_exe() # skip this line >>> import pathlib >>> import graphviz >>> src = 'digraph "the holy hand grenade" { rankdir=LR; 1 -> 2 -> 3 -> lob }' >>> filepath = pathlib.Path('doctest-output/the-holy-hand-grenade.gv') >>> filepath.write_text(src, encoding='ascii') 66 >>> graphviz.render('dot', 'png', filepath).replace('\\', '/') 'doctest-output/the-holy-hand-grenade.gv.png' To directly display the rendered visualization of an existing DOT_ source file inside a Jupyter `notebook `_ or `Qt Console `_, you can use :meth:`graphviz.Source.from_file` (alternative constructor): .. image:: _static/qtconsole-source.png :align: center Note that :meth:`~.Source.render` and :meth:`~.Source.view` on :class:`.Source` instances returned by :meth:`graphviz.Source.from_file` skip writing the loaded file back. The same holds for :meth:`~.Source.save`. The instances resolve default ``.save(skip_existing=None)`` to ``.save(skip_existing_run=True)`` to skip writing the read :attr:`~.Source.source` back into the same file (specifically the same path that it was loaded from). Call ``.save(skip_existing=False)`` if you want to re-write the loaded source. .. admonition:: Historical note Before version ``0.18`` of this library, :meth:`.Source.save`, :meth:`.Source.render`, and :meth:`.Source.view`, wrote the content read into source back into the file. It was advised to use :func:`graphviz.render` and :func:`graphviz.view` to directly work on files if the superfluous saving needed to be avoided. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/formats.rst0000666000000000000000000000130014575646724013373 0ustar00Formats ------- To use a different `output file format `_ than the default PDF, you can use the ``format`` argument when creating your :class:`.Graph` or :class:`.Digraph` object: .. doctest:: >>> import graphviz >>> g = graphviz.Graph(format='png') # doctest: +NO_EXE You can also change the :attr:`~.Graph.format` attribute on an existing graph object: .. doctest:: >>> doctest_mark_exe() # skip this line >>> dot = graphviz.Digraph('hello') >>> dot.edge('hello', 'world') >>> dot.format = 'svg' >>> dot.render(directory='doctest-output').replace('\\', '/') 'doctest-output/hello.gv.svg' .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1655040335.0 graphviz-0.21/docs/index.rst0000666000000000000000000000107714251364517013026 0ustar00.. graphviz documentation master file .. include:: ../README.rst User Guide ========== .. toctree:: :maxdepth: 2 manual Examples ======== .. toctree:: :maxdepth: 2 examples Notebooks ========= .. toctree:: :maxdepth: 2 notebooks API Reference ============= .. toctree:: :maxdepth: 2 api Project Info ============ .. toctree:: :maxdepth: 2 changelog license Development =========== .. toctree:: :maxdepth: 2 development release_process ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1748876599.0 graphviz-0.21/docs/installation.rst0000666000000000000000000000364215017336467014424 0ustar00Installation ------------ :doc:`graphviz ` provides a simple pure-Python interface for the Graphviz_ graph-drawing software. It runs under Python 3.9+. To install it with pip_, run the following: .. code:: bash $ pip install graphviz For a system-wide install, this typically requires administrator access. For an isolated install, you can run the same inside a :mod:`venv` or a virtualenv_. The only dependency is a working installation of Graphviz_ (`download page `_, `archived versions `_, `installation procedure for Windows `_). After installing Graphviz, make sure that its ``bin/`` subdirectory containing the ``dot`` `layout command `_ for rendering graph descriptions is on your systems' ``PATH`` (sometimes done by the installer; setting ``PATH`` on `Linux `_, `Mac `_, and `Windows `_): On the command-line, ``dot -V`` should print the version of your Graphiz installation. .. admonition:: Platform: Windows Windows users might want to check the status of known issues (gvedit.exe__, sfdp__, commands__) and consider trying an older archived version as a workaround (e.g. graphviz-2.38.msi__). __ https://gitlab.com/graphviz/graphviz/-/issues/1816 __ https://gitlab.com/graphviz/graphviz/-/issues/1269 __ https://gitlab.com/graphviz/graphviz/-/issues/1753 __ https://www2.graphviz.org/Archive/stable/windows/graphviz-2.38.msi .. admonition:: Platform: Anaconda See the downstream conda-forge_ distribution `conda-forge/python-graphviz `_ (`feedstock `_), which should automatically ``conda install`` `conda-forge/graphviz `_ (`feedstock `_) as dependency. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/integration_with_viewers.rst0000666000000000000000000000262214575646724017052 0ustar00Integration with viewers ------------------------ On platforms such as Windows, viewer programs opened by :meth:`~.Graph.render` with ``view=True`` (or equivalently with the :meth:`~.Graph.view` shortcut-method) might **lock** the (PDF, PNG, etc.) file for as long as the viewer is open (blocking re-rendering it with a ``Permission denied`` error). .. tip:: You can use the :func:`tempfile.mktemp` function from the stdlib :mod:`tempfile` module to render to a different file for each invocation. This avoids needing to close the viewer window each time within such an incremental workflow (and also serves to preserves the intermediate steps). .. doctest:: >>> import tempfile # doctest: +NO_EXE >>> import graphviz >>> g = graphviz.Graph() >>> g.node('spam') .. doctest:: >>> doctest_mark_exe() # skip this line >>> g.view(tempfile.mktemp('.gv')) # doctest: +SKIP 'C:\\Users\\User\\AppData\\Local\\Temp\\tmp3aoie8d0.gv.pdf' >>> g.view(tempfile.mktemp('.gv')) # doctest: +SKIP 'C:\\Users\\User\\AppData\\Local\\Temp\\tmphh4ig7a_.gv.pdf' Other options: - use a viewer that `support live updates `_ - use the `Jupyter notebook`_ or `Qt Console `_ (display the current version of the rendered graph in repeated add/render/view cycles) .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745680.0 graphviz-0.21/docs/jupyter_notebooks.rst0000666000000000000000000000227214432626720015500 0ustar00Jupyter notebooks ----------------- :class:`.Graph` and :class:`.Digraph` objects have a :meth:`~.Graph._repr_mimebundle_` method so they can be rendered and displayed directly inside a `Jupyter notebook`_. For an example, check the ``examples/graphviz-notebook.ipynb`` file in the `source repository/distribution `_ (or the same notebook in `nbviewer `_). This also allows direct displaying within the `Jupyter Qt Console`_ (also `the one `_ inside `Spyder IDE`_): .. image:: _static/qtconsole.png :align: center By default :meth:`~.Graph._repr_mimebundle_` uses ``'svg'`` format. You can use the :func:`graphviz.set_jupyter_format` to override the default format that is used for displaying in IPython/Jupyter. (`example `_, `nbviewer `_). .. hint:: You can also use ``display_svg()``, ``display_png()``, or ``.display_jpeg()`` from `IPython.display`_ to display the rendered :class:`.Graph` or :class:`.Digraph` as SVG, PNG or JPEG in IPython/Jupyter. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626114366.0 graphviz-0.21/docs/license.rst0000666000000000000000000000010114073104476013322 0ustar00.. _license: License ======= .. include:: ../LICENSE.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1658576039.0 graphviz-0.21/docs/manual.rst0000666000000000000000000000141714266756247013205 0ustar00.. _manual: User Guide ========== .. include:: installation.rst .. include:: basic_usage.rst .. include:: formats.rst .. include:: piped_output.rst .. include:: jupyter_notebooks.rst .. include:: styling.rst .. _attributes: .. include:: attributes.rst .. _node-ports-compass: .. include:: node_ports.rst .. _backslash-escapes: .. include:: escapes.rst .. _quoting-and-html-like-labels: .. include:: quoting.rst .. _subgraphs-clusters: .. include:: subgraphs_and_clusters.rst .. _engines: .. include:: engines.rst .. include:: neato_no_op.rst .. include:: unflatten.rst .. include:: custom_dot.rst .. _using-raw-dot: .. include:: raw_dot.rst .. include:: existing_files.rst .. include:: integration_with_viewers.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/neato_no_op.rst0000666000000000000000000000214214575646724014225 0ustar00``neato`` no-op flag -------------------- The `neato `_ layout engine supports an additional `rendering flag `_ that allows more control over the node positioning and the edge layout via the `pos `_, `overlap `_, and `splines `_ attributes. Use the ``neato_no_op`` keyword argument of :meth:`~.Graph.render` or :meth:`~.Graph.pipe` to pass it to the layout command: .. doctest:: >>> doctest_mark_exe() # skip this line >>> import graphviz >>> n = graphviz.Digraph(name='splines', engine='neato', ... graph_attr={'splines': 'true'}, ... node_attr={'shape': 'point'}) >>> n.node('a', pos='0,0!', color='blue') >>> n.node('b', pos='100,0!', color='green') >>> n.node('c', pos='50,50!', color='red') >>> n.edge('a', 'b', pos='0,0 30,66 70,60 100,0') >>> n.render(neato_no_op=2, directory='doctest-output').replace('\\', '/') 'doctest-output/splines.gv.pdf' .. image:: _static/splines.svg :align: center .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/docs/node_ports.rst0000666000000000000000000000224714150757034014071 0ustar00Node ports & compass -------------------- The :meth:`~.Graph.edge` and :meth:`~.Graph.edges` methods use the colon-separated ``node[:port[:compass]]`` format for ``tail`` and ``head`` nodes. This allows to specify an optional node ``port`` plus an optional ``compass`` point the edge should aim at for the given tail or head node (:ref:`example `). .. caution:: As colons are used to indicate ``port`` and ``compass`` for edges, node names containing one or more literal colons ``:`` are currently not supported. `GH #54 `_ .. tip:: There is no such restriction for the ``label`` argument, so you can work around by choosing a colon-free ``name`` together with the wanted ``label`` as demonstrated below .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> cpp = graphviz.Digraph('C++') >>> cpp.node('A', 'std::string') >>> cpp.node('B', '"spam"') >>> cpp.edge('A', 'B') >>> print(cpp.source) # doctest: +NORMALIZE_WHITESPACE digraph "C++" { A [label="std::string"] B [label="\"spam\""] A -> B } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649449412.0 graphviz-0.21/docs/notebooks.rst0000666000000000000000000000142314224114704013704 0ustar00.. _notebooks: Notebooks ========= - Render `graphviz.org gallery`_ examples with logging: `examples/graphviz-notebook.ipynb `_ (`source `_) - `Layout engine `_ comparison: `examples/graphviz-engines.ipynb `_ (`source `_) - Example for :meth:`graphviz.set_jupyter_format`: `examples/graphviz-jupyter-format.ipynb `_ (`source `_) - Verify `escaping `_ and quoting: `examples/graphviz-escapes.ipynb `_ (`source `_) .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749978282.0 graphviz-0.21/docs/piped_output.rst0000666000000000000000000000235215023506252014425 0ustar00Piped output ------------ To directly access the raw results from the Graphviz_ ``dot`` `layout command `_ as binary :class:`bytes` or as decoded :class:`str` (for plain-text formats like SVG) instead of writing to a file, use the :meth:`~.Graph.pipe` method of your :class:`.Graph` or :class:`.Digraph` object: .. doctest:: >>> import graphviz >>> h = graphviz.Graph('hello', format='svg') # doctest: +NO_EXE >>> h.edge('Hello', 'World') .. doctest:: >>> doctest_mark_exe() # skip this line >>> h.pipe(format='pdf')[:4] b'%PDF' >>> print(h.pipe(encoding='utf-8')) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS .. tip:: Because :meth:`~.Graph.pipe` returns the raw ``stdout`` from the layout subprocess by default (:class:`bytes`), you usually want to decode the return value when piping into formats like ``'svg'``, ``'svg_inline'`` or ``'plain'``, e.g. by passing ``encoding='utf-8'``. .. caution:: The output for :meth:`~.Graph.pipe` is buffered in memory, so avoid this method if the data size is large. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/quoting.rst0000666000000000000000000000403714575646724013420 0ustar00Quoting and HTML-like labels ---------------------------- The graph-building methods of :class:`.Graph` and :class:`.Digraph` objects automatically take care of quoting (and escaping quotes) `where needed `_ (whitespace, keywords, double quotes, etc.): .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> q = graphviz.Digraph() >>> q.edge('spam', 'eggs eggs') >>> q.edge('node', '"here\'s a quote"') >>> print(q.source) # doctest: +NORMALIZE_WHITESPACE digraph { spam -> "eggs eggs" "node" -> "\"here's a quote\"" } If a string starts with ``'<'`` and ends with ``'>'``, it is passed on **as is**, i.e. without quoting/escaping: The content between the angle brackets is treated by the Graphviz_ layout `engine `_ as special **HTML string** that can be used for `HTML-like labels `_: .. doctest:: >>> h = graphviz.Graph('html_table') # doctest: +NO_EXE >>> h.node('tab', label='''< ... ... ... ... ...
leftright
>''') .. image:: _static/html_table.svg :align: center For strings that should literally begin with ``'<'`` and end with ``'>'``, use the :func:`graphviz.nohtml` function to disable the special meaning of angled parenthesis and apply normal quoting/escaping: .. doctest:: >>> d = graphviz.Digraph('diamond', format='svg') # doctest: +NO_EXE >>> d.node('diamond', label=graphviz.nohtml('<>')) >>> print(d.source) # doctest: +NORMALIZE_WHITESPACE digraph diamond { diamond [label="<>"] } .. doctest:: >>> doctest_mark_exe() # skip this line >>> d.render(directory='doctest-output').replace('\\', '/') 'doctest-output/diamond.gv.svg' .. image:: _static/diamond.svg :align: center .. admonition:: Historical note Before version ``0.8.2``, the only workaround was to add leading or trailing space (``label=' <>'``): .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/raw_dot.rst0000666000000000000000000000274414575646724013374 0ustar00Using raw DOT ------------- To render a ready-made DOT_ source code string (instead of assembling one with the higher-level interface of :class:`.Graph` or :class:`.Digraph`), create a :class:`graphviz.Source` object holding your DOT string: .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> src = graphviz.Source('digraph "the holy hand grenade" { rankdir=LR; 1 -> 2 -> 3 -> lob }') >>> src #doctest: +ELLIPSIS Use the :meth:`~.Source.render` method to save and render it: .. doctest:: >>> doctest_mark_exe() # skip this line >>> src.render('doctest-output/holy-grenade.gv').replace('\\', '/') 'doctest-output/holy-grenade.gv.pdf' .. doctest:: >>> doctest_mark_exe() # skip this line >>> src.render('doctest-output/holy-grenade.gv', view=True).replace('\\', '/') # doctest: +SKIP 'doctest-output/holy-grenade.gv.pdf' .. image:: _static/holy-grenade.svg :align: center .. hint:: Apart from lacking editing methods, :class:`.Source` objects have the same basic API as the higher-level :class:`.Graph` and :class:`.Digraph` objects (e.g. :meth:`~.Source.save`, :meth:`~.Source.render`, :meth:`~.Source.view`, :meth:`~.Source.pipe` methods, :attr:`~.Source.engine` and :attr:`~.Source.format` attributes, Jupyter notebook :meth:`~.Source._repr_mimebundle_`, etc. See :class:`API docs <.Source>`). .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749759863.0 graphviz-0.21/docs/release_process.rst0000666000000000000000000000721515022633567015076 0ustar00Release process =============== Build ----- Update build dependencies: .. code:: bash $ pip install -U build setuptools wheel twine Create ``release`` branch from main: .. code:: bash $ git checkout -b release **Cleanup** (remove all untracked files and directories): .. code:: bash $ git clean -f -d -x Update ``help()`` output: .. code:: bash $ python update-help.py Set **release version** (remove ``.dev0`` from ``$MAJOR.$MINOR[.$BUGFIX]`` version): - ``docs/conf.py`` - ``graphviz/__init__.py`` Document release: - remove ``(in development)`` from ``CHANGES.rst`` header Run the tests, lint the code, and build the documentation: .. code:: bash $ python -m tox -r -- -W error # --recreate, raise error on warning $ python lint-code.py --disable-noqa $ python build-docs.py -b doctest $ python build-docs.py $ git clean -f -d -x Commit to ``release`` branch and push to ``origin``: .. code:: bash $ git add * $ git commit -m "release $MAJOR.$MINOR[.$BUGFIX]" $ git push --set-upstream origin release - Check GitHub Actions ``relase`` `Build workflow `_ - Check Codecov ``release`` build `test coverage `_ **Build** and check the release files: .. code:: bash $ python -m build $ python -m twine check --strict dist/* - ``dist/graphviz-$MAJOR.$MINOR[.$BUGFIX].zip`` - ``dist/graphviz-$MAJOR.$MINOR[.$BUGFIX]-py3-none-any.whl`` If changes are needed (and go back to: **Cleanup** step): .. code:: bash $ git commit --amend --date=now **Tag** with annotated release version tag: .. code:: bash $ git tag -a -m "$MAJOR.$MINOR[.$BUGFIX] release" Bump **post-release version** to ``$MAJOR.$MINOR.[.$BUGFIX].dev0``: - ``docs/conf.py`` - ``graphviz/__init__.py`` Document post-release: - add new ``Version $MAJOR.$MINOR[.$BUGFIX] (in development)`` heading to ``CHANGES.rst`` Commit version bump to ``relase`` branch: .. code:: bash $ git commit -m "bump version for development" Switch to main branch and merge ``release``: .. code:: bash $ git switch master $ git merge --ff-only release Publish ------- Publish the release with twine_: .. code:: bash $ python -m twine upload dist/* Push main branch and push all new tags: .. code:: bash $ git push --tags Update `stable `_ branch to the latest release: .. code:: bash $ git switch stable $ git merge --ff-only $MAJOR.$MINOR[.$BUGFIX] $ git push Verify ------ Verify publication: - Check `PyPI files `_ - Check GitHub `Main page `_ - Check GitHub Actions `main branch Build workflow `_ - Check Read the Docs `builds `_ - Check `latest release notes `_ - Check `stable release notes `_ - Check ``stable`` binder: https://mybinder.org/v2/gh/xflr6/graphviz/stable Install in default environment: .. code:: bash $ pip install -U graphviz $ python -c "import graphviz; print((graphviz.__version__, graphviz.version()))" Downstream ---------- - Check downstream `conda-forge release `_ .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626617321.0 graphviz-0.21/docs/round-table.png0000666000000000000000000003531014075032751014100 0ustar00PNG  IHDRcbKGD IDATxyXwos#Mܠ/ Ј3ɘ`bn&n$flld&c<L">Z1j@!(xhljΝ;L?J^^^Xx1xNSBz BFjj*?@$aXl~aiooG~~>Ξ=cǎ󰱱󑘘$&!TSS=}ᅦ#.\իW#11 :ddd ==PՈ?uuAʕ+qahZ,[ ?8.]JW[;qj*.]˗#99mmmBE4  !!A萈8|06mڄX|W1b!"@Z/*˗/=Jž={*,୷«!Ģ@ 믿iӦa֭ŠP(glܸy{DHLLDyyyaϜ9DD"=ӯRT*1z)s-* [l۱sNT}\[lc3c2C՝2П|ɉ)JNP@TfkkT*EXBBٟ7'ѧT*;t{n,͉'L$G 2}BBȰQZZ H$Atl߾o雜lus/իܩz q݄PjxzzbԨQtV@ Fcc#\\\~dd$d2_@8x ??sNzz:JKK- BAP@ OOO'00Gdd$~;򾊌DZZݻH۷#%%/rfggcݺu`ux^/1f1TWWKP6Ο?7nŦk^{SO=L8 oZzz:ϟ߫&-[w1:,{BL͏9qvBqq1hAb >d={6|||pMSR6o B97x] 9x LHOOO]t5.{B͒XTFvv6ⷔcԨQ1cСb9,!m۶yxx?{ׁ~w@.df= {dp=m֡bwd2>4/]SRR:ǜ^ MP0Dvai":@J0̜9oXR899!00롡VBtHHH@QQȕ S dxqssáCpqvĉ;닔v؁ӧO/ ;BA%=...xW燨(CW_}%(--DZi&>CO‡~+W !N-dXz O?4^yo6q?Byw /`زe" k?uV]k5HQ[[gyسg~!Bi {'NO<… ɓx'?/pD.R$dɒ%(,,̙3͛7C.  lڴ K,QXXH![-¡C_Ә8q" FQxW .СC/1rHC#*)B 477+~m888`֭HNNС~P]]>}cضmy888!քA Ĕ/ >c466bƍx饗0i$C#PTT H3_Сb@ ;---ꫯ;눊ƍaxzz J‘#G~HR?[n͛1b#ĚQ@t:N<{ȑ#˗chllĩS_ ==xGcѢECڄ 0* Jo{ŋE!!!<|}}qX8z(!J҂yaӦMHJJD":DB*+c;3f`…X`͛X,tCZ瑙gիpvvF||<_ &!F>}=Ν;ׯ3f@ll,bbbQF RQQ8w^ N)S 66Gy?T 2p9q t:#** 3f@TT8Ϗ3PVVB 77999P(ɓ `:dB**$ZW"''޺u 1i$1eL8C5 PZZb\~7n7H &Ԍ30}t :!MTBjj*lllT*m^^^ DPP ۛPSS( TUUAP 2 2 xnnn`aժU={60yd,BE!b߾}xg1vX8pǏݻwQVV`mMM mllBA"puu b1Ű燕H$OV*hhhFFRFA}}=_t:~X[[[MPP_;cǎL&Æ puxt9BB!BS(ؼy3=[3붿1~\SS*zh4T*jh4444@V_$u wRHbbݝ>>> ///Z5Z-z-ر-g}!!Q@{ۇ qFcϞ=HLL:$B+gBԄ^xK,ܹs7իWb ,_?8&!B,ظq# aݺuBd:dH$߿s:$BjA Rbܹ µkר8ªUp5 Za2lP !p]lܸx?66T1={ॗ^ԩSq!C 2ۇj\t IAD"Ftnoގ?,!)":APt*NK">> ,&W2#//8|0\ԊfccoooSpFR 4 Z-Fkooﲏ3ggg$2D9r$vvv=D?~DVȑ#l2,]-WK`RSSSNĉ8qjkkV N,_x\RjojjBssas1nnn& M"r\\\`cc77<@mm-߿Zwww~VzD~#+G;cɋ. ͟ZCѢRk[5hP__'3BB888Z ikk! /&bZ[[;䉪*(AommmthmmEDD|||: gg.ֹt-[ uix cXtժR)_pR FNuvv yǧoeX^^wUUUw *++ \g񁗗"ꯜ sjh򩫫JBmm-_pJ<@UUU)񁿿? F1c 00AAAԬ! 2 ?wո~a$ |qss{ivV Jen>x߇B@KK ?{{{>OGPPΩUB}}=JKKQVVLQ^^Zʢ pPYYJ( ?r娪j@h Bpp0BBB'\!rܺur666G@@@<1jԨ; "ZCr9a<==;\af:VEEEܹ"Ν;s޽ߘdȑ7nƍojQSSJ~sr9*++q 4#Ə0~7nNJT*qBQQ s=Sرc \)Gpe2:ۗL2CTq %ˑb!777n܀N?0w\̙3݊w_bɃȍ !Zrŕ+WP]] ;;;L8CU W\T*Ņ p%(J899a鈎Ftt4bbb(DJ˗K.ҥK>0m4 .. ,WKR!33R˃V7'fϞYf2%FYYY>gϦc_KK J+W1iӦaܹ|?~СoZ[[O?"33rE||<0iqIDAT$4Bkk+Μ9T?~rnnnXp!-8X9sJ{.\\\pB> .naAѣtR$%%a…y@Bss3N:TARaƌX|90sL0=o߆T*űcp)!66IIIxGos/kرBHhkk˗4HJJ¢E؟!++ ?ߢ111HJJʕ+ dȫVf̟?7oFRRҠ? ҂CaݸpFe˖!)) K.ppBiwÇl")) ɘ5kVLoF_~O`ʕ HKK|Ic|gPTXb֯_ŋQByݻÇO?E~~>e]/ZzAP^zI$6mIJ{3*B~gkfccy? V.]Ė.]lllѣora2d]xmذ9::2www/ތGFao&H$ߟ{0!Z-;t3g/^Rzj&؜9sط~˴Za2l԰wy#F?'0@tlϞ=ϏI$o2Fӻ-l۶ml۶mBaRuu5KIIa Bo{N % -_1XCC?#fkkVZeUВcnƶws?7PyRz(8< fcǎe]}tp5̙c5S;N_Me@P*F@Srre/VRR)FVHӱ^{D"k.qg˖-c)))GD"{z\az 2D*fќpKV۶m8Z̃9H12QtZ7o۶h¶m8^0FmJOp/_BBB̺+ lÆ ,--ӸZ[[Ypp0>\akը[qN131c~W묱xBۑt>-]}]}7{Mee%`򊱷; L">`냒 W ~ei8ngo8|ڲ򁞝>tWv7ݮͭlVVVvd4G`μ陓M}}7{;<==Yss[ n(++Lt}2йٜ+>_]]Ͷm:lL1C x"V*vjjCwÙy7pG2{,MS;*iӦ+V|e`X7nկ~"""M +":4Ow5SNWz'/<@֓\L}7{ K^jVرc Lt 7#L7;5q M}hy\ ^鯫XJJJ:l؆=y7%>>D"wyH$<K cvmvz3?;}9c繺^keddչ@v\bz05/wFoj܆Ww7L&3y4X9I"ɴqMRFmR˦;iX$5VKkoogGflww]B@@{O?Ǝt:w%W0knyMŗØq^0fz&WLospnݺeVAӱ'͛7[&^j믍؅Cyqs^լnԆnfcccv7)K4hu잣5)j70OJJJUD?j[Q,qG+pwp1[GWi?عsg}?fZ󄆸hp]+ֲcDzիWyޞD"`0; ,$$T~#D0]M^d2Yflkl1<םlv>2rMd^Ikm{&7|cm7JZj9r$zbKJJ:Y@}Iܣgt7ݻ~2_y;}F\_KJx,Sn93mcqp./c˳26mc!噼jT|}m}ټyؘ1cXUUYl3vUs'?3 `?pod B 2^+O{p99paX*/4Wwv\Os5amذGZD1#l2⫯ŋ}b%***J9sSL1k۷oڵkb Mqqq;v,9ބO4_w}ck15'''8q˗/ҥK/y"&6dgg]q1lذDDD ++ jHMM8!=VXuȑ#w^M4`111l&ZVˢ6e{1'''avގt/s>2̙3IIIXr%2jB-FǏСC8vlmm~z$''#22Rz姟~w|$%%aɒ%!d0+++Çq!deea„ x^^^}uk Nɓ'tT*̜9IIIHJJc2 YJGEjj*N< VXY=\]]_T*os%K )) <$!bn޼T"''HLLĺuJ TT|wxx,X`$;Bz999ȀT*ŋqqqHJJŠ+)p}RSSq`Μ9G\\`kk+tJV#33RO+V )) ?p=za` }mmmĉ'B"::qqqGttt}1 *_T TgBR! qqqXx1-[wwwD]]=SN!##UUU9r$bcc!44T0 pʂT*EFF\Ncɒ%X`@_ Aff&222piܽ{9s(Y2,BFA^^rssqE;w ...[ԦOH$tVΝ;sRfΜ9sCP \ .իhjj?Νx,[ FdX/ ݺu .\Kk׮ FLL Ox{{ *!&555?._~:AAA={6>Ϙ1vvvB<ʕ+Bvv6QQQ[[[L<_ӦMCXX  l\t Cxx8bbbsbܸqB*|`999|tr9ሌDxx81e:=A,12((([X(̞=io \l梡 +"""$Z[[QTTB"??5 |ˆ#+*.QXX" ĉ'bĉ@zy&n޼Rj"ƍCDD"""Ѹq bC:oF~~~ݻ` SLAhh('@zKPqJKK'''*Q ֆR`r;;9!!! Ahh(1vX" y9BrCAPQQEpp0# CDDNګ6"W__k׮EEE,H1cbĉF`` C"' ݻwQRR R 1bD;p [ CEEE#?.! ((G|||[Cuu5d2d2_ pϹBR$ߟ߸ <44ƍSXCTkk+n߾\jtvvsDPP+QFэj( TTT9B?Or ķRDƃ+CeeL&Z?ooo5 ?|}}xzzt%Gmm-Av<羾C>QAVhN0G[[qX,-jnwuuFGGG888t8b\X,6zg>n7sn^҂V444jh4HWWW9A'|||w@4SձRk4~gFƍmprrⓋ _us \\\\nɩw !Skoll4T*t:_sE=%9]\Aȑ#b4_ VV+C!B:irB!P@!N@ BH'v:B!X>`sIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/styling.rst0000666000000000000000000000207114575646724013417 0ustar00Styling ------- Use the ``graph_attr``, ``node_attr``, and ``edge_attr`` arguments of the :class:`.Graph` and :class:`.Digraph` constructors to change the default `attributes `_ for your graph, nodes, and edges. .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> ps = graphviz.Digraph('pet-shop', node_attr={'shape': 'plaintext'}) >>> ps.node('parrot') >>> ps.node('dead') >>> ps.edge('parrot', 'dead') After creation, the :attr:`~.Graph.graph_attr`, :attr:`~.Graph.node_attr`, and :attr:`~.Graph.edge_attr` attributes be edited on instances: .. doctest:: >>> ps.graph_attr['rankdir'] = 'LR' # doctest: +NO_EXE >>> ps.edge_attr.update(arrowhead='vee', arrowsize='2') >>> print(ps.source) # doctest: +NORMALIZE_WHITESPACE digraph "pet-shop" { graph [rankdir=LR] node [shape=plaintext] edge [arrowhead=vee arrowsize=2] parrot dead parrot -> dead } .. image:: _static/pet-shop.svg :align: center .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/subgraphs_and_clusters.rst0000666000000000000000000000444514575646724016501 0ustar00Subgraphs & clusters -------------------- :class:`.Graph` and :class:`.Digraph` objects have a :meth:`~.Graph.subgraph` method for adding a subgraph to the instance. There are two ways to use it: Either with a ready-made instance of the same kind as the only argument (whose content is added as a subgraph) or omitting the ``graph`` argument (returning a context manager for defining the subgraph content more elegantly within a ``with``-block). First option, with ``graph`` as the only argument: .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> p = graphviz.Graph(name='parent') >>> p.edge('spam', 'eggs') >>> c = graphviz.Graph(name='child', node_attr={'shape': 'box'}) >>> c.edge('foo', 'bar') >>> p.subgraph(c) Second usage, with a ``with``-block (omitting the ``graph`` argument): .. doctest:: >>> p = graphviz.Graph('parent') # doctest: +NO_EXE >>> p.edge('spam', 'eggs') >>> with p.subgraph(name='child', node_attr={'shape': 'box'}) as c: ... c.edge('foo', 'bar') Both produce the same result: .. doctest:: >>> print(p.source) # doctest: +NORMALIZE_WHITESPACE +NO_EXE graph parent { spam -- eggs subgraph child { node [shape=box] foo -- bar } } .. tip:: If the ``name`` of a subgraph begins with ``'cluster'`` (all lowercase), the layout engine treats it as a special **cluster** subgraph (:ref:`example ` ). See the `Subgraphs and Clusters` section in `DOT language `_. When :meth:`~.Graph.subgraph` is used as a context manager, the new graph instance is created with ``strict=None`` copying the **parent graph values** for ``directory``, ``engine``, ``format``, ``renderer``, ``formatter``, and ``encoding``: .. doctest:: >>> doctest_mark_exe() # skip this line >>> p = graphviz.Graph('parent', directory='doctest-output') >>> with p.subgraph(name='child') as c: ... c.edge('bacon', 'eggs') ... c.render().replace('\\', '/') 'doctest-output/child.gv.pdf' .. note:: These copied attributes are only relevant for rendering the subgraph **independently** (i.e. as a stand-alone graph) from within the ``with``-block. .. include:: _links.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/docs/unflatten.rst0000666000000000000000000000301414575646724013724 0ustar00Unflatten --------- To preprocess the DOT_ source of a :class:`.Graph` or :class:`.Digraph` with the `unflatten `_ preprocessor (`manpage `_, `PDF `_), use the :meth:`~.Graph.unflatten` method. .. doctest:: >>> import graphviz # doctest: +NO_EXE >>> w = graphviz.Digraph('wide') >>> w.edges(('0', str(i)) for i in range(1, 10)) .. doctest:: >>> doctest_mark_exe() # skip this line >>> w.view() # doctest: +SKIP .. image:: _static/wide.svg :align: center .. hint:: :meth:`~.Graph.unflatten` improves the aspect ratio of graphs with many leaves or disconnected nodes. .. doctest:: >>> u = w.unflatten(stagger=3) # doctest: +NO_EXE .. doctest:: >>> doctest_mark_exe() # skip this line >>> u.view() # doctest: +SKIP .. image:: _static/wide-unflatten-stagger-3.svg :align: center The method returns a :class:`.Source` object that you can :meth:`~.Source.render`, :meth:`~.Source.view`, etc. with the same basic API as :class:`.Graph` or :class:`.Digraph` objects (minus modification, see details :ref:`below `). .. doctest:: >>> u = w.unflatten(stagger=2) # doctest: +NO_EXE >>> u # doctest: +ELLIPSIS .. doctest:: >>> doctest_mark_exe() # skip this line >>> u.view() # doctest: +SKIP .. image:: _static/wide-unflatten-stagger-2.svg :align: center .. include:: _links.rst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1749979899.416098 graphviz-0.21/examples/0000777000000000000000000000000015023511373012036 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/angles.py0000666000000000000000000000221114150757034013663 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/Gallery/gradient/angles.html""" import graphviz g = graphviz.Digraph('G', filename='angles.gv') g.attr(bgcolor='blue') with g.subgraph(name='cluster_1') as c: c.attr(fontcolor='white') c.attr('node', shape='circle', style='filled', fillcolor='white:black', gradientangle='360', label='n9:360', fontcolor='black') c.node('n9') for i, a in zip(range(8, 0, -1), range(360 - 45, -1, -45)): c.attr('node', gradientangle=f'{a:d}', label=f'n{i:d}:{a:d}') c.node(f'n{i:d}') c.attr(label='Linear Angle Variations (white to black gradient)') with g.subgraph(name='cluster_2') as c: c.attr(fontcolor='white') c.attr('node', shape='circle', style='radial', fillcolor='white:black', gradientangle='360', label='n18:360', fontcolor='black') c.node('n18') for i, a in zip(range(17, 9, -1), range(360 - 45, -1, -45)): c.attr('node', gradientangle=f'{a:d}', label=f'n{i:d}:{a:d}') c.node(f'n{i:d}') c.attr(label='Radial Angle Variations (white to black gradient)') g.edge('n5', 'n14') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/btree.py0000666000000000000000000000165314150757034013524 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/pdf/dotguide.pdf, Figure 13""" import graphviz from graphviz import nohtml g = graphviz.Digraph('g', filename='btree.gv', node_attr={'shape': 'record', 'height': '.1'}) g.node('node0', nohtml(' | G|')) g.node('node1', nohtml(' | E|')) g.node('node2', nohtml(' | B|')) g.node('node3', nohtml(' | F|')) g.node('node4', nohtml(' | R|')) g.node('node5', nohtml(' | H|')) g.node('node6', nohtml(' | Y|')) g.node('node7', nohtml(' | A|')) g.node('node8', nohtml(' | C|')) g.edge('node0:f2', 'node4:f1') g.edge('node0:f0', 'node1:f1') g.edge('node1:f0', 'node2:f1') g.edge('node1:f2', 'node3:f1') g.edge('node2:f2', 'node8:f1') g.edge('node2:f0', 'node7:f1') g.edge('node4:f2', 'node6:f1') g.edge('node4:f0', 'node5:f1') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/cluster.py0000666000000000000000000000170414150757034014101 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/directed/cluster.html""" from graphviz import Digraph g = Digraph('G', filename='cluster.gv') # NOTE: the subgraph name needs to begin with 'cluster' (all lowercase) # so that Graphviz recognizes it as a special cluster subgraph with g.subgraph(name='cluster_0') as c: c.attr(style='filled', color='lightgrey') c.node_attr.update(style='filled', color='white') c.edges([('a0', 'a1'), ('a1', 'a2'), ('a2', 'a3')]) c.attr(label='process #1') with g.subgraph(name='cluster_1') as c: c.attr(color='blue') c.node_attr['style'] = 'filled' c.edges([('b0', 'b1'), ('b1', 'b2'), ('b2', 'b3')]) c.attr(label='process #2') g.edge('start', 'a0') g.edge('start', 'b0') g.edge('a1', 'b3') g.edge('b2', 'a3') g.edge('a3', 'a0') g.edge('a3', 'end') g.edge('b3', 'end') g.node('start', shape='Mdiamond') g.node('end', shape='Msquare') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/cluster_edge.py0000666000000000000000000000100114150757034015053 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/pdf/dotguide.pdf, Figure 20""" import graphviz g = graphviz.Digraph('G', filename='cluster_edge.gv') g.attr(compound='true') with g.subgraph(name='cluster0') as c: c.edges(['ab', 'ac', 'bd', 'cd']) with g.subgraph(name='cluster1') as c: c.edges(['eg', 'ef']) g.edge('b', 'f', lhead='cluster1') g.edge('d', 'e') g.edge('c', 'g', ltail='cluster0', lhead='cluster1') g.edge('c', 'e', ltail='cluster0') g.edge('d', 'h') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/colors.py0000666000000000000000000000100414150757034013712 0ustar00#!/usr/bin/env python3 """https://graphviz.org/docs/attr-types/color""" import graphviz g = graphviz.Graph(filename='colors.gv') red, green, blue = 64, 224, 208 assert f'#{red:x}{green:x}{blue:x}' == '#40e0d0' g.node('RGB: #40e0d0', style='filled', fillcolor='#40e0d0') g.node('RGBA: #ff000042', style='filled', fillcolor='#ff000042') g.node('HSV: 0.051 0.718 0.627', style='filled', fillcolor='0.051 0.718 0.627') g.node('name: deeppink', style='filled', fillcolor='deeppink') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/er.py0000666000000000000000000000212614150757034013025 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/undirected/ER.html""" import graphviz e = graphviz.Graph('ER', filename='er.gv', engine='neato') e.attr('node', shape='box') e.node('course') e.node('institute') e.node('student') e.attr('node', shape='ellipse') e.node('name0', label='name') e.node('name1', label='name') e.node('name2', label='name') e.node('code') e.node('grade') e.node('number') e.attr('node', shape='diamond', style='filled', color='lightgrey') e.node('C-I') e.node('S-C') e.node('S-I') e.edge('name0', 'course') e.edge('code', 'course') e.edge('course', 'C-I', label='n', len='1.00') e.edge('C-I', 'institute', label='1', len='1.00') e.edge('institute', 'name1') e.edge('institute', 'S-I', label='1', len='1.00') e.edge('S-I', 'student', label='n', len='1.00') e.edge('student', 'grade') e.edge('student', 'name2') e.edge('student', 'number') e.edge('student', 'S-C', label='m', len='1.00') e.edge('S-C', 'course', label='n', len='1.00') e.attr(label=r'\n\nEntity Relation Diagram\ndrawn by NEATO') e.attr(fontsize='20') e.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/fdpclust.py0000666000000000000000000000073514150757034014247 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/Gallery/undirected/fdpclust.html""" import graphviz g = graphviz.Graph('G', filename='fdpclust.gv', engine='fdp') g.node('e') with g.subgraph(name='clusterA') as a: a.edge('a', 'b') with a.subgraph(name='clusterC') as c: c.edge('C', 'D') with g.subgraph(name='clusterB') as b: b.edge('d', 'f') g.edge('d', 'D') g.edge('e', 'clusterB') g.edge('clusterC', 'clusterB') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/fsm.py0000666000000000000000000000157114150757034013207 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/directed/fsm.html""" import graphviz f = graphviz.Digraph('finite_state_machine', filename='fsm.gv') f.attr(rankdir='LR', size='8,5') f.attr('node', shape='doublecircle') f.node('LR_0') f.node('LR_3') f.node('LR_4') f.node('LR_8') f.attr('node', shape='circle') f.edge('LR_0', 'LR_2', label='SS(B)') f.edge('LR_0', 'LR_1', label='SS(S)') f.edge('LR_1', 'LR_3', label='S($end)') f.edge('LR_2', 'LR_6', label='SS(b)') f.edge('LR_2', 'LR_5', label='SS(a)') f.edge('LR_2', 'LR_4', label='S(A)') f.edge('LR_5', 'LR_7', label='S(b)') f.edge('LR_5', 'LR_5', label='S(a)') f.edge('LR_6', 'LR_6', label='S(b)') f.edge('LR_6', 'LR_5', label='S(a)') f.edge('LR_7', 'LR_8', label='S(b)') f.edge('LR_7', 'LR_5', label='S(a)') f.edge('LR_8', 'LR_6', label='S(b)') f.edge('LR_8', 'LR_5', label='S(a)') f.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/g_c_n.py0000666000000000000000000000101014150757034013453 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/Gallery/gradient/g_c_n.html""" import graphviz g = graphviz.Graph('G', filename='g_c_n.gv') g.attr(bgcolor='purple:pink', label='agraph', fontcolor='white') with g.subgraph(name='cluster1') as c: c.attr(fillcolor='blue:cyan', label='acluster', fontcolor='white', style='filled', gradientangle='270') c.attr('node', shape='box', fillcolor='red:yellow', style='filled', gradientangle='90') c.node('anode') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745689.0 graphviz-0.21/examples/graphviz-engines.ipynb0000666000000000000000000006514214432626731016401 0ustar00{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('0.20.dev0', (2, 40, 1))" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%cd -q ..\n", "\n", "import graphviz\n", "\n", "graphviz.__version__, graphviz.version()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "circo\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "dot\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "fdp\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "neato\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "osage\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "patchwork\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "sfdp\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "twopi\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "A\n", "\n", "\n", "\n", "B\n", "\n", "B\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "C\n", "\n", "C\n", "\n", "\n", "\n", "A->C\n", "\n", "\n", "\n", "\n", "\n", "B->C\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "from IPython import display\n", "\n", "dot = graphviz.Digraph()\n", "dot.edges(['AB', 'BC', 'AC'])\n", "\n", "for engine in sorted(graphviz.ENGINES):\n", " print(engine)\n", " dot.engine = engine\n", " display.display(dot)\n", " print()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 4 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745689.0 graphviz-0.21/examples/graphviz-escapes.ipynb0000666000000000000000000004006314432626731016367 0ustar00{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('0.20.dev0', (2, 40, 1))" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%cd -q ..\n", "\n", "import graphviz\n", "\n", "graphviz.__version__, graphviz.version()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def make_graph(node_label=None, graph_name=None, **node_kwargs):\n", " result = graphviz.Digraph(name=graph_name)\n", " result.node('A', label=node_label, **node_kwargs)\n", " print(result)\n", " return result" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"\\\"\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", ""\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label='\"')" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"\\\"\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", ""\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label='\\\\\"')" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/jovyan/graphviz/quoting.py:88: DotSyntaxWarning: expect syntax error scanning invalid quoted string: '\\\\'\n", " category=exceptions.DotSyntaxWarning)\n", "Error: : syntax error in line 2 scanning a quoted string (missing endquote? longer than 16384?)\n", "String starting:\"\"]\n", "}\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"\\\"]\n", "}\n", "\n" ] } ], "source": [ "try:\n", " make_graph(node_label='\\\\').pipe(format='svg', encoding='utf-8');\n", "except graphviz.CalledProcessError as e:\n", " assert 'syntax error' in e.stderr" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"\\\\\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "\\\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label=r'\\\\')" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"\\\\\\\"\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "\\"\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label=r'\\\\\"')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "https://www.graphviz.org/doc/info/attrs.html#k:escString" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"node: \\N\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "node: A\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label=r'node: \\N')" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph spam {\n", "\tA [label=\"graph: \\G\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "spam\n", "\n", "\n", "\n", "A\n", "\n", "graph: spam\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label=r'graph: \\G', graph_name='spam')" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=spam URL=\"https://example.org/\\L\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "\n", "spam\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label='spam', URL=r'https://example.org/\\L')" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "digraph {\n", "\tA [label=\"centered\\nleft\\lright\\r\"]\n", "}\n", "\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "centered\n", "left\n", "right\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "make_graph(node_label=r'centered\\nleft\\lright\\r')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 4 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745689.0 graphviz-0.21/examples/graphviz-jupyter-format.ipynb0000666000000000000000000007477414432626731017754 0ustar00{ "cells": [ { "cell_type": "code", "execution_count": 1, "id": "d31aac11", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('0.20.dev0', (2, 40, 1))" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%cd -q ..\n", "\n", "import graphviz\n", "\n", "graphviz.__version__, graphviz.version()" ] }, { "cell_type": "code", "execution_count": 2, "id": "b8d54151", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'svg'" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "graphviz.set_jupyter_format('png')" ] }, { "cell_type": "code", "execution_count": 3, "id": "9e904725", "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dot = graphviz.Digraph(comment='The Round Table')\n", "\n", "dot.node('A', 'King Arthur')\n", "dot.node('B', 'Sir Bedevere the Wise')\n", "dot.node('L', 'Sir Lancelot the Brave')\n", "\n", "dot.edges(['AB', 'AL'])\n", "dot.edge('B', 'L', constraint='false')\n", "\n", "dot" ] }, { "cell_type": "code", "execution_count": 4, "id": "a0a07753", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "King Arthur\n", "\n", "\n", "\n", "B\n", "\n", "Sir Bedevere the Wise\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "L\n", "\n", "Sir Lancelot the Brave\n", "\n", "\n", "\n", "A->L\n", "\n", "\n", "\n", "\n", "\n", "B->L\n", "\n", "\n", "\n", "\n", "\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from IPython import display\n", "\n", "display.display_svg(dot)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 5 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745689.0 graphviz-0.21/examples/graphviz-notebook.ipynb0000666000000000000000000010133114432626731016560 0ustar00{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[DEBUG@graphviz.backend.execute] run [PosixPath('dot'), '-V']\n" ] }, { "data": { "text/plain": [ "('0.20.dev0', (2, 40, 1))" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%cd -q ..\n", "\n", "import logging\n", "\n", "import graphviz\n", "\n", "logging.basicConfig(format='[%(levelname)s@%(name)s] %(message)s', level=logging.DEBUG)\n", "\n", "graphviz.__version__, graphviz.version()" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[DEBUG@graphviz.backend.execute] run [PosixPath('dot'), '-Kdot', '-Tsvg']\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "%3\n", "\n", "\n", "\n", "A\n", "\n", "King Arthur\n", "\n", "\n", "\n", "B\n", "\n", "Sir Bedevere the Wise\n", "\n", "\n", "\n", "A->B\n", "\n", "\n", "\n", "\n", "\n", "L\n", "\n", "Sir Lancelot the Brave\n", "\n", "\n", "\n", "A->L\n", "\n", "\n", "\n", "\n", "\n", "B->L\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dot = graphviz.Digraph(comment='The Round Table')\n", "\n", "dot.node('A', 'King Arthur')\n", "dot.node('B', 'Sir Bedevere the Wise')\n", "dot.node('L', 'Sir Lancelot the Brave')\n", "\n", "dot.edges(['AB', 'AL'])\n", "dot.edge('B', 'L', constraint='false')\n", "\n", "dot" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[DEBUG@graphviz.backend.execute] run [PosixPath('dot'), '-Kdot', '-Tsvg']\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "the holy hand grenade\n", "\n", "\n", "\n", "1\n", "\n", "1\n", "\n", "\n", "\n", "2\n", "\n", "2\n", "\n", "\n", "\n", "1->2\n", "\n", "\n", "\n", "\n", "\n", "3\n", "\n", "3\n", "\n", "\n", "\n", "2->3\n", "\n", "\n", "\n", "\n", "\n", "lob\n", "\n", "lob\n", "\n", "\n", "\n", "3->lob\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "src = graphviz.Source('digraph \"the holy hand grenade\" { rankdir=LR; 1 -> 2 -> 3 -> lob }')\n", "src" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[DEBUG@graphviz.backend.execute] run [PosixPath('dot'), '-Kdot', '-Tsvg']\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "finite_state_machine\n", "\n", "\n", "\n", "LR_0\n", "\n", "\n", "LR_0\n", "\n", "\n", "\n", "LR_2\n", "\n", "LR_2\n", "\n", "\n", "\n", "LR_0->LR_2\n", "\n", "\n", "SS(B)\n", "\n", "\n", "\n", "LR_1\n", "\n", "LR_1\n", "\n", "\n", "\n", "LR_0->LR_1\n", "\n", "\n", "SS(S)\n", "\n", "\n", "\n", "LR_3\n", "\n", "\n", "LR_3\n", "\n", "\n", "\n", "LR_4\n", "\n", "\n", "LR_4\n", "\n", "\n", "\n", "LR_8\n", "\n", "\n", "LR_8\n", "\n", "\n", "\n", "LR_6\n", "\n", "LR_6\n", "\n", "\n", "\n", "LR_8->LR_6\n", "\n", "\n", "S(b)\n", "\n", "\n", "\n", "LR_5\n", "\n", "LR_5\n", "\n", "\n", "\n", "LR_8->LR_5\n", "\n", "\n", "S(a)\n", "\n", "\n", "\n", "LR_2->LR_4\n", "\n", "\n", "S(A)\n", "\n", "\n", "\n", "LR_2->LR_6\n", "\n", "\n", "SS(b)\n", "\n", "\n", "\n", "LR_2->LR_5\n", "\n", "\n", "SS(a)\n", "\n", "\n", "\n", "LR_1->LR_3\n", "\n", "\n", "S($end)\n", "\n", "\n", "\n", "LR_6->LR_6\n", "\n", "\n", "S(b)\n", "\n", "\n", "\n", "LR_6->LR_5\n", "\n", "\n", "S(a)\n", "\n", "\n", "\n", "LR_5->LR_5\n", "\n", "\n", "S(a)\n", "\n", "\n", "\n", "LR_7\n", "\n", "LR_7\n", "\n", "\n", "\n", "LR_5->LR_7\n", "\n", "\n", "S(b)\n", "\n", "\n", "\n", "LR_7->LR_8\n", "\n", "\n", "S(b)\n", "\n", "\n", "\n", "LR_7->LR_5\n", "\n", "\n", "S(a)\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# http://www.graphviz.org/content/fsm\n", "\n", "f = graphviz.Digraph('finite_state_machine', filename='fsm.gv')\n", "\n", "f.attr(rankdir='LR', size='8,5')\n", "\n", "f.attr('node', shape='doublecircle')\n", "f.node('LR_0')\n", "f.node('LR_3')\n", "f.node('LR_4')\n", "f.node('LR_8')\n", "\n", "f.attr('node', shape='circle')\n", "f.edge('LR_0', 'LR_2', label='SS(B)')\n", "f.edge('LR_0', 'LR_1', label='SS(S)')\n", "f.edge('LR_1', 'LR_3', label='S($end)')\n", "f.edge('LR_2', 'LR_6', label='SS(b)')\n", "f.edge('LR_2', 'LR_5', label='SS(a)')\n", "f.edge('LR_2', 'LR_4', label='S(A)')\n", "f.edge('LR_5', 'LR_7', label='S(b)')\n", "f.edge('LR_5', 'LR_5', label='S(a)')\n", "f.edge('LR_6', 'LR_6', label='S(b)')\n", "f.edge('LR_6', 'LR_5', label='S(a)')\n", "f.edge('LR_7', 'LR_8', label='S(b)')\n", "f.edge('LR_7', 'LR_5', label='S(a)')\n", "f.edge('LR_8', 'LR_6', label='S(b)')\n", "f.edge('LR_8', 'LR_5', label='S(a)')\n", "\n", "f" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[DEBUG@graphviz.backend.execute] run [PosixPath('dot'), '-Kdot', '-Tsvg']\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "G\n", "\n", "\n", "cluster0\n", "\n", "\n", "\n", "cluster1\n", "\n", "\n", "\n", "\n", "a\n", "\n", "a\n", "\n", "\n", "\n", "b\n", "\n", "b\n", "\n", "\n", "\n", "a->b\n", "\n", "\n", "\n", "\n", "\n", "c\n", "\n", "c\n", "\n", "\n", "\n", "a->c\n", "\n", "\n", "\n", "\n", "\n", "d\n", "\n", "d\n", "\n", "\n", "\n", "b->d\n", "\n", "\n", "\n", "\n", "\n", "f\n", "\n", "f\n", "\n", "\n", "\n", "b->f\n", "\n", "\n", "\n", "\n", "\n", "c->d\n", "\n", "\n", "\n", "\n", "\n", "e\n", "\n", "e\n", "\n", "\n", "\n", "c->e\n", "\n", "\n", "\n", "\n", "\n", "g\n", "\n", "g\n", "\n", "\n", "\n", "c->g\n", "\n", "\n", "\n", "\n", "\n", "d->e\n", "\n", "\n", "\n", "\n", "\n", "h\n", "\n", "h\n", "\n", "\n", "\n", "d->h\n", "\n", "\n", "\n", "\n", "\n", "e->g\n", "\n", "\n", "\n", "\n", "\n", "e->f\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# http://www.graphviz.org/pdf/dotguide.pdf, Figure 20\n", "\n", "g = graphviz.Digraph('G', filename='cluster_edge.gv')\n", "\n", "g.attr(compound='true')\n", "\n", "with g.subgraph(name='cluster0') as c:\n", " c.edges(['ab', 'ac', 'bd', 'cd'])\n", "\n", "with g.subgraph(name='cluster1') as c:\n", " c.edges(['eg', 'ef'])\n", "\n", "g.edge('b', 'f', lhead='cluster1')\n", "g.edge('d', 'e')\n", "g.edge('c', 'g', ltail='cluster0', lhead='cluster1')\n", "g.edge('c', 'e', ltail='cluster0')\n", "g.edge('d', 'h')\n", "\n", "g" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.12" } }, "nbformat": 4, "nbformat_minor": 4 } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/graphviz_transform_recipe.py0000666000000000000000000000706414150757034017701 0ustar00#!/usr/bin/env python3 """graphviz.(Di)graph instance transformation: LazyGraph and LazyDigraph.""" import functools from unittest import mock import graphviz WRAPS = ['Graph', 'Digraph'] RENDERING_METHODS = ('pipe', 'save', 'render', 'view', 'unflatten') INPUT_PROPERTIES = ('source',) __all__ = ['LazyGraph', 'LazyDigraph'] def lazy_graph_cls(wrapped_cls_name: str): return functools.partial(create_lazy_graph_instance, wrapped_cls_name) def create_lazy_graph_instance(cls_name: str, *init_args, **init_kwargs) -> mock.NonCallableMagicMock: cls = getattr(graphviz, cls_name) if cls not in (graphviz.Graph, graphviz.Digraph): raise ValueError(f'cls_name: {cls_name!r}') fake = mock.create_autospec(cls, instance=True, spec_set=True) fake.copy.side_effect = NotImplementedError # TODO def make_real_inst(calls): dot = cls(*init_args, **init_kwargs) for method_name, args, kwargs in calls: method = getattr(dot, method_name) method(*args, **kwargs) # ignore return value return dot def make_methodcaller(method_name): def call_method(*args, **kwargs): last_call = fake.mock_calls.pop() assert last_call == getattr(mock.call, method_name)(*args, **kwargs) dot = make_real_inst(fake.mock_calls) method = getattr(dot, method_name) return method(*args, **kwargs) return call_method for method_name in RENDERING_METHODS: getattr(fake, method_name).side_effect = make_methodcaller(method_name) def make_property(property_name): def property_func(*args): last_call = property_mock.mock_calls.pop() assert last_call == mock.call(*args) assert not property_mock.mock_calls dot = make_real_inst(fake.mock_calls) property_obj = getattr(dot.__class__, property_name) property_func = property_obj.fset if args else property_obj.fget return property_func(dot, *args) property_mock = mock.PropertyMock(side_effect=property_func) return property_mock for property_name in INPUT_PROPERTIES: setattr(type(fake), property_name, make_property(property_name)) return fake LazyGraph, LazyDigraph = map(lazy_graph_cls, WRAPS) if __name__ == '__main__': dot = LazyDigraph(filename='round-table.gv', comment='The Round Table') dot.node('A', 'King Arthur') dot.node('B', 'Sir Bedevere the Wise') dot.node('L', 'Sir Lancelot the Brave') dot.edges(['AB', 'AL']) dot.edge('B', 'L', constraint='false') print(repr(dot), dot.mock_calls, dot.source, sep='\n') #dot.view() # noqa: E265 def transform(mock_calls): """Replace full name labels with first names.""" for call in mock_calls: method_name, args, kwargs = call if method_name == 'node': name, label = args label = label.split()[1] yield mock.call.node(name, label, **kwargs) else: yield call dot.mock_calls = list(transform(dot.mock_calls)) # reverse the Bedvedere -> Lancelot edge method_name, tail_head, edge_attrs = dot.mock_calls.pop() assert method_name == 'edge' dot.edge(*reversed(tail_head), **edge_attrs) print(repr(dot), dot.mock_calls, dot.source, sep='\n') #dot.view('round-table-transformed.gv') # noqa: E265 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/hello.py0000666000000000000000000000027714150757034013527 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/directed/hello.html""" import graphviz g = graphviz.Digraph('G', filename='hello.gv') g.edge('Hello', 'World') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/process.py0000666000000000000000000000101314150757034014067 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/undirected/process.html""" import graphviz g = graphviz.Graph('G', filename='process.gv', engine='sfdp') g.edge('run', 'intr') g.edge('intr', 'runbl') g.edge('runbl', 'run') g.edge('run', 'kernel') g.edge('kernel', 'zombie') g.edge('kernel', 'sleep') g.edge('kernel', 'runmem') g.edge('sleep', 'swap') g.edge('swap', 'runswap') g.edge('runswap', 'new') g.edge('runswap', 'runmem') g.edge('new', 'runmem') g.edge('sleep', 'runmem') g.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/rank_same.py0000666000000000000000000000067514150757034014366 0ustar00#!/usr/bin/env python3 """https://stackoverflow.com/questions/25734244/how-do-i-place-nodes-on-the-same-level-in-dot""" import graphviz d = graphviz.Digraph(filename='rank_same.gv') with d.subgraph() as s: s.attr(rank='same') s.node('A') s.node('X') d.node('C') with d.subgraph() as s: s.attr(rank='same') s.node('B') s.node('D') s.node('Y') d.edges(['AB', 'AC', 'CD', 'XY']) d.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/structs.py0000666000000000000000000000177114150757034014133 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/doc/info/shapes.html#html""" import graphviz s = graphviz.Digraph('structs', filename='structs.gv', node_attr={'shape': 'plaintext'}) s.node('struct1', '''<
left middle right
>''') s.node('struct2', '''<
one two
>''') s.node('struct3', '''<
hello
world
b g h
c d e
f
>''') s.edges([('struct1:f1', 'struct2:f0'), ('struct1:f2', 'struct3:here')]) s.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/structs_revisited.py0000666000000000000000000000073614150757034016211 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/pdf/dotguide.pdf, Figure 12""" import graphviz s = graphviz.Digraph('structs', filename='structs_revisited.gv', node_attr={'shape': 'record'}) s.node('struct1', ' left| middle| right') s.node('struct2', ' one| two') s.node('struct3', r'hello\nworld |{ b |{c| d|e}| f}| g | h') s.edges([('struct1:f1', 'struct2:f0'), ('struct1:f2', 'struct3:here')]) s.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/traffic_lights.py0000666000000000000000000000203714150757034015410 0ustar00#!/usr/bin/env python3 """https://www.graphviz.org/Gallery/directed/traffic_lights.html""" import graphviz t = graphviz.Digraph('TrafficLights', filename='traffic_lights.gv', engine='neato') t.attr('node', shape='box') for i in (2, 1): t.node(f'gy{i:d}') t.node(f'yr{i:d}') t.node(f'rg{i:d}') t.attr('node', shape='circle', fixedsize='true', width='0.9') for i in (2, 1): t.node(f'green{i:d}') t.node(f'yellow{i:d}') t.node(f'red{i:d}') t.node(f'safe{i:d}') for i, j in [(2, 1), (1, 2)]: t.edge(f'gy{i:d}', f'yellow{i:d}') t.edge(f'rg{i:d}', f'green{i:d}') t.edge(f'yr{i:d}', f'safe{j:d}') t.edge(f'yr{i:d}', f'red{i:d}') t.edge(f'safe{i:d}', f'rg{i:d}') t.edge(f'green{i:d}', f'gy{i:d}') t.edge(f'yellow{i:d}', f'yr{i:d}') t.edge(f'red{i:d}', f'rg{i:d}') t.attr(overlap='false') t.attr(label=r'PetriNet Model TrafficLights\n' r'Extracted from ConceptBase and layed out by Graphviz') t.attr(fontsize='12') t.view() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/examples/unix.py0000666000000000000000000000347414150757034013411 0ustar00#!/usr/bin/env python3 """https://graphviz.org/Gallery/directed/unix.html""" import graphviz u = graphviz.Digraph('unix', filename='unix.gv', node_attr={'color': 'lightblue2', 'style': 'filled'}) u.attr(size='6,6') u.edge('5th Edition', '6th Edition') u.edge('5th Edition', 'PWB 1.0') u.edge('6th Edition', 'LSX') u.edge('6th Edition', '1 BSD') u.edge('6th Edition', 'Mini Unix') u.edge('6th Edition', 'Wollongong') u.edge('6th Edition', 'Interdata') u.edge('Interdata', 'Unix/TS 3.0') u.edge('Interdata', 'PWB 2.0') u.edge('Interdata', '7th Edition') u.edge('7th Edition', '8th Edition') u.edge('7th Edition', '32V') u.edge('7th Edition', 'V7M') u.edge('7th Edition', 'Ultrix-11') u.edge('7th Edition', 'Xenix') u.edge('7th Edition', 'UniPlus+') u.edge('V7M', 'Ultrix-11') u.edge('8th Edition', '9th Edition') u.edge('1 BSD', '2 BSD') u.edge('2 BSD', '2.8 BSD') u.edge('2.8 BSD', 'Ultrix-11') u.edge('2.8 BSD', '2.9 BSD') u.edge('32V', '3 BSD') u.edge('3 BSD', '4 BSD') u.edge('4 BSD', '4.1 BSD') u.edge('4.1 BSD', '4.2 BSD') u.edge('4.1 BSD', '2.8 BSD') u.edge('4.1 BSD', '8th Edition') u.edge('4.2 BSD', '4.3 BSD') u.edge('4.2 BSD', 'Ultrix-32') u.edge('PWB 1.0', 'PWB 1.2') u.edge('PWB 1.0', 'USG 1.0') u.edge('PWB 1.2', 'PWB 2.0') u.edge('USG 1.0', 'CB Unix 1') u.edge('USG 1.0', 'USG 2.0') u.edge('CB Unix 1', 'CB Unix 2') u.edge('CB Unix 2', 'CB Unix 3') u.edge('CB Unix 3', 'Unix/TS++') u.edge('CB Unix 3', 'PDP-11 Sys V') u.edge('USG 2.0', 'USG 3.0') u.edge('USG 3.0', 'Unix/TS 3.0') u.edge('PWB 2.0', 'Unix/TS 3.0') u.edge('Unix/TS 1.0', 'Unix/TS 3.0') u.edge('Unix/TS 3.0', 'TS 4.0') u.edge('Unix/TS++', 'TS 4.0') u.edge('CB Unix 3', 'TS 4.0') u.edge('TS 4.0', 'System V.0') u.edge('System V.0', 'System V.2') u.edge('System V.2', 'System V.3') u.view() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.4331021 graphviz-0.21/graphviz/0000777000000000000000000000000015023511373012052 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979392.0 graphviz-0.21/graphviz/__init__.py0000666000000000000000000000665615023510400014166 0ustar00# graphviz - create dot, save, render, view """Assemble DOT source code and render it with Graphviz. Example: >>> import graphviz # doctest: +NO_EXE >>> dot = graphviz.Digraph(comment='The Round Table') >>> dot.node('A', 'King Arthur') >>> dot.node('B', 'Sir Bedevere the Wise') >>> dot.node('L', 'Sir Lancelot the Brave') >>> dot.edges(['AB', 'AL']) >>> dot.edge('B', 'L', constraint='false') >>> print(dot) #doctest: +NORMALIZE_WHITESPACE // The Round Table digraph { A [label="King Arthur"] B [label="Sir Bedevere the Wise"] L [label="Sir Lancelot the Brave"] A -> B A -> L B -> L [constraint=false] } """ from ._defaults import set_default_engine, set_default_format, set_jupyter_format from .backend import (DOT_BINARY, UNFLATTEN_BINARY, render, pipe, pipe_string, pipe_lines, pipe_lines_string, unflatten, version, view) from .exceptions import (ExecutableNotFound, CalledProcessError, RequiredArgumentError, FileExistsError, UnknownSuffixWarning, FormatSuffixMismatchWarning, DotSyntaxWarning) from .graphs import Graph, Digraph from .jupyter_integration import SUPPORTED_JUPYTER_FORMATS from .parameters import ENGINES, FORMATS, RENDERERS, FORMATTERS from .quoting import escape, nohtml from .sources import Source __all__ = ['ENGINES', 'FORMATS', 'RENDERERS', 'FORMATTERS', 'DOT_BINARY', 'UNFLATTEN_BINARY', 'SUPPORTED_JUPYTER_FORMATS', 'Graph', 'Digraph', 'Source', 'escape', 'nohtml', 'render', 'pipe', 'pipe_string', 'pipe_lines', 'pipe_lines_string', 'unflatten', 'version', 'view', 'ExecutableNotFound', 'CalledProcessError', 'RequiredArgumentError', 'FileExistsError', 'UnknownSuffixWarning', 'FormatSuffixMismatchWarning', 'DotSyntaxWarning', 'set_default_engine', 'set_default_format', 'set_jupyter_format'] __title__ = 'graphviz' __version__ = '0.21' __author__ = 'Sebastian Bank ' __license__ = 'MIT, see LICENSE.txt' __copyright__ = 'Copyright (c) 2013-2025 Sebastian Bank' ENGINES = ENGINES """:class:`set` of known layout commands used for rendering (``'dot'``, ``'neato'``, ...).""" FORMATS = FORMATS """:class:`set` of known output formats for rendering (``'pdf'``, ``'png'``, ...).""" RENDERERS = RENDERERS """:class:`set` of known output renderers for rendering (``'cairo'``, ``'gd'``, ...).""" FORMATTERS = FORMATTERS """:class:`set` of known output formatters for rendering (``'cairo'``, ``'gd'``, ...).""" SUPPORTED_JUPYTER_FORMATS = SUPPORTED_JUPYTER_FORMATS """:class:`set` of supported formats for ``_repr_mimebundle_()`` (``'svg'``, ``'png'``, ...).""" DOT_BINARY = DOT_BINARY """:class:`pathlib.Path` of rendering command (``Path('dot')``).""" UNFLATTEN_BINARY = UNFLATTEN_BINARY """:class:`pathlib.Path` of unflatten command (``Path('unflatten')``).""" ExecutableNotFound = ExecutableNotFound CalledProcessError = CalledProcessError RequiredArgumentError = RequiredArgumentError FileExistsError = FileExistsError UnknownSuffixWarning = UnknownSuffixWarning FormatSuffixMismatchWarning = FormatSuffixMismatchWarning DotSyntaxWarning = DotSyntaxWarning ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1748876599.0 graphviz-0.21/graphviz/_compat.py0000666000000000000000000000143115017336467014061 0ustar00"""Platform compatibility.""" import platform def get_startupinfo() -> None: """Return None for startupinfo argument of ``subprocess.Popen``.""" return None assert get_startupinfo() is None, 'get_startupinfo() defaults to a no-op' if platform.system() == 'Windows': # pragma: no cover import subprocess def get_startupinfo() -> subprocess.STARTUPINFO: # pytype: disable=module-attr """Return subprocess.STARTUPINFO instance hiding the console window.""" startupinfo = subprocess.STARTUPINFO() # pytype: disable=module-attr startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # pytype: disable=module-attr startupinfo.wShowWindow = subprocess.SW_HIDE # pytype: disable=module-attr return startupinfo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/_defaults.py0000666000000000000000000000431714150757034014405 0ustar00"""Set package-wide default parameters and IPython/Jupyter display format.""" __all_ = ['DEFAULT_SOURCE_EXTENSION', 'set_default_engine', 'set_default_format', 'set_jupyter_format'] DEFAULT_SOURCE_EXTENSION = 'gv' def set_default_engine(engine: str) -> str: """Change the default ``engine`` and return the old default value. Args: engine: new default ``engine`` used by all present and newly created instances without explicitly set ``engine`` (``'dot'``, ``'neato'``, ...). Returns: The old default value used for ``engine``. """ from . import parameters parameters.verify_engine(engine) old_default_engine = parameters.Parameters._engine parameters.Parameters._engine = engine return old_default_engine def set_default_format(format: str) -> str: """Change the default ``format`` and return the old default value. Args: format: new default ``format`` used by all present and newly created instances without explicitly set ``format`` (``'pdf'``, ``'png'``, ...). Returns: The old default value used for ``format``. """ from . import parameters parameters.verify_format(format) old_default_format = parameters.Parameters._format parameters.Parameters._format = format return old_default_format def set_jupyter_format(jupyter_format: str) -> str: """Change the default mimetype format for ``_repr_mimebundle_()`` and return the old value. Args: jupyter_format: new default IPython/Jupyter display format used by all present and newly created instances (``'svg'``, ``'png'``, ...). Returns: The old default value used for IPython/Jupyter display format. """ from . import jupyter_integration mimetype = jupyter_integration.get_jupyter_format_mimetype(jupyter_format) old_mimetype = jupyter_integration.JupyterIntegration._jupyter_mimetype old_format = jupyter_integration.get_jupyter_mimetype_format(old_mimetype) jupyter_integration.JupyterIntegration._jupyter_mimetype = mimetype return old_format ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715620895.0 graphviz-0.21/graphviz/_tools.py0000666000000000000000000001476114620446037013742 0ustar00"""Generic re-useable self-contained helper functions.""" import functools import inspect import itertools import logging import os import pathlib import typing import warnings __all__ = ['attach', 'mkdirs', 'mapping_items', 'promote_pathlike', 'promote_pathlike_directory', 'deprecate_positional_args'] log = logging.getLogger(__name__) def attach(object: typing.Any, /, name: str) -> typing.Callable: """Return a decorator doing ``setattr(object, name)`` with its argument. >>> spam = type('Spam', (object,), {})() # doctest: +NO_EXE >>> @attach(spam, 'eggs') ... def func(): ... pass >>> spam.eggs # doctest: +ELLIPSIS """ def decorator(func): setattr(object, name, func) return func return decorator def mkdirs(filename: typing.Union[os.PathLike, str], /, *, mode: int = 0o777) -> None: """Recursively create directories up to the path of ``filename`` as needed.""" dirname = os.path.dirname(filename) if not dirname: return log.debug('os.makedirs(%r)', dirname) os.makedirs(dirname, mode=mode, exist_ok=True) def mapping_items(mapping, /): """Return an iterator over the ``mapping`` items, sort if it's a plain dict. >>> list(mapping_items({'spam': 0, 'ham': 1, 'eggs': 2})) # doctest: +NO_EXE [('eggs', 2), ('ham', 1), ('spam', 0)] >>> from collections import OrderedDict >>> list(mapping_items(OrderedDict(enumerate(['spam', 'ham', 'eggs'])))) [(0, 'spam'), (1, 'ham'), (2, 'eggs')] """ result = iter(mapping.items()) if type(mapping) is dict: result = iter(sorted(result)) return result @typing.overload def promote_pathlike(filepath: typing.Union[os.PathLike, str], /) -> pathlib.Path: """Return path object for path-like-object.""" @typing.overload def promote_pathlike(filepath: None, /) -> None: """Return None for None.""" @typing.overload def promote_pathlike(filepath: typing.Union[os.PathLike, str, None], /, ) -> typing.Optional[pathlib.Path]: """Return path object or ``None`` depending on ``filepath``.""" def promote_pathlike(filepath: typing.Union[os.PathLike, str, None] ) -> typing.Optional[pathlib.Path]: """Return path-like object ``filepath`` promoted into a path object. See also: https://docs.python.org/3/glossary.html#term-path-like-object """ return pathlib.Path(filepath) if filepath is not None else None def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], /, *, default: typing.Union[os.PathLike, str, None] = None, ) -> pathlib.Path: """Return path-like object ``directory`` promoted into a path object (default to ``os.curdir``). See also: https://docs.python.org/3/glossary.html#term-path-like-object """ return pathlib.Path(directory if directory is not None else default or os.curdir) def deprecate_positional_args(*, supported_number: int, ignore_arg: typing.Optional[str] = None, category: typing.Type[Warning] = PendingDeprecationWarning, stacklevel: int = 1): """Mark supported_number of positional arguments as the maximum. Args: supported_number: Number of positional arguments for which no warning is raised. ignore_arg: Name of positional argument to ignore. category: Type of Warning to raise or None to return a nulldecorator returning the undecorated function. stacklevel: See :func:`warning.warn`. Returns: Return a decorator raising a category warning on more than supported_number positional args. See also: https://docs.python.org/3/library/exceptions.html#FutureWarning https://docs.python.org/3/library/exceptions.html#DeprecationWarning https://docs.python.org/3/library/exceptions.html#PendingDeprecationWarning """ assert supported_number >= 0, f'supported_number => 0: {supported_number!r}' if category is None: def nulldecorator(func): """Return the undecorated function.""" return func return nulldecorator assert issubclass(category, Warning) stacklevel += 1 def decorator(func): signature = inspect.signature(func) argnames = [name for name, param in signature.parameters.items() if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD] check_number = supported_number if ignore_arg is not None: ignored = [name for name in argnames if name == ignore_arg] assert ignored, 'ignore_arg must be a positional arg' check_number += len(ignored) qualification = f' (ignoring {ignore_arg}))' else: qualification = '' deprecated = argnames[supported_number:] assert deprecated log.debug('deprecate positional args: %s.%s(%r)', func.__module__, func.__qualname__, deprecated) # mangle function name in message for this package func_name = func.__name__.lstrip('_') func_name, sep, rest = func_name.partition('_legacy') assert func_name and (not sep or not rest) s_ = 's' if supported_number > 1 else '' @functools.wraps(func) def wrapper(*args, **kwargs): if len(args) > check_number: call_args = zip(argnames, args) supported = dict(itertools.islice(call_args, check_number)) deprecated = dict(call_args) assert deprecated wanted = ', '.join(f'{name}={value!r}' for name, value in deprecated.items()) warnings.warn(f'The signature of {func_name} will be reduced' f' to {supported_number} positional arg{s_}{qualification}' f' {list(supported)}: pass {wanted} as keyword arg{s_}', stacklevel=stacklevel, category=category) return func(*args, **kwargs) return wrapper return decorator ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.4951146 graphviz-0.21/graphviz/backend/0000777000000000000000000000000015023511373013441 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/backend/__init__.py0000666000000000000000000000141214150757034015556 0ustar00"""Execute rendering and unflattening subprocesses, open files in viewer.""" from .dot_command import DOT_BINARY from .execute import ExecutableNotFound, CalledProcessError from .mixins import Render, Pipe, Unflatten, View from .piping import pipe, pipe_string, pipe_lines, pipe_lines_string from .rendering import render from .unflattening import UNFLATTEN_BINARY, unflatten from .upstream_version import version from .viewing import view __all__ = ['DOT_BINARY', 'UNFLATTEN_BINARY', 'render', 'pipe', 'pipe_string', 'pipe_lines', 'pipe_lines_string', 'unflatten', 'version', 'view', 'ExecutableNotFound', 'CalledProcessError', 'Render', 'Pipe', 'Unflatten', 'View'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/graphviz/backend/dot_command.py0000666000000000000000000000270114226560631016304 0ustar00"""Check and assemble commands for running Graphviz ``dot``.""" import os import pathlib import typing from .. import exceptions from .. import parameters __all__ = ['DOT_BINARY', 'command'] DOT_BINARY = pathlib.Path('dot') def command(engine: str, format_: str, *, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None ) -> typing.List[typing.Union[os.PathLike, str]]: """Return ``subprocess.Popen`` argument list for rendering. See also: Upstream documentation: - https://www.graphviz.org/doc/info/command.html#-K - https://www.graphviz.org/doc/info/command.html#-T - https://www.graphviz.org/doc/info/command.html#-n """ if formatter is not None and renderer is None: raise exceptions.RequiredArgumentError('formatter given without renderer') parameters.verify_engine(engine, required=True) parameters.verify_format(format_, required=True) parameters.verify_renderer(renderer, required=False) parameters.verify_formatter(formatter, required=False) output_format = [f for f in (format_, renderer, formatter) if f is not None] output_format_flag = ':'.join(output_format) cmd = [DOT_BINARY, f'-K{engine}', f'-T{output_format_flag}'] if neato_no_op: cmd.append(f'-n{neato_no_op:d}') return cmd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/graphviz/backend/execute.py0000666000000000000000000001055214575646724015504 0ustar00"""Run subprocesses with ``subprocess.run()`` and ``subprocess.Popen()``.""" import errno import logging import os import subprocess import sys import typing from .. import _compat __all__ = ['run_check', 'ExecutableNotFound', 'CalledProcessError'] log = logging.getLogger(__name__) BytesOrStrIterator = typing.Union[typing.Iterator[bytes], typing.Iterator[str]] @typing.overload def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *, input_lines: typing.Optional[typing.Iterator[bytes]] = ..., encoding: None = ..., quiet: bool = ..., **kwargs) -> subprocess.CompletedProcess: """Accept bytes input_lines with default ``encoding=None```.""" @typing.overload def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *, input_lines: typing.Optional[typing.Iterator[str]] = ..., encoding: str, quiet: bool = ..., **kwargs) -> subprocess.CompletedProcess: """Accept string input_lines when given ``encoding``.""" @typing.overload def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *, input_lines: typing.Optional[BytesOrStrIterator] = ..., encoding: typing.Optional[str] = ..., capture_output: bool = ..., quiet: bool = ..., **kwargs) -> subprocess.CompletedProcess: """Accept bytes or string input_lines depending on ``encoding``.""" def run_check(cmd: typing.Sequence[typing.Union[os.PathLike, str]], *, input_lines: typing.Optional[BytesOrStrIterator] = None, encoding: typing.Optional[str] = None, quiet: bool = False, **kwargs) -> subprocess.CompletedProcess: """Run the command described by ``cmd`` with ``check=True`` and return its completed process. Raises: CalledProcessError: if the returncode of the subprocess is non-zero. """ log.debug('run %r', cmd) if not kwargs.pop('check', True): # pragma: no cover raise NotImplementedError('check must be True or omited') if encoding is not None: kwargs['encoding'] = encoding kwargs.setdefault('startupinfo', _compat.get_startupinfo()) try: if input_lines is not None: assert kwargs.get('input') is None assert iter(input_lines) is input_lines if kwargs.pop('capture_output'): kwargs['stdout'] = kwargs['stderr'] = subprocess.PIPE proc = _run_input_lines(cmd, input_lines, kwargs=kwargs) else: proc = subprocess.run(cmd, **kwargs) except OSError as e: if e.errno == errno.ENOENT: raise ExecutableNotFound(cmd) from e raise if not quiet and proc.stderr: _write_stderr(proc.stderr) try: proc.check_returncode() except subprocess.CalledProcessError as e: raise CalledProcessError(*e.args) return proc def _run_input_lines(cmd, input_lines, *, kwargs): popen = subprocess.Popen(cmd, stdin=subprocess.PIPE, **kwargs) stdin_write = popen.stdin.write for line in input_lines: stdin_write(line) stdout, stderr = popen.communicate() return subprocess.CompletedProcess(popen.args, popen.returncode, stdout=stdout, stderr=stderr) def _write_stderr(stderr) -> None: if isinstance(stderr, bytes): stderr_encoding = (getattr(sys.stderr, 'encoding', None) or sys.getdefaultencoding()) stderr = stderr.decode(stderr_encoding) sys.stderr.write(stderr) sys.stderr.flush() return None class ExecutableNotFound(RuntimeError): """:exc:`RuntimeError` raised if the Graphviz executable is not found.""" _msg = ('failed to execute {!r}, ' 'make sure the Graphviz executables are on your systems\' PATH') def __init__(self, args) -> None: super().__init__(self._msg.format(*args)) class CalledProcessError(subprocess.CalledProcessError): """:exc:`~subprocess.CalledProcessError` raised if a subprocess ``returncode`` is not ``0``.""" # noqa: E501 def __str__(self) -> 'str': return f'{super().__str__()} [stderr: {self.stderr!r}]' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/backend/mixins.py0000666000000000000000000000441714150757034015336 0ustar00"""Mixin classes used by Base subclasses to inherit backend functionality.""" import os import typing from .. import parameters from . import piping from . import rendering from . import unflattening from . import viewing __all__ = ['Render', 'Pipe', 'Unflatten', 'View'] class Render(parameters.Parameters): """Parameters for calling and calling ``graphviz.render()``.""" def _get_render_parameters(self, outfile: typing.Union[os.PathLike, str, None] = None, raise_if_result_exists: bool = False, overwrite_source: bool = False, **kwargs): kwargs = self._get_parameters(**kwargs) kwargs.update(outfile=outfile, raise_if_result_exists=raise_if_result_exists, overwrite_filepath=overwrite_source) return [kwargs.pop('engine'), kwargs.pop('format')], kwargs @property def _render(_): # noqa: N805 """Simplify ``._render()`` mocking.""" return rendering.render class Pipe(parameters.Parameters): """Parameters for calling and calling ``graphviz.pipe()``.""" _get_format = staticmethod(rendering.get_format) _get_filepath = staticmethod(rendering.get_filepath) def _get_pipe_parameters(self, **kwargs): kwargs = self._get_parameters(**kwargs) return [kwargs.pop('engine'), kwargs.pop('format')], kwargs @property def _pipe_lines(_): # noqa: N805 """Simplify ``._pipe_lines()`` mocking.""" return piping.pipe_lines @property def _pipe_lines_string(_): # noqa: N805 """Simplify ``._pipe_lines_string()`` mocking.""" return piping.pipe_lines_string class Unflatten: @property def _unflatten(_): # noqa: N805 """Simplify ``._unflatten mocking.""" return unflattening.unflatten class View: """Open filepath with its default viewing application (platform-specific).""" _view_darwin = staticmethod(viewing.view_darwin) _view_freebsd = staticmethod(viewing.view_unixoid) _view_linux = staticmethod(viewing.view_unixoid) _view_windows = staticmethod(viewing.view_windows) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/graphviz/backend/piping.py0000666000000000000000000002142614226560631015313 0ustar00"""Pipe bytes, strings, or string iterators through Graphviz ``dot``.""" import typing from .. import _tools from . import dot_command from . import execute __all__ = ['pipe', 'pipe_string', 'pipe_lines', 'pipe_lines_string'] @_tools.deprecate_positional_args(supported_number=3) def pipe(engine: str, format: str, data: bytes, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None, quiet: bool = False) -> bytes: """Return ``data`` (``bytes``) piped through ``engine`` into ``format`` as ``bytes``. Args: engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). format: Output format for rendering (``'pdf'``, ``'png'``, ...). data: Binary (encoded) DOT source bytes to render. renderer: Output renderer (``'cairo'``, ``'gd'``, ...). formatter: Output formatter (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet: Suppress ``stderr`` output from the layout subprocess. Returns: Binary (encoded) stdout of the layout command. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. Example: >>> doctest_mark_exe() >>> import graphviz >>> graphviz.pipe('dot', 'svg', b'graph { hello -- world }')[:14] b' str: """Return ``input_string`` piped through ``engine`` into ``format`` as string. Args: engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). format: Output format for rendering (``'pdf'``, ``'png'``, ...). input_string: Binary (encoded) DOT source bytes to render. encoding: Encoding to en/decode subprocess stdin and stdout (required). renderer: Output renderer (``'cairo'``, ``'gd'``, ...). formatter: Output formatter (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet: Suppress ``stderr`` output from the layout subprocess. Returns: Decoded stdout of the layout command. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. Example: >>> doctest_mark_exe() >>> import graphviz >>> graphviz.pipe_string('dot', 'svg', 'graph { spam }', ... encoding='ascii')[:14] ' bytes: r"""Return ``input_lines`` piped through ``engine`` into ``format`` as ``bytes``. Args: engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). format: Output format for rendering (``'pdf'``, ``'png'``, ...). input_lines: DOT source lines to render (including final newline). input_encoding: Encode input_lines for subprocess stdin (required). renderer: Output renderer (``'cairo'``, ``'gd'``, ...). formatter: Output formatter (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet: Suppress ``stderr`` output from the layout subprocess. Returns: Binary stdout of the layout command. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. Example: >>> doctest_mark_exe() >>> import graphviz >>> graphviz.pipe_lines('dot', 'svg', iter(['graph { spam }\n']), ... input_encoding='ascii')[:14] b' str: r"""Return ``input_lines`` piped through ``engine`` into ``format`` as string. Args: engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). format: Output format for rendering (``'pdf'``, ``'png'``, ...). input_lines: DOT source lines to render (including final newline). encoding: Encoding to en/decode subprocess stdin and stdout (required). renderer: Output renderer (``'cairo'``, ``'gd'``, ...). formatter: Output formatter (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet: Suppress ``stderr`` output from the layout subprocess. Returns: Decoded stdout of the layout command. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. Example: >>> doctest_mark_exe() >>> import graphviz >>> graphviz.pipe_lines_string('dot', 'svg', iter(['graph { spam }\n']), ... encoding='ascii')[:14] ' str: """Return format inferred from outfile suffix and/or given ``format``. Args: outfile: Path for the rendered output file. format: Output format for rendering (``'pdf'``, ``'png'``, ...). Returns: The given ``format`` falling back to the inferred format. Warns: graphviz.UnknownSuffixWarning: If the suffix of ``outfile`` is empty/unknown. graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile`` does not match the given ``format``. """ try: inferred_format = infer_format(outfile) except ValueError: if format is None: msg = ('cannot infer rendering format' f' from suffix {outfile.suffix!r}' f' of outfile: {os.fspath(outfile)!r}' ' (provide format or outfile with a suffix' f' from {get_supported_suffixes()!r})') raise exceptions.RequiredArgumentError(msg) warnings.warn(f'unknown outfile suffix {outfile.suffix!r}' f' (expected: {"." + format!r})', category=exceptions.UnknownSuffixWarning) return format else: assert inferred_format is not None if format is not None and format.lower() != inferred_format: warnings.warn(f'expected format {inferred_format!r} from outfile' f' differs from given format: {format!r}', category=exceptions.FormatSuffixMismatchWarning) return format return inferred_format def get_supported_suffixes() -> typing.List[str]: """Return a sorted list of supported outfile suffixes for exception/warning messages. >>> get_supported_suffixes() # doctest: +ELLIPSIS ['.bmp', ...] """ return [f'.{format}' for format in get_supported_formats()] def get_supported_formats() -> typing.List[str]: """Return a sorted list of supported formats for exception/warning messages. >>> get_supported_formats() # doctest: +ELLIPSIS ['bmp', ...] """ return sorted(parameters.FORMATS) def infer_format(outfile: pathlib.Path) -> str: """Return format inferred from outfile suffix. Args: outfile: Path for the rendered output file. Returns: The inferred format. Raises: ValueError: If the suffix of ``outfile`` is empty/unknown. >>> infer_format(pathlib.Path('spam.pdf')) # doctest: +NO_EXE 'pdf' >>> infer_format(pathlib.Path('spam.gv.svg')) 'svg' >>> infer_format(pathlib.Path('spam.PNG')) 'png' >>> infer_format(pathlib.Path('spam')) Traceback (most recent call last): ... ValueError: cannot infer rendering format from outfile: 'spam' (missing suffix) >>> infer_format(pathlib.Path('spam.wav')) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: cannot infer rendering format from suffix '.wav' of outfile: 'spam.wav' (unknown format: 'wav', provide outfile with a suffix from ['.bmp', ...]) """ if not outfile.suffix: raise ValueError('cannot infer rendering format from outfile:' f' {os.fspath(outfile)!r} (missing suffix)') start, sep, format_ = outfile.suffix.partition('.') assert sep and not start, f"{outfile.suffix!r}.startswith('.')" format_ = format_.lower() try: parameters.verify_format(format_) except ValueError: raise ValueError('cannot infer rendering format' f' from suffix {outfile.suffix!r}' f' of outfile: {os.fspath(outfile)!r}' f' (unknown format: {format_!r},' ' provide outfile with a suffix' f' from {get_supported_suffixes()!r})') return format_ def get_outfile(filepath: typing.Union[os.PathLike, str], *, format: str, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None) -> pathlib.Path: """Return ``filepath`` + ``[[.formatter].renderer].format``. See also: https://www.graphviz.org/doc/info/command.html#-O """ filepath = _tools.promote_pathlike(filepath) parameters.verify_format(format, required=True) parameters.verify_renderer(renderer, required=False) parameters.verify_formatter(formatter, required=False) suffix_args = (formatter, renderer, format) suffix = '.'.join(a for a in suffix_args if a is not None) return filepath.with_suffix(f'{filepath.suffix}.{suffix}') def get_filepath(outfile: typing.Union[os.PathLike, str]) -> pathlib.Path: """Return ``outfile.with_suffix('.gv')``.""" outfile = _tools.promote_pathlike(outfile) return outfile.with_suffix(f'.{DEFAULT_SOURCE_EXTENSION}') @typing.overload def render(engine: str, format: str, filepath: typing.Union[os.PathLike, str], renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = ..., *, outfile: typing.Union[os.PathLike, str, None] = ..., raise_if_result_exists: bool = ..., overwrite_filepath: bool = ...) -> str: """Require ``format`` and ``filepath`` with default ``outfile=None``.""" @typing.overload def render(engine: str, format: typing.Optional[str] = ..., filepath: typing.Union[os.PathLike, str, None] = ..., renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = False, *, outfile: typing.Union[os.PathLike, str, None] = ..., raise_if_result_exists: bool = ..., overwrite_filepath: bool = ...) -> str: """Optional ``format`` and ``filepath`` with given ``outfile``.""" @typing.overload def render(engine: str, format: typing.Optional[str] = ..., filepath: typing.Union[os.PathLike, str, None] = ..., renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = False, *, outfile: typing.Union[os.PathLike, str, None] = ..., raise_if_result_exists: bool = ..., overwrite_filepath: bool = ...) -> str: """Required/optional ``format`` and ``filepath`` depending on ``outfile``.""" @_tools.deprecate_positional_args(supported_number=3) def render(engine: str, format: typing.Optional[str] = None, filepath: typing.Union[os.PathLike, str, None] = None, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None, quiet: bool = False, *, outfile: typing.Union[os.PathLike, str, None] = None, raise_if_result_exists: bool = False, overwrite_filepath: bool = False) -> str: r"""Render file with ``engine`` into ``format`` and return result filename. Args: engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). format: Output format for rendering (``'pdf'``, ``'png'``, ...). Can be omitted if an ``outfile`` with a known ``format`` is given, i.e. if ``outfile`` ends with a known ``.{format}`` suffix. filepath: Path to the DOT source file to render. Can be omitted if ``outfile`` is given, in which case it defaults to ``outfile.with_suffix('.gv')``. renderer: Output renderer (``'cairo'``, ``'gd'``, ...). formatter: Output formatter (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet: Suppress ``stderr`` output from the layout subprocess. outfile: Path for the rendered output file. raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` if the result file exists. overwrite_filepath: Allow ``dot`` to write to the file it reads from. Incompatible with ``raise_if_result_exists``. Returns: The (possibly relative) path of the rendered file. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``format`` or ``filepath`` are None unless ``outfile`` is given. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. ValueError: If ``outfile`` and ``filename`` are the same file unless ``overwite_filepath=True``. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. graphviz.FileExistsError: If ``raise_if_exists`` and the result file exists. Warns: graphviz.UnknownSuffixWarning: If the suffix of ``outfile`` is empty or unknown. graphviz.FormatSuffixMismatchWarning: If the suffix of ``outfile`` does not match the given ``format``. Example: >>> doctest_mark_exe() >>> import pathlib >>> import graphviz >>> assert pathlib.Path('doctest-output/spam.gv').write_text('graph { spam }') == 14 >>> graphviz.render('dot', 'png', 'doctest-output/spam.gv').replace('\\', '/') 'doctest-output/spam.gv.png' >>> graphviz.render('dot', filepath='doctest-output/spam.gv', ... outfile='doctest-output/spam.png').replace('\\', '/') 'doctest-output/spam.png' >>> graphviz.render('dot', outfile='doctest-output/spam.pdf').replace('\\', '/') 'doctest-output/spam.pdf' Note: The layout command is started from the directory of ``filepath``, so that references to external files (e.g. ``[image=images/camelot.png]``) can be given as paths relative to the DOT source file. See also: Upstream docs: https://www.graphviz.org/doc/info/command.html """ if raise_if_result_exists and overwrite_filepath: raise ValueError('overwrite_filepath cannot be combined' ' with raise_if_result_exists') filepath, outfile = map(_tools.promote_pathlike, (filepath, outfile)) if outfile is not None: format = get_format(outfile, format=format) if filepath is None: filepath = get_filepath(outfile) if (not overwrite_filepath and outfile.name == filepath.name and outfile.resolve() == filepath.resolve()): # noqa: E129 raise ValueError(f'outfile {outfile.name!r} must be different' f' from input file {filepath.name!r}' ' (pass overwrite_filepath=True to override)') outfile_arg = (outfile.resolve() if outfile.parent != filepath.parent else outfile.name) # https://www.graphviz.org/doc/info/command.html#-o args = ['-o', outfile_arg, filepath.name] elif filepath is None: raise exceptions.RequiredArgumentError('filepath: (required if outfile is not given,' f' got {filepath!r})') elif format is None: raise exceptions.RequiredArgumentError('format: (required if outfile is not given,' f' got {format!r})') else: outfile = get_outfile(filepath, format=format, renderer=renderer, formatter=formatter) # https://www.graphviz.org/doc/info/command.html#-O args = ['-O', filepath.name] cmd = dot_command.command(engine, format, renderer=renderer, formatter=formatter, neato_no_op=neato_no_op) if raise_if_result_exists and os.path.exists(outfile): raise exceptions.FileExistsError(f'output file exists: {os.fspath(outfile)!r}') cmd += args assert filepath is not None, 'work around pytype false alarm' execute.run_check(cmd, cwd=filepath.parent if filepath.parent.parts else None, quiet=quiet, capture_output=True) return os.fspath(outfile) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/backend/unflattening.py0000666000000000000000000000413414150757034016521 0ustar00"""Pipe DOT source code through ``unflatten``.""" import pathlib import typing from ..encoding import DEFAULT_ENCODING from .. import _tools from .. import exceptions from . import execute __all__ = ['UNFLATTEN_BINARY', 'unflatten'] UNFLATTEN_BINARY = pathlib.Path('unflatten') @_tools.deprecate_positional_args(supported_number=1) def unflatten(source: str, stagger: typing.Optional[int] = None, fanout: bool = False, chain: typing.Optional[int] = None, encoding: str = DEFAULT_ENCODING) -> str: """Return DOT ``source`` piped through ``unflatten`` preprocessor as string. Args: source: DOT source to process (improve layout aspect ratio). stagger: Stagger the minimum length of leaf edges between 1 and this small integer. fanout: Fanout nodes with indegree = outdegree = 1 when staggering (requires ``stagger``). chain: Form disconnected nodes into chains of up to this many nodes. encoding: Encoding to encode unflatten stdin and decode its stdout. Returns: Decoded stdout of the Graphviz unflatten command. Raises: graphviz.RequiredArgumentError: If ``fanout`` is given but no ``stagger``. graphviz.ExecutableNotFound: If the Graphviz 'unflatten' executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the unflattening 'unflatten' subprocess is non-zero. See also: Upstream documentation: https://www.graphviz.org/pdf/unflatten.1.pdf """ if fanout and stagger is None: raise exceptions.RequiredArgumentError('fanout given without stagger') cmd = [UNFLATTEN_BINARY] if stagger is not None: cmd += ['-l', str(stagger)] if fanout: cmd.append('-f') if chain is not None: cmd += ['-c', str(chain)] proc = execute.run_check(cmd, input=source, encoding=encoding, capture_output=True) return proc.stdout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/backend/upstream_version.py0000666000000000000000000000373714150757034017440 0ustar00"""Return the version number from running ``dot -V``.""" import logging import re import subprocess import typing from . import dot_command from . import execute VERSION_PATTERN = re.compile(r''' graphviz[ ] version[ ] (\d+)\.(\d+) (?:\.(\d+) (?: ~dev\.\d{8}\.\d{4} | \.(\d+) )? )? [ ] ''', re.VERBOSE) log = logging.getLogger(__name__) def version() -> typing.Tuple[int, ...]: """Return the upstream version number tuple from ``stderr`` of ``dot -V``. Returns: Two, three, or four ``int`` version ``tuple``. Raises: graphviz.ExecutableNotFound: If the Graphviz executable is not found. graphviz.CalledProcessError: If the exit status is non-zero. RuntimeError: If the output cannot be parsed into a version number. Example: >>> doctest_mark_exe() >>> import graphviz >>> graphviz.version() # doctest: +ELLIPSIS (...) Note: Ignores the ``~dev.`` portion of development versions. See also: Upstream release version entry format: https://gitlab.com/graphviz/graphviz/-/blob/f94e91ba819cef51a4b9dcb2d76153684d06a913/gen_version.py#L17-20 """ cmd = [dot_command.DOT_BINARY, '-V'] proc = execute.run_check(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='ascii') ma = VERSION_PATTERN.search(proc.stdout) if ma is None: raise RuntimeError(f'cannot parse {cmd!r} output: {proc.stdout!r}') return tuple(int(d) for d in ma.groups() if d is not None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/backend/viewing.py0000666000000000000000000000431714150757034015476 0ustar00"""Open files in platform-specific default viewing application.""" import logging import os import platform import subprocess import typing from .. import _tools __all__ = ['view'] PLATFORM = platform.system().lower() log = logging.getLogger(__name__) @_tools.deprecate_positional_args(supported_number=1) def view(filepath: typing.Union[os.PathLike, str], quiet: bool = False) -> None: """Open filepath with its default viewing application (platform-specific). Args: filepath: Path to the file to open in viewer. quiet: Suppress ``stderr`` output from the viewer process (ineffective on Windows). Raises: RuntimeError: If the current platform is not supported. Note: There is no option to wait for the application to close, and no way to retrieve the application's exit status. """ try: view_func = getattr(view, PLATFORM) except AttributeError: raise RuntimeError(f'platform {PLATFORM!r} not supported') view_func(filepath, quiet=quiet) @_tools.attach(view, 'darwin') def view_darwin(filepath: typing.Union[os.PathLike, str], *, quiet: bool) -> None: """Open filepath with its default application (mac).""" cmd = ['open', filepath] log.debug('view: %r', cmd) kwargs = {'stderr': subprocess.DEVNULL} if quiet else {} subprocess.Popen(cmd, **kwargs) @_tools.attach(view, 'linux') @_tools.attach(view, 'freebsd') def view_unixoid(filepath: typing.Union[os.PathLike, str], *, quiet: bool) -> None: """Open filepath in the user's preferred application (linux, freebsd).""" cmd = ['xdg-open', filepath] log.debug('view: %r', cmd) kwargs = {'stderr': subprocess.DEVNULL} if quiet else {} subprocess.Popen(cmd, **kwargs) @_tools.attach(view, 'windows') def view_windows(filepath: typing.Union[os.PathLike, str], *, quiet: bool) -> None: """Start filepath with its associated application (windows).""" # TODO: implement quiet=True filepath = os.path.normpath(filepath) log.debug('view: %r', filepath) os.startfile(filepath) # pytype: disable=module-attr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/graphviz/base.py0000666000000000000000000000170514144205436013344 0ustar00"""Iterables of DOT source code lines (including final newline).""" import typing from . import copying __all__ = ['Base'] class LineIterable: """Iterable of DOT Source code lines (mimics ``file`` objects in text mode).""" def __iter__(self) -> typing.Iterator[str]: # pragma: no cover r"""Yield the generated DOT source line by line. Yields: Line ending with a newline (``'\n'``). """ raise NotImplementedError('to be implemented by concrete subclasses') # Common base interface for all exposed classes class Base(LineIterable, copying.CopyBase): """LineIterator with ``.source`` attribute, that it returns for ``str()``.""" @property def source(self) -> str: # pragma: no cover raise NotImplementedError('to be implemented by concrete subclasses') def __str__(self) -> str: """The DOT source code as string.""" return self.source ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/graphviz/copying.py0000666000000000000000000000106514144205436014101 0ustar00"""Create new instance copies with cooperative ``super()`` calls.""" __all__ = ['CopyBase'] class CopyBase: """Create new instance copies with cooperative ``super()`` calls.""" def copy(self): """Return a copied instance of the object. Returns: An independent copy of the current object. """ kwargs = self._copy_kwargs() return self.__class__(**kwargs) def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" return kwargs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715621575.0 graphviz-0.21/graphviz/dot.py0000666000000000000000000003206514620447307013227 0ustar00"""Create DOT code with method-calls.""" import contextlib import typing from . import _tools from . import base from . import quoting __all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot'] def comment(line: str) -> str: """Return comment header line.""" return f'// {line}\n' def graph_head(name: str) -> str: """Return DOT graph head line.""" return f'graph {name}{{\n' def digraph_head(name: str) -> str: """Return DOT digraph head line.""" return f'digraph {name}{{\n' def graph_edge(*, tail: str, head: str, attr: str) -> str: """Return DOT graph edge statement line.""" return f'\t{tail} -- {head}{attr}\n' def digraph_edge(*, tail: str, head: str, attr: str) -> str: """Return DOT digraph edge statement line.""" return f'\t{tail} -> {head}{attr}\n' class GraphSyntax: """DOT graph head and edge syntax.""" _head = staticmethod(graph_head) _edge = staticmethod(graph_edge) class DigraphSyntax: """DOT digraph head and edge syntax.""" _head = staticmethod(digraph_head) _edge = staticmethod(digraph_edge) def subgraph(name: str) -> str: """Return DOT subgraph head line.""" return f'subgraph {name}{{\n' def subgraph_plain(name: str) -> str: """Return plain DOT subgraph head line.""" return f'{name}{{\n' def node(left: str, right: str) -> str: """Return DOT node statement line.""" return f'\t{left}{right}\n' class Dot(quoting.Quote, base.Base): """Assemble DOT source code.""" directed: bool _comment = staticmethod(comment) @staticmethod def _head(name: str) -> str: # pragma: no cover """Return DOT head line.""" raise NotImplementedError('must be implemented by concrete subclasses') @classmethod def _head_strict(cls, name: str) -> str: """Return DOT strict head line.""" return f'strict {cls._head(name)}' _tail = '}\n' _subgraph = staticmethod(subgraph) _subgraph_plain = staticmethod(subgraph_plain) _node = _attr = staticmethod(node) @classmethod def _attr_plain(cls, left: str) -> str: return cls._attr(left, '') @staticmethod def _edge(*, tail: str, head: str, attr: str) -> str: # pragma: no cover """Return DOT edge statement line.""" raise NotImplementedError('must be implemented by concrete subclasses') @classmethod def _edge_plain(cls, *, tail: str, head: str) -> str: """Return plain DOT edge statement line.""" return cls._edge(tail=tail, head=head, attr='') def __init__(self, *, name: typing.Optional[str] = None, comment: typing.Optional[str] = None, graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, **kwargs) -> None: super().__init__(**kwargs) self.name = name """str: DOT source identifier for the ``graph`` or ``digraph`` statement.""" self.comment = comment """str: DOT source comment for the first source line.""" self.graph_attr = dict(graph_attr) if graph_attr is not None else {} """~typing.Dict[str, str]: Attribute-value pairs applying to the graph.""" self.node_attr = dict(node_attr) if node_attr is not None else {} """~typing.Dict[str, str]: Attribute-value pairs applying to all nodes.""" self.edge_attr = dict(edge_attr) if edge_attr is not None else {} """~typing.Dict[str, str]: Attribute-value pairs applying to all edges.""" self.body = list(body) if body is not None else [] """~typing.List[str]: Verbatim DOT source lines including final newline.""" self.strict = strict """bool: Rendering should merge multi-edges.""" def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" return super()._copy_kwargs(name=self.name, comment=self.comment, graph_attr=dict(self.graph_attr), node_attr=dict(self.node_attr), edge_attr=dict(self.edge_attr), body=list(self.body), strict=self.strict) @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self') def clear(self, keep_attrs: bool = False) -> None: """Reset content to an empty body, clear graph/node/egde_attr mappings. Args: keep_attrs (bool): preserve graph/node/egde_attr mappings """ if not keep_attrs: for a in (self.graph_attr, self.node_attr, self.edge_attr): a.clear() self.body.clear() @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self') def __iter__(self, subgraph: bool = False) -> typing.Iterator[str]: r"""Yield the DOT source code line by line (as graph or subgraph). Yields: Line ending with a newline (``'\n'``). """ if self.comment: yield self._comment(self.comment) if subgraph: if self.strict: raise ValueError('subgraphs cannot be strict') head = self._subgraph if self.name else self._subgraph_plain else: head = self._head_strict if self.strict else self._head yield head(self._quote(self.name) + ' ' if self.name else '') for kw in ('graph', 'node', 'edge'): attrs = getattr(self, f'{kw}_attr') if attrs: yield self._attr(kw, self._attr_list(None, kwargs=attrs)) yield from self.body yield self._tail @_tools.deprecate_positional_args(supported_number=2, ignore_arg='self', category=DeprecationWarning) def node(self, name: str, label: typing.Optional[str] = None, _attributes=None, **attrs) -> None: """Create a node. Args: name: Unique identifier for the node inside the source. label: Caption to be displayed (defaults to the node ``name``). attrs: Any additional node attributes (must be strings). Attention: When rendering ``label``, backslash-escapes and strings of the form ``<...>`` have a special meaning. See the sections :ref:`backslash-escapes` and :ref:`quoting-and-html-like-labels` in the user guide for details. """ name = self._quote(name) attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes) line = self._node(name, attr_list) self.body.append(line) @_tools.deprecate_positional_args(supported_number=3, ignore_arg='self', category=DeprecationWarning) def edge(self, tail_name: str, head_name: str, label: typing.Optional[str] = None, _attributes=None, **attrs) -> None: """Create an edge between two nodes. Args: tail_name: Start node identifier (format: ``node[:port[:compass]]``). head_name: End node identifier (format: ``node[:port[:compass]]``). label: Caption to be displayed near the edge. attrs: Any additional edge attributes (must be strings). Note: The ``tail_name`` and ``head_name`` strings are separated by (optional) colon(s) into ``node`` name, ``port`` name, and ``compass`` (e.g. ``sw``). See :ref:`details in the User Guide `. Attention: When rendering ``label``, backslash-escapes and strings of the form ``<...>`` have a special meaning. See the sections :ref:`backslash-escapes` and :ref:`quoting-and-html-like-labels` in the user guide for details. """ tail_name = self._quote_edge(tail_name) head_name = self._quote_edge(head_name) attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes) line = self._edge(tail=tail_name, head=head_name, attr=attr_list) self.body.append(line) def edges(self, tail_head_iter) -> None: """Create a bunch of edges. Args: tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs (format:``node[:port[:compass]]``). Note: The ``tail_name`` and ``head_name`` strings are separated by (optional) colon(s) into ``node`` name, ``port`` name, and ``compass`` (e.g. ``sw``). See :ref:`details in the User Guide `. """ edge = self._edge_plain quote = self._quote_edge self.body += [edge(tail=quote(t), head=quote(h)) for t, h in tail_head_iter] @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self', category=DeprecationWarning) def attr(self, kw: typing.Optional[str] = None, _attributes=None, **attrs) -> None: """Add a general or graph/node/edge attribute statement. Args: kw: Attributes target (``None`` or ``'graph'``, ``'node'``, ``'edge'``). attrs: Attributes to be set (must be strings, may be empty). See the :ref:`usage examples in the User Guide `. """ if kw is not None and kw.lower() not in ('graph', 'node', 'edge'): raise ValueError('attr statement must target graph, node, or edge:' f' {kw!r}') if attrs or _attributes: if kw is None: a_list = self._a_list(None, kwargs=attrs, attributes=_attributes) line = self._attr_plain(a_list) else: attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes) line = self._attr(kw, attr_list) self.body.append(line) @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def subgraph(self, graph=None, name: typing.Optional[str] = None, comment: typing.Optional[str] = None, graph_attr=None, node_attr=None, edge_attr=None, body=None): """Add the current content of the given sole ``graph`` argument as subgraph or return a context manager returning a new graph instance created with the given (``name``, ``comment``, etc.) arguments whose content is added as subgraph when leaving the context manager's ``with``-block. Args: graph: An instance of the same kind (:class:`.Graph`, :class:`.Digraph`) as the current graph (sole argument in non-with-block use). name: Subgraph name (``with``-block use). comment: Subgraph comment (``with``-block use). graph_attr: Subgraph-level attribute-value mapping (``with``-block use). node_attr: Node-level attribute-value mapping (``with``-block use). edge_attr: Edge-level attribute-value mapping (``with``-block use). body: Verbatim lines to add to the subgraph ``body`` (``with``-block use). See the :ref:`usage examples in the User Guide `. When used as a context manager, the returned new graph instance uses ``strict=None`` and the parent graph's values for ``directory``, ``format``, ``engine``, and ``encoding`` by default. Note: If the ``name`` of the subgraph begins with ``'cluster'`` (all lowercase) the layout engine will treat it as a special cluster subgraph. """ if graph is None: kwargs = self._copy_kwargs() kwargs.pop('filename', None) kwargs.update(name=name, comment=comment, graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr, body=body, strict=None) subgraph = self.__class__(**kwargs) @contextlib.contextmanager def subgraph_contextmanager(*, parent): """Return subgraph and add to parent on exit.""" yield subgraph parent.subgraph(subgraph) return subgraph_contextmanager(parent=self) args = [name, comment, graph_attr, node_attr, edge_attr, body] if not all(a is None for a in args): raise ValueError('graph must be sole argument of subgraph()') if graph.directed != self.directed: raise ValueError(f'{self!r} cannot add subgraph of different kind:' f' {graph!r}') self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1637399506.0 graphviz-0.21/graphviz/encoding.py0000666000000000000000000000212314146135722014215 0ustar00"""Encoding parameter handling and default.""" import typing import codecs import locale from . import copying __all__ = ['DEFAULT_ENCODING', 'Encoding'] DEFAULT_ENCODING = 'utf-8' class Encoding(copying.CopyBase): """Encoding used for input and output with ``'utf-8'`` default.""" _encoding = DEFAULT_ENCODING def __init__(self, *, encoding: typing.Optional[str] = DEFAULT_ENCODING, **kwargs) -> None: super().__init__(**kwargs) self.encoding = encoding def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" return super()._copy_kwargs(encoding=self._encoding, **kwargs) @property def encoding(self) -> str: """The encoding for the saved source file.""" return self._encoding @encoding.setter def encoding(self, encoding: typing.Optional[str]) -> None: if encoding is None: encoding = locale.getpreferredencoding() codecs.lookup(encoding) # raise early self._encoding = encoding ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/graphviz/exceptions.py0000666000000000000000000000206114226560631014611 0ustar00"""Commonly used exception classes.""" from .backend.execute import ExecutableNotFound, CalledProcessError __all__ = ['ExecutableNotFound', 'CalledProcessError', 'RequiredArgumentError', 'FileExistsError', 'UnknownSuffixWarning', 'FormatSuffixMismatchWarning', 'DotSyntaxWarning'] class RequiredArgumentError(TypeError): """:exc:`TypeError` raised if a required argument is missing.""" class FileExistsError(FileExistsError): """:exc:`FileExistsError` raised with ``raise_if_exists=True``.""" class UnknownSuffixWarning(RuntimeWarning): """:exc:`RuntimeWarning` raised if the suffix of ``outfile`` is unknown and the given ``format`` is used instead.""" class FormatSuffixMismatchWarning(UserWarning): """:exc:`UserWarning` raised if the suffix ``outfile`` does not match the given ``format``.""" class DotSyntaxWarning(RuntimeWarning): """:exc:`RuntimeWarning` raised if a quoted string is expected to cause a ``CalledProcessError`` from rendering.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715621074.0 graphviz-0.21/graphviz/graphs.py0000666000000000000000000001053414620446322013716 0ustar00r"""Assemble DOT source code objects. Example: >>> doctest_mark_exe() >>> import graphviz >>> dot = graphviz.Graph(comment='Mønti Pythøn ik den Hølie Grailen') >>> dot.node('Møøse') >>> dot.node('trained_by', 'trained by') >>> dot.node('tutte', 'TUTTE HERMSGERVORDENBROTBORDA') >>> dot.edge('Møøse', 'trained_by') >>> dot.edge('trained_by', 'tutte') >>> dot.node_attr['shape'] = 'rectangle' >>> print(dot.source) #doctest: +NORMALIZE_WHITESPACE // Mønti Pythøn ik den Hølie Grailen graph { node [shape=rectangle] "Møøse" trained_by [label="trained by"] tutte [label="TUTTE HERMSGERVORDENBROTBORDA"] "Møøse" -- trained_by trained_by -- tutte } >>> dot.render('doctest-output/m00se.gv').replace('\\', '/') 'doctest-output/m00se.gv.pdf' """ import typing from .encoding import DEFAULT_ENCODING from . import _tools from . import dot from . import jupyter_integration from . import piping from . import rendering from . import unflattening __all__ = ['Graph', 'Digraph'] class BaseGraph(dot.Dot, rendering.Render, jupyter_integration.JupyterIntegration, piping.Pipe, unflattening.Unflatten): """Dot language creation and source code rendering.""" @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def __init__(self, name: typing.Optional[str] = None, comment: typing.Optional[str] = None, filename=None, directory=None, format: typing.Optional[str] = None, engine: typing.Optional[str] = None, encoding: typing.Optional[str] = DEFAULT_ENCODING, graph_attr=None, node_attr=None, edge_attr=None, body=None, strict: bool = False, *, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None) -> None: if filename is None and name is not None: filename = f'{name}.{self._default_extension}' super().__init__(name=name, comment=comment, graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr, body=body, strict=strict, filename=filename, directory=directory, encoding=encoding, format=format, engine=engine, renderer=renderer, formatter=formatter) @property def source(self) -> str: """The generated DOT source code as string.""" return ''.join(self) class Graph(dot.GraphSyntax, BaseGraph): """Graph source code in the DOT language. Args: name: Graph name used in the source code. comment: Comment added to the first line of the source. filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``). directory: (Sub)directory for source saving and rendering. format: Rendering output format (``'pdf'``, ``'png'``, ...). engine: Layout command used (``'dot'``, ``'neato'``, ...). renderer: Output renderer used (``'cairo'``, ``'gd'``, ...). formatter: Output formatter used (``'cairo'``, ``'gd'``, ...). encoding: Encoding for saving the source. graph_attr: Mapping of ``(attribute, value)`` pairs for the graph. node_attr: Mapping of ``(attribute, value)`` pairs set for all nodes. edge_attr: Mapping of ``(attribute, value)`` pairs set for all edges. body: Iterable of verbatim lines (including their final newline) to add to the graph ``body``. strict (bool): Rendering should merge multi-edges. Note: All parameters are `optional` and can be changed under their corresponding attribute name after instance creation. """ @property def directed(self) -> bool: """``False``""" return False class Digraph(dot.DigraphSyntax, BaseGraph): """Directed graph source code in the DOT language.""" if Graph.__doc__ is not None: __doc__ += Graph.__doc__.partition('.')[2] @property def directed(self) -> bool: """``True``""" return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745680.0 graphviz-0.21/graphviz/jupyter_integration.py0000666000000000000000000001054314432626720016542 0ustar00"""Display rendered graph as SVG in Jupyter Notebooks and QtConsole.""" import typing from . import piping __all__ = ['JUPYTER_FORMATS', 'SUPPORTED_JUPYTER_FORMATS', 'DEFAULT_JUPYTER_FORMAT', 'get_jupyter_format_mimetype', 'JupyterIntegration'] _IMAGE_JPEG = 'image/jpeg' JUPYTER_FORMATS = {'jpeg': _IMAGE_JPEG, 'jpg': _IMAGE_JPEG, 'png': 'image/png', 'svg': 'image/svg+xml'} SUPPORTED_JUPYTER_FORMATS = set(JUPYTER_FORMATS) DEFAULT_JUPYTER_FORMAT = next(_ for _ in SUPPORTED_JUPYTER_FORMATS if _ == 'svg') MIME_TYPES = {'image/jpeg': '_repr_image_jpeg', 'image/png': '_repr_image_png', 'image/svg+xml': '_repr_image_svg_xml'} assert MIME_TYPES.keys() == set(JUPYTER_FORMATS.values()) SVG_ENCODING = 'utf-8' def get_jupyter_format_mimetype(jupyter_format: str) -> str: try: return JUPYTER_FORMATS[jupyter_format] except KeyError: raise ValueError(f'unknown jupyter_format: {jupyter_format!r}' f' (must be one of {sorted(JUPYTER_FORMATS)})') def get_jupyter_mimetype_format(mimetype: str) -> str: if mimetype not in MIME_TYPES: raise ValueError(f'unsupported mimetype: {mimetype!r}' f' (must be one of {sorted(MIME_TYPES)})') assert mimetype in JUPYTER_FORMATS.values() for format, jupyter_mimetype in JUPYTER_FORMATS.items(): if jupyter_mimetype == mimetype: return format raise RuntimeError # pragma: no cover class JupyterIntegration(piping.Pipe): """Display rendered graph as SVG in Jupyter Notebooks and QtConsole.""" _jupyter_mimetype = get_jupyter_format_mimetype(DEFAULT_JUPYTER_FORMAT) def _repr_mimebundle_(self, include: typing.Optional[typing.Iterable[str]] = None, exclude: typing.Optional[typing.Iterable[str]] = None, **_) -> typing.Dict[str, typing.Union[bytes, str]]: r"""Return the rendered graph as IPython mimebundle. Args: include: Iterable of mimetypes to include in the result. If not given or ``None``: ``['image/sxg+xml']``. exclude: Iterable of minetypes to exclude from the result. Overrides ``include``. Returns: Mapping from mimetypes to data. Example: >>> doctest_mark_exe() >>> import graphviz >>> dot = graphviz.Graph() >>> dot._repr_mimebundle_() # doctest: +ELLIPSIS {'image/svg+xml': '>> dot._repr_mimebundle_(include=['image/png']) # doctest: +ELLIPSIS {'image/png': b'\x89PNG... >>> dot._repr_mimebundle_(include=[]) {} >>> dot._repr_mimebundle_(include=['image/svg+xml', 'image/jpeg'], ... exclude=['image/svg+xml']) # doctest: +ELLIPSIS {'image/jpeg': b'\xff... >>> list(dot._repr_mimebundle_(include=['image/png', 'image/jpeg'])) ['image/jpeg', 'image/png'] See also: IPython documentation: - https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#functions - https://ipython.readthedocs.io/en/stable/config/integrating.html#MyObject._repr_mimebundle_ # noqa: E501 - https://nbviewer.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Custom%20Display%20Logic.ipynb#Custom-Mimetypes-with-_repr_mimebundle_ # noqa: E501 """ include = set(include) if include is not None else {self._jupyter_mimetype} include -= set(exclude or []) return {mimetype: getattr(self, method_name)() for mimetype, method_name in MIME_TYPES.items() if mimetype in include} def _repr_image_jpeg(self) -> bytes: """Return the rendered graph as JPEG bytes.""" return self.pipe(format='jpeg') def _repr_image_png(self) -> bytes: """Return the rendered graph as PNG bytes.""" return self.pipe(format='png') def _repr_image_svg_xml(self) -> str: """Return the rendered graph as SVG string.""" return self.pipe(format='svg', encoding=SVG_ENCODING) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1749979899.503118 graphviz-0.21/graphviz/parameters/0000777000000000000000000000000015023511374014216 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/graphviz/parameters/__init__.py0000666000000000000000000000074214144205436016334 0ustar00"""Hold and verify parameters for running Graphviz ``dot``.""" from .engines import ENGINES, verify_engine from .formats import FORMATS, verify_format from .renderers import RENDERERS, verify_renderer from .formatters import FORMATTERS, verify_formatter from . mixins import Parameters __all__ = ['ENGINES', 'FORMATS', 'RENDERERS', 'FORMATTERS', 'verify_engine', 'verify_format', 'verify_renderer', 'verify_formatter', 'Parameters'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/graphviz/parameters/base.py0000666000000000000000000000072114144205436015504 0ustar00"""Rendering parameter handling.""" from .. import copying __all__ = ['ParameterBase'] class ParameterBase(copying.CopyBase): """Rendering parameter.""" def _getattr_from_dict(self, attrname: str, *, default=None): """Return self.attrname if attrname is in the instance dictionary (as oposed to on the type).""" if attrname in self.__dict__: return getattr(self, attrname) return default ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/parameters/engines.py0000666000000000000000000000326414150757034016232 0ustar00"""Rendering engine parameter handling.""" import typing from . import base __all__ = ['ENGINES', 'verify_engine', 'Engine'] ENGINES = {'dot', # https://www.graphviz.org/pdf/dot.1.pdf 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage'} DEFAULT_ENGINE = 'dot' REQUIRED = True def verify_engine(engine: str, *, required: bool = REQUIRED) -> None: if engine is None: if required: raise ValueError('missing engine') elif engine.lower() not in ENGINES: raise ValueError(f'unknown engine: {engine!r}' f' (must be one of {sorted(ENGINES)})') class Engine(base.ParameterBase): """Rendering engine parameter with ``'dot''`` default.""" _engine = DEFAULT_ENGINE _verify_engine = staticmethod(verify_engine) def __init__(self, *, engine: typing.Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) if engine is not None: self.engine = engine def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" engine = self._getattr_from_dict('_engine') if engine is not None: kwargs['engine'] = engine return super()._copy_kwargs(**kwargs) @property def engine(self) -> str: """The layout engine used for rendering (``'dot'``, ``'neato'``, ...).""" return self._engine @engine.setter def engine(self, engine: str) -> None: engine = engine.lower() self._verify_engine(engine) self._engine = engine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749978355.0 graphviz-0.21/graphviz/parameters/formats.py0000666000000000000000000000465715023506363016261 0ustar00"""Rendering format parameter handling.""" import typing from . import base __all__ = ['FORMATS', 'verify_format', 'Format'] FORMATS = {'bmp', # https://graphviz.org/docs/outputs/ 'canon', 'dot', 'gv', 'xdot', 'xdot1.2', 'xdot1.4', 'cgimage', 'cmap', 'eps', 'exr', 'fig', 'gd', 'gd2', 'gif', 'gtk', 'ico', 'imap', 'cmapx', 'imap_np', 'cmapx_np', 'ismap', 'jp2', 'jpg', 'jpeg', 'jpe', 'json', 'json0', 'dot_json', 'xdot_json', # Graphviz 2.40 'pct', 'pict', 'pdf', 'pic', 'plain', 'plain-ext', 'png', 'pov', 'ps', 'ps2', 'psd', 'sgi', 'svg', 'svg_inline', 'svgz', # svg_linline: Graphviz 10.0.1 'tga', 'tif', 'tiff', 'tk', 'vml', 'vmlz', 'vrml', 'wbmp', 'webp', 'xlib', 'x11'} DEFAULT_FORMAT = 'pdf' REQUIRED = True def verify_format(format: str, *, required: bool = REQUIRED) -> None: if format is None: if required: raise ValueError('missing format') elif format.lower() not in FORMATS: raise ValueError(f'unknown format: {format!r}' f' (must be one of {sorted(FORMATS)})') class Format(base.ParameterBase): """Rendering format parameter with ``'pdf'`` default.""" _format = DEFAULT_FORMAT _verify_format = staticmethod(verify_format) def __init__(self, *, format: typing.Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) if format is not None: self.format = format def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" format = self._getattr_from_dict('_format') if format is not None: kwargs['format'] = format return super()._copy_kwargs(**kwargs) @property def format(self) -> str: """The output format used for rendering (``'pdf'``, ``'png'``, ...).""" return self._format @format.setter def format(self, format: str) -> None: format = format.lower() self._verify_format(format) self._format = format ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/parameters/formatters.py0000666000000000000000000000351314150757034016765 0ustar00"""Rendering formatter parameter handling.""" import typing from . import base __all__ = ['FORMATTERS', 'verify_formatter', 'Formatter'] FORMATTERS = {'cairo', 'core', 'gd', 'gdiplus', 'gdwbmp', 'xlib'} REQUIRED = False def verify_formatter(formatter: typing.Optional[str], *, required: bool = REQUIRED) -> None: if formatter is None: if required: raise ValueError('missing formatter') elif formatter.lower() not in FORMATTERS: raise ValueError(f'unknown formatter: {formatter!r}' f' (must be None or one of {sorted(FORMATTERS)})') class Formatter(base.ParameterBase): """Rendering engine parameter (no default).""" _formatter = None _verify_formatter = staticmethod(verify_formatter) def __init__(self, *, formatter: typing.Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) self.formatter = formatter def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" formatter = self._getattr_from_dict('_formatter') if formatter is not None: kwargs['formatter'] = formatter return super()._copy_kwargs(**kwargs) @property def formatter(self) -> typing.Optional[str]: """The output formatter used for rendering (``'cairo'``, ``'gd'``, ...).""" return self._formatter @formatter.setter def formatter(self, formatter: typing.Optional[str]) -> None: if formatter is None: self.__dict__.pop('_formatter', None) else: formatter = formatter.lower() self._verify_formatter(formatter) self._formatter = formatter ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/parameters/mixins.py0000666000000000000000000000264614150757034016114 0ustar00"""Mixin classes used to inherit parameter functionality.""" import typing from . import engines from . import formats from . import renderers from . import formatters __all__ = ['Parameters'] class Parameters(engines.Engine, formats.Format, renderers.Renderer, formatters.Formatter): """Parameters for calling ``graphviz.render()`` and ``graphviz.pipe()``.""" def _get_parameters(self, *, engine: typing.Optional[str] = None, format: typing.Optional[str] = None, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, verify: bool = False, **kwargs): if engine is None: engine = self.engine elif verify: self._verify_engine(engine) if format is None: format = self.format elif verify: self._verify_format(format) if renderer is None: renderer = self.renderer elif verify: self._verify_renderer(renderer) if formatter is None: formatter = self.formatter elif verify: self._verify_formatter(formatter) kwargs.update(engine=engine, format=format, renderer=renderer, formatter=formatter) return kwargs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/graphviz/parameters/renderers.py0000666000000000000000000000372114150757034016571 0ustar00"""Rendering renderer parameter handling.""" import typing from . import base __all__ = ['RENDERERS', 'verify_renderer', 'Renderer'] RENDERERS = {'cairo', # $ dot -T: 'dot', 'fig', 'gd', 'gdiplus', 'map', 'pic', 'pov', 'ps', 'svg', 'tk', 'vml', 'vrml', 'xdot'} REQUIRED = False def verify_renderer(renderer: typing.Optional[str], *, required: bool = REQUIRED) -> None: if renderer is None: if required: raise ValueError('missing renderer') elif renderer.lower() not in RENDERERS: raise ValueError(f'unknown renderer: {renderer!r}' f' (must be None or one of {sorted(RENDERERS)})') class Renderer(base.ParameterBase): """Rendering renderer parameter (no default).""" _renderer = None _verify_renderer = staticmethod(verify_renderer) def __init__(self, *, renderer: typing.Optional[str] = None, **kwargs) -> None: super().__init__(**kwargs) self.renderer = renderer def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" renderer = self._getattr_from_dict('_renderer') if renderer is not None: kwargs['renderer'] = renderer return super()._copy_kwargs(**kwargs) @property def renderer(self) -> typing.Optional[str]: """The output renderer used for rendering (``'cairo'``, ``'gd'``, ...).""" return self._renderer @renderer.setter def renderer(self, renderer: typing.Optional[str]) -> None: if renderer is None: self.__dict__.pop('_renderer', None) else: renderer = renderer.lower() self._verify_renderer(renderer) self._renderer = renderer ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715621115.0 graphviz-0.21/graphviz/piping.py0000666000000000000000000001576414620446373013740 0ustar00"""Pipe DOT code objects through Graphviz ``dot``.""" import codecs import logging import typing from . import _tools from . import backend from . import exceptions from . import base from . import encoding __all__ = ['Pipe'] log = logging.getLogger(__name__) class Pipe(encoding.Encoding, base.Base, backend.Pipe): """Pipe source lines through the Graphviz layout command.""" @typing.overload def pipe(self, format: typing.Optional[str] = ..., renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = ..., *, engine: typing.Optional[str] = ..., encoding: None = ...) -> bytes: """Return bytes with default ``encoding=None``.""" @typing.overload def pipe(self, format: typing.Optional[str] = ..., renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = ..., *, engine: typing.Optional[str] = ..., encoding: str) -> str: """Return string when given encoding.""" @typing.overload def pipe(self, format: typing.Optional[str] = ..., renderer: typing.Optional[str] = ..., formatter: typing.Optional[str] = ..., neato_no_op: typing.Union[bool, int, None] = ..., quiet: bool = ..., *, engine: typing.Optional[str] = ..., encoding: typing.Optional[str]) -> typing.Union[bytes, str]: """Return bytes or string depending on encoding argument.""" def pipe(self, format: typing.Optional[str] = None, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None, quiet: bool = False, *, engine: typing.Optional[str] = None, encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]: """Return the source piped through the Graphviz layout command. Args: format: The output format used for rendering (``'pdf'``, ``'png'``, etc.). renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet (bool): Suppress ``stderr`` output from the layout subprocess. engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). encoding: Encoding for decoding the stdout. Returns: Bytes or if encoding is given decoded string (stdout of the layout command). Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. Example: >>> doctest_mark_exe() >>> import graphviz >>> source = 'graph { spam }' >>> graphviz.Source(source, format='svg').pipe()[:14] b'>> graphviz.Source(source, format='svg').pipe(encoding='ascii')[:14] '>> graphviz.Source(source, format='svg').pipe(encoding='utf-8')[:14] ' typing.Union[bytes, str]: return self._pipe_future(format, renderer=renderer, formatter=formatter, neato_no_op=neato_no_op, quiet=quiet, engine=engine, encoding=encoding) def _pipe_future(self, format: typing.Optional[str] = None, *, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None, quiet: bool = False, engine: typing.Optional[str] = None, encoding: typing.Optional[str] = None) -> typing.Union[bytes, str]: args, kwargs = self._get_pipe_parameters(engine=engine, format=format, renderer=renderer, formatter=formatter, neato_no_op=neato_no_op, quiet=quiet, verify=True) args.append(iter(self)) if encoding is not None: if codecs.lookup(encoding) is codecs.lookup(self.encoding): # common case: both stdin and stdout need the same encoding return self._pipe_lines_string(*args, encoding=encoding, **kwargs) try: raw = self._pipe_lines(*args, input_encoding=self.encoding, **kwargs) except exceptions.CalledProcessError as e: *args, output, stderr = e.args if output is not None: output = output.decode(self.encoding) if stderr is not None: stderr = stderr.decode(self.encoding) raise e.__class__(*args, output=output, stderr=stderr) else: return raw.decode(encoding) return self._pipe_lines(*args, input_encoding=self.encoding, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1650123161.0 graphviz-0.21/graphviz/quoting.py0000666000000000000000000001464414226560631014130 0ustar00"""Quote strings to be valid DOT identifiers, assemble quoted attribute lists.""" import functools import re import typing import warnings from . import _tools from . import exceptions __all__ = ['quote', 'quote_edge', 'a_list', 'attr_list', 'escape', 'nohtml'] # https://www.graphviz.org/doc/info/lang.html # https://www.graphviz.org/doc/info/attrs.html#k:escString HTML_STRING = re.compile(r'<.*>$', re.DOTALL) ID = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$') KEYWORDS = {'node', 'edge', 'graph', 'digraph', 'subgraph', 'strict'} COMPASS = {'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'} # TODO FINAL_ODD_BACKSLASHES = re.compile(r'(?(?:\\{2})*) \\? # treat \" same as " (?P") ''', flags=re.VERBOSE) ESCAPE_UNESCAPED_QUOTES = functools.partial(QUOTE_WITH_OPTIONAL_BACKSLASHES.sub, r'\g' r'\\' r'\g') @_tools.deprecate_positional_args(supported_number=1) def quote(identifier: str, is_html_string=HTML_STRING.match, is_valid_id=ID.match, dot_keywords=KEYWORDS, endswith_odd_number_of_backslashes=FINAL_ODD_BACKSLASHES.search, escape_unescaped_quotes=ESCAPE_UNESCAPED_QUOTES) -> str: r"""Return DOT identifier from string, quote if needed. >>> quote('') # doctest: +NO_EXE '""' >>> quote('spam') 'spam' >>> quote('spam spam') '"spam spam"' >>> quote('-4.2') '-4.2' >>> quote('.42') '.42' >>> quote('<spam>') '<spam>' >>> quote(nohtml('<>')) '"<>"' >>> print(quote('"')) "\"" >>> print(quote('\\"')) "\"" >>> print(quote('\\\\"')) "\\\"" >>> print(quote('\\\\\\"')) "\\\"" """ if is_html_string(identifier) and not isinstance(identifier, NoHtml): pass elif not is_valid_id(identifier) or identifier.lower() in dot_keywords: if endswith_odd_number_of_backslashes(identifier): warnings.warn('expect syntax error scanning invalid quoted string:' f' {identifier!r}', category=exceptions.DotSyntaxWarning) return f'"{escape_unescaped_quotes(identifier)}"' return identifier def quote_edge(identifier: str) -> str: """Return DOT edge statement node_id from string, quote if needed. >>> quote_edge('spam') # doctest: +NO_EXE 'spam' >>> quote_edge('spam spam:eggs eggs') '"spam spam":"eggs eggs"' >>> quote_edge('spam:eggs:s') 'spam:eggs:s' """ node, _, rest = identifier.partition(':') parts = [quote(node)] if rest: port, _, compass = rest.partition(':') parts.append(quote(port)) if compass: parts.append(compass) return ':'.join(parts) @_tools.deprecate_positional_args(supported_number=1) def a_list(label: typing.Optional[str] = None, kwargs=None, attributes=None) -> str: """Return assembled DOT a_list string. >>> a_list('spam', kwargs={'spam': None, 'ham': 'ham ham', 'eggs': ''}) # doctest: +NO_EXE 'label=spam eggs="" ham="ham ham"' """ result = [f'label={quote(label)}'] if label is not None else [] if kwargs: result += [f'{quote(k)}={quote(v)}' for k, v in _tools.mapping_items(kwargs) if v is not None] if attributes: if hasattr(attributes, 'items'): attributes = _tools.mapping_items(attributes) result += [f'{quote(k)}={quote(v)}' for k, v in attributes if v is not None] return ' '.join(result) @_tools.deprecate_positional_args(supported_number=1) def attr_list(label: typing.Optional[str] = None, kwargs=None, attributes=None) -> str: """Return assembled DOT attribute list string. Sorts ``kwargs`` and ``attributes`` if they are plain dicts (to avoid unpredictable order from hash randomization in Python < 3.7). >>> attr_list() # doctest: +NO_EXE '' >>> attr_list('spam spam', kwargs={'eggs': 'eggs', 'ham': 'ham ham'}) ' [label="spam spam" eggs=eggs ham="ham ham"]' >>> attr_list(kwargs={'spam': None, 'eggs': ''}) ' [eggs=""]' """ content = a_list(label, kwargs=kwargs, attributes=attributes) if not content: return '' return f' [{content}]' class Quote: """Quote strings to be valid DOT identifiers, assemble quoted attribute lists.""" _quote = staticmethod(quote) _quote_edge = staticmethod(quote_edge) _a_list = staticmethod(a_list) _attr_list = staticmethod(attr_list) def escape(s: str) -> str: r"""Return string disabling special meaning of backslashes and ``'<...>'``. Args: s: String in which backslashes and ``'<...>'`` should be treated as literal. Returns: Escaped string subclass instance. Raises: TypeError: If ``s`` is not a ``str``. Example: >>> import graphviz # doctest: +NO_EXE >>> print(graphviz.escape(r'\l')) \\l See also: Upstream documentation: https://www.graphviz.org/doc/info/attrs.html#k:escString """ return nohtml(s.replace('\\', '\\\\')) class NoHtml(str): """String subclass that does not treat ``'<...>'`` as DOT HTML string.""" __slots__ = () def nohtml(s: str) -> str: """Return string not treating ``'<...>'`` as DOT HTML string in quoting. Args: s: String in which leading ``'<'`` and trailing ``'>'`` should be treated as literal. Returns: String subclass instance. Raises: TypeError: If ``s`` is not a ``str``. Example: >>> import graphviz # doctest: +NO_EXE >>> g = graphviz.Graph() >>> g.node(graphviz.nohtml('<>-*-<>')) >>> print(g.source) # doctest: +NORMALIZE_WHITESPACE graph { "<>-*-<>" } """ return NoHtml(s) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715620791.0 graphviz-0.21/graphviz/rendering.py0000666000000000000000000002005414620445667014420 0ustar00"""Save DOT code objects, render with Graphviz ``dot``, and open in viewer.""" import logging import os import pathlib import typing from . import _tools from . import backend from . import saving __all__ = ['Render'] log = logging.getLogger(__name__) class Render(saving.Save, backend.Render, backend.View): """Write source lines to file and render with Graphviz.""" @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def render(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, view: bool = False, cleanup: bool = False, format: typing.Optional[str] = None, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, neato_no_op: typing.Union[bool, int, None] = None, quiet: bool = False, quiet_view: bool = False, *, outfile: typing.Union[os.PathLike, str, None] = None, engine: typing.Optional[str] = None, raise_if_result_exists: bool = False, overwrite_source: bool = False) -> str: r"""Save the source to file and render with the Graphviz engine. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``).s directory: (Sub)directory for source saving and rendering. view (bool): Open the rendered result with the default application. cleanup (bool): Delete the source file after successful rendering. format: The output format used for rendering (``'pdf'``, ``'png'``, etc.). renderer: The output renderer used for rendering (``'cairo'``, ``'gd'``, ...). formatter: The output formatter used for rendering (``'cairo'``, ``'gd'``, ...). neato_no_op: Neato layout engine no-op flag. quiet (bool): Suppress ``stderr`` output from the layout subprocess. quiet_view (bool): Suppress ``stderr`` output from the viewer process (implies ``view=True``, ineffective on Windows platform). outfile: Path for the rendered output file. engine: Layout engine for rendering (``'dot'``, ``'neato'``, ...). raise_if_result_exists: Raise :exc:`graphviz.FileExistsError` if the result file exists. overwrite_source: Allow ``dot`` to write to the file it reads from. Incompatible with ``raise_if_result_exists``. Returns: The (possibly relative) path of the rendered file. Raises: ValueError: If ``engine``, ``format``, ``renderer``, or ``formatter`` are unknown. graphviz.RequiredArgumentError: If ``formatter`` is given but ``renderer`` is None. ValueError: If ``outfile`` is the same file as the source file unless ``overwite_source=True``. graphviz.ExecutableNotFound: If the Graphviz ``dot`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the rendering ``dot`` subprocess is non-zero. RuntimeError: If viewer opening is requested but not supported. Example: >>> doctest_mark_exe() >>> import graphviz >>> dot = graphviz.Graph(name='spam', directory='doctest-output') >>> dot.render(format='png').replace('\\', '/') 'doctest-output/spam.gv.png' >>> dot.render(outfile='spam.svg').replace('\\', '/') 'doctest-output/spam.svg' Note: The layout command is started from the directory of ``filepath``, so that references to external files (e.g. ``[image=images/camelot.png]``) can be given as paths relative to the DOT source file. """ outfile = _tools.promote_pathlike(outfile) if outfile is not None: format = self._get_format(outfile, format=format) if directory is None: outfile = pathlib.Path(self.directory, outfile) args, kwargs = self._get_render_parameters(engine=engine, format=format, renderer=renderer, formatter=formatter, neato_no_op=neato_no_op, quiet=quiet, outfile=outfile, raise_if_result_exists=raise_if_result_exists, overwrite_source=overwrite_source, verify=True) if outfile is not None and filename is None: filename = self._get_filepath(outfile) filepath = self.save(filename, directory=directory, skip_existing=None) args.append(filepath) rendered = self._render(*args, **kwargs) if cleanup: log.debug('delete %r', filepath) os.remove(filepath) if quiet_view or view: self._view(rendered, format=self._format, quiet=quiet_view) return rendered def _view(self, filepath: typing.Union[os.PathLike, str], *, format: str, quiet: bool) -> None: """Start the right viewer based on file format and platform.""" methodnames = [ f'_view_{format}_{backend.viewing.PLATFORM}', f'_view_{backend.viewing.PLATFORM}', ] for name in methodnames: view_method = getattr(self, name, None) if view_method is not None: break else: raise RuntimeError(f'{self.__class__!r} has no built-in viewer' f' support for {format!r}' f' on {backend.viewing.PLATFORM!r} platform') view_method(filepath, quiet=quiet) @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def view(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, cleanup: bool = False, quiet: bool = False, quiet_view: bool = False) -> str: """Save the source to file, open the rendered result in a viewer. Convenience short-cut for running ``.render(view=True)``. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``). directory: (Sub)directory for source saving and rendering. cleanup (bool): Delete the source file after successful rendering. quiet (bool): Suppress ``stderr`` output from the layout subprocess. quiet_view (bool): Suppress ``stderr`` output from the viewer process (ineffective on Windows). Returns: The (possibly relative) path of the rendered file. Raises: graphviz.ExecutableNotFound: If the Graphviz executable is not found. graphviz.CalledProcessError: If the exit status is non-zero. RuntimeError: If opening the viewer is not supported. Short-cut method for calling :meth:`.render` with ``view=True``. Note: There is no option to wait for the application to close, and no way to retrieve the application's exit status. """ return self.render(filename=filename, directory=directory, view=True, cleanup=cleanup, quiet=quiet, quiet_view=quiet_view) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715619529.0 graphviz-0.21/graphviz/saving.py0000666000000000000000000000530514620443311013714 0ustar00"""Save DOT source lines to a file.""" import logging import os import typing from . import _defaults from . import _tools from . import base from . import encoding __all__ = ['Save'] log = logging.getLogger(__name__) class Save(encoding.Encoding, base.Base): """Save DOT source lines to file.""" directory: typing.Union[str, bytes] = '' _default_extension = _defaults.DEFAULT_SOURCE_EXTENSION _mkdirs = staticmethod(_tools.mkdirs) def __init__(self, *, filename: typing.Union[os.PathLike, str], directory: typing.Union[os.PathLike, str, None] = None, **kwargs) -> None: super().__init__(**kwargs) if filename is None: filename = f'{self.__class__.__name__}.{self._default_extension}' self.filename = os.fspath(filename) """str: Target file name for saving the DOT source file.""" if directory is not None: self.directory = os.fspath(directory) def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" assert 'directory' not in kwargs if 'directory' in self.__dict__: kwargs['directory'] = self.directory return super()._copy_kwargs(filename=self.filename, **kwargs) @property def filepath(self) -> str: """The target path for saving the DOT source file.""" return os.path.join(self.directory, self.filename) @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def save(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, *, skip_existing: typing.Optional[bool] = False) -> str: """Save the DOT source to file. Ensure the file ends with a newline. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) directory: (Sub)directory for source saving and rendering. skip_existing: Skip write if file exists (default: ``False``). Returns: The (possibly relative) path of the saved source file. """ if filename is not None: self.filename = filename if directory is not None: self.directory = directory filepath = self.filepath if skip_existing and os.path.exists(filepath): return filepath self._mkdirs(filepath) log.debug('write lines to %r', filepath) with open(filepath, 'w', encoding=self.encoding) as fd: for uline in self: fd.write(uline) return filepath ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715619529.0 graphviz-0.21/graphviz/sources.py0000666000000000000000000001413514620443311014111 0ustar00"""Save DOT code objects, render with Graphviz dot, and open in viewer.""" import locale import logging import os import typing from .encoding import DEFAULT_ENCODING from . import _tools from . import saving from . import jupyter_integration from . import piping from . import rendering from . import unflattening __all__ = ['Source'] log = logging.getLogger(__name__) class Source(rendering.Render, saving.Save, jupyter_integration.JupyterIntegration, piping.Pipe, unflattening.Unflatten): """Verbatim DOT source code string to be rendered by Graphviz. Args: source: The verbatim DOT source code string. filename: Filename for saving the source (defaults to ``'Source.gv'``). directory: (Sub)directory for source saving and rendering. format: Rendering output format (``'pdf'``, ``'png'``, ...). engine: Layout engine used (``'dot'``, ``'neato'``, ...). encoding: Encoding for saving the source. Note: All parameters except ``source`` are optional. All of them can be changed under their corresponding attribute name after instance creation. """ @classmethod @_tools.deprecate_positional_args(supported_number=1, ignore_arg='cls') def from_file(cls, filename: typing.Union[os.PathLike, str], directory: typing.Union[os.PathLike, str, None] = None, format: typing.Optional[str] = None, engine: typing.Optional[str] = None, encoding: typing.Optional[str] = DEFAULT_ENCODING, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None) -> 'Source': """Return an instance with the source string read from the given file. Args: filename: Filename for loading/saving the source. directory: (Sub)directory for source loading/saving and rendering. format: Rendering output format (``'pdf'``, ``'png'``, ...). engine: Layout command used (``'dot'``, ``'neato'``, ...). encoding: Encoding for loading/saving the source. """ directory = _tools.promote_pathlike_directory(directory) filepath = (os.path.join(directory, filename) if directory.parts else os.fspath(filename)) if encoding is None: encoding = locale.getpreferredencoding() log.debug('read %r with encoding %r', filepath, encoding) with open(filepath, encoding=encoding) as fd: source = fd.read() return cls(source, filename=filename, directory=directory, format=format, engine=engine, encoding=encoding, renderer=renderer, formatter=formatter, loaded_from_path=filepath) @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def __init__(self, source: str, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, format: typing.Optional[str] = None, engine: typing.Optional[str] = None, encoding: typing.Optional[str] = DEFAULT_ENCODING, *, renderer: typing.Optional[str] = None, formatter: typing.Optional[str] = None, loaded_from_path: typing.Optional[os.PathLike] = None) -> None: super().__init__(filename=filename, directory=directory, format=format, engine=engine, renderer=renderer, formatter=formatter, encoding=encoding) self._loaded_from_path = loaded_from_path self._source = source # work around pytype false alarm _source: str _loaded_from_path: typing.Optional[os.PathLike] def _copy_kwargs(self, **kwargs): """Return the kwargs to create a copy of the instance.""" return super()._copy_kwargs(source=self._source, loaded_from_path=self._loaded_from_path, **kwargs) def __iter__(self) -> typing.Iterator[str]: r"""Yield the DOT source code read from file line by line. Yields: Line ending with a newline (``'\n'``). """ lines = self._source.splitlines(keepends=True) yield from lines[:-1] for line in lines[-1:]: suffix = '\n' if not line.endswith('\n') else '' yield line + suffix @property def source(self) -> str: """The DOT source code as string. Normalizes so that the string always ends in a final newline. """ source = self._source if not source.endswith('\n'): source += '\n' return source @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def save(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, *, skip_existing: typing.Optional[bool] = None) -> str: """Save the DOT source to file. Ensure the file ends with a newline. Args: filename: Filename for saving the source (defaults to ``name`` + ``'.gv'``) directory: (Sub)directory for source saving and rendering. skip_existing: Skip write if file exists (default: ``None``). By default skips if instance was loaded from the target path: ``.from_file(self.filepath)``. Returns: The (possibly relative) path of the saved source file. """ skip = (skip_existing is None and self._loaded_from_path and os.path.samefile(self._loaded_from_path, self.filepath)) if skip: log.debug('.save(skip_existing=None) skip writing Source.from_file(%r)', self.filepath) return super().save(filename=filename, directory=directory, skip_existing=skip) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715620756.0 graphviz-0.21/graphviz/unflattening.py0000666000000000000000000000472514620445624015141 0ustar00"""Pipe source through the Graphviz *unflatten* preprocessor.""" import typing import graphviz from . import _tools from . import base from . import backend from . import encoding __all__ = ['Unflatten'] class Unflatten(encoding.Encoding, base.Base, backend.Unflatten): """Pipe source through the Graphviz *unflatten* preprocessor.""" @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self') def unflatten(self, stagger: typing.Optional[int] = None, fanout: bool = False, chain: typing.Optional[int] = None) -> 'graphviz.Source': """Return a new :class:`.Source` instance with the source piped through the Graphviz *unflatten* preprocessor. Args: stagger: Stagger the minimum length of leaf edges between 1 and this small integer. fanout: Fanout nodes with indegree = outdegree = 1 when staggering (requires ``stagger``). chain: Form disconnected nodes into chains of up to this many nodes. Returns: Prepocessed DOT source code (improved layout aspect ratio). Raises: graphviz.RequiredArgumentError: If ``fanout`` is given but ``stagger`` is None. graphviz.ExecutableNotFound: If the Graphviz ``unflatten`` executable is not found. graphviz.CalledProcessError: If the returncode (exit status) of the unflattening 'unflatten' subprocess is non-zero. See also: Upstream documentation: https://www.graphviz.org/pdf/unflatten.1.pdf """ from . import sources out = self._unflatten(self.source, stagger=stagger, fanout=fanout, chain=chain, encoding=self.encoding) kwargs = self._copy_kwargs() return sources.Source(out, filename=kwargs.get('filename'), directory=kwargs.get('directory'), format=kwargs.get('format'), engine=kwargs.get('engine'), encoding=kwargs.get('encoding'), renderer=kwargs.get('renderer'), formatter=kwargs.get('formatter'), loaded_from_path=None) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5241222 graphviz-0.21/graphviz.egg-info/0000777000000000000000000000000015023511374013545 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979899.0 graphviz-0.21/graphviz.egg-info/PKG-INFO0000666000000000000000000003002215023511373014636 0ustar00Metadata-Version: 2.4 Name: graphviz Version: 0.21 Summary: Simple Python interface for Graphviz Author-email: Sebastian Bank License-Expression: MIT Project-URL: Homepage, https://github.com/xflr6/graphviz Project-URL: Documentation, https://graphviz.readthedocs.io Project-URL: Changelog, https://graphviz.readthedocs.io/en/latest/changelog.html Project-URL: Issue Tracker, https://github.com/xflr6/graphviz/issues Project-URL: CI, https://github.com/xflr6/graphviz/actions Project-URL: Coverage, https://codecov.io/gh/xflr6/graphviz Keywords: graph,visualization,dot,render Platform: any Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Scientific/Engineering :: Visualization Requires-Python: >=3.9 Description-Content-Type: text/x-rst License-File: LICENSE.txt Provides-Extra: dev Requires-Dist: build; extra == "dev" Requires-Dist: wheel; extra == "dev" Requires-Dist: twine; extra == "dev" Requires-Dist: flake8; extra == "dev" Requires-Dist: Flake8-pyproject; extra == "dev" Requires-Dist: pep8-naming; extra == "dev" Requires-Dist: tox>=3; extra == "dev" Provides-Extra: test Requires-Dist: pytest<8.1,>=7; extra == "test" Requires-Dist: pytest-mock>=3; extra == "test" Requires-Dist: pytest-cov; extra == "test" Requires-Dist: coverage; extra == "test" Provides-Extra: docs Requires-Dist: sphinx<7,>=5; extra == "docs" Requires-Dist: sphinx-autodoc-typehints; extra == "docs" Requires-Dist: sphinx-rtd-theme>=0.2.5; extra == "docs" Dynamic: license-file Graphviz ======== |PyPI version| |License| |Supported Python| |Downloads| |Build| |Codecov| |Readthedocs-stable| |Readthedocs-latest| |Binder-stable| This package facilitates the creation and rendering of graph descriptions in the DOT_ language of the Graphviz_ graph drawing software (`upstream repo`_) from Python. Create a graph object, assemble the graph by adding nodes and edges, and retrieve its DOT source code string. Save the source code to a file and render it with the Graphviz installation of your system. Use the ``view`` option/method to directly inspect the resulting (PDF, PNG, SVG, etc.) file with its default application. Graphs can also be rendered and displayed within `Jupyter notebooks`_ (formerly known as `IPython notebooks`_, `example `_, `nbviewer `_) as well as the `Jupyter QtConsole`_. Links ----- - GitHub: https://github.com/xflr6/graphviz - PyPI: https://pypi.org/project/graphviz/ - Documentation: https://graphviz.readthedocs.io - Changelog: https://graphviz.readthedocs.io/en/latest/changelog.html - Issue Tracker: https://github.com/xflr6/graphviz/issues - Download: https://pypi.org/project/graphviz/#files - Development documentation: https://graphviz.readthedocs.io/en/latest/development.html - Release process: https://graphviz.readthedocs.io/en/latest/release_process.html Installation ------------ This package runs under Python 3.9+, use pip_ to install: .. code:: bash $ pip install graphviz To render the generated DOT source code, you also need to install Graphviz_ (`download page `_, `archived versions `_, `installation procedure for Windows `_). Make sure that the directory containing the ``dot`` executable is on your systems' ``PATH`` (sometimes done by the installer; setting ``PATH`` on `Linux `_, `Mac `_, and `Windows `_). Anaconda_: see the conda-forge_ package `conda-forge/python-graphviz `_ (`feedstock `_), which should automatically ``conda install`` `conda-forge/graphviz `_ (`feedstock `_) as dependency. Quickstart ---------- Create a graph object: .. code:: python >>> import graphviz # doctest: +NO_EXE >>> dot = graphviz.Digraph(comment='The Round Table') >>> dot #doctest: +ELLIPSIS Add nodes and edges: .. code:: python >>> dot.node('A', 'King Arthur') # doctest: +NO_EXE >>> dot.node('B', 'Sir Bedevere the Wise') >>> dot.node('L', 'Sir Lancelot the Brave') >>> dot.edges(['AB', 'AL']) >>> dot.edge('B', 'L', constraint='false') Check the generated source code: .. code:: python >>> print(dot.source) # doctest: +NORMALIZE_WHITESPACE +NO_EXE // The Round Table digraph { A [label="King Arthur"] B [label="Sir Bedevere the Wise"] L [label="Sir Lancelot the Brave"] A -> B A -> L B -> L [constraint=false] } Save and render the source code (skip/ignore any ``doctest_mark_exe()`` lines): .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv').replace('\\', '/') 'doctest-output/round-table.gv.pdf' Save and render and view the result: .. code:: python >>> doctest_mark_exe() # skip this line >>> dot.render('doctest-output/round-table.gv', view=True) # doctest: +SKIP 'doctest-output/round-table.gv.pdf' .. image:: https://raw.github.com/xflr6/graphviz/master/docs/_static/round-table.svg :align: center :alt: round-table.svg **Caveat:** Backslash-escapes and strings of the form ``<...>`` have a special meaning in the DOT language. If you need to render arbitrary strings (e.g. from user input), check the details in the `user guide`_. See also -------- - pygraphviz_ |--| full-blown interface wrapping the Graphviz C library with SWIG - graphviz-python_ |--| official Python bindings (`documentation `_) - pydot_ |--| stable pure-Python approach, requires pyparsing License ------- This package is distributed under the `MIT license`_. .. _Graphviz: https://www.graphviz.org .. _DOT: https://www.graphviz.org/doc/info/lang.html .. _upstream repo: https://gitlab.com/graphviz/graphviz/ .. _upstream-download: https://www.graphviz.org/download/ .. _upstream-archived: https://www2.graphviz.org/Archive/stable/ .. _upstream-windows: https://forum.graphviz.org/t/new-simplified-installation-procedure-on-windows/224 .. _set-path-windows: https://www.computerhope.com/issues/ch000549.htm .. _set-path-linux: https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix .. _set-path-darwin: https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently .. _pip: https://pip.pypa.io .. _Jupyter notebooks: https://jupyter.org .. _IPython notebooks: https://ipython.org/notebook.html .. _Jupyter QtConsole: https://qtconsole.readthedocs.io .. _notebook: https://github.com/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _notebook-nbviewer: https://nbviewer.org/github/xflr6/graphviz/blob/master/examples/graphviz-notebook.ipynb .. _Anaconda: https://docs.anaconda.com/anaconda/install/ .. _conda-forge: https://conda-forge.org .. _conda-forge-python-graphviz: https://anaconda.org/conda-forge/python-graphviz .. _conda-forge-python-graphviz-feedstock: https://github.com/conda-forge/python-graphviz-feedstock .. _conda-forge-graphviz: https://anaconda.org/conda-forge/graphviz .. _conda-forge-graphviz-feedstock: https://github.com/conda-forge/graphviz-feedstock .. _user guide: https://graphviz.readthedocs.io/en/stable/manual.html .. _pygraphviz: https://pypi.org/project/pygraphviz/ .. _graphviz-python: https://pypi.org/project/graphviz-python/ .. _graphviz-python-docs: https://www.graphviz.org/pdf/gv.3python.pdf .. _pydot: https://pypi.org/project/pydot/ .. _MIT license: https://opensource.org/licenses/MIT .. |--| unicode:: U+2013 .. |PyPI version| image:: https://img.shields.io/pypi/v/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Latest PyPI Version .. |License| image:: https://img.shields.io/pypi/l/graphviz.svg :target: https://github.com/xflr6/graphviz/blob/master/LICENSE.txt :alt: License .. |Supported Python| image:: https://img.shields.io/pypi/pyversions/graphviz.svg :target: https://pypi.org/project/graphviz/ :alt: Supported Python Versions .. |Downloads| image:: https://img.shields.io/pypi/dm/graphviz.svg :target: https://pypistats.org/packages/graphviz :alt: Monthly downloads .. |Build| image:: https://github.com/xflr6/graphviz/actions/workflows/build.yaml/badge.svg?branch=master :target: https://github.com/xflr6/graphviz/actions/workflows/build.yaml?query=branch%3Amaster :alt: Build .. |Codecov| image:: https://codecov.io/gh/xflr6/graphviz/branch/master/graph/badge.svg :target: https://codecov.io/gh/xflr6/graphviz :alt: Codecov .. |Readthedocs-stable| image:: https://readthedocs.org/projects/graphviz/badge/?version=stable :target: https://graphviz.readthedocs.io/en/stable/ :alt: Readthedocs (stable) .. |Readthedocs-latest| image:: https://readthedocs.org/projects/graphviz/badge/?version=latest :target: https://graphviz.readthedocs.io/en/latest/ :alt: Readthedocs (latest) .. |Binder-stable| image:: https://img.shields.io/badge/launch-binder%20(stable)-579ACA.svg?logo=data:image/png;base64,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 :target: https://mybinder.org/v2/gh/xflr6/graphviz/stable :alt: Binder (stable) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979899.0 graphviz-0.21/graphviz.egg-info/SOURCES.txt0000666000000000000000000000675015023511373015440 0ustar00CHANGES.rst LICENSE.txt MANIFEST.in README.rst build-docs.py conftest.py lint-code.py pyproject.toml requirements.txt run-tests.py try-examples.py update-help.py docs/_links.rst docs/api.rst docs/attributes.rst docs/basic_usage.rst docs/changelog.rst docs/conf.py docs/custom_dot.rst docs/development.rst docs/engines.rst docs/escapes.rst docs/examples.rst docs/existing_files.rst docs/formats.rst docs/index.rst docs/installation.rst docs/integration_with_viewers.rst docs/jupyter_notebooks.rst docs/license.rst docs/manual.rst docs/neato_no_op.rst docs/node_ports.rst docs/notebooks.rst docs/piped_output.rst docs/quoting.rst docs/raw_dot.rst docs/release_process.rst docs/round-table.png docs/styling.rst docs/subgraphs_and_clusters.rst docs/unflatten.rst docs/_static/angles.svg docs/_static/btree.svg docs/_static/cluster.svg docs/_static/cluster_edge.svg docs/_static/colors.svg docs/_static/diamond.svg docs/_static/er.svg docs/_static/escapes.svg docs/_static/fdpclust.svg docs/_static/fsm.svg docs/_static/g_c_n.svg docs/_static/hello.svg docs/_static/holy-grenade.svg docs/_static/html_table.svg docs/_static/literal_backslash.svg docs/_static/ni.svg docs/_static/pet-shop.svg docs/_static/process.svg docs/_static/qtconsole-source.png docs/_static/qtconsole.png docs/_static/rank_same.svg docs/_static/round-table.svg docs/_static/splines.svg docs/_static/structs.svg docs/_static/structs_revisited.svg docs/_static/traffic_lights.svg docs/_static/unix.svg docs/_static/wide-unflatten-stagger-2.svg docs/_static/wide-unflatten-stagger-3.svg docs/_static/wide.svg examples/angles.py examples/btree.py examples/cluster.py examples/cluster_edge.py examples/colors.py examples/er.py examples/fdpclust.py examples/fsm.py examples/g_c_n.py examples/graphviz-engines.ipynb examples/graphviz-escapes.ipynb examples/graphviz-jupyter-format.ipynb examples/graphviz-notebook.ipynb examples/graphviz_transform_recipe.py examples/hello.py examples/process.py examples/rank_same.py examples/structs.py examples/structs_revisited.py examples/traffic_lights.py examples/unix.py graphviz/__init__.py graphviz/_compat.py graphviz/_defaults.py graphviz/_tools.py graphviz/base.py graphviz/copying.py graphviz/dot.py graphviz/encoding.py graphviz/exceptions.py graphviz/graphs.py graphviz/jupyter_integration.py graphviz/piping.py graphviz/quoting.py graphviz/rendering.py graphviz/saving.py graphviz/sources.py graphviz/unflattening.py graphviz.egg-info/PKG-INFO graphviz.egg-info/SOURCES.txt graphviz.egg-info/dependency_links.txt graphviz.egg-info/requires.txt graphviz.egg-info/top_level.txt graphviz/backend/__init__.py graphviz/backend/dot_command.py graphviz/backend/execute.py graphviz/backend/mixins.py graphviz/backend/piping.py graphviz/backend/rendering.py graphviz/backend/unflattening.py graphviz/backend/upstream_version.py graphviz/backend/viewing.py graphviz/parameters/__init__.py graphviz/parameters/base.py graphviz/parameters/engines.py graphviz/parameters/formats.py graphviz/parameters/formatters.py graphviz/parameters/mixins.py graphviz/parameters/renderers.py tests/_common.py tests/conftest.py tests/dot_red.png tests/test_all_classes.py tests/test_graphs.py tests/test_init.py tests/test_jupyter_integration.py tests/test_parameters.py tests/test_quoting.py tests/test_saving.py tests/test_sources.py tests/test_tools.py tests/backend/conftest.py tests/backend/test_execute.py tests/backend/test_piping.py tests/backend/test_rendering.py tests/backend/test_unflattening.py tests/backend/test_upstream_version.py tests/backend/test_viewing.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979899.0 graphviz-0.21/graphviz.egg-info/dependency_links.txt0000666000000000000000000000000115023511373017612 0ustar00 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979899.0 graphviz-0.21/graphviz.egg-info/requires.txt0000666000000000000000000000030415023511373016141 0ustar00 [dev] build wheel twine flake8 Flake8-pyproject pep8-naming tox>=3 [docs] sphinx<7,>=5 sphinx-autodoc-typehints sphinx-rtd-theme>=0.2.5 [test] pytest<8.1,>=7 pytest-mock>=3 pytest-cov coverage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749979899.0 graphviz-0.21/graphviz.egg-info/top_level.txt0000666000000000000000000000001115023511373016266 0ustar00graphviz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639307241.0 graphviz-0.21/lint-code.py0000666000000000000000000000131014155353751012454 0ustar00#!/usr/bin/env python3 """Run code linting with https://flake8.pycqa.org.""" import pathlib import platform import subprocess import sys SELF = pathlib.Path(__file__) PYTHON = 'py' if platform.system() == 'Windows' else 'python' CMD = [PYTHON, '-m', 'flake8'] print('run', [SELF.name] + sys.argv[1:]) cmd = CMD + sys.argv[1:] print(f'subprocess.run({cmd!r})') try: proc = subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: assert e.returncode != 0, f'non-zero returncode: {e}' print('FAILED:', e) sys.exit(e.returncode) else: assert proc.returncode == 0, f'passed: {proc}' print('PASSED:', proc) sys.exit(proc.returncode) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1749759863.0 graphviz-0.21/pyproject.toml0000666000000000000000000000533115022633567013147 0ustar00[project] name = "graphviz" authors = [{ name = "Sebastian Bank", email = "sebastian.bank@uni-leipzig.de" }] description = "Simple Python interface for Graphviz" keywords = ["graph", "visualization", "dot", "render"] readme = "README.rst" license = "MIT" license-files = ["LICENSE.txt"] dynamic = ["version"] requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Visualization", ] [project.urls] Homepage = "https://github.com/xflr6/graphviz" Documentation = "https://graphviz.readthedocs.io" Changelog = "https://graphviz.readthedocs.io/en/latest/changelog.html" "Issue Tracker" = "https://github.com/xflr6/graphviz/issues" CI = "https://github.com/xflr6/graphviz/actions" Coverage = "https://codecov.io/gh/xflr6/graphviz" [project.optional-dependencies] dev = ["build", "wheel", "twine", "flake8", "Flake8-pyproject", "pep8-naming", "tox>=3"] test = [ "pytest>=7,<8.1", # https://github.com/pytest-dev/pytest/issues/12123 "pytest-mock>=3", "pytest-cov", "coverage", ] docs = ["sphinx>=5,<7", "sphinx-autodoc-typehints", "sphinx-rtd-theme>=0.2.5"] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools] platforms = ["any"] [tool.setuptools.dynamic] version = { attr = "graphviz.__version__" } [tool.pytype] inputs = ["graphviz"] [tool.flake8] exclude = ["docs", "build", ".tox"] ignore = ["E126", "E128", "W503"] max-line-length = 100 [tool.pytest.ini_options] minversion = "6" testpaths = ["README.rst", "docs", "graphviz", "tests"] addopts = [ "--doctest-modules", "--doctest-glob='*.rst'", "--ignore=docs/conf.py", "--doctest-continue-on-failure", # pytest summary: all except (E)rror # - (f)ailed # - (s)kipped # - (x/X)failed/passed # - (p/P)assed (no output) "-r fsxX", "--durations=10", "--cov", "--cov-report=term", "--cov-report=html", "--strict-config", "--strict-markers", ] log_cli = true log_cli_level = "WARNING" log_file = "test-log.txt" log_file_level = "DEBUG" [tool.coverage.run] source = ["graphviz"] branch = false omit = ["*/conftest.py"] [tool.tox] legacy_tox_ini = """ [tox] envlist = py{313,312,311,310,39} skip_missing_interpreters = true [testenv] extras = test commands = python -X dev run-tests.py {posargs} """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636109361.0 graphviz-0.21/requirements.txt0000666000000000000000000000016614141206061013502 0ustar00# development environment: --editable install in development mode (includes all extras_require) -e .[dev,test,docs] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649449412.0 graphviz-0.21/run-tests.py0000666000000000000000000000122714224114704012537 0ustar00#!/usr/bin/env python3 # flake8: noqa """Run the tests with https://pytest.org.""" import pathlib import platform import sys import pytest SELF = pathlib.Path(__file__) ARGS = [#'--skip-exe', #'--only-exe', #'--collect-only', #'--verbose', #'--pdb', #'--exitfirst', # a.k.a. -x #'-W', 'error', #'--doctest-report none', ] if platform.system() == 'Windows' and 'idlelib' in sys.modules: ARGS += ['--capture=sys', '--color=no'] print('run', [SELF.name] + sys.argv[1:]) args = ARGS + sys.argv[1:] print(f'pytest.main({args!r})') sys.exit(pytest.main(args)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5281236 graphviz-0.21/setup.cfg0000666000000000000000000000005215023511374012037 0ustar00[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5151205 graphviz-0.21/tests/0000777000000000000000000000000015023511374011363 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/tests/_common.py0000666000000000000000000000267414575646724013420 0ustar00"""Test helpers and test globals.""" import contextlib import os import pathlib import platform import subprocess __all__ = ['EXPECTED_DOT_BINARY', 'EXPECTED_UNFLATTEN_BINARY', 'EXPECTED_DEFAULT_ENGINE', 'EXPECTED_DEFAULT_ENCODING', 'INVALID_CMD', 'as_cwd', 'check_startupinfo', 'StartupinfoMatcher'] EXPECTED_DOT_BINARY = pathlib.Path('dot') EXPECTED_UNFLATTEN_BINARY = pathlib.Path('unflatten') EXPECTED_DEFAULT_ENGINE = 'dot' EXPECTED_DEFAULT_ENCODING = 'utf-8' INVALID_CMD = [''] @contextlib.contextmanager def as_cwd(path): """Return a context manager, which changes to the path's directory during the managed ``with`` context.""" cwd = pathlib.Path().resolve() os.chdir(path) yield os.chdir(cwd) def check_startupinfo(startupinfo) -> bool: # noqa: N803 return startupinfo is None if platform.system().lower() == 'windows': def check_startupinfo(startupinfo) -> bool: # noqa: N803,F811 return (isinstance(startupinfo, subprocess.STARTUPINFO) and startupinfo.dwFlags & subprocess.STARTF_USESHOWWINDOW and startupinfo.wShowWindow == subprocess.SW_HIDE) class StartupinfoMatcher: """Verify the given startupinfo argument is as expected for the plaform.""" def __eq__(self, startupinfo) -> bool: return check_startupinfo(startupinfo) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1749979899.5231223 graphviz-0.21/tests/backend/0000777000000000000000000000000015023511374012752 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/tests/backend/conftest.py0000666000000000000000000000100014144205436015142 0ustar00"""pytest fixtures for backend.""" import pytest @pytest.fixture def mock_run(mocker): yield mocker.patch('subprocess.run', autospec=True) @pytest.fixture def mock_popen(mocker): yield mocker.patch('subprocess.Popen', autospec=True) @pytest.fixture def mock_startfile(mocker, platform): if platform == 'windows': kwargs = {'autospec': True} else: kwargs = {'create': True, 'new_callable': mocker.Mock} yield mocker.patch('os.startfile', **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/tests/backend/test_execute.py0000666000000000000000000000656614144205436016044 0ustar00import errno import io import subprocess import pytest import graphviz from graphviz.backend import execute import _common @pytest.fixture def empty_path(monkeypatch): monkeypatch.setenv('PATH', '') @pytest.mark.usefixtures('empty_path') @pytest.mark.parametrize( 'func, args', [(graphviz.render, ['dot', 'pdf', 'nonfilepath']), (graphviz.pipe, ['dot', 'pdf', b'nongraph']), (graphviz.unflatten, ['graph {}']), (graphviz.version, [])]) def test_missing_executable(func, args): with pytest.raises(graphviz.ExecutableNotFound, match=r'execute'): func(*args) def test_run_check_oserror(): with pytest.raises(OSError) as e: execute.run_check(_common.INVALID_CMD) assert e.value.errno in (errno.EACCES, errno.EINVAL) def test_run_check_called_process_error_mocked(capsys, mock_run, quiet, stdout='I am the messiah', stderr='I am not the messiah!'): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=500, stdout=stdout, stderr=stderr) with pytest.raises(execute.CalledProcessError, match=stderr): execute.run_check(_common.INVALID_CMD, capture_output=True, quiet=quiet) assert capsys.readouterr() == ('', '' if quiet else stderr) def test_run_check_input_lines_mocked(mocker, sentinel, mock_popen, line=b'sp\xc3\xa4m'): # noqa: N803 mock_sys_stderr = mocker.patch('sys.stderr', autospec=True, flush=mocker.Mock(), encoding=sentinel.encoding) mock_out = mocker.create_autospec(bytes, instance=True, name='mock_out') mock_err = mocker.create_autospec(bytes, instance=True, name='mock_err', **{'__len__.return_value': 1}) proc = mock_popen.return_value proc.configure_mock(args=_common.INVALID_CMD, returncode=0, stdin=mocker.create_autospec(io.BytesIO, instance=True)) proc.communicate.return_value = (mock_out, mock_err) result = execute.run_check(proc.args, input_lines=iter([line]), capture_output=True) # subprocess.CompletedProcess.__eq__() is not implemented assert isinstance(result, subprocess.CompletedProcess) assert result.args is proc.args assert result.returncode == proc.returncode assert result.stdout is mock_out assert result.stderr is mock_err mock_popen.assert_called_once_with(_common.INVALID_CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=_common.StartupinfoMatcher()) proc.communicate.assert_called_once_with() mock_out.decode.assert_not_called() mock_err.decode.assert_called_once_with(sentinel.encoding) mock_sys_stderr.write.assert_called_once_with(mock_err.decode.return_value) mock_sys_stderr.flush.assert_called_once_with() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715614555.0 graphviz-0.21/tests/backend/test_piping.py0000666000000000000000000001711514620431533015656 0ustar00import io import re import subprocess import pytest import graphviz import _common SVG_PATTERN = r'(?s)^<\?xml .+\s*$' @pytest.mark.exe @pytest.mark.xfail('graphviz.version() == (2, 36, 0)', reason='https://bugs.launchpad.net/ubuntu/+source/graphviz/+bug/1694108') def test_pipe_invalid_data(capsys, quiet, engine='dot', format_='svg'): with pytest.raises(subprocess.CalledProcessError) as e: graphviz.pipe(engine, format_, b'nongraph', quiet=quiet) assert e.value.returncode == 1 assert 'syntax error in line' in str(e.value) out, err = capsys.readouterr() assert out == '' if quiet: assert err == '' else: assert 'syntax error in line' in err def test_pipe_pipe_invalid_data_mocked(mocker, sentinel, mock_run, quiet): mock_sys_stderr = mocker.patch('sys.stderr', autospec=True, flush=mocker.Mock(), encoding=sentinel.encoding) mock_out = mocker.create_autospec(bytes, instance=True, name='mock_out') mock_err = mocker.create_autospec(bytes, instance=True, name='mock_err', **{'__len__.return_value': 1}) mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=5, stdout=mock_out, stderr=mock_err) with pytest.raises(subprocess.CalledProcessError) as e: graphviz.pipe('dot', 'png', b'nongraph', quiet=quiet) assert e.value.returncode == 5 assert e.value.cmd == _common.INVALID_CMD assert e.value.stdout is mock_out assert e.value.stderr is mock_err e.value.stdout = sentinel.new_stdout assert e.value.stdout is sentinel.new_stdout mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpng'], input=b'nongraph', capture_output=True, startupinfo=_common.StartupinfoMatcher()) if not quiet: mock_out.decode.assert_not_called() mock_err.decode.assert_called_once_with(sentinel.encoding) mock_sys_stderr.write.assert_called_once_with(mock_err.decode.return_value) mock_sys_stderr.flush.assert_called_once_with() @pytest.mark.exe @pytest.mark.parametrize( 'engine, format_, renderer, formatter, pattern', [('dot', 'svg', None, None, SVG_PATTERN), ('dot', 'ps', 'ps', 'core', r'%!PS-'), # Error: remove_overlap: Graphviz not built with triangulation library pytest.param('sfdp', 'svg', None, None, SVG_PATTERN, marks=pytest.mark.xfail('graphviz.version() > (2, 38, 0)' " and platform.system().lower() == 'windows'", reason='https://gitlab.com/graphviz/graphviz/-/issues/1269'))]) def test_pipe(capsys, engine, format_, renderer, formatter, pattern, data=b'graph { spam }'): with pytest.deprecated_call(match=r'\b3 positional args\b'): out = graphviz.pipe(engine, format_, data, renderer, formatter).decode('ascii') if pattern is not None: assert re.match(pattern, out) assert capsys.readouterr() == ('', '') def test_pipe_mocked(capsys, mock_run, quiet): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout=b'stdout', stderr=b'stderr') assert graphviz.pipe('dot', 'png', b'nongraph', quiet=quiet) == b'stdout' mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpng'], input=b'nongraph', capture_output=True, startupinfo=_common.StartupinfoMatcher()) assert capsys.readouterr() == ('', '' if quiet else 'stderr') def test_pipe_string_mocked(capsys, mock_run, quiet, encoding='ascii'): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout='stdout', stderr='stderr') assert graphviz.pipe_string('dot', 'png', 'nongraph', encoding=encoding, quiet=quiet) == 'stdout' mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpng'], input='nongraph', encoding=encoding, capture_output=True, startupinfo=_common.StartupinfoMatcher()) assert capsys.readouterr() == ('', '' if quiet else 'stderr') def test_pipe_lines_mocked(capsys, mock_popen, quiet, input_encoding='ascii'): proc = mock_popen.return_value proc.configure_mock(args=_common.EXPECTED_DOT_BINARY, returncode=0, stdin=io.BytesIO(), stdout=io.BytesIO(b'stdout'), stderr=io.BytesIO(b'stderr')) proc.communicate.side_effect = lambda: (proc.stdout.read(), proc.stderr.read()) assert graphviz.pipe_lines('dot', 'png', iter(['nongraph\n']), input_encoding=input_encoding, quiet=quiet) == b'stdout' assert proc.stdin.getvalue() == b'nongraph\n' mock_popen.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpng'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=_common.StartupinfoMatcher()) assert capsys.readouterr() == ('', '' if quiet else 'stderr') def test_pipe_lines_string_mocked(capsys, mock_popen, quiet, encoding='ascii'): proc = mock_popen.return_value proc.configure_mock(args=_common.INVALID_CMD, returncode=0, stdin=io.StringIO(), stdout=io.StringIO('stdout'), stderr=io.StringIO('stderr')) proc.communicate.side_effect = lambda: (proc.stdout.read(), proc.stderr.read()) assert graphviz.pipe_lines_string('dot', 'png', iter(['nongraph\n']), encoding=encoding, quiet=quiet) == 'stdout' assert proc.stdin.getvalue() == 'nongraph\n' mock_popen.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpng'], encoding=encoding, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=_common.StartupinfoMatcher()) assert capsys.readouterr() == ('', '' if quiet else 'stderr') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715614586.0 graphviz-0.21/tests/backend/test_rendering.py0000666000000000000000000002315414620431572016350 0ustar00import contextlib import pathlib import os import shutil import subprocess import pytest import graphviz from graphviz import _tools from graphviz.backend import rendering import _common TEST_FILES_DIRECTORY = pathlib.Path(__file__).parent.parent @pytest.fixture(scope='module') def files_path(): return TEST_FILES_DIRECTORY @pytest.mark.exe def test_render_missing_file(quiet, engine='dot', format_='pdf'): with pytest.raises(subprocess.CalledProcessError) as e: graphviz.render(engine, format_, 'nonexisting', quiet=quiet) assert e.value.returncode == 2 @pytest.mark.parametrize( 'args, expected_exception, match', [(['', 'pdf', 'nonfilepath'], ValueError, r'unknown engine'), (['dot', '', 'nonfilepath'], ValueError, r'unknown format'), (['dot', 'ps', 'nonfilepath', '', None], ValueError, r'unknown renderer'), (['dot', 'ps', 'nonfilepath', None, 'core'], graphviz.RequiredArgumentError, r'without renderer'), (['dot', 'ps', 'nonfilepath', 'ps', ''], ValueError, r'unknown formatter')], ids=lambda x: getattr(x, '__name__', x)) def test_render_unknown_parameter_raises(args, expected_exception, match, supported_number=3): checker = (pytest.deprecated_call(match=rf'\b{supported_number:d} positional args\b') if len(args) > supported_number else contextlib.nullcontext()) with pytest.raises(expected_exception, match=match), checker: graphviz.render(*args) @pytest.mark.exe @pytest.mark.parametrize( 'format_, renderer, formatter, expected_suffix', [('pdf', None, None, 'pdf'), pytest.param('plain', 'dot', 'core', 'core.dot.plain', marks=pytest.mark.xfail('graphviz.version() == (5, 0, 1)', reason='https://gitlab.com/graphviz/graphviz/-/issues/2270'))]) @pytest.mark.parametrize('engine', ['dot']) def test_render(capsys, tmp_path, engine, format_, renderer, formatter, expected_suffix, filename='hello.gv', data=b'digraph { hello -> world }'): lpath = tmp_path / filename assert lpath.write_bytes(data) == len(data) == lpath.stat().st_size rendered = lpath.with_suffix(f'{lpath.suffix}.{expected_suffix}') with pytest.deprecated_call(match=r'\b3 positional args\b'): result = graphviz.render(engine, format_, str(lpath), renderer, formatter) assert result == str(rendered) assert rendered.exists() assert rendered.stat().st_size assert capsys.readouterr() == ('', '') @pytest.mark.exe def test_render_img(capsys, tmp_path, files_path, engine='dot', format_='pdf'): subdir = tmp_path / 'subdir' subdir.mkdir() img_path = subdir / 'dot_red.png' shutil.copy(files_path / img_path.name, img_path) assert img_path.exists() assert img_path.stat().st_size gv_path = subdir / 'img.gv' rendered = gv_path.with_suffix(f'{gv_path.suffix}.{format_}') gv_rel, rendered_rel = (p.relative_to(tmp_path) for p in (gv_path, rendered)) assert all(str(s).startswith('subdir') for s in (gv_rel, rendered_rel)) gv_path.write_text(f'graph {{ red_dot [image="{img_path.name}"] }}', encoding='ascii') with _common.as_cwd(tmp_path): assert graphviz.render(engine, format_, gv_rel) == str(rendered_rel) assert rendered.exists() assert rendered.stat().st_size assert capsys.readouterr() == ('', '') @pytest.mark.exe def test_render_outfile_differnt_parent(capsys, tmp_path, engine='dot', outfile='spam.pdf', source='graph { spam }'): outfile = tmp_path / 'rendered' / outfile outfile.parent.mkdir() assert not outfile.exists() filepath = tmp_path / 'sources' / outfile.with_suffix('.gv').name filepath.parent.mkdir() assert filepath.write_text(source) == len(source) == filepath.stat().st_size result = graphviz.render(engine, filepath=filepath, outfile=outfile) assert result == os.fspath(outfile) assert outfile.exists() assert outfile.stat().st_size assert capsys.readouterr() == ('', '') @pytest.mark.parametrize( 'directory', [None, 'dot_sources']) def test_render_mocked(capsys, mock_run, quiet, directory, filepath='nonfilepath'): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout='stdout', stderr='stderr') if directory is not None: filepath = os.path.join(directory, filepath) result = graphviz.render('dot', 'pdf', filepath, neato_no_op=True, quiet=quiet) assert result == f'{filepath}.pdf' mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-Kdot', '-Tpdf', '-n1', '-O', 'nonfilepath'], capture_output=True, cwd=_tools.promote_pathlike(directory), startupinfo=_common.StartupinfoMatcher()) assert capsys.readouterr() == ('', '' if quiet else 'stderr') @pytest.mark.parametrize( 'args, kwargs, expected_exception, match', [(['dot'], {}, graphviz.RequiredArgumentError, r'filepath: \(required'), (['dot'], {'format': 'png'}, graphviz.RequiredArgumentError, r'filepath: \(required'), (['dot'], {'filepath': 'spam'}, graphviz.RequiredArgumentError, r'format: \(required'), (['dot', 'svg'], {}, graphviz.RequiredArgumentError, r'filepath: \(required'), (['dot', 'gv', 'spam.gv'], {'outfile': 'spam.gv'}, ValueError, r"outfile 'spam\.gv' must be different from input file 'spam\.gv'"), (['dot'], {'outfile': 'spam.png', 'raise_if_result_exists': True, 'overwrite_filepath': True}, ValueError, r'overwrite_filepath cannot be combined with raise_if_result_exists'), (['dot'], {'outfile': 'spam.png', 'raise_if_result_exists': True}, graphviz.FileExistsError, r"output file exists: 'spam.png'")]) def test_render_raises_mocked(tmp_path, mock_run, args, kwargs, expected_exception, match): if issubclass(expected_exception, FileExistsError): existing_outfile = tmp_path / kwargs['outfile'] existing_outfile.touch() with _common.as_cwd(tmp_path): with pytest.raises(expected_exception, match=match): graphviz.render(*args, **kwargs) @pytest.mark.parametrize( 'filepath, kwargs, expected_fspath', [('spam.gv', {'format': 'pdf'}, 'spam.gv.pdf'), ('spam.gv', {'format': 'plain', 'renderer': 'dot'}, 'spam.gv.dot.plain')]) def test_get_outfile(filepath, kwargs, expected_fspath): result = rendering.get_outfile(filepath, **kwargs) assert os.fspath(result) == expected_fspath @pytest.mark.parametrize( 'outfile, expected_fspath', [('spam.pdf', 'spam.gv'), ('spam', 'spam.gv')]) def test_get_filepath(outfile, expected_fspath): result = rendering.get_filepath(outfile) assert os.fspath(result) == expected_fspath @pytest.mark.parametrize( 'outfile_name, format, expected_result', [('spam.gv.pdf', None, 'pdf'), ('spam.jpeg', None, 'jpeg'), ('spam.SVG', None, 'svg'), ('spam.pdf', None, 'pdf'), ('spam.pdf', 'pdf', 'pdf')]) def test_get_format(outfile_name, format, expected_result): outfile = pathlib.Path(outfile_name) result = rendering.get_format(outfile, format=format) assert result == expected_result @pytest.mark.parametrize( 'outfile_name, format, expected_result, expected_warning, match', [('spam.jpg', 'jpeg', 'jpeg', graphviz.FormatSuffixMismatchWarning, r"expected format 'jpg' from outfile differs from given format: 'jpeg'"), ('spam.dot', 'plain', 'plain', graphviz.FormatSuffixMismatchWarning, r"expected format 'dot' from outfile differs from given format: 'plain'"), ('spam', 'svg', 'svg', graphviz.UnknownSuffixWarning, r"unknown outfile suffix '' \(expected: '\.svg'\)"), ('spam.peng', 'png', 'png', graphviz.UnknownSuffixWarning, r"unknown outfile suffix '.peng' \(expected: '\.png'\)"), ('spam', 'pdf', 'pdf', graphviz.UnknownSuffixWarning, r"unknown outfile suffix '' \(expected: '\.pdf'\)")]) def test_get_format_warns(outfile_name, format, expected_result, expected_warning, match): outfile = pathlib.Path(outfile_name) with pytest.warns(expected_warning, match=match): result = rendering.get_format(outfile, format=format) assert result == expected_result @pytest.mark.parametrize( 'outfile_name, expected_exception, match', [('spam', graphviz.RequiredArgumentError, r"cannot infer rendering format from suffix '' of outfile: 'spam'"), ('spam.peng', graphviz.RequiredArgumentError, r"cannot infer rendering format from suffix '.peng' of outfile: 'spam.peng'"), ('spam.wav', graphviz.RequiredArgumentError, r"cannot infer rendering format from suffix '.wav' of outfile: 'spam.wav'")]) def test_get_format_raises(outfile_name, expected_exception, match): outfile = pathlib.Path(outfile_name) with pytest.raises(expected_exception, match=match): rendering.get_format(outfile, format=None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1649449412.0 graphviz-0.21/tests/backend/test_unflattening.py0000666000000000000000000000330614224114704017061 0ustar00import re import subprocess import pytest import graphviz import _common def test_unflatten_stagger_missing(): with pytest.raises(graphviz.RequiredArgumentError, match=r'without stagger'): graphviz.unflatten('graph {}', fanout=True) @pytest.mark.exe @pytest.mark.parametrize( 'source, kwargs, expected', [('digraph {1 -> 2; 1 -> 3; 1 -> 4}', {'stagger': 3, 'fanout': True, 'chain': 42}, 'digraph { 1 -> 2 [minlen=1]; 1 -> 3 [minlen=2]; 1 -> 4 [minlen=3]; }')]) def test_unflatten(source, kwargs, expected): result = graphviz.unflatten(source, **kwargs) normalized = re.sub(r'\s+', ' ', result.strip()) assert normalized == expected def test_unflatten_mocked(capsys, sentinel, mock_run, stagger=10, fanout=True, chain=23): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout=sentinel.stdout, stderr='') result = graphviz.unflatten('nonsource', stagger=stagger, fanout=fanout, chain=chain) assert result is sentinel.stdout mock_run.assert_called_once_with([_common.EXPECTED_UNFLATTEN_BINARY, '-l', '10', '-f', '-c', '23'], input='nonsource', capture_output=True, startupinfo=_common.StartupinfoMatcher(), encoding='utf-8') assert capsys.readouterr() == ('', '') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/tests/backend/test_upstream_version.py0000666000000000000000000000440414144205436017774 0ustar00import subprocess import pytest import graphviz import _common @pytest.mark.exe(xfail=True, raises=graphviz.ExecutableNotFound) def test_version(capsys): result = graphviz.version() assert isinstance(result, tuple) and result assert all(isinstance(d, int) for d in result) assert capsys.readouterr() == ('', '') @pytest.mark.parametrize( 'stdout, expected', [('dot - graphviz version 1.2.3 (mocked)', (1, 2, 3)), ('dot - graphviz version 2.43.20190912.0211 (20190912.0211)\n', (2, 43, 20190912, 211)), ('dot - graphviz version 2.44.2~dev.20200927.0217 (20200927.0217)\n', (2, 44, 2)), ('dot - graphviz version 2.44.1 (mocked)\n', (2, 44, 1)), ('dot - graphviz version 2.44.2~dev.20200704.1652 (mocked)\n', (2, 44, 2))]) def test_version_mocked(mock_run, stdout, expected): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout=stdout, stderr=None) assert graphviz.version() == expected mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-V'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=_common.StartupinfoMatcher(), encoding='ascii') def test_version_parsefail_mocked(mock_run): mock_run.return_value = subprocess.CompletedProcess(_common.INVALID_CMD, returncode=0, stdout='nonversioninfo', stderr=None) with pytest.raises(RuntimeError, match=r'nonversioninfo'): graphviz.version() mock_run.assert_called_once_with([_common.EXPECTED_DOT_BINARY, '-V'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, startupinfo=_common.StartupinfoMatcher(), encoding='ascii') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636895518.0 graphviz-0.21/tests/backend/test_viewing.py0000666000000000000000000000156514144205436016044 0ustar00import subprocess import pytest import graphviz @pytest.mark.usefixtures('unknown_platform') def test_view_unknown_platform(): with pytest.raises(RuntimeError, match=r'platform'): graphviz.view('nonfilepath') def test_view_mocked(mocker, mock_platform, mock_popen, mock_startfile, quiet): assert graphviz.view('nonfilepath', quiet=quiet) is None if mock_platform == 'windows': mock_startfile.assert_called_once_with('nonfilepath') return if quiet: kwargs = {'stderr': subprocess.DEVNULL} else: kwargs = {} if mock_platform == 'darwin': mock_popen.assert_called_once_with(['open', 'nonfilepath'], **kwargs) elif mock_platform in ('linux', 'freebsd'): mock_popen.assert_called_once_with(['xdg-open', 'nonfilepath'], **kwargs) else: raise RuntimeError ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639307241.0 graphviz-0.21/tests/conftest.py0000666000000000000000000000527314155353751013601 0ustar00"""pytest markers and fixtures.""" import pytest SKIP_EXE = '--skip-exe' ONLY_EXE = '--only-exe' def pytest_configure(config): config.addinivalue_line('markers', f'exe(xfail): skip/xfail if {SKIP_EXE} is given') def pytest_collection_modifyitems(config, items): if config.getoption(ONLY_EXE, None) or config.getoption(SKIP_EXE, None): only = config.getoption(ONLY_EXE, None) for item in items: exe_marker = _make_exe_marker(item, only=only) if exe_marker is not None: item.add_marker(exe_marker) def _make_exe_marker(item, only: bool = False): def make_kwargs(**kwargs): return kwargs exe_values = [make_kwargs(*m.args, **m.kwargs) for m in item.iter_markers(name='exe')] if only: if exe_values: return None return pytest.mark.skip(reason=f'skipped by {ONLY_EXE} flag') elif exe_values: assert len(exe_values) == 1 kwargs, = exe_values if kwargs.pop('xfail', None): kwargs.setdefault('reason', f'xfail by {SKIP_EXE} flag') return pytest.mark.xfail(**kwargs) else: kwargs.setdefault('reason', f'skipped by {SKIP_EXE} flag') return pytest.mark.skip(**kwargs) return None @pytest.fixture(scope='session') def platform(): import platform return platform.system().lower() @pytest.fixture(params=[False, True], ids=lambda q: f'quiet={q!r}') def quiet(request): return request.param @pytest.fixture def sentinel(mocker): return mocker.sentinel @pytest.fixture def mock_render(mocker): yield mocker.patch('graphviz.backend.rendering.render', autospec=True) @pytest.fixture def mock_pipe(mocker): yield mocker.patch('graphviz.backend.piping.pipe', autospec=True) @pytest.fixture def mock_pipe_lines(mocker): yield mocker.patch('graphviz.backend.piping.pipe_lines', autospec=True) @pytest.fixture def mock_pipe_lines_string(mocker): yield mocker.patch('graphviz.backend.piping.pipe_lines_string', autospec=True) @pytest.fixture def mock_unflatten(mocker): yield mocker.patch('graphviz.backend.unflattening.unflatten', autospec=True) @pytest.fixture(params=['darwin', 'freebsd', 'linux', 'windows'], ids=lambda p: f'platform={p!r}') def mock_platform(monkeypatch, request): monkeypatch.setattr('graphviz.backend.viewing.PLATFORM', request.param) yield request.param @pytest.fixture def unknown_platform(monkeypatch, name='nonplatform'): monkeypatch.setattr('graphviz.backend.viewing.PLATFORM', name) yield name ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1626114366.0 graphviz-0.21/tests/dot_red.png0000666000000000000000000000263514073104476013524 0ustar00PNG  IHDR@@gAMA a cHRMz&u0`:pQ<PLTENN$$ ((;;>>****((**8877&&$$KKDDFF;;99 33@@ 99&&##7799))++..))CC;;UURR&& %%UURR,,))((66++##IICC @@==&&99 --++,,<<AA(( NNtRNS[Z+ʀ)UUYV).li$#Ĥkm*(XTXQ*'~X˨Zǁ}VPR&lff((SR&~'Ẏ[5IbKGD)Ԏ6tIME %zr IDATXý;[Q*R1ĈRU{VFQJVZ)ZTS=\7PqJ,Ch<ժ^nl#/=y zC@4xXH)GEL1xl dDJR0 I3 Ӂ:/? /)Y7W9}.Ow 8vWpH,aPr%(E`Ͽ62 *K+#o@F"h @!xA  Z/vCyE ^`qu+wk|O+HX'KU1 ^ qӆZax\GF/삯~gǾ+c;Y &Z!v.3BU՘FE world }\n') return cls() @pytest.fixture def invalid_dot(cls): if cls.__name__ == 'Source': return cls('graph { spam -- \\ }') else: invalid_dot = cls() with pytest.warns(graphviz.DotSyntaxWarning, match=r'syntax error'): invalid_dot.edge('spam', '\\') return invalid_dot def test_copy(cls, dot): assert type(dot) is cls assert dot.copy() is not dot assert dot.copy() is not dot.copy() assert type(dot.copy()) is type(dot) assert dot.copy().__dict__ == dot.__dict__ == dot.copy().__dict__ def test_str(dot): assert str(dot) == dot.source @pytest.mark.parametrize( 'parameter, expected_exception, match', [('engine', ValueError, r'unknown engine'), ('format', ValueError, r'unknown format'), ('renderer', ValueError, r'unknown renderer'), ('formatter', ValueError, r'unknown formatter'), ('encoding', LookupError, r'encoding')]) def test_invalid_parameter_raises_valuerror(dot, parameter, expected_exception, match): with pytest.raises(expected_exception, match=match): setattr(dot, parameter, 'invalid_parameter') def test_encoding_none(dot): dot_copy = dot.copy() dot_copy.encoding = None assert dot_copy.encoding == locale.getpreferredencoding() @pytest.mark.exe @pytest.mark.parametrize( 'kwargs', [{'engine': 'spam'}]) def test_render_raises_before_save(tmp_path, cls, kwargs, filename='dot.gv'): args = ['graph { spam }'] if cls.__name__ == 'Source' else [] dot = cls(*args, filename=filename, directory=tmp_path) expected_source = tmp_path / filename assert not expected_source.exists() with pytest.raises(ValueError, match=r''): dot.render(**kwargs) assert not expected_source.exists() pdf = dot.render(engine='dot') assert pdf == f'{expected_source}.pdf' assert expected_source.exists() assert expected_source.stat().st_size @pytest.mark.parametrize( 'kwargs', [{'engine': 'spam'}, {'format': 'spam'}, {'renderer': 'spam'}, {'formatter': 'spam'}]) def test_render_raises_before_save_mocked(tmp_path, mock_render, cls, kwargs, filename='dot.gv'): args = [''] if cls.__name__ == 'Source' else [] dot = cls(*args, filename=filename, directory=tmp_path) expected_source = tmp_path / filename assert not expected_source.exists() first_arg = next(iter(kwargs)) with pytest.raises(ValueError, match=f'unknown {first_arg}'): dot.render(**kwargs) assert not expected_source.exists() def test_render_mocked(mocker, mock_render, dot): mock_save = mocker.patch.object(dot, 'save', autospec=True) mock_view = mocker.patch.object(dot, '_view', autospec=True) mock_remove = mocker.patch('os.remove', autospec=True) assert dot.render(cleanup=True, view=True) is mock_render.return_value mock_save.assert_called_once_with(None, None, skip_existing=None) mock_render.assert_called_once_with(dot.engine, dot.format, mock_save.return_value, renderer=None, formatter=None, neato_no_op=None, outfile=None, raise_if_result_exists=False, overwrite_filepath=False, quiet=False) mock_remove.assert_called_once_with(mock_save.return_value) mock_view.assert_called_once_with(mock_render.return_value, format=dot.format, quiet=False) def test_render_outfile_mocked(mocker, mock_render, dot): mock_save = mocker.patch.object(dot, 'save', autospec=True) mock_view = mocker.patch.object(dot, '_view', autospec=True) mock_remove = mocker.patch('os.remove', autospec=True) outfile = 'spam.pdf' assert dot.render(outfile=outfile, raise_if_result_exists=True, overwrite_source=True, cleanup=True, view=True) is mock_render.return_value expected_filename = pathlib.Path('spam.gv') mock_save.assert_called_once_with(expected_filename, None, skip_existing=None) mock_render.assert_called_once_with(dot.engine, dot.format, mock_save.return_value, renderer=None, formatter=None, neato_no_op=None, outfile=pathlib.Path(outfile), raise_if_result_exists=True, overwrite_filepath=True, quiet=False) mock_remove.assert_called_once_with(mock_save.return_value) mock_view.assert_called_once_with(mock_render.return_value, format=dot.format, quiet=False) def test_format_renderer_formatter_mocked(mocker, mock_render, quiet, cls, filename='format.gv', format='jpg', renderer='cairo', formatter='core'): dot = cls(*[''] if cls.__name__ == 'Source' else [], filename=filename, format=format, renderer=renderer, formatter=formatter) assert dot.format == format assert dot.renderer == renderer assert dot.formatter == formatter mock_save = mocker.patch.object(dot, 'save', autospec=True) assert dot.render(quiet=quiet) is mock_render.return_value mock_save.assert_called_once_with(None, None, skip_existing=None) mock_render.assert_called_once_with('dot', format, mock_save.return_value, renderer=renderer, formatter=formatter, neato_no_op=None, outfile=None, raise_if_result_exists=False, overwrite_filepath=False, quiet=quiet) @pytest.mark.parametrize( 'neato_no_op', [None, False, True, 0, 1, 2]) def test_neato_no_op_mocked(mocker, mock_render, quiet, cls, neato_no_op, engine='neato', filename='neato_no_op.gv', format='svg'): dot = cls(*[''] if cls.__name__ == 'Source' else [], engine=engine, filename=filename, format=format) mock_save = mocker.patch.object(dot, 'save', autospec=True) assert dot.render(neato_no_op=neato_no_op, quiet=quiet) is mock_render.return_value mock_save.assert_called_once_with(None, None, skip_existing=None) mock_render.assert_called_once_with(engine, format, mock_save.return_value, renderer=None, formatter=None, neato_no_op=neato_no_op, outfile=None, raise_if_result_exists=False, overwrite_filepath=False, quiet=quiet) def test_save_mocked(mocker, dot, filename='nonfilename', directory='nondirectory'): mock_makedirs = mocker.patch('os.makedirs', autospec=True) mock_open = mocker.patch('builtins.open', mocker.mock_open()) with pytest.deprecated_call(match=r'\b1 positional arg\b'): assert dot.save(filename, directory) == dot.filepath assert dot.filename == filename assert dot.directory == directory mock_makedirs.assert_called_once_with(dot.directory, 0o777, exist_ok=True) mock_open.assert_called_once_with(dot.filepath, 'w', encoding=dot.encoding) expected_calls = ([mocker.call(dot.source)] if type(dot).__name__ == 'Source' else [mocker.call(mocker.ANY), mocker.call('}\n')]) assert mock_open.return_value.write.call_args_list == expected_calls @pytest.mark.exe @pytest.mark.parametrize( 'plain_text_format, expected_start', [('svg', ' world; }' else: assert normalized.startswith('digraph {' if dot.directed else 'graph {') def test_unflatten_mocked(sentinel, mock_unflatten, dot): kwargs = {'stagger': sentinel.stagger, 'fanout': sentinel.fanout, 'chain': sentinel.chain} result = dot.unflatten(**kwargs) assert result is not None assert isinstance(result, graphviz.Source) assert type(result) is graphviz.Source assert result.source is mock_unflatten.return_value assert result.filename == dot.filename assert result.directory == dot.directory assert result.engine == dot.engine assert result.format == dot.format assert result.renderer == dot.renderer assert result.formatter == dot.formatter assert result.encoding == dot.encoding assert result._loaded_from_path is None mock_unflatten.assert_called_once_with(dot.source, encoding=dot.encoding, **kwargs) def test_view_mocked(mocker, dot): mock_render = mocker.patch.object(dot, 'render', autospec=True) kwargs = {'filename': 'filename', 'directory': 'directory', 'cleanup': True, 'quiet': True, 'quiet_view': True} assert dot.view(**kwargs) is mock_render.return_value mock_render.assert_called_once_with(view=True, **kwargs) def test__view_unknown_platform(unknown_platform, dot): with pytest.raises(RuntimeError, match=r'support'): dot._view('name', format='png', quiet=False) def test__view_mocked(mocker, sentinel, mock_platform, dot): _view_platform = mocker.patch.object(dot, f'_view_{mock_platform}', autospec=True) kwargs = {'quiet': False} assert dot._view(sentinel.name, format='png', **kwargs) is None _view_platform.assert_called_once_with(sentinel.name, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/tests/test_graphs.py0000666000000000000000000001701314575646724014305 0ustar00import itertools import pytest import graphviz BASE_GRAPHS = [graphviz.Graph, graphviz.Digraph] @pytest.fixture(params=BASE_GRAPHS) def cls(request): return request.param @pytest.fixture(params=list(itertools.permutations(BASE_GRAPHS, 2)), ids=lambda c: f'{c[0].__name__}, {c[1].__name__}') def classes(request): return request.param def test_init_filename(cls): assert cls().filename == f'{cls.__name__}.gv' assert type('Subcls', (cls,), {})().filename == 'Subcls.gv' assert cls('spam').filename == 'spam.gv' @pytest.mark.parametrize( 'cls, body_lines, expected', [(graphviz.Graph, ['\tspam -- {\n', '\t\teggs, ham\n', '\t}\n'], 'graph {\n\tspam -- {\n\t\teggs, ham\n\t}\n}\n'), (graphviz.Digraph, ['\tspam -> {\n', '\t\teggs, ham\n', '\t}\n'], 'digraph {\n\tspam -> {\n\t\teggs, ham\n\t}\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_init_body(cls, body_lines, expected): dot = cls(body=iter(body_lines)) assert dot.source == expected @pytest.mark.exe @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'graph {\n\tC\n}\n'), (graphviz.Digraph, 'digraph {\n\tC\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_subgraph_render(capsys, tmp_path, cls, expected): lpath = tmp_path / 's1.gv' rendered = lpath.with_suffix('.gv.pdf') dot = cls() dot.edge('A', 'B') with dot.subgraph() as s1: s1.node('C') result = s1.render(str(lpath)) assert result == str(rendered) assert lpath.read_text(encoding='ascii') == expected assert rendered.exists() assert rendered.stat().st_size assert capsys.readouterr() == ('', '') @pytest.mark.parametrize( 'keep_attrs', [False, True]) def test_clear(cls, keep_attrs): kwargs = {f'{a}_attr': {a: a} for a in ('graph', 'node', 'edge')} c = cls(**kwargs) assert all(getattr(c, k) == v for k, v in kwargs.items()) c.node('spam') assert len(c.body) == 1 body = c.body c.clear(keep_attrs=keep_attrs) assert c.body == [] assert c.body is body if keep_attrs: assert all(getattr(c, k) == v for k, v in kwargs.items()) else: assert all(getattr(c, k) == {} for k in kwargs) def test_iter_subgraph_strict(cls): with pytest.raises(ValueError, match=r'strict'): cls().subgraph(cls(strict=True)) @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'strict graph {\n}\n'), (graphviz.Digraph, 'strict digraph {\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_iter_strict(cls, expected): assert cls(strict=True).source == expected def test_attr_invalid_kw(cls): with pytest.raises(ValueError, match=r'attr'): cls().attr('spam') @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'graph {\n\tspam=eggs\n}\n'), (graphviz.Digraph, 'digraph {\n\tspam=eggs\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_attr_kw_none(cls, expected): dot = cls() dot.attr(spam='eggs') assert dot.source == expected @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'graph {\n\tA [label="%s"]\n\tB [label="%s"]\n}\n' % (r'\\', r'\"\\\"')), (graphviz.Digraph, 'digraph {\n\tA [label="%s"]\n\tB [label="%s"]\n}\n' % (r'\\', r'\"\\\"'))], ids=lambda p: getattr(p, '__name__', '...')) def test_escaped_quotes_and_escapes(cls, expected): dot = cls() dot.node('A', label='\\\\') dot.node('B', label=r'"\\"') assert dot.source == expected @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'graph {\n\t// comment\n\tsubgraph name {\n\t}\n}\n'), (graphviz.Digraph, 'digraph {\n\t// comment\n\tsubgraph name {\n\t}\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_subgraph_graph_none(cls, expected): dot = cls(directory='nondirectory', format='png', encoding='ascii', engine='neato') assert dot.strict is False with dot.subgraph(name='name', comment='comment') as child: assert child.directory == dot.directory assert child.format == dot.format assert child.engine == dot.engine assert child.encoding == dot.encoding assert child.strict is None assert dot.source == expected def test_subgraph_graph_notsole(cls): with pytest.raises(ValueError, match=r'sole'): cls().subgraph(cls(), name='spam') def test_subgraph_mixed(classes): cls1, cls2 = classes with pytest.raises(ValueError, match=r'kind'): cls1().subgraph(cls2()) @pytest.mark.parametrize( 'cls, expected', [(graphviz.Graph, 'graph {\n\t{\n\t}\n}\n'), (graphviz.Digraph, 'digraph {\n\t{\n\t}\n}\n')], ids=lambda p: getattr(p, '__name__', '...')) def test_subgraph_reflexive(cls, expected): # guard against potential infinite loop dot = cls() dot.subgraph(dot) assert dot.source == expected def test_subgraph(): s1 = graphviz.Graph() s1.node('A') s1.node('B') s1.node('C') s1.edge('A', 'B', constraint='false') s1.edges(['AC', 'BC']) s2 = graphviz.Graph() s2.node('D') s2.node('E') s2.node('F') s2.edge('D', 'E', constraint='false') s2.edges(['DF', 'EF']) dot = graphviz.Graph() dot.subgraph(s1) dot.subgraph(s2) dot.attr('edge', style='dashed') dot.edges(['AD', 'BE', 'CF']) assert dot.source == '''graph { \t{ \t\tA \t\tB \t\tC \t\tA -- B [constraint=false] \t\tA -- C \t\tB -- C \t} \t{ \t\tD \t\tE \t\tF \t\tD -- E [constraint=false] \t\tD -- F \t\tE -- F \t} \tedge [style=dashed] \tA -- D \tB -- E \tC -- F } ''' def test_label_html(): """https://www.graphviz.org/doc/info/shapes.html#html""" dot = graphviz.Digraph('structs', node_attr={'shape': 'plaintext'}) dot.node('struct1', '''<
left middle right
>''') dot.node('struct2', '''<
one two
>''') dot.node('struct3', '''<
hello
world
b g h
c d e
f
>''') dot.edge('struct1:f1', 'struct2:f0') dot.edge('struct1:f2', 'struct3:here') assert dot.source == '''digraph structs { \tnode [shape=plaintext] \tstruct1 [label=<
left middle right
>] \tstruct2 [label=<
one two
>] \tstruct3 [label=<
hello
world
b g h
c d e
f
>] \tstruct1:f1 -> struct2:f0 \tstruct1:f2 -> struct3:here } ''' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/tests/test_init.py0000666000000000000000000000776014150757034013756 0ustar00import pytest import graphviz DEFAULT_ENGINE = 'dot' DEFAULT_FORMAT = 'pdf' DEFAULT_JUPYTER_FORMAT = 'svg' DEFAULT_JUPYTER_MIMETYPE = 'image/svg+xml' def test_set_default_engine_invalid(): with pytest.raises(ValueError, match=r'unknown engine'): graphviz.set_default_engine('nonengine') def test_set_default_format_invalid(): with pytest.raises(ValueError, match=r'unknown format'): graphviz.set_default_format('nonformat') def test_set_default_engine(monkeypatch, *, engine='neato', explicit_engine='sfdp'): assert len({DEFAULT_ENGINE, engine, explicit_engine}) == 3 from graphviz.parameters import Parameters assert Parameters._engine == DEFAULT_ENGINE # isolate the test monkeypatch.setattr('graphviz.parameters.Parameters._engine', DEFAULT_ENGINE) assert Parameters._engine == DEFAULT_ENGINE g1 = graphviz.Graph() assert g1.engine == DEFAULT_ENGINE g2 = graphviz.Graph(engine=explicit_engine) assert g2.engine == explicit_engine old = graphviz.set_default_engine(engine) assert old == DEFAULT_ENGINE assert g1.engine == engine assert g2.engine == explicit_engine g3 = graphviz.Graph() assert g3.engine == engine g4 = graphviz.Graph(engine=explicit_engine) assert g4.engine == explicit_engine old = graphviz.set_default_engine(DEFAULT_ENGINE) assert old == engine assert g1.engine == DEFAULT_ENGINE assert g2.engine == explicit_engine assert g3.engine == DEFAULT_ENGINE assert g4.engine == explicit_engine def test_set_default_format(monkeypatch, *, format='png', explicit_format='jpeg'): assert len({DEFAULT_FORMAT, format, explicit_format}) == 3 from graphviz.parameters import Parameters assert Parameters._format == DEFAULT_FORMAT # isolate the test monkeypatch.setattr('graphviz.parameters.Parameters._format', DEFAULT_FORMAT) assert Parameters._format == DEFAULT_FORMAT g1 = graphviz.Graph() assert g1.format == DEFAULT_FORMAT g2 = graphviz.Graph(format=explicit_format) assert g2.format == explicit_format old = graphviz.set_default_format(format) assert old == DEFAULT_FORMAT assert g1.format == format assert g2.format == explicit_format g3 = graphviz.Graph() assert g3.format == format g4 = graphviz.Graph(format=explicit_format) assert g4.format == explicit_format old = graphviz.set_default_format(DEFAULT_FORMAT) assert old == format assert g1.format == DEFAULT_FORMAT assert g2.format == explicit_format assert g3.format == DEFAULT_FORMAT assert g4.format == explicit_format def test_set_jupyter_format(monkeypatch, *, jupyter_format='jpg', expected_old_format='svg', expected_normalized_format='jpeg', expected_mimetype='image/jpeg'): assert len({DEFAULT_JUPYTER_MIMETYPE, jupyter_format}) == 2 from graphviz import jupyter_integration assert (jupyter_integration.JupyterIntegration._jupyter_mimetype == DEFAULT_JUPYTER_MIMETYPE) # isolate the test monkeypatch.setattr('graphviz.jupyter_integration.JupyterIntegration._jupyter_mimetype', DEFAULT_JUPYTER_MIMETYPE) assert (jupyter_integration.JupyterIntegration._jupyter_mimetype == DEFAULT_JUPYTER_MIMETYPE) g1 = graphviz.Graph() assert g1._jupyter_mimetype == DEFAULT_JUPYTER_MIMETYPE old = graphviz.set_jupyter_format(jupyter_format) assert old == jupyter_integration.DEFAULT_JUPYTER_FORMAT assert g1._jupyter_mimetype == expected_mimetype g2 = graphviz.Graph() assert g2._jupyter_mimetype == expected_mimetype old = graphviz.set_jupyter_format(DEFAULT_JUPYTER_FORMAT) assert old == expected_normalized_format assert g1._jupyter_mimetype == DEFAULT_JUPYTER_MIMETYPE assert g2._jupyter_mimetype == DEFAULT_JUPYTER_MIMETYPE ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1684745680.0 graphviz-0.21/tests/test_jupyter_integration.py0000666000000000000000000000417314432626720017113 0ustar00import pytest import graphviz from graphviz import jupyter_integration EXPECTED_SVG_ENCODING = 'utf-8' def test_get_jupyter_format_mimetype_invalid_raises_unknown(): with pytest.raises(ValueError, match=r'unknown'): jupyter_integration.get_jupyter_format_mimetype('Brian!') def test_get_jupyter_mimetype_format_normalizes(): assert jupyter_integration.get_jupyter_mimetype_format( jupyter_integration.get_jupyter_format_mimetype('jpg')) == 'jpeg' def test_get_jupyter_mimetype_format_raises_unsupported(): with pytest.raises(ValueError, match=r"unsupported .*\(must be one of .+'image/svg\+xml'"): jupyter_integration.get_jupyter_mimetype_format('A boy called Brian!') @pytest.mark.exe def test_repr_image_svg_xml_encoding(input_encoding='latin1'): assert input_encoding != EXPECTED_SVG_ENCODING dot = graphviz.Graph(comment='Mønti Pythøn ik den Hølie Grailen', encoding=input_encoding) result = dot._repr_image_svg_xml() assert result.startswith('') @pytest.mark.exe @pytest.mark.parametrize('input_encoding', ['utf-8', 'ascii', 'latin1']) def test_repr_image_svg_xml_encoding_mocked(mocker, mock_pipe_lines_string, mock_pipe_lines, input_encoding): dot = graphviz.Graph(encoding=input_encoding) result = dot._repr_image_svg_xml() if input_encoding == 'utf-8': assert result is mock_pipe_lines_string.return_value mock_pipe_lines_string.assert_called_once() mock_pipe_lines.assert_not_called() assert (mock_pipe_lines_string.call_args.kwargs['encoding'] == EXPECTED_SVG_ENCODING) else: assert result is mock_pipe_lines.return_value.decode.return_value mock_pipe_lines.assert_called_once() mock_pipe_lines_string.assert_not_called() assert 'encoding' not in mock_pipe_lines.call_args.kwargs (mock_pipe_lines.return_value.decode .assert_called_once_with(EXPECTED_SVG_ENCODING)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638129180.0 graphviz-0.21/tests/test_parameters.py0000666000000000000000000000336014150757034015146 0ustar00import pytest import graphviz from graphviz import parameters VERIFY_FUNCS = [parameters.verify_engine, parameters.verify_format, parameters.verify_renderer, parameters.verify_formatter] @pytest.mark.parametrize( 'cls', [graphviz.Graph, graphviz.Digraph, graphviz.Source]) def test_parameters(cls, engine='patchwork', format='tiff', renderer='map', formatter='core'): args = [''] if cls is graphviz.Source else [] dot = cls(*args, engine=engine, format=format, renderer=renderer, formatter=formatter) assert isinstance(dot, cls) assert type(dot) is cls assert dot.engine == engine assert dot.format == format assert dot.renderer == renderer assert dot.formatter == formatter dot_copy = dot.copy() assert dot_copy is not dot assert isinstance(dot_copy, cls) assert type(dot_copy) is cls assert dot.engine == engine assert dot.format == format assert dot_copy.renderer == renderer assert dot_copy.formatter == formatter @pytest.mark.parametrize( 'verify_func', VERIFY_FUNCS) def test_verify_parameter_raises_unknown(verify_func): with pytest.raises(ValueError, match=r'unknown .*\(must be .*one of'): verify_func('Brian!') @pytest.mark.parametrize( 'verify_func', VERIFY_FUNCS) def test_verify_parameter_none_required_false_passes(verify_func): assert verify_func(None, required=False) is None @pytest.mark.parametrize( 'verify_func', VERIFY_FUNCS) def test_verify_parameter_none_required_raises_missing(verify_func): with pytest.raises(ValueError, match=r'missing'): verify_func(None, required=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/tests/test_quoting.py0000666000000000000000000000357414575646724014516 0ustar00import sys import warnings import pytest import graphviz from graphviz import quoting @pytest.mark.parametrize( 'char', ['G', 'E', 'T', 'H', 'L', 'l']) def test_deprecated_escape(recwarn, char): warnings.simplefilter('always') escape = eval(rf'"\{char}"') assert len(recwarn) == 1 w = recwarn.pop(DeprecationWarning if sys.version_info < (3, 12) else SyntaxWarning) assert str(w.message).startswith('invalid escape sequence') assert escape == f'\\{char}' assert quoting.quote(escape) == f'"\\{char}"' @pytest.mark.parametrize( 'identifier, expected', [('"spam"', r'"\"spam\""'), ('node', '"node"'), ('EDGE', '"EDGE"'), ('Graph', '"Graph"'), ('\\G \\N \\E \\T \\H \\L', '"\\G \\N \\E \\T \\H \\L"'), ('\\n \\l \\r', '"\\n \\l \\r"'), ('\r\n', '"\r\n"'), ('\\\\n', r'"\\n"'), ('\u0665.\u0660', '"\u0665.\u0660"'), ('\\"spam', r'"\"spam"'), ('\\\\"spam', r'"\\\"spam"'), ('\\\\\\"spam', r'"\\\"spam"'), ('\\\\\\\\"spam', r'"\\\\\"spam"')]) def test_quote(identifier, expected): assert quoting.quote(identifier) == expected @pytest.mark.parametrize( 'attributes, expected', [([('spam', 'eggs')], ' [spam=eggs]'), ({'spam': 'eggs'}, ' [spam=eggs]')]) def test_attr_list(attributes, expected): assert quoting.attr_list(attributes=attributes) == expected @pytest.mark.parametrize( 'string, expected, expected_quoted', [('spam', 'spam', 'spam'), ('<>-*-<>', '<>-*-<>', '"<>-*-<>"')]) def test_nohtml(string, expected, expected_quoted): result = graphviz.nohtml(string) assert isinstance(result, str) assert isinstance(result, quoting.NoHtml) assert result == expected quoted = quoting.quote(result) assert isinstance(quoted, str) assert quoted == expected_quoted ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1636219361.0 graphviz-0.21/tests/test_saving.py0000666000000000000000000000071714141534741014273 0ustar00import graphviz def test_saves_source_from_file(tmp_path, src='graph spam { spam }'): path = tmp_path / 'spam.gv' path.write_text(src) stat_before = path.stat() source = graphviz.Source.from_file(path) source.save() assert path.stat().st_mtime == stat_before.st_mtime, 'file not overwritten' assert path.read_text() == src, 'file contents unchanged' assert ''.join(source) == f'{src}\n', 'src with final newline' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715619529.0 graphviz-0.21/tests/test_sources.py0000666000000000000000000000520614620443311014457 0ustar00import locale import pytest import graphviz SOURCE = {'source': 'digraph { hello -> world }\n', 'filename': 'hello.gv', 'directory': 'test-output', 'format': 'PNG', 'engine': 'NEATO', 'encoding': 'utf-8'} @pytest.fixture(scope='module') def source(): return graphviz.Source(**SOURCE) @pytest.mark.parametrize( 'parameter', ['engine', 'format', 'encoding']) def test_source_parameter(source, parameter): if parameter != 'encoding': assert not SOURCE[parameter].islower() assert getattr(source, parameter) == SOURCE[parameter].lower() def test_init(source): assert source.source == SOURCE['source'] assert source.filename == SOURCE['filename'] assert source.directory == SOURCE['directory'] def test_init_filename(): assert graphviz.Source('').filename == 'Source.gv' assert type('Subcls', (graphviz.Source,), {'name': 'name'})('').filename == 'Subcls.gv' def test_filepath(platform, source): if platform == 'windows': assert source.filepath == 'test-output\\hello.gv' else: assert source.filepath == 'test-output/hello.gv' def test_from_file(tmp_path, filename='hello.gv', directory='source_hello', data='digraph { hello -> world }', encoding='utf-8', deprecation_match=r'\b1 positional arg\b'): lpath = tmp_path / directory lpath.mkdir() (lpath / filename).write_text(data, encoding=encoding) with pytest.deprecated_call(match=deprecation_match): source = graphviz.Source.from_file(filename, str(lpath)) assert source.encoding == 'utf-8' with pytest.deprecated_call(match=deprecation_match): source = graphviz.Source.from_file(filename, str(lpath), encoding=None) assert source.encoding == locale.getpreferredencoding() renderer = 'xdot' formatter = 'core' with pytest.deprecated_call(match=deprecation_match): source = graphviz.Source.from_file(filename, str(lpath), encoding=encoding, renderer=renderer, formatter=formatter) assert source.source == data + '\n' assert source.filename == filename assert source.directory == str(lpath) assert source.encoding == encoding assert source.renderer == renderer assert source.formatter == formatter def test_source_iter(source): source_without_newline = graphviz.Source(source.source + source.source.rstrip()) lines = list(source_without_newline) assert lines == list(source) * 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715624864.0 graphviz-0.21/tests/test_tools.py0000666000000000000000000000445514620455640014151 0ustar00import functools import os import warnings import pytest from graphviz import _tools import _common def itertree(root): for path, dirs, files in os.walk(root): base = os.path.relpath(path, root) rel_path = functools.partial(os.path.join, base if base != '.' else '') for is_file, names in enumerate((dirs, files)): for n in names: yield bool(is_file), rel_path(n).replace('\\', '/') def test_mkdirs_invalid(tmp_path): with _common.as_cwd(tmp_path): (tmp_path / 'spam.eggs').write_bytes(b'') with pytest.raises(OSError): _tools.mkdirs('spam.eggs/spam') def test_mkdirs(tmp_path): with _common.as_cwd(tmp_path): _tools.mkdirs('spam.eggs') assert list(itertree(str(tmp_path))) == [] for _ in range(2): _tools.mkdirs('spam/eggs/spam.eggs') assert list(itertree(str(tmp_path))) == [(False, 'spam'), (False, 'spam/eggs')] @pytest.mark.parametrize('category', [FutureWarning, DeprecationWarning, PendingDeprecationWarning]) def test_deprecate_positional_args(category): result = object() @_tools.deprecate_positional_args(supported_number=2, category=category) def func(first, second, third=None, **_): return result # supported call with warnings.catch_warnings(): warnings.simplefilter('error') # should fail if warnings are emitted assert func('first', 'second', third='third', extra='extra') is result # deprecated call with pytest.warns(category, match=r" third='third' "): assert func('first', 'second', 'third', extra='extra') is result def test_deprecate_positional_args_category_none_should_disable(): result = object() @_tools.deprecate_positional_args(supported_number=2, category=None) def func(first, second, third=None, **_): return result with warnings.catch_warnings(): warnings.simplefilter('error') # should fail if warnings are emitted assert func('first', 'second', third='third', extra='extra') is result assert func('first', 'second', 'third', extra='extra') is result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639307241.0 graphviz-0.21/try-examples.py0000666000000000000000000000274714155353751013247 0ustar00#!/usr/bin/env python3 # flake8: noqa """Import ``graphviz`` here and run all scripts in the ``examples/`` dir.""" import os import pathlib import sys import unittest.mock import warnings import graphviz # noqa: F401 SELF = pathlib.Path(__file__) EXAMPLES = pathlib.Path('examples') IO_KWARGS = {'encoding': 'utf-8'} DEFAULT_FORMAT = 'pdf' print('run', [SELF.name] + sys.argv[1:]) os.chdir(EXAMPLES) graphviz.set_default_format(DEFAULT_FORMAT) raised = [] with unittest.mock.patch.object(graphviz.graphs.BaseGraph, '_view') as mock_view: for path in pathlib.Path().glob('*.py'): print(path) code = path.read_text(**IO_KWARGS) try: exec(code) except Exception as e: raised.append(e) warnings.warn(e) else: if path.name.endswith('_recipe.py'): continue rendered = f'{path.stem}.gv.{DEFAULT_FORMAT}' assert pathlib.Path(rendered).stat().st_size, f'non-empty {rendered}' mock_view.assert_called_once_with(rendered, format=DEFAULT_FORMAT, quiet=False) mock_view.reset_mock() if not raised: print('PASSED: all examples passed without raising') sys.exit(None) else: message = f'FAILED: {len(raised)} examples raised (WARNING)' print(message, *raised, sep='\n') sys.exit(message) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710706132.0 graphviz-0.21/update-help.py0000666000000000000000000001063114575646724013027 0ustar00#!/usr/bin/env python3 """Update the ``help()`` outputs in ``docs/api.rst``.""" import contextlib import difflib import io import operator import pathlib import re import sys import typing import graphviz SELF = pathlib.Path(__file__) ALL_CLASSES = [graphviz.Graph, graphviz.Digraph, graphviz.Source] ARGS_LINE = re.compile(r'(?:class | \| {2})\w+\(') WRAP_AFTER = 80 INDENT = ' ' * 4 TARGET = pathlib.Path('docs/api.rst') PATTERN_TMPL = (r''' ( \ {{4}}>>>\ help\(graphviz\.{cls_name}\).*\n) \ {{4}}Help\ on\ class\ {cls_name} \ in\ module\ graphviz\.(?:graphs|sources):\n \ {{4}}\n (?:.*\n)+? \ {{4}}\n ''') IO_KWARGS = {'encoding': 'utf-8'} def get_help(obj) -> str: print(f'capture help() output for {obj}') with io.StringIO() as buf: with contextlib.redirect_stdout(buf): help(obj) buf.seek(0) return ''.join(iterlines(buf)) def rpartition_initial(value: str, *, sep: str) -> typing.Tuple[str, str, str]: """Return (value, '', '') if sep not in value else value.rpartition(sep).""" _, sep_found, _ = parts = value.rpartition(sep) return tuple(reversed(parts)) if not sep_found else parts def iterarguments(unwrapped_line: str) -> typing.Iterator[str]: """Yield unwrapped line of argument definitions divided into one line per arg. >>> list(iterarguments('spam: str, eggs: typing.Union[str, None], ham')) ['spam: str,', 'eggs: typing.Union[str, None],', 'ham'] """ pos = 0 bracket_level = paren_level = 0 for i, char in enumerate(unwrapped_line): if char == '[': bracket_level += 1 elif char == ']': bracket_level -= 1 elif char == '(': paren_level += 1 elif char == ')': paren_level -= 1 elif (bracket_level == 0 and paren_level == 0 and char == ',' and unwrapped_line[i + 1: i + 3].strip() != '*'): pos_including_comma = i + 1 yield unwrapped_line[pos:pos_including_comma].lstrip() pos = pos_including_comma yield unwrapped_line[pos:].lstrip() def iterlines(stdout_lines, *, line_indent: str = INDENT, wrap_after: int = WRAP_AFTER) -> typing.Iterator[str]: """Yield post-processed help() stdout lines: rstrip, indent, wrap.""" for line in stdout_lines: line = line.rstrip() + '\n' line = line.replace("``'\\n'``", r"``'\\n'``") if len(line) > wrap_after and ARGS_LINE.match(line): indent = line_indent + ' ' * (line.index('(') + 1) *start, rest = line.partition('(') argument_line, *rest = rpartition_initial(rest, sep=' -> ') arguments = list(iterarguments(argument_line)) print(len(line), 'character line wrapped into', len(arguments), 'lines') assert len(arguments) > 1, 'wrapped long argument line' line = ''.join(start + [f'\n{indent}'.join(arguments)] + rest) yield line_indent + line print('run', [SELF.name] + sys.argv[1:]) help_docs = {cls.__name__: get_help(cls) for cls in ALL_CLASSES} print('read', TARGET) target = target_before = TARGET.read_text(**IO_KWARGS) for cls_name, doc in help_docs.items(): print('replace', cls_name, 'PATTERN_TMPL match') pattern = re.compile(PATTERN_TMPL.format(cls_name=cls_name), flags=re.VERBOSE) target, found = pattern.subn(fr'\1{doc}', target, count=1) assert found, f'replaced {cls_name} section' target = target.replace(INDENT + '\n', INDENT + '\n') if target == target_before: print(f'PASSED: unchanged {TARGET} (OK)') sys.exit(None) else: print('write', TARGET) splitlines = operator.methodcaller('splitlines', keepends=True) target_before, target = map(splitlines, (target_before, target)) print(len(target_before), 'lines before') print(len(target), 'lines after') with TARGET.open('w', **IO_KWARGS) as f: for line in target: f.write(line) for diff in difflib.context_diff(target_before, target): print(diff) message = f'FAILED: changed {TARGET!r} (WARNING)' print(f'sys.exit({message!r})') sys.exit(message)