Source code for syside.reqif

  1"""
  2Syside Automator ReqIF import/export.
  3
  4This module converts between ReqIF (Requirements Interchange Format) files
  5and SysMLv2 workspaces. It can be driven from the command line or from
  6Python scripts; both surfaces share the same underlying entry points.
  7
  8Installation
  9------------
 10
 11The public functions and CLI both require the ``[reqif]`` extra which pulls in
 12additional third-party dependencies. Install it with:
 13
 14.. code-block:: shell
 15
 16    pip install 'syside[reqif]'
 17    # or, for uv-managed projects:
 18    uv add 'syside[reqif]'
 19
 20When the extra is missing, the captured ``ImportError`` is exposed as
 21``_missing_extra_error`` for callers building their own CLI.
 22
 23Command-line usage
 24------------------
 25
 26The CLI is reachable both through the top-level ``syside`` console script
 27and as a runnable module. The three are equivalent:
 28
 29.. code-block:: shell
 30
 31    syside reqif <subcommand> [options]
 32    python -m syside reqif <subcommand> [options]
 33    python -m syside.reqif <subcommand> [options]
 34
 35All subcommands operate on the current working directory as the workspace
 36root. The available subcommands are:
 37
 38- ``init`` — write the canonical ``SysideReqIF`` SysMLv2 library into the
 39  workspace so subsequent imports have something to reference. Use
 40  ``--lib-dir`` to override the default :data:`LIBRARY_DIR` location.
 41- ``import <file>`` — convert a ``.reqif`` or ``.reqifz`` file into
 42  SysMLv2 packages under the workspace, performing a fresh build or a
 43  delta update depending on existing content. Attachments are extracted
 44  into ``--attachments-dir`` (default: :data:`ATTACHMENTS_DIR`).
 45- ``lock`` — assign stable UUIDs and last-change timestamps to any
 46  ReqIF-tagged elements that don't yet have them, so subsequent
 47  round-trips preserve identity. ``--source`` stamps the minted identities
 48  with the ReqIF exchange they belong to.
 49- ``link scaffold <file>`` — reconcile a ReqIF file's *type-level*
 50  concepts (spec types, their attribute definitions, those attributes'
 51  datatypes and enum values) with the ``@reqif`` annotations the workspace
 52  already carries for ``--source``, and emit a JSON mapping whose
 53  ``binding`` fields are pre-filled where the workspace already claims the
 54  ReqIF identity and ``null`` where it does not. Writes to stdout unless
 55  ``-o`` names a file.
 56- ``link apply <mapping.json>`` — write, move, or remove ``@reqif``
 57  annotations on your own ontology elements so they carry the identities
 58  the edited mapping assigns. All-or-nothing: every problem is reported
 59  together and nothing is written unless the whole mapping is clean. The
 60  JSON is ephemeral — regenerate it with ``link scaffold`` whenever needed;
 61  the annotations are the durable record.
 62- ``export`` — convert the SysMLv2 models from the workspace into a
 63  ``.reqif`` or ``.reqifz`` file. ``--source`` selects which ReqIF exchange
 64  to export when elements carry one annotation per exchange partner.
 65- ``check reqif <file>`` — validate that a ReqIF file satisfies the
 66  invariants required for round-trippable conversion (relation-group
 67  ownership, no hierarchy cycles, no name shadowing, etc.).
 68- ``check sysml`` — validate the workspace SysMLv2 model (currently
 69  enforces at most one top-level package per ``.sysml`` document, and
 70  that all ReqIF tags are locked).
 71
 72Each subcommand exits ``0`` on success and raises on failure.
 73
 74Library usage (deprecated)
 75--------------------------
 76
 77.. deprecated:: next major version
 78    Every name exported from this module will be removed in the next
 79    major version of Syside; the ReqIF functionality will only be
 80    reachable through the ``syside reqif`` CLI. Reading any of these
 81    names emits a :exc:`DeprecationWarning`.
 82
 83Every CLI subcommand has a public Python counterpart: an options
 84dataclass plus a function named after the subcommand. The functions
 85return the CLI exit code and raise ``ValueError`` on validation
 86failures, and operate on the current working directory (use
 87``os.chdir`` to target a different workspace).
 88
 89.. code-block:: python
 90
 91    from pathlib import Path
 92
 93    from syside.reqif import (
 94        ImportOptions,
 95        CheckReqifOptions,
 96        InitOptions,
 97        check_reqif,
 98        import_,
 99        init,
100    )
101
102    # Initialise the workspace once.
103    init(InitOptions(lib_dir=None))
104
105    # Validate a ReqIF file before importing it.
106    reqif_file = Path("requirements.reqif")
107    check_reqif(CheckReqifOptions(file=reqif_file))
108
109    # Import the file; attachments land in ./attachments_reqif/.
110    import_(ImportOptions(file=reqif_file, attachments_dir=None))
111
112The default-path constants (:data:`LIBRARY_PKG`, :data:`LIBRARY_DIR`,
113:data:`ATTACHMENTS_DIR`) are re-exported here so callers can reference
114the same defaults the CLI uses without reaching into private modules.
115"""
116
117import warnings
118from types import ModuleType
119from typing import TYPE_CHECKING, Any
120
121__unstable__ = True
122
123DEPRECATION_MESSAGE = (
124    "This method/class/attribute will be removed in the next major version "
125    "of Syside. Please use `syside reqif` CLI instead."
126)
127"""Text of the :exc:`DeprecationWarning` every public name here raises."""
128
129if TYPE_CHECKING:
130    # Resolved lazily at run time (see ``__getattr__`` below) so that reading
131    # any of them warns; spelled out here so type checkers still see them.
132    from syside.reqif._cli import (
133        CheckReqifOptions,
134        CheckSysmlOptions,
135        ImportOptions,
136        InitOptions,
137        ExportOptions,
138        LinkApplyOptions,
139        LinkScaffoldOptions,
140        LockOptions,
141        _OptionsBase as OptionsBase,
142    )
143    from syside.reqif._cmd_check import run_reqif as check_reqif
144    from syside.reqif._cmd_check import run_sysml as check_sysml
145    from syside.reqif._cmd_export import run as export
146    from syside.reqif._cmd_import import run as import_
147    from syside.reqif._cmd_init import run as init
148    from syside.reqif._cmd_link import run_apply as link_apply
149    from syside.reqif._cmd_link import run_scaffold as link_scaffold
150    from syside.reqif._cmd_lock import run as lock
151    from syside.reqif._config import ATTACHMENTS_DIR, LIBRARY_DIR, LIBRARY_PKG
152
153# Public name -> (defining module, attribute name). Deliberately not bound
154# into this namespace: the warning has to fire on *access*, and an eager
155# binding would make ``from syside.reqif import export`` a plain global
156# lookup that never reaches ``__getattr__``.
157_DEPRECATED: dict[str, tuple[ModuleType, str]]
158
159_missing_extra_error: BaseException | None
160try:
161    # Import the chain that transitively loads the [reqif]-extra deps
162    # (reqif, markdown-it-py, markdownify) first, so that a missing extra
163    # bails before any partial bindings can land in this namespace.
164    from syside.reqif import _cmd_check as _cmd_check
165    from syside.reqif import _cmd_import as _cmd_import
166    from syside.reqif import _cmd_init as _cmd_init
167    from syside.reqif import _cmd_lock as _cmd_lock
168    from syside.reqif import _cmd_export as _cmd_export
169    from syside.reqif import _cmd_link as _cmd_link
170    from syside.reqif import _cli as _cli
171    from syside.reqif import _config as _config
172except ImportError as e:
173    # Anything rooted in ``syside`` is an internal regression (typo, missing
174    # submodule, etc.) and must surface untouched — the friendly install-hint
175    # path is reserved for failures rooted in the third-party deps the
176    # ``[reqif]`` extra pulls in (reqif, markdown-it-py, markdownify, and
177    # their transitive dependencies). This avoids having to maintain a
178    # hand-rolled list of extra deps in lockstep with pyproject.toml.
179    if (e.name or "").split(".", 1)[0] == "syside":
180        raise
181    __all__: list[str] = []
182    _missing_extra_error = e
183    _DEPRECATED = {}
184else:
185    __all__ = [
186        "ATTACHMENTS_DIR",
187        "CheckReqifOptions",
188        "CheckSysmlOptions",
189        "ImportOptions",
190        "InitOptions",
191        "ExportOptions",
192        "LinkApplyOptions",
193        "LinkScaffoldOptions",
194        "LockOptions",
195        "OptionsBase",
196        "LIBRARY_DIR",
197        "LIBRARY_PKG",
198        "check_reqif",
199        "check_sysml",
200        "import_",
201        "init",
202        "link_apply",
203        "link_scaffold",
204        "lock",
205        "export",
206    ]
207    _missing_extra_error = None
208    _DEPRECATED = {
209        "ATTACHMENTS_DIR": (_config, "ATTACHMENTS_DIR"),
210        "LIBRARY_DIR": (_config, "LIBRARY_DIR"),
211        "LIBRARY_PKG": (_config, "LIBRARY_PKG"),
212        "CheckReqifOptions": (_cli, "CheckReqifOptions"),
213        "CheckSysmlOptions": (_cli, "CheckSysmlOptions"),
214        "ImportOptions": (_cli, "ImportOptions"),
215        "InitOptions": (_cli, "InitOptions"),
216        "ExportOptions": (_cli, "ExportOptions"),
217        "LinkApplyOptions": (_cli, "LinkApplyOptions"),
218        "LinkScaffoldOptions": (_cli, "LinkScaffoldOptions"),
219        "LockOptions": (_cli, "LockOptions"),
220        "OptionsBase": (_cli, "_OptionsBase"),
221        "check_reqif": (_cmd_check, "run_reqif"),
222        "check_sysml": (_cmd_check, "run_sysml"),
223        "import_": (_cmd_import, "run"),
224        "init": (_cmd_init, "run"),
225        "link_apply": (_cmd_link, "run_apply"),
226        "link_scaffold": (_cmd_link, "run_scaffold"),
227        "lock": (_cmd_lock, "run"),
228        "export": (_cmd_export, "run"),
229    }
230
231
232# Hidden from type checkers on purpose: a visible module-level ``__getattr__``
233# would type every unknown ``syside.reqif.<x>`` as ``Any``, silently weakening
234# the checking of the names the ``TYPE_CHECKING`` block above declares.
235if not TYPE_CHECKING:
236
237    def __getattr__(name: str) -> Any:
238        """
239        Resolve a deprecated public name, warning that it is going away.
240
241        Only the names in :data:`__all__` are served here; everything else
242        raises :exc:`AttributeError` as usual. Submodules and the private
243        module globals are found by the normal lookup, which runs first, so
244        neither the CLI nor the internals ever trip the warning.
245        """
246        try:
247            module, attribute = _DEPRECATED[name]
248        except KeyError:
249            raise AttributeError(
250                f"module {__name__!r} has no attribute {name!r}"
251            ) from None
252        warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2)
253        return getattr(module, attribute)