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)