From 136154b874e66c559c5edcbb79728fe0f842ea95 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 5 May 2026 11:14:49 +0200 Subject: [PATCH 1/2] Add SysML / Gaphor diagrams under sphinx-needs items Demonstrates that ``.. diagram::`` from gaphor.extensions.sphinx works the same way as ``.. uml::`` does today: it can be embedded directly inside a sphinx-needs ``arch::`` item and renders to SVG/PDF on every build. Wires up: - gaphor>=3.3.2 as a dependency and ``gaphor.extensions.sphinx`` in ``conf.py`` with a ``gaphor_models`` mapping. - Two model files under ``docs/automotive-adas/sysml/``: ``adas-tsr-bdd.gaphor`` (regenerated by ``author_adas_bdd.py``) and Gaphor's stock ``sysml-car.gaphor`` example. - A diagram embedded under ARCH_007 (Traffic Sign Recognition) next to the existing PlantUML view. - A new ``sysml/index.rst`` page that explains the integration, the authoring workflow (GUI vs Python API) and why dropping in raw SysML 1.5 XMI is not viable today (Gaphor removed XMI export in 3.1.0; the ``.gaphor`` schema differs from XMI). - Read-the-Docs apt packages required by PyGObject + Cairo. - ``matplotlib.use("Agg")`` early in ``conf.py`` to keep matplotlib off the GTK4 backend that PyGObject would otherwise default to (the cause of a build hang at "writing output... [4%]" on headless hosts). Notes for review: - Gaphor implements SysML 1.6, the minor revision of 1.5 that retains the same core constructs (Block, Property, Port, Connector, Requirement); for a "SysML 1.5" customer showcase this is the right fit. - ``adas-tsr-bdd.gaphor`` is text/XML and tracked as code; rerun ``uv run python docs/automotive-adas/sysml/author_adas_bdd.py`` to regenerate after structural changes. --- .readthedocs.yaml | 7 + docs/automotive-adas/index.rst | 1 + docs/automotive-adas/sys_3_sys_arch.rst | 13 + .../automotive-adas/sysml/adas-tsr-bdd.gaphor | 318 +++++++++++++++ docs/automotive-adas/sysml/author_adas_bdd.py | 95 +++++ docs/automotive-adas/sysml/index.rst | 120 ++++++ docs/automotive-adas/sysml/sysml-car.gaphor | 373 ++++++++++++++++++ docs/conf.py | 19 + pyproject.toml | 3 +- uv.lock | 190 +++++++++ 10 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 docs/automotive-adas/sysml/adas-tsr-bdd.gaphor create mode 100644 docs/automotive-adas/sysml/author_adas_bdd.py create mode 100644 docs/automotive-adas/sysml/index.rst create mode 100644 docs/automotive-adas/sysml/sysml-car.gaphor diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1521c6e..36836f4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,6 +11,13 @@ build: apt_packages: - default-jdk - graphviz + # Required by Gaphor (PyGObject + Cairo) for the .. diagram:: directive. + # See https://docs.gaphor.org/en/latest/sphinx.html#read-the-docs + - libgirepository-2.0-dev + - libcairo2-dev + - pkg-config + - python3-dev + - gir1.2-pango-1.0 tools: python: "3.12" # You can also specify other tool versions: diff --git a/docs/automotive-adas/index.rst b/docs/automotive-adas/index.rst index d226e3b..dbba8f9 100644 --- a/docs/automotive-adas/index.rst +++ b/docs/automotive-adas/index.rst @@ -36,6 +36,7 @@ sys_4_sys_integation_tests sys_5_sys_quali_test sys_5_sys_quali_test_results + sysml/index V-model and service connectors diff --git a/docs/automotive-adas/sys_3_sys_arch.rst b/docs/automotive-adas/sys_3_sys_arch.rst index 0a37b95..8781079 100644 --- a/docs/automotive-adas/sys_3_sys_arch.rst +++ b/docs/automotive-adas/sys_3_sys_arch.rst @@ -207,6 +207,11 @@ SYS.3 Architecture Design Design the system architecture for traffic sign recognition, including camera capture, sign classification, and distribution of detected speed limits to vehicle control functions. + The component view (PlantUML) and the SysML Block Definition Diagram below + describe the same architecture from two notations โ€” the Gaphor-rendered BDD + is sourced from a ``.gaphor`` model file and can be opened in the Gaphor + GUI for further editing. + .. uml:: @startuml @@ -229,3 +234,11 @@ SYS.3 Architecture Design TrafficSignRecognition --> SignInterpreter SignInterpreter --> VehicleControl @enduml + + .. diagram:: TSR Block Definition + :model: tsr + :align: center + :alt: SysML Block Definition Diagram for the Traffic Sign Recognition system + + SysML BDD: ``TrafficSignRecognition`` decomposed into ``FrontCamera``, + ``SignInterpreter`` and ``VehicleControl`` parts. diff --git a/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor b/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor new file mode 100644 index 0000000..21d7540 --- /dev/null +++ b/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor @@ -0,0 +1,318 @@ + + + + + +ADAS + + + + + + + + + + + + + + + + + + + + + + + + + +TrafficSignRecognition + + + + + + + + + + + + + + + + + + + + + + +FrontCamera + + + + + + + + + + + + + + + +SignInterpreter + + + + + + + + + + + + + + + +VehicleControl + + + + + + + + + + + + + + + +composite + + +camera + + + + + + + + + + + + + + + +composite + + +interpreter + + + + + + + + + + + + + + + +composite + + +control + + + + + + + + + + + + + + + + + + +TSR Block Definition + + + + + + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 80.0, 60.0) + + +(0.0, 0.0) + + +480.0 + + +220.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 100.0, 160.0) + + +(0.0, 0.0) + + +130.0 + + +60.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 250.0, 160.0) + + +(0.0, 0.0) + + +130.0 + + +60.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 400.0, 160.0) + + +(0.0, 0.0) + + +130.0 + + +60.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 80.0, 380.0) + + +(0.0, 0.0) + + +160.0 + + +70.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 280.0, 380.0) + + +(0.0, 0.0) + + +160.0 + + +70.0 + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 480.0, 380.0) + + +(0.0, 0.0) + + +160.0 + + +70.0 + + + + + + + + + + \ No newline at end of file diff --git a/docs/automotive-adas/sysml/author_adas_bdd.py b/docs/automotive-adas/sysml/author_adas_bdd.py new file mode 100644 index 0000000..b6dc4a0 --- /dev/null +++ b/docs/automotive-adas/sysml/author_adas_bdd.py @@ -0,0 +1,95 @@ +"""Programmatically author the ADAS Traffic Sign Recognition BDD. + +This script regenerates ``adas-tsr-bdd.gaphor`` from scratch using Gaphor's +Python API, so the model can be tracked as code rather than a binary-ish +diagram blob. Run it whenever you change the structure: + +.. code:: console + + uv run python docs/automotive-adas/sysml/author_adas_bdd.py + +The generated file is committed alongside this script and consumed by +Sphinx via the ``gaphor.extensions.sphinx`` extension and the +``gaphor_models`` mapping in ``docs/conf.py``. +""" + +from __future__ import annotations + +from pathlib import Path + +from gaphor.core.modeling import ElementFactory +from gaphor.storage import storage +from gaphor.SysML import sysml +from gaphor.SysML.blocks import BlockItem, PropertyItem +from gaphor.UML import uml + +OUTPUT = Path(__file__).with_name("adas-tsr-bdd.gaphor") + + +def _make_block(ef: ElementFactory, package, name: str): + block = ef.create(sysml.Block) + block.name = name + block.package = package + return block + + +def _make_part(ef: ElementFactory, parent, name: str, type_block): + prop = ef.create(uml.Property) + prop.name = name + prop.aggregation = "composite" + prop.type = type_block + prop.class_ = parent + return prop + + +def build() -> ElementFactory: + ef = ElementFactory() + + pkg = ef.create(uml.Package) + pkg.name = "ADAS" + + tsr = _make_block(ef, pkg, "TrafficSignRecognition") + cam = _make_block(ef, pkg, "FrontCamera") + interpreter = _make_block(ef, pkg, "SignInterpreter") + veh_ctl = _make_block(ef, pkg, "VehicleControl") + + p_cam = _make_part(ef, tsr, "camera", cam) + p_int = _make_part(ef, tsr, "interpreter", interpreter) + p_veh = _make_part(ef, tsr, "control", veh_ctl) + + diag = ef.create(sysml.BlockDefinitionDiagram) + diag.name = "TSR Block Definition" + diag.element = pkg + + # Parent block with parts compartment + tsr_item = diag.create(BlockItem, subject=tsr) + tsr_item.matrix.translate(80, 60) + tsr_item.width = 480 + tsr_item.height = 220 + + for i, prop in enumerate((p_cam, p_int, p_veh)): + item = diag.create(PropertyItem, subject=prop) + col_x = 100 + i * 150 + item.matrix.translate(col_x, 160) + item.width = 130 + item.height = 60 + + # Sub-block definitions below (typical BDD layout) + for i, sub in enumerate((cam, interpreter, veh_ctl)): + sub_item = diag.create(BlockItem, subject=sub) + sub_item.matrix.translate(80 + i * 200, 380) + sub_item.width = 160 + sub_item.height = 70 + + return ef + + +def main() -> None: + ef = build() + with OUTPUT.open("w", encoding="utf-8") as fp: + storage.save(fp, ef) + print(f"Wrote {OUTPUT} ({OUTPUT.stat().st_size} bytes)") + + +if __name__ == "__main__": + main() diff --git a/docs/automotive-adas/sysml/index.rst b/docs/automotive-adas/sysml/index.rst new file mode 100644 index 0000000..6dd483e --- /dev/null +++ b/docs/automotive-adas/sysml/index.rst @@ -0,0 +1,120 @@ +{% set page="index.rst" %} +{% include "demo_page_header.rst" with context %} + +.. _SysML_Demo: + +๐Ÿงฉ SysML diagrams via Gaphor +============================= + +This page demonstrates how `Gaphor `__ models can be +embedded inside Sphinx-Needs alongside the existing ``.. uml::`` PlantUML +directives. Gaphor implements a large part of the OMG **SysML 1.6** +specification (a minor revision of SysML 1.5 that retains the same core +constructs โ€” Block, Property, Port, Connector, Requirement). For a customer +showcase that says "SysML 1.5", Gaphor is the same modeling family. + +How it works +------------ + +Three pieces are needed: + +1. **A** ``.gaphor`` **model file** โ€” an XML document that holds blocks, + properties, ports, diagrams and their layout. It can be authored in the + Gaphor desktop GUI or programmatically through the Gaphor Python API + (see :file:`author_adas_bdd.py` for an example). +2. **The** ``gaphor.extensions.sphinx`` **extension** โ€” registers a + ``.. diagram::`` directive and renders the chosen diagram to SVG/PDF + on every Sphinx build. +3. **A** ``gaphor_models`` **mapping in** :file:`conf.py` โ€” maps logical + model names (``"tsr"``, ``"car"``, ...) to ``.gaphor`` files relative + to ``docs/``. + +Example: under :need:`ARCH_007` the BDD lives next to a PlantUML view of the +same architecture and is rendered from the model file by: + +.. code:: rst + + .. diagram:: TSR Block Definition + :model: tsr + :align: center + +The directive accepts the standard ``image``/``figure`` options +(``:align:``, ``:figwidth:``, ``:alt:``, plus a caption block). + +Using a stock Gaphor SysML example +---------------------------------- + +The ``car`` model below is the un-modified ``sysml-car.gaphor`` example +from the Gaphor distribution, demonstrating that an existing ``.gaphor`` +file can be dropped into the docs tree and referenced verbatim. + +.. diagram:: main + :model: car + :align: center + :alt: SysML Block Definition Diagram of a Car + + ``Car`` block from Gaphor's ``examples/sysml-car.gaphor``, decomposed + into ``rear: Wheel[2]``, ``: Engine`` and two ``axle`` proxy ports. + +Programmatic authoring +---------------------- + +For repeatable, code-reviewable models we generate ``adas-tsr-bdd.gaphor`` +from a small Python script using Gaphor's API: + +.. code:: console + + uv run python docs/automotive-adas/sysml/author_adas_bdd.py + +The script creates a ``Package``, four ``Block`` elements, three +composite ``Property`` parts and a ``BlockDefinitionDiagram`` with the +items laid out, then ``storage.save()`` writes the XML. Re-running the +script regenerates the file deterministically. + +Converting existing SysML 1.5 XMI +--------------------------------- + +A common customer ask is *"we already have SysML 1.5 XMI files from +Cameo/Papyrus/MagicDraw โ€” can we just drop them in?"*. Today the +honest answer is **no, not directly**: + +- Gaphor's own XMI export was removed in `Gaphor 3.1.0 + `__ after the maintainers + declared it under-tested and unmaintained. +- The ``.gaphor`` format is **not** XMI. It is a Gaphor-native XML schema + (root ```` namespaced under + ``https://gaphor.org/model``) that mixes semantic elements + (````, ````, ````, ...) with + per-diagram presentation items (```` carrying a + transformation matrix, width and height). +- Conversion therefore requires a custom XMI โ†’ ``.gaphor`` translator + that walks the source tree, generates UUIDs, maps element types and + rebuilds layout. There is no off-the-shelf tool today. + +The pragmatic options for a project that needs SysML 1.5 content in the +docs are: + +1. **Re-author** the diagrams in Gaphor (GUI) or via the Python API + shown above โ€” fastest for a small, curated set of system views that + matter for the documentation. +2. **Write a converter** โ€” feasible for a constrained SysML 1.5 + subset (Blocks, Properties, BDDs, simple Connectors). The ``.gaphor`` + file format is documented at + https://docs.gaphor.org/en/latest/storage.html and only ~10 element + types are needed for a typical BDD. +3. **Keep the original tool** for full-fidelity authoring and only + import a curated subset into Gaphor for publication. + +Limitations to be aware of +-------------------------- + +- The Gaphor Sphinx extension renders SVG **and** PDF for each diagram. + Rendering uses Cairo via PyGObject, so the build host needs the GTK + girepository / Cairo system packages. See ``.readthedocs.yaml`` for + the apt list. +- With Gaphor installed, matplotlib auto-selects the ``gtk4agg`` + backend, which hangs the build on a headless host. ``conf.py`` pins + ``matplotlib.use("Agg")`` before any extension imports it. +- ``gaphor`` is a heavy dependency (it pulls in ``pycairo`` and + ``PyGObject``). If only a few pages need it, consider extracting the + SysML pages into an opt-in toctree. diff --git a/docs/automotive-adas/sysml/sysml-car.gaphor b/docs/automotive-adas/sysml/sysml-car.gaphor new file mode 100644 index 0000000..b93cf6d --- /dev/null +++ b/docs/automotive-adas/sysml/sysml-car.gaphor @@ -0,0 +1,373 @@ + + + + +New model + + + + + + + + + + + + + + +main + + + + + + + +(1.0, 0.0, 0.0, 1.0, 6.5, 59.0) + + +462.5 + + +206.5 + + + + + + +(1.0, 0.0, 0.0, 1.0, 279.5, 93.5) + + +111.0 + + +50.0 + + + + + + + +(1.0, 0.0, 0.0, 1.0, 71.0, 93.5) + + +121.0 + + +50.0 + + + + + + + +(1.0, 0.0, 0.0, 1.0, 339.5, 206.5) + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 127.5, 206.5) + + + + + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 134.18468468468467, 202.5) + + +0 + + +0 + + +[(0.0, 0.0), (0.8153153153153312, 55.0)] + + + + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 346.0, 202.5) + + +0 + + +0 + + +[(0.0, 0.0), (0.5, 55.0)] + + + + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 133.5, 273.5) + + +1 + + +0 + + +[(0.0, 0.0), (0.0, 86.0), (213.5, 86.0), (213.5, 0.0), (213.5, 0.0)] + + + + + + + + + + +(1.0, 0.0, 0.0, 1.0, 54.0, 398.0) + + +126.0 + + +142.0 + + +1 + + +1 + + + + + + + + + +* { + background-color: transparent; + color: black; + font-family: sans; + font-size: 14; + highlight-color: rgba(0, 0, 255, 0.4); + line-width: 2; + padding: 0; +} + +diagram { + background-color: white; + line-style: normal; + /* line-style: sloppy 0.3; */ +} + + + + + +Car + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +rear: Wheel[2] + + + + + + + + + +composite + + + + + + + + + + +: Engine + + + + + + + + + + + + + + + + + + +axle + + + + + + + + + + + + + + + + + + +axle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index a7c4de7..c0912b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,14 @@ import shutil import sys +# Force a headless matplotlib backend before sphinx-needs (or any other +# extension) imports matplotlib. With Gaphor installed, PyGObject makes +# matplotlib auto-pick "gtk4agg", which then hangs the build trying to open +# a GDK surface in headless / RTD environments. +import matplotlib + +matplotlib.use("Agg") + import jinja2 # We need to make Python aware of our project source code, which is stored outside `/docs`, @@ -39,8 +47,19 @@ "sphinx_preview", "sphinx_design", "ubt_sphinx", + "gaphor.extensions.sphinx", # SysML / UML diagrams from .gaphor model files ] +# Map a logical model name to a .gaphor file path (relative to docs/). +# The ``.. diagram::`` directive looks up models by name from this dict; +# ``:model: `` selects which file to load. The ``default`` entry is +# used when ``:model:`` is omitted. +gaphor_models = { + "default": "automotive-adas/sysml/adas-tsr-bdd.gaphor", + "tsr": "automotive-adas/sysml/adas-tsr-bdd.gaphor", + "car": "automotive-adas/sysml/sysml-car.gaphor", +} + ubtrace_organization = "useblocks" ubtrace_project = "sphinx-needs-demo" ubtrace_version = "main" diff --git a/pyproject.toml b/pyproject.toml index 542d170..eb77953 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "sphinx-test-reports>=1.3.2", # pin as RTD consumes this file, not uv.lock "furo>=2024.8.6", "sphinx-preview>=0.1.2", - "ubt-sphinx==0.7.1" + "ubt-sphinx==0.7.1", + "gaphor>=3.3.2", ] readme = "README.md" requires-python = ">= 3.12" diff --git a/uv.lock b/uv.lock index 6bd4dec..7654918 100644 --- a/uv.lock +++ b/uv.lock @@ -67,6 +67,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "better-exceptions" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/d8/30b745b965765c08ee132fd590fca46c31296e8f1a606de0c53cc6b5a68f/better_exceptions-0.3.3.tar.gz", hash = "sha256:e4e6bc18444d5f04e6e894b10381e5e921d3d544240418162c7db57e9eb3453b", size = 30156, upload-time = "2021-01-29T16:48:54.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/50/abf6850135f1e95d321a525d0a36e05255a039b3fc118b7d88413e8a8207/better_exceptions-0.3.3-py3-none-any.whl", hash = "sha256:9c70b1c61d5a179b84cd2c9d62c3324b667d74286207343645ed4306fdaad976", size = 11857, upload-time = "2021-01-29T16:48:53.642Z" }, +] + [[package]] name = "brotli" version = "1.2.0" @@ -449,6 +461,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "dulwich" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/0f/46df53e30b03cc8fee9d1bbd7ca624b4d1b579ce2e4efeaa1cb712d119b0/dulwich-1.2.1.tar.gz", hash = "sha256:ba43bfb3a7cad40d9607170561e8c3be42e7083b4b57af89a5f54e01577ff791", size = 1223320, upload-time = "2026-04-29T15:12:19.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c7/a9417106607171af98fe4fedc03693fa406fe74e3eec6b73438c1621dec4/dulwich-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:23560643c1cc85737c87985761666a59b35b06a1cbb06db5ba642fa35c67be93", size = 1328929, upload-time = "2026-04-29T15:11:28.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/b8/83c156e349656de8a7bbc504d94fd318a847fce575caa33b27ffabad7b52/dulwich-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da2fdcc9e4f178b2b4ec1194d8e17d111d572f111fdaa7c6c5a3f46bbe686eaf", size = 1313906, upload-time = "2026-04-29T15:11:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/03/db/b2706c46f0776a838d13850a70426b1d72268a937cab783c88cbf726c0ba/dulwich-1.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d0ddda4e165ea14c70e1d0531aa97e527332f1133ea0c65eebaa5afb8786029e", size = 1396200, upload-time = "2026-04-29T15:11:32.166Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/17e1c3e6846c0eb31da3e5d5f876a91fabe0fa47fc8033d4780b30101723/dulwich-1.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9d0ea88273a7ee6fd3b1d75e231cc6fc614774e19bdd7c1de5df274b4b492dde", size = 1414087, upload-time = "2026-04-29T15:11:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/64/95/bbafb223fe9e26633f6c143e93d52ae849fe0a246716e0d1ce573be46dd4/dulwich-1.2.1-cp312-cp312-win32.whl", hash = "sha256:5e875729df04991732959f52e29c1de96d00eaf62feddfc60f0b2b08c6e38870", size = 998474, upload-time = "2026-04-29T15:11:35.664Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9c/34b5d72a1b411d5433a403be2885e87113a122f69a2dcdb4100a298e4fb5/dulwich-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6349b475a1b17b224191a4801687a2486574c9526e00835db2ba9ed081124b11", size = 1013707, upload-time = "2026-04-29T15:11:37.207Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/c842f2d092a277fee3e465403d525ccff8c2a01862a58431ac45729d0675/dulwich-1.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3a95dd2649ef3c6e59095d5bd3de08470b3ba908dc41343f5823666a10a326f8", size = 1464426, upload-time = "2026-04-29T15:11:39.219Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3c/500e3aace2c2c20cbd317218053bbc368ab3e44e183d5eb03038a9fcabe1/dulwich-1.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:11a51dba8454b4c64bf242a918ca4c4300097f7cec84a13164845e638a26f9de", size = 1451868, upload-time = "2026-04-29T15:11:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ed/257cedc41ef184c1ac0591bc03009ed4afc1fad4f06c5609cf5d34e9bbe7/dulwich-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:121d2d181407cd7bb051922dc3bc00841ca0959b8299880529525db93dfbdca9", size = 1327229, upload-time = "2026-04-29T15:11:42.475Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2a/520f8aceab83cd54d529c9735d8a091d832da5090716537110642cd55510/dulwich-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96846ef600378739a64e347412c22d5f87f1b67d68526c633b465589335e9387", size = 1312813, upload-time = "2026-04-29T15:11:44.034Z" }, + { url = "https://files.pythonhosted.org/packages/08/04/f28a380f1aaef5fecf9fb6b4dd261eeb988ab1543211494f647845a7de75/dulwich-1.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3412eca24fd79d5413ac7003f8f29b2e01008d7c9d6fce159f77602f70058aea", size = 1396372, upload-time = "2026-04-29T15:11:45.697Z" }, + { url = "https://files.pythonhosted.org/packages/c6/16/2eac51723d07eb1e4077f2bf0a6034af415bb82be45f4987120fa95de84d/dulwich-1.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0b1181f4ae225bbde373fc16279c4e61fdf04aedd30ec21fab387be642567453", size = 1413924, upload-time = "2026-04-29T15:11:47.302Z" }, + { url = "https://files.pythonhosted.org/packages/a2/14/e0ebb351b1b293f0824150d949ffaab8d15a0e89feb08ec2907d2bf59f1c/dulwich-1.2.1-cp313-cp313-win32.whl", hash = "sha256:e2f14d20aea48dd1d48714d637c70399b140825d930b4f5aa6fbb62199429740", size = 998185, upload-time = "2026-04-29T15:11:49.285Z" }, + { url = "https://files.pythonhosted.org/packages/de/f4/b17cd27f3cefb8062dc08313d96ee1859673c1b3ec6b2fd2f6528a04dba3/dulwich-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:bad94416de9a76ad36dc19f0e65a829b89fc845282bdcc8c43285a4addc1ad2e", size = 1013429, upload-time = "2026-04-29T15:11:51.146Z" }, + { url = "https://files.pythonhosted.org/packages/0d/39/39515e92676caea97cb2bfa63e37cf9cc579d10421243aa087d0a1d4e67e/dulwich-1.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:74b01505b8e2ba81a715213101c790e0794f5c0b8021f8ce1100131a1e8cfa55", size = 1463937, upload-time = "2026-04-29T15:11:52.682Z" }, + { url = "https://files.pythonhosted.org/packages/5a/62/30abf318e048f053a5609227952be0773e76203618197e530a4453da161b/dulwich-1.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:011d37afa0922b500c938d6417317e3e6d29d33a6fbaf12b3696fe2a216aa170", size = 1450968, upload-time = "2026-04-29T15:11:54.577Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/de61a118b2df06ba9b87d1751cfa2d2110a0b596a1e46eaed4a18d9c72c7/dulwich-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:96c160d68173ae55f68bf734e7fddae04ea025f3541996b0b5f28dc2025b42dc", size = 1328562, upload-time = "2026-04-29T15:11:56.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3f/bbb212960d8f1419ba2b4bb5c9d5b4745c4cd9d21f4fb0b58abf302b1e51/dulwich-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a0245dccd69fba9dfe2ed88eff9212bad3e04107bc41676fffbc7f706428cdb", size = 1313625, upload-time = "2026-04-29T15:11:57.982Z" }, + { url = "https://files.pythonhosted.org/packages/25/af/0c9612199d33a22499be405b610e3a4215183e041ce066ec0cc21ebf017c/dulwich-1.2.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:674b21a9025c00a00495fa03a9bb5f2da67a13ee513c74f16bd91c56678d0a77", size = 1396126, upload-time = "2026-04-29T15:11:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/de/d3/a1217ebacff0d57a7da39f56d31ef7efd076890eb89ef4d23016e8cf8dda/dulwich-1.2.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:fd774825510350b614121bfa84d95c6eb08f7e93c0fe7faf8760423a43dbad8b", size = 1413818, upload-time = "2026-04-29T15:12:01.215Z" }, + { url = "https://files.pythonhosted.org/packages/0b/61/aa3ace71b694b3aff9da14205881760856f00bad189af12530b0c7ebfba7/dulwich-1.2.1-cp314-cp314-win32.whl", hash = "sha256:cf3aa983ca4907c96a0b98b357a3cac7381192786495522a65944da1b284bc02", size = 1005464, upload-time = "2026-04-29T15:12:02.93Z" }, + { url = "https://files.pythonhosted.org/packages/10/b9/69e3efa49603664f4a84d19847c999f9afb60f8b77889aa9ce189f3c94d0/dulwich-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:fb855620d04d0c286058db941c106cfc60bc1e4882ea20b6dd499f3668a1afbe", size = 1021562, upload-time = "2026-04-29T15:12:04.747Z" }, + { url = "https://files.pythonhosted.org/packages/c1/97/73f1f90d0b1361cb05498838736eed253b8cb84bcdc302abd1662a74a907/dulwich-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f944b742962d9933e60863f577491743328b287251ae57a1e7cd84c289acfc23", size = 1327227, upload-time = "2026-04-29T15:12:07.309Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/1c3eb585deedd6c3f7ab0286b6d1d2e6404abfd8f8bcc62a371532a55997/dulwich-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4198f79ff04b07f6f6dc81047041f341b35e774cf0207f496b35f34dd2e1bb9c", size = 1312021, upload-time = "2026-04-29T15:12:09.507Z" }, + { url = "https://files.pythonhosted.org/packages/31/59/2e5405851a0bdfe6f6ea947210ddb6f0784727fa6126dd27152efc1d8b32/dulwich-1.2.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41dfe1b1cb0f0202102e90f9f47f54458f27da6b746939a6ed85362b68e3e3a5", size = 1393001, upload-time = "2026-04-29T15:12:11.165Z" }, + { url = "https://files.pythonhosted.org/packages/3f/15/78d2fefa8bf05168b11c61903d9c8eff10844b1d5cd0d9511d44e58599c2/dulwich-1.2.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a65527eda5f6a463168c6338d781b05d325ba85dc05c6f8217ddd8454911bb53", size = 1414765, upload-time = "2026-04-29T15:12:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/11/4a/d9a45e4ab08088053d6571e80e27e1b18fb2d4213d34e6e231e2840441ec/dulwich-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:04215befa00c82accda97d0e3ce760d9344a67f081d5b92ade8ad00007fb38d8", size = 1003379, upload-time = "2026-04-29T15:12:14.765Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/8f5b9f142cabc47f881155b33330110290e501ffcae85745b01ce19b56dd/dulwich-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3183b7db09282842459fbeb37e77e498c8339e6db4d0cb6b5d3685c97ce79dac", size = 1020397, upload-time = "2026-04-29T15:12:16.373Z" }, + { url = "https://files.pythonhosted.org/packages/08/55/f5470846eb8bdbf204f1c1b47c3d25f542fe4a0134f027c672498fb6e0d3/dulwich-1.2.1-py3-none-any.whl", hash = "sha256:1961e0b6c0b1f2920f4ab05821652d8eb12f19ddb5a4c167c385902391c08dd3", size = 674611, upload-time = "2026-04-29T15:12:18.177Z" }, +] + [[package]] name = "execnet" version = "2.1.2" @@ -522,6 +574,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, ] +[[package]] +name = "gaphas" +version = "5.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycairo" }, + { name = "pygobject" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/cf/2699e0beff737dd2d51e5390cdf44cf243ae2d75130ffa5029102e6f3995/gaphas-5.1.2.tar.gz", hash = "sha256:5dd6b05931f3856a96b623269b50187231b4abf7b9849f48f7eec52898563696", size = 59989, upload-time = "2025-12-27T12:06:09.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/c2/03e1d69be2fe316250a3e2116a9ff811313b074d069162da9cbff5026c62/gaphas-5.1.2-py3-none-any.whl", hash = "sha256:e2f5fcbab3d8278eacfb13529551608ecfdc45ac580fd8f22dff5964efb5b1d0", size = 73708, upload-time = "2025-12-27T12:06:07.629Z" }, +] + +[[package]] +name = "gaphor" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "better-exceptions" }, + { name = "defusedxml" }, + { name = "dulwich" }, + { name = "gaphas" }, + { name = "generic" }, + { name = "jedi" }, + { name = "pillow" }, + { name = "pycairo" }, + { name = "pydot" }, + { name = "pygobject" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "tinycss2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/9f/33c39e6c936a167a299f99a1ac87b0a487974ffd8b8eb33398e20b5566f9/gaphor-3.3.2.tar.gz", hash = "sha256:dc4cad4df94e861584923091e5eab45fc13e4c84d3bd0bd8606b0f2b3ba2bfba", size = 800828, upload-time = "2026-05-02T19:28:42.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/44/8d54ef9e7674b91d3d41ed8e9ab2d7a2d5ef7d7f72d3a2120bb6f428b508/gaphor-3.3.2-py3-none-any.whl", hash = "sha256:6a8a84bbce429677af664c9857d9433ea66e782015d7a53f679f7e7c4bf208d0", size = 1070767, upload-time = "2026-05-02T19:28:40.014Z" }, +] + +[[package]] +name = "generic" +version = "1.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/a6/b1d3ed364b780137f1fc5e85737f42d2d8c6fda5b254c03cb916b7b7715e/generic-1.1.7.tar.gz", hash = "sha256:d14dae6628542eb084434588d350756c88f2f30c7e2348ad5fcfa01fbdb7ceed", size = 8225, upload-time = "2026-03-09T14:39:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/6d/459ded13938c2f2dadf71e107f78001d30b8466fda3acac27a55934b246e/generic-1.1.7-py3-none-any.whl", hash = "sha256:9048a52debd50e6d7671bb44826567a9c81d2e8302253179adfb8054ab415077", size = 9815, upload-time = "2026-03-09T14:39:16.301Z" }, +] + [[package]] name = "gitignore-parser" version = "0.1.13" @@ -564,6 +662,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1024,6 +1134,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -1158,6 +1277,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "pycairo" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz", hash = "sha256:f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142", size = 665871, upload-time = "2025-11-11T19:13:01.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/28/6363087b9e60af031398a6ee5c248639eefc6cc742884fa2789411b1f73b/pycairo-1.29.0-cp312-cp312-win32.whl", hash = "sha256:91bcd7b5835764c616a615d9948a9afea29237b34d2ed013526807c3d79bb1d0", size = 751486, upload-time = "2025-11-11T19:11:54.451Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d2/d146f1dd4ef81007686ac52231dd8f15ad54cf0aa432adaefc825475f286/pycairo-1.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f01c3b5e49ef9411fff6bc7db1e765f542dc1c9cfed4542958a5afa3a8b8e76", size = 845383, upload-time = "2025-11-11T19:12:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/6e6f33bb79ec4a527c9e633915c16dc55a60be26b31118dbd0d5859e8c51/pycairo-1.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:eafe3d2076f3533535ad4a361fa0754e0ee66b90e548a3a0f558fed00b1248f2", size = 694518, upload-time = "2025-11-11T19:12:06.561Z" }, + { url = "https://files.pythonhosted.org/packages/f0/21/3f477dc318dd4e84a5ae6301e67284199d7e5a2384f3063714041086b65d/pycairo-1.29.0-cp313-cp313-win32.whl", hash = "sha256:3eb382a4141591807073274522f7aecab9e8fa2f14feafd11ac03a13a58141d7", size = 750949, upload-time = "2025-11-11T19:12:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:91114e4b3fbf4287c2b0788f83e1f566ce031bda49cf1c3c3c19c3e986e95c38", size = 844149, upload-time = "2025-11-11T19:12:19.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/43/e782131e23df69e5c8e631a016ed84f94bbc4981bf6411079f57af730a23/pycairo-1.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:09b7f69a5ff6881e151354ea092137b97b0b1f0b2ab4eb81c92a02cc4a08e335", size = 693595, upload-time = "2025-11-11T19:12:23.445Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fa/87eaeeb9d53344c769839d7b2854db7ff2cd596211e00dd1b702eeb1838f/pycairo-1.29.0-cp314-cp314-win32.whl", hash = "sha256:69e2a7968a3fbb839736257bae153f547bca787113cc8d21e9e08ca4526e0b6b", size = 767198, upload-time = "2025-11-11T19:12:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/3c/90/3564d0f64d0a00926ab863dc3c4a129b1065133128e96900772e1c4421f8/pycairo-1.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:e91243437a21cc4c67c401eff4433eadc45745275fa3ade1a0d877e50ffb90da", size = 871579, upload-time = "2025-11-11T19:12:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/5e/91/93632b6ba12ad69c61991e3208bde88486fdfc152be8cfdd13444e9bc650/pycairo-1.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:b72200ea0e5f73ae4c788cd2028a750062221385eb0e6d8f1ecc714d0b4fdf82", size = 719537, upload-time = "2025-11-11T19:12:55.016Z" }, + { url = "https://files.pythonhosted.org/packages/93/23/37053c039f8d3b9b5017af9bc64d27b680c48a898d48b72e6d6583cf0155/pycairo-1.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5e45fce6185f553e79e4ef1722b8e98e6cde9900dbc48cb2637a9ccba86f627a", size = 874015, upload-time = "2025-11-11T19:12:28.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/54/123f6239685f5f3f2edc123f1e38d2eefacebee18cf3c532d2f4bd51d0ef/pycairo-1.29.0-cp314-cp314t-win_arm64.whl", hash = "sha256:caba0837a4b40d47c8dfb0f24cccc12c7831e3dd450837f2a356c75f21ce5a15", size = 721404, upload-time = "2025-11-11T19:12:36.919Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1167,6 +1305,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydot" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, +] + [[package]] name = "pydyf" version = "0.12.1" @@ -1185,6 +1335,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pygobject" +version = "3.56.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycairo" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/80/09247a2be28af2c2240132a0af6c1005a2b1d089242b13a2cd782d2de8d7/pygobject-3.56.2.tar.gz", hash = "sha256:b816098969544081de9eecedb94ad6ac59c77e4d571fe7051f18bebcec074313", size = 1409059, upload-time = "2026-03-25T16:14:04.008Z" } + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1671,6 +1859,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "furo" }, + { name = "gaphor" }, { name = "sphinx", extra = ["test"] }, { name = "sphinx-codelinks" }, { name = "sphinx-design" }, @@ -1685,6 +1874,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "furo", specifier = ">=2024.8.6" }, + { name = "gaphor", specifier = ">=3.3.2" }, { name = "sphinx", extras = ["test"], specifier = ">=8.2.3" }, { name = "sphinx-codelinks", specifier = ">=1.2.0" }, { name = "sphinx-design", specifier = ">=0.6.1" }, From cb96b170e759debb6a5970769c806cc65dda1748 Mon Sep 17 00:00:00 2001 From: Marco Heinemann Date: Tue, 5 May 2026 12:25:06 +0200 Subject: [PATCH 2/2] Pre-render SysML SVGs; drop Cairo / Gaphor build dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two review issues: 1. Cairo / PyGObject build failure on Read-the-Docs. Gaphor's Sphinx extension needs Cairo + GTK girepository at build time, which require a C toolchain. They are now decoupled from the doc build: - ``gaphor`` moves to a ``[render]`` optional extra; the doc build only sees the ordinary Sphinx + sphinx-needs stack. - ``gaphor.extensions.sphinx`` is dropped from ``conf.py``; the ``gaphor_models`` mapping is removed. - The Gaphor-related ``apt_packages`` are removed from ``.readthedocs.yaml``. - SysML diagrams are now ordinary committed SVGs embedded with ``.. figure::``. 2. BDD layout looked broken. The previous Python authoring placed ``PropertyItem`` parts at hard-coded offsets that overlapped the parent block's name and ``ยซblockยป`` stereotype. The script now sets ``BlockItem.show_parts = 1`` so Gaphor renders parts inside the parent's "parts" compartment, and arranges the parent + three sub-blocks on a clean two-row grid. The committed ``adas-tsr-bdd__tsr-block-definition.svg`` reflects the new layout. New rendering workflow (see ``docs/automotive-adas/sysml/index.rst``): uv sync --extra render uv run python docs/automotive-adas/sysml/render_sysml.py The render script walks every ``.gaphor`` file in the directory and writes one SVG per diagram. Run on a developer machine that has Cairo / GTK system libraries; commit the regenerated SVGs. CI / RTD do not run this step. The matplotlib ``Agg`` backend pin in ``conf.py`` stays as a defensive guard for developers who install the ``[render]`` extra in the same venv they build docs from โ€” without it, the presence of PyGObject flips matplotlib to the GTK4 backend and the build hangs at "writing output... [4%]". For real SysML 1.5 XMI files from Cameo / Papyrus / MagicDraw / Rhapsody, the recommended path is now to export individual diagrams to SVG from the source tool (all of them support this out of the box), commit the SVG, and reference it the same way as the Gaphor-rendered example. No ``.gaphor`` conversion needed. --- .readthedocs.yaml | 7 - docs/automotive-adas/sys_3_sys_arch.rst | 9 +- .../automotive-adas/sysml/adas-tsr-bdd.gaphor | 205 +++------ .../adas-tsr-bdd__tsr-block-definition.svg | 431 ++++++++++++++++++ docs/automotive-adas/sysml/author_adas_bdd.py | 57 +-- docs/automotive-adas/sysml/index.rst | 199 ++++---- docs/automotive-adas/sysml/render_sysml.py | 63 +++ .../automotive-adas/sysml/sysml-car__main.svg | 232 ++++++++++ docs/conf.py | 22 +- pyproject.toml | 16 +- uv.lock | 9 +- 11 files changed, 948 insertions(+), 302 deletions(-) create mode 100644 docs/automotive-adas/sysml/adas-tsr-bdd__tsr-block-definition.svg create mode 100644 docs/automotive-adas/sysml/render_sysml.py create mode 100644 docs/automotive-adas/sysml/sysml-car__main.svg diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 36836f4..1521c6e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,13 +11,6 @@ build: apt_packages: - default-jdk - graphviz - # Required by Gaphor (PyGObject + Cairo) for the .. diagram:: directive. - # See https://docs.gaphor.org/en/latest/sphinx.html#read-the-docs - - libgirepository-2.0-dev - - libcairo2-dev - - pkg-config - - python3-dev - - gir1.2-pango-1.0 tools: python: "3.12" # You can also specify other tool versions: diff --git a/docs/automotive-adas/sys_3_sys_arch.rst b/docs/automotive-adas/sys_3_sys_arch.rst index 8781079..2985a7e 100644 --- a/docs/automotive-adas/sys_3_sys_arch.rst +++ b/docs/automotive-adas/sys_3_sys_arch.rst @@ -208,9 +208,9 @@ SYS.3 Architecture Design capture, sign classification, and distribution of detected speed limits to vehicle control functions. The component view (PlantUML) and the SysML Block Definition Diagram below - describe the same architecture from two notations โ€” the Gaphor-rendered BDD - is sourced from a ``.gaphor`` model file and can be opened in the Gaphor - GUI for further editing. + describe the same architecture from two notations. The BDD is a static + SVG pre-rendered from a ``.gaphor`` model โ€” see :ref:`SysML_Demo` for + the workflow. .. uml:: @@ -235,8 +235,7 @@ SYS.3 Architecture Design SignInterpreter --> VehicleControl @enduml - .. diagram:: TSR Block Definition - :model: tsr + .. figure:: sysml/adas-tsr-bdd__tsr-block-definition.svg :align: center :alt: SysML Block Definition Diagram for the Traffic Sign Recognition system diff --git a/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor b/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor index 21d7540..a0f21f2 100644 --- a/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor +++ b/docs/automotive-adas/sysml/adas-tsr-bdd.gaphor @@ -1,262 +1,187 @@ - + ADAS - + - - - - + + + + - - - - + + + + - + TrafficSignRecognition - - - + + + - + - + - + - + FrontCamera - + - + - + - + SignInterpreter - + - + - + - + VehicleControl - + - + - + - + composite camera - - - - - - + - + - + composite interpreter - - - - - - + - + - + composite control - - - - - - + - + - + - + TSR Block Definition - - - - - - - + + + + - + -(1.0, 0.0, 0.0, 1.0, 80.0, 60.0) +(1.0, 0.0, 0.0, 1.0, 200.0, 40.0) (0.0, 0.0) -480.0 +240.0 -220.0 +160.0 - + + +1 + - + - - -(1.0, 0.0, 0.0, 1.0, 100.0, 160.0) - - -(0.0, 0.0) - - -130.0 - - -60.0 - - - - - - - - - - -(1.0, 0.0, 0.0, 1.0, 250.0, 160.0) - - -(0.0, 0.0) - - -130.0 - - -60.0 - - - - - - - - - - -(1.0, 0.0, 0.0, 1.0, 400.0, 160.0) - - -(0.0, 0.0) - - -130.0 - - -60.0 - - - - - - - - - + -(1.0, 0.0, 0.0, 1.0, 80.0, 380.0) +(1.0, 0.0, 0.0, 1.0, 40.0, 280.0) (0.0, 0.0) @@ -268,15 +193,15 @@ 70.0 - + - + - + -(1.0, 0.0, 0.0, 1.0, 280.0, 380.0) +(1.0, 0.0, 0.0, 1.0, 240.0, 280.0) (0.0, 0.0) @@ -288,15 +213,15 @@ 70.0 - + - + - + -(1.0, 0.0, 0.0, 1.0, 480.0, 380.0) +(1.0, 0.0, 0.0, 1.0, 440.0, 280.0) (0.0, 0.0) @@ -308,10 +233,10 @@ 70.0 - + - + diff --git a/docs/automotive-adas/sysml/adas-tsr-bdd__tsr-block-definition.svg b/docs/automotive-adas/sysml/adas-tsr-bdd__tsr-block-definition.svg new file mode 100644 index 0000000..a40696d --- /dev/null +++ b/docs/automotive-adas/sysml/adas-tsr-bdd__tsr-block-definition.svg @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/automotive-adas/sysml/author_adas_bdd.py b/docs/automotive-adas/sysml/author_adas_bdd.py index b6dc4a0..02522f1 100644 --- a/docs/automotive-adas/sysml/author_adas_bdd.py +++ b/docs/automotive-adas/sysml/author_adas_bdd.py @@ -1,16 +1,17 @@ """Programmatically author the ADAS Traffic Sign Recognition BDD. This script regenerates ``adas-tsr-bdd.gaphor`` from scratch using Gaphor's -Python API, so the model can be tracked as code rather than a binary-ish -diagram blob. Run it whenever you change the structure: +Python API. The model can then be opened and tweaked in the Gaphor GUI, and +re-exported to SVG via :mod:`render_sysml`. Re-run after structural changes: .. code:: console uv run python docs/automotive-adas/sysml/author_adas_bdd.py + uv run python docs/automotive-adas/sysml/render_sysml.py -The generated file is committed alongside this script and consumed by -Sphinx via the ``gaphor.extensions.sphinx`` extension and the -``gaphor_models`` mapping in ``docs/conf.py``. +The generated file is committed alongside the rendered SVG; the doc build +itself does **not** depend on Gaphor or Cairo (it just embeds the SVG via +``.. figure::``). """ from __future__ import annotations @@ -20,7 +21,7 @@ from gaphor.core.modeling import ElementFactory from gaphor.storage import storage from gaphor.SysML import sysml -from gaphor.SysML.blocks import BlockItem, PropertyItem +from gaphor.SysML.blocks import BlockItem from gaphor.UML import uml OUTPUT = Path(__file__).with_name("adas-tsr-bdd.gaphor") @@ -42,6 +43,15 @@ def _make_part(ef: ElementFactory, parent, name: str, type_block): return prop +def _place_block(diag, block, x: float, y: float, width: float = 180, height: float = 80, *, show_parts: bool = False): + item = diag.create(BlockItem, subject=block) + item.matrix.translate(x, y) + item.width = width + item.height = height + item.show_parts = 1 if show_parts else 0 + return item + + def build() -> ElementFactory: ef = ElementFactory() @@ -53,33 +63,28 @@ def build() -> ElementFactory: interpreter = _make_block(ef, pkg, "SignInterpreter") veh_ctl = _make_block(ef, pkg, "VehicleControl") - p_cam = _make_part(ef, tsr, "camera", cam) - p_int = _make_part(ef, tsr, "interpreter", interpreter) - p_veh = _make_part(ef, tsr, "control", veh_ctl) + # Parts on the parent block (rendered automatically by Gaphor inside a + # "parts" compartment when ``show_parts`` is on). + _make_part(ef, tsr, "camera", cam) + _make_part(ef, tsr, "interpreter", interpreter) + _make_part(ef, tsr, "control", veh_ctl) diag = ef.create(sysml.BlockDefinitionDiagram) diag.name = "TSR Block Definition" diag.element = pkg - # Parent block with parts compartment - tsr_item = diag.create(BlockItem, subject=tsr) - tsr_item.matrix.translate(80, 60) - tsr_item.width = 480 - tsr_item.height = 220 - - for i, prop in enumerate((p_cam, p_int, p_veh)): - item = diag.create(PropertyItem, subject=prop) - col_x = 100 + i * 150 - item.matrix.translate(col_x, 160) - item.width = 130 - item.height = 60 + # Parent block at top centre, parts compartment turned on so the named + # parts render inside the block instead of as separate items that would + # otherwise overlap the parent label. + _place_block(diag, tsr, x=200, y=40, width=240, height=160, show_parts=True) - # Sub-block definitions below (typical BDD layout) + # Sub-block definitions in a row below. + spacing = 40 + sub_w, sub_h = 160, 70 + row_y = 280 + start_x = 200 + (240 - (3 * sub_w + 2 * spacing)) / 2 for i, sub in enumerate((cam, interpreter, veh_ctl)): - sub_item = diag.create(BlockItem, subject=sub) - sub_item.matrix.translate(80 + i * 200, 380) - sub_item.width = 160 - sub_item.height = 70 + _place_block(diag, sub, x=start_x + i * (sub_w + spacing), y=row_y, width=sub_w, height=sub_h) return ef diff --git a/docs/automotive-adas/sysml/index.rst b/docs/automotive-adas/sysml/index.rst index 6dd483e..bbd1dc2 100644 --- a/docs/automotive-adas/sysml/index.rst +++ b/docs/automotive-adas/sysml/index.rst @@ -3,118 +3,103 @@ .. _SysML_Demo: -๐Ÿงฉ SysML diagrams via Gaphor -============================= +๐Ÿงฉ SysML diagrams alongside sphinx-needs items +=============================================== -This page demonstrates how `Gaphor `__ models can be -embedded inside Sphinx-Needs alongside the existing ``.. uml::`` PlantUML -directives. Gaphor implements a large part of the OMG **SysML 1.6** -specification (a minor revision of SysML 1.5 that retains the same core -constructs โ€” Block, Property, Port, Connector, Requirement). For a customer -showcase that says "SysML 1.5", Gaphor is the same modeling family. +This page demonstrates how SysML diagrams can be embedded inside +Sphinx-Needs items the same way PlantUML diagrams already are. The doc +build itself stays free of any modeling-tool / Cairo / GTK +dependencies โ€” diagrams are **pre-rendered to SVG** and committed into +the repo, then included via plain ``.. figure::`` directives. -How it works ------------- +The example model +----------------- -Three pieces are needed: +The Block Definition Diagram below is rendered from +``adas-tsr-bdd.gaphor`` and shows the same Traffic Sign Recognition +architecture that appears in :need:`ARCH_007` as a PlantUML component +view. It is committed as ``adas-tsr-bdd__tsr-block-definition.svg``. -1. **A** ``.gaphor`` **model file** โ€” an XML document that holds blocks, - properties, ports, diagrams and their layout. It can be authored in the - Gaphor desktop GUI or programmatically through the Gaphor Python API - (see :file:`author_adas_bdd.py` for an example). -2. **The** ``gaphor.extensions.sphinx`` **extension** โ€” registers a - ``.. diagram::`` directive and renders the chosen diagram to SVG/PDF - on every Sphinx build. -3. **A** ``gaphor_models`` **mapping in** :file:`conf.py` โ€” maps logical - model names (``"tsr"``, ``"car"``, ...) to ``.gaphor`` files relative - to ``docs/``. - -Example: under :need:`ARCH_007` the BDD lives next to a PlantUML view of the -same architecture and is rendered from the model file by: - -.. code:: rst - - .. diagram:: TSR Block Definition - :model: tsr - :align: center - -The directive accepts the standard ``image``/``figure`` options -(``:align:``, ``:figwidth:``, ``:alt:``, plus a caption block). +.. figure:: adas-tsr-bdd__tsr-block-definition.svg + :align: center + :alt: SysML BDD: TrafficSignRecognition with FrontCamera, SignInterpreter, VehicleControl -Using a stock Gaphor SysML example ----------------------------------- + ``TrafficSignRecognition`` (top, with parts compartment) decomposed + into ``FrontCamera``, ``SignInterpreter`` and ``VehicleControl`` + (bottom row) โ€” pre-rendered SVG, no build-time tooling. -The ``car`` model below is the un-modified ``sysml-car.gaphor`` example -from the Gaphor distribution, demonstrating that an existing ``.gaphor`` -file can be dropped into the docs tree and referenced verbatim. +A second example uses Gaphor's stock SysML car BDD, dropped into the +docs tree unchanged: -.. diagram:: main - :model: car +.. figure:: sysml-car__main.svg :align: center - :alt: SysML Block Definition Diagram of a Car - - ``Car`` block from Gaphor's ``examples/sysml-car.gaphor``, decomposed - into ``rear: Wheel[2]``, ``: Engine`` and two ``axle`` proxy ports. - -Programmatic authoring ----------------------- - -For repeatable, code-reviewable models we generate ``adas-tsr-bdd.gaphor`` -from a small Python script using Gaphor's API: - -.. code:: console - - uv run python docs/automotive-adas/sysml/author_adas_bdd.py - -The script creates a ``Package``, four ``Block`` elements, three -composite ``Property`` parts and a ``BlockDefinitionDiagram`` with the -items laid out, then ``storage.save()`` writes the XML. Re-running the -script regenerates the file deterministically. - -Converting existing SysML 1.5 XMI ---------------------------------- - -A common customer ask is *"we already have SysML 1.5 XMI files from -Cameo/Papyrus/MagicDraw โ€” can we just drop them in?"*. Today the -honest answer is **no, not directly**: - -- Gaphor's own XMI export was removed in `Gaphor 3.1.0 - `__ after the maintainers - declared it under-tested and unmaintained. -- The ``.gaphor`` format is **not** XMI. It is a Gaphor-native XML schema - (root ```` namespaced under - ``https://gaphor.org/model``) that mixes semantic elements - (````, ````, ````, ...) with - per-diagram presentation items (```` carrying a - transformation matrix, width and height). -- Conversion therefore requires a custom XMI โ†’ ``.gaphor`` translator - that walks the source tree, generates UUIDs, maps element types and - rebuilds layout. There is no off-the-shelf tool today. - -The pragmatic options for a project that needs SysML 1.5 content in the -docs are: - -1. **Re-author** the diagrams in Gaphor (GUI) or via the Python API - shown above โ€” fastest for a small, curated set of system views that - matter for the documentation. -2. **Write a converter** โ€” feasible for a constrained SysML 1.5 - subset (Blocks, Properties, BDDs, simple Connectors). The ``.gaphor`` - file format is documented at - https://docs.gaphor.org/en/latest/storage.html and only ~10 element - types are needed for a typical BDD. -3. **Keep the original tool** for full-fidelity authoring and only - import a curated subset into Gaphor for publication. - -Limitations to be aware of --------------------------- - -- The Gaphor Sphinx extension renders SVG **and** PDF for each diagram. - Rendering uses Cairo via PyGObject, so the build host needs the GTK - girepository / Cairo system packages. See ``.readthedocs.yaml`` for - the apt list. -- With Gaphor installed, matplotlib auto-selects the ``gtk4agg`` - backend, which hangs the build on a headless host. ``conf.py`` pins - ``matplotlib.use("Agg")`` before any extension imports it. -- ``gaphor`` is a heavy dependency (it pulls in ``pycairo`` and - ``PyGObject``). If only a few pages need it, consider extracting the - SysML pages into an opt-in toctree. + :alt: SysML BDD of a Car with Wheel and Engine parts and axle ports + + ``Car`` block with ``rear: Wheel[2]``, ``: Engine`` parts and two + ``axle`` proxy ports โ€” Gaphor's ``examples/sysml-car.gaphor`` + committed as-is. + +Workflow +-------- + +The recommended workflow has two stages โ€” **author once / re-render** +(needs Gaphor + Cairo) and **build docs** (no extra deps): + +1. **Edit the model.** Author / tweak the diagram in the Gaphor desktop + GUI (https://gaphor.org/), or programmatically via Gaphor's Python + API. The script :file:`author_adas_bdd.py` in this directory shows + the API path for the TSR BDD. + +2. **Re-render to SVG** on a developer machine that has the Cairo / + GTK system libraries installed: + + .. code:: console + + uv sync --extra render + uv run python docs/automotive-adas/sysml/render_sysml.py + + Every ``.gaphor`` file under this directory gets rendered into one + SVG per diagram, named ``__.svg``. + +3. **Commit** the regenerated SVG (and the ``.gaphor`` source). + +4. **Build docs** with the normal toolchain โ€” no Gaphor, no Cairo, no + GTK on Read-the-Docs or in CI. + +Using existing SysML 1.5 XMI +----------------------------- + +A common ask is *"we already have SysML 1.5 XMI files from +Cameo / Papyrus / MagicDraw / Rhapsody โ€” can we drop them in?"* The +practical answer is: **render them once in your existing tool, commit +the resulting SVG, and skip the conversion entirely**. + +- All major SysML authoring tools (Cameo Systems Modeler, Eclipse + Papyrus, IBM Rhapsody, Sparx Enterprise Architect, MagicDraw) export + individual diagrams as SVG or PNG out of the box. +- The ``.gaphor`` format is **not** XMI โ€” it is a Gaphor-native XML + schema with embedded layout. Converting XMI โ†’ ``.gaphor`` requires a + custom translator (Gaphor's own XMI export was removed in 3.1.0, + `gaphor/gaphor#3444 `__). +- Treating the SysML diagram as **just an image asset** sidesteps the + format-translation problem and works for every SysML tool the + customer might already own. + +If a project still wants the ``.gaphor``-as-code path (text diff, easy +PR review, `author_adas_bdd.py` style), Gaphor's GUI also imports a +limited subset of UML XMI 2.5 โ€” see the Gaphor docs for the current +state. + +Trade-offs to be aware of +------------------------- + +- The pre-render step is **manual**: edit a model โ†’ run the render + script โ†’ commit. Mitigations: a ``make render`` target, a CI job + that regenerates SVGs when ``.gaphor`` files change and pushes back, + or a pre-commit hook. +- The diagram is now **a binary-ish asset in the repo** (SVG is text, + but auto-laid-out XML diffs poorly). The ``.gaphor`` source is the + reviewable artifact; the SVG is the build output. +- For a customer with hundreds of SysML 1.5 XMI diagrams, scripting a + bulk export (e.g., a Cameo macro or a Papyrus headless build) is the + next step beyond this prototype. diff --git a/docs/automotive-adas/sysml/render_sysml.py b/docs/automotive-adas/sysml/render_sysml.py new file mode 100644 index 0000000..2a653ef --- /dev/null +++ b/docs/automotive-adas/sysml/render_sysml.py @@ -0,0 +1,63 @@ +"""Render every ``.gaphor`` file in this directory to a sibling ``.svg``. + +This is a one-shot rendering step run from a developer / CI machine that +has Gaphor (and therefore Cairo + PyGObject) installed. The resulting SVG +is committed into the repo and embedded by the docs via plain +``.. figure::`` directives, so the documentation build itself has zero +dependency on Cairo / GTK. + +Usage: + +.. code:: console + + uv run --extra render python docs/automotive-adas/sysml/render_sysml.py + +Each ``.gaphor`` file may contain multiple diagrams. Every diagram is +rendered as ``__.svg``. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +from gaphor.core.modeling import Diagram, ElementFactory +from gaphor.diagram.export import save_svg +from gaphor.services.modelinglanguage import ModelingLanguageService +from gaphor.storage import storage + +HERE = Path(__file__).resolve().parent + + +def _slugify(name: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "diagram" + + +def _render(model_path: Path) -> list[Path]: + ef = ElementFactory() + ml = ModelingLanguageService() + with model_path.open(encoding="utf-8") as fp: + storage.load(fp, ef, ml) + + written: list[Path] = [] + for diagram in ef.select(lambda e: isinstance(e, Diagram)): + out = model_path.with_name(f"{model_path.stem}__{_slugify(diagram.name)}.svg") + save_svg(out, diagram) + written.append(out) + return written + + +def main() -> int: + sources = sorted(HERE.glob("*.gaphor")) + if not sources: + print(f"No .gaphor files found under {HERE}", file=sys.stderr) + return 1 + for src in sources: + for out in _render(src): + print(f"Rendered {src.name} -> {out.name} ({out.stat().st_size} bytes)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/automotive-adas/sysml/sysml-car__main.svg b/docs/automotive-adas/sysml/sysml-car__main.svg new file mode 100644 index 0000000..f1e4c10 --- /dev/null +++ b/docs/automotive-adas/sysml/sysml-car__main.svg @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/conf.py b/docs/conf.py index c0912b9..eea981b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,10 +6,11 @@ import shutil import sys -# Force a headless matplotlib backend before sphinx-needs (or any other -# extension) imports matplotlib. With Gaphor installed, PyGObject makes -# matplotlib auto-pick "gtk4agg", which then hangs the build trying to open -# a GDK surface in headless / RTD environments. +# Defensive: if a developer also has Gaphor (or any other PyGObject-using +# package) installed in the same venv, matplotlib auto-picks the +# "gtk4agg" backend and the build hangs at "writing output... [4%]" +# trying to open a GDK surface on a headless host. Force the headless +# Agg backend before any Sphinx extension imports matplotlib. import matplotlib matplotlib.use("Agg") @@ -47,18 +48,11 @@ "sphinx_preview", "sphinx_design", "ubt_sphinx", - "gaphor.extensions.sphinx", # SysML / UML diagrams from .gaphor model files ] -# Map a logical model name to a .gaphor file path (relative to docs/). -# The ``.. diagram::`` directive looks up models by name from this dict; -# ``:model: `` selects which file to load. The ``default`` entry is -# used when ``:model:`` is omitted. -gaphor_models = { - "default": "automotive-adas/sysml/adas-tsr-bdd.gaphor", - "tsr": "automotive-adas/sysml/adas-tsr-bdd.gaphor", - "car": "automotive-adas/sysml/sysml-car.gaphor", -} +# SysML / Gaphor diagrams are pre-rendered to SVG and embedded with +# plain ``.. figure::`` โ€” see docs/automotive-adas/sysml/index.rst. The +# doc build therefore needs no Gaphor / PyGObject / Cairo runtime. ubtrace_organization = "useblocks" ubtrace_project = "sphinx-needs-demo" diff --git a/pyproject.toml b/pyproject.toml index eb77953..0c79067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,25 @@ dependencies = [ "furo>=2024.8.6", "sphinx-preview>=0.1.2", "ubt-sphinx==0.7.1", - "gaphor>=3.3.2", ] readme = "README.md" requires-python = ">= 3.12" +# Optional extra used only when re-rendering the SysML / Gaphor diagrams to +# SVG. Gaphor pulls in PyGObject + pycairo, which need the Cairo / GTK +# girepository system libraries to build, so we keep them out of the doc +# build path. Run: +# +# uv sync --extra render +# uv run python docs/automotive-adas/sysml/render_sysml.py +# +# on a developer machine that has those libraries installed, then commit +# the regenerated SVGs. +[project.optional-dependencies] +render = [ + "gaphor>=3.3.2", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/uv.lock b/uv.lock index 7654918..9fbefe8 100644 --- a/uv.lock +++ b/uv.lock @@ -1859,7 +1859,6 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "furo" }, - { name = "gaphor" }, { name = "sphinx", extra = ["test"] }, { name = "sphinx-codelinks" }, { name = "sphinx-design" }, @@ -1871,10 +1870,15 @@ dependencies = [ { name = "ubt-sphinx" }, ] +[package.optional-dependencies] +render = [ + { name = "gaphor" }, +] + [package.metadata] requires-dist = [ { name = "furo", specifier = ">=2024.8.6" }, - { name = "gaphor", specifier = ">=3.3.2" }, + { name = "gaphor", marker = "extra == 'render'", specifier = ">=3.3.2" }, { name = "sphinx", extras = ["test"], specifier = ">=8.2.3" }, { name = "sphinx-codelinks", specifier = ">=1.2.0" }, { name = "sphinx-design", specifier = ">=0.6.1" }, @@ -1885,6 +1889,7 @@ requires-dist = [ { name = "sphinxcontrib-plantuml", specifier = ">=0.30" }, { name = "ubt-sphinx", specifier = "==0.7.1" }, ] +provides-extras = ["render"] [[package]] name = "sphinx-preview"