shexec 2.2.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shexec
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Run a shell-type command over every file of an input tree: a ChRIS DS plugin on pfdo_run
5
5
  Home-page: https://github.com/FNNDSC/pl-shexec
6
6
  Author: FNNDSC
@@ -18,8 +18,8 @@ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: chris_plugin>=0.4.0
21
- Requires-Dist: pfdo>=6.2.0
22
- Requires-Dist: pfdo_run>=4.2.0
21
+ Requires-Dist: pfdo>=6.3.0
22
+ Requires-Dist: pfdo_run>=4.3.0
23
23
  Requires-Dist: hostlib>=1.2.0
24
24
  Requires-Dist: pflog>=2.0.0
25
25
  Provides-Extra: dev
@@ -142,7 +142,7 @@ pl-shexec is a library as well as a plugin, in the same way pfdo_run is a librar
142
142
  ))
143
143
  main = PLUGIN.main
144
144
 
145
- ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
145
+ ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. A plugin may also decline engine flags it should not offer: ``omit=("threads", "threadsSplit")`` removes them from its parser, its ChRIS description and its manual, and fixes their values, as pl-freesurfer does because two ``recon-all`` runs in one container exhaust its memory. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
146
146
 
147
147
  The rest of pl-freesurfer is a ``pyproject.toml`` naming ``chris_plugin`` and ``shexec`` as its dependencies, the first so that ``chris_plugin_info`` finds the plugin, a ``setup.cfg``, a Dockerfile that starts from the FreeSurfer image and installs the package, tests, and docs. pl-shexec itself is the first plugin built this way; its ``__main__`` is the same call with an empty default command. The pf* stack's ``docs/plugin.adoc`` has the template.
148
148
 
@@ -159,7 +159,7 @@ From the command line, with the image:
159
159
  fnndsc/pl-shexec shexec --fileFilter txt \
160
160
  --exec "wc -l %{inputWorkingDir}/%{inputWorkingFile}" /incoming /outgoing
161
161
 
162
- docker run --rm fnndsc/pl-shexec shexec --man /tmp /tmp
162
+ docker run --rm fnndsc/pl-shexec shexec --man
163
163
 
164
164
  With Apptainer, ``apptainer exec docker://fnndsc/pl-shexec shexec ...``. The image carries the coreutils, zip and unzip, bc, perl, tar, ImageMagick and the DCMTK tools.
165
165
 
@@ -113,7 +113,7 @@ pl-shexec is a library as well as a plugin, in the same way pfdo_run is a librar
113
113
  ))
114
114
  main = PLUGIN.main
115
115
 
116
- ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
116
+ ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. A plugin may also decline engine flags it should not offer: ``omit=("threads", "threadsSplit")`` removes them from its parser, its ChRIS description and its manual, and fixes their values, as pl-freesurfer does because two ``recon-all`` runs in one container exhaust its memory. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
117
117
 
118
118
  The rest of pl-freesurfer is a ``pyproject.toml`` naming ``chris_plugin`` and ``shexec`` as its dependencies, the first so that ``chris_plugin_info`` finds the plugin, a ``setup.cfg``, a Dockerfile that starts from the FreeSurfer image and installs the package, tests, and docs. pl-shexec itself is the first plugin built this way; its ``__main__`` is the same call with an empty default command. The pf* stack's ``docs/plugin.adoc`` has the template.
119
119
 
@@ -130,7 +130,7 @@ From the command line, with the image:
130
130
  fnndsc/pl-shexec shexec --fileFilter txt \
131
131
  --exec "wc -l %{inputWorkingDir}/%{inputWorkingFile}" /incoming /outgoing
132
132
 
133
- docker run --rm fnndsc/pl-shexec shexec --man /tmp /tmp
133
+ docker run --rm fnndsc/pl-shexec shexec --man
134
134
 
135
135
  With Apptainer, ``apptainer exec docker://fnndsc/pl-shexec shexec ...``. The image carries the coreutils, zip and unzip, bc, perl, tar, ImageMagick and the DCMTK tools.
136
136
 
@@ -13,7 +13,7 @@ authors = [{ email = "dev@babyMRI.org" }]
13
13
  # fields, which the [project] table does not emit; setup.cfg supplies them, and the
14
14
  # author name lives there alone so Author-email stays a bare address.
15
15
  requires-python = ">=3.11"
16
- dependencies = ["chris_plugin>=0.4.0", "pfdo>=6.2.0", "pfdo_run>=4.2.0", "hostlib>=1.2.0", "pflog>=2.0.0"]
16
+ dependencies = ["chris_plugin>=0.4.0", "pfdo>=6.3.0", "pfdo_run>=4.3.0", "hostlib>=1.2.0", "pflog>=2.0.0"]
17
17
  classifiers = [
18
18
  "Programming Language :: Python :: 3",
19
19
  "Programming Language :: Python :: 3.11",
@@ -14,7 +14,7 @@ Dependencies:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- __version__: str = "2.2.0"
17
+ __version__: str = "2.4.0"
18
18
 
19
19
  from shexec.models import (
20
20
  DirNode,
@@ -81,6 +81,10 @@ class PluginSpec:
81
81
  minCpu: the CPU ChRIS reserves, in millicores, as ``1000m``.
82
82
  minGpu: GPUs required; 0 for none.
83
83
  defaults: flag defaults, ``exec`` above all; a derived plugin's usual command.
84
+ omit: engine flags this plugin does not offer, by destination name, as
85
+ ``("threads", "threadsSplit")``. Each is removed from the parser, so
86
+ neither ChRIS's description nor the manual shows it, and its value is
87
+ pinned to the engine's default, or to ``defaults`` when that names it.
84
88
  bannerArt: the ASCII art on the manual's first page.
85
89
  bannerTitle: the one-line title under it.
86
90
  bannerText: the banner prose; ``{version}`` in it is replaced.
@@ -101,6 +105,7 @@ class PluginSpec:
101
105
  minCpu: str = "1000m"
102
106
  minGpu: int = 0
103
107
  defaults: dict[str, Any] = field(default_factory=dict)
108
+ omit: tuple[str, ...] = ()
104
109
  bannerArt: str = DEFAULT_BANNER_ART
105
110
  bannerTitle: str = ""
106
111
  bannerText: str = ""
@@ -12,6 +12,7 @@ the first call of it; pl-freesurfer is the second, with FreeSurfer in the image
12
12
  Key components:
13
13
  - plugin_build: the factory
14
14
  - synopsis_build: the manual text for a spec
15
+ - parser_omit, manualText_omit: take flags a plugin does not offer out of its parser and manual
15
16
  - PLUGIN_SIGNATURE: the signature chris_plugin inspects
16
17
 
17
18
  Typical usage:
@@ -30,13 +31,14 @@ from __future__ import annotations
30
31
 
31
32
  import argparse
32
33
  import inspect
33
- from collections.abc import Callable
34
+ import re
35
+ from collections.abc import Callable, Sequence
34
36
  from pathlib import Path
35
37
 
36
38
  from chris_plugin import chris_plugin
37
39
  from pfdo_run.__main__ import ARGS_SYNOPSIS_DS, CLI_DS, TAG_HELP, parser_build
38
40
  from pflog import timed
39
- from pftree.manual import Manual, manual_print
41
+ from pftree.manual import Manual, manual_attach
40
42
 
41
43
  from shexec.models import Plugin, PluginSpec
42
44
  from shexec.shexec import plugin_run
@@ -51,6 +53,47 @@ PLUGIN_SIGNATURE: inspect.Signature = inspect.Signature(
51
53
  )
52
54
 
53
55
 
56
+ ENTRY_LINE: re.Pattern[str] = re.compile(r"^ {8}(?:\[--(?P<flag>[A-Za-z][\w-]*)|[\[<])")
57
+
58
+
59
+ def manualText_omit(text: str, flags: Sequence[str]) -> str:
60
+ r"""The shared manual text without the entries for the given flags.
61
+
62
+ The shared sections write each flag in one of two forms, both indented eight
63
+ spaces: a synopsis line, ``[--threads <numThreads>]`` ending in a continuation
64
+ backslash, and an ARGS entry, that line followed by its prose up to the next
65
+ entry, which opens with ``[`` or ``<``, or the next section heading. Both are
66
+ removed. A synopsis line that loses its successor keeps no dangling backslash.
67
+
68
+ Args:
69
+ text: a block of the shared manual, ``CLI_DS`` or ``ARGS_SYNOPSIS_DS``.
70
+ flags: flag names without dashes.
71
+
72
+ Returns:
73
+ The block without those flags.
74
+
75
+ Example:
76
+ >>> manualText_omit(" [--a] \\\n [--b]\n", ["b"])
77
+ ' [--a]\n'
78
+ """
79
+ if not flags:
80
+ return text
81
+ omitted: set[str] = set(flags)
82
+ kept: list[str] = []
83
+ skipping: bool = False
84
+ for line in text.splitlines(keepends=True):
85
+ match: re.Match[str] | None = ENTRY_LINE.match(line)
86
+ if match:
87
+ skipping = match.group("flag") in omitted
88
+ elif skipping and line.strip() and not line.startswith(" " * 8):
89
+ skipping = False
90
+ if not skipping:
91
+ kept.append(line)
92
+ if kept and text.rstrip().endswith("]") and kept[-1].rstrip().endswith("\\"):
93
+ kept[-1] = kept[-1].rstrip()[:-1].rstrip() + "\n"
94
+ return "".join(kept)
95
+
96
+
54
97
  def synopsis_build(spec: PluginSpec, shortOnly: bool = False) -> str:
55
98
  """The manual text for a plugin: its NAME, SYNOPSIS and brief example, then the shared sections.
56
99
 
@@ -70,13 +113,20 @@ def synopsis_build(spec: PluginSpec, shortOnly: bool = False) -> str:
70
113
  SYNOPSIS
71
114
 
72
115
  {spec.name} \\"""
73
- + CLI_DS
116
+ + manualText_omit(CLI_DS, spec.omit)
74
117
  + "\n\n BRIEF EXAMPLE\n"
75
118
  + spec.briefExample
76
119
  )
77
120
  if shortOnly:
78
121
  return shortSynopsis
79
- return shortSynopsis + spec.description + "\n ARGS " + ARGS_SYNOPSIS_DS + TAG_HELP + spec.examples
122
+ return (
123
+ shortSynopsis
124
+ + spec.description
125
+ + "\n ARGS "
126
+ + manualText_omit(ARGS_SYNOPSIS_DS, spec.omit)
127
+ + TAG_HELP
128
+ + spec.examples
129
+ )
80
130
 
81
131
 
82
132
  def manual_build(spec: PluginSpec) -> Manual:
@@ -92,14 +142,60 @@ def manual_build(spec: PluginSpec) -> Manual:
92
142
  )
93
143
 
94
144
 
145
+ def parser_omit(parser: argparse.ArgumentParser, names: Sequence[str]) -> None:
146
+ """Take flags out of a parser, keeping each one's default as a fixed value.
147
+
148
+ argparse has no public way to remove an argument, and the parser is assembled
149
+ from pftree's, pfdo's and pfdo_run's parents, so the flag is removed from the
150
+ three places argparse keeps it: the parser's actions, which chris_plugin
151
+ serialises; the group that lists it in ``--help``; and the option-string map
152
+ the command line is parsed against. Its default stays in the namespace, set
153
+ with ``set_defaults``, so the engine reads the value it always did.
154
+
155
+ Args:
156
+ parser: the assembled parser, changed in place.
157
+ names: destination names, as ``threads``.
158
+
159
+ Raises:
160
+ ValueError: a name the parser does not have; a misspelt omission would
161
+ otherwise leave the flag in place unnoticed.
162
+
163
+ Example:
164
+ >>> p = argparse.ArgumentParser(); _ = p.add_argument("--n", dest="n", default=3)
165
+ >>> parser_omit(p, ["n"]); p.parse_args([]).n
166
+ 3
167
+ """
168
+ # argparse internals: _actions, _action_groups, _group_actions and
169
+ # _option_string_actions are private but stable from 3.11 to 3.14, and
170
+ # chris_plugin itself reads _actions; CI runs every one of those versions.
171
+ actions: dict[str, argparse.Action] = {action.dest: action for action in parser._actions}
172
+ unknown: list[str] = [name for name in names if name not in actions]
173
+ if unknown:
174
+ raise ValueError(f"cannot omit flags the parser does not have: {', '.join(unknown)}")
175
+ for name in names:
176
+ action: argparse.Action = actions[name]
177
+ parser._remove_action(action)
178
+ for group in parser._action_groups:
179
+ if action in group._group_actions:
180
+ group._group_actions.remove(action)
181
+ for optionString in action.option_strings:
182
+ parser._option_string_actions.pop(optionString, None)
183
+ parser.set_defaults(**{name: action.default})
184
+
185
+
95
186
  def parser_forSpec(spec: PluginSpec) -> argparse.ArgumentParser:
96
- """pfdo_run's parser named for the plugin, its defaults set from the spec.
187
+ """pfdo_run's parser named for the plugin, its omitted flags removed, its defaults set from the spec.
97
188
 
98
189
  The defaults are set on the parser rather than applied afterwards, so the
99
190
  description ``chris_plugin_info`` serialises carries them and ChRIS shows them.
191
+ They are set after the omission, so a spec can pin an omitted flag to a value
192
+ of its own. ``--man`` and ``--synopsis`` print the plugin's own manual and exit as
193
+ they are read, so they need no ``inputdir outputdir``.
100
194
  """
101
195
  parser: argparse.ArgumentParser = parser_build(standalone=False, command=spec.name, version=spec.version)
196
+ parser_omit(parser, spec.omit)
102
197
  parser.set_defaults(**spec.defaults)
198
+ manual_attach(parser, manual_build(spec))
103
199
  return parser
104
200
 
105
201
 
@@ -123,9 +219,6 @@ def plugin_build(spec: PluginSpec, singleton: bool = True) -> Plugin:
123
219
  manual: Manual = manual_build(spec)
124
220
 
125
221
  def body(options: argparse.Namespace, inputdir: Path, outputdir: Path) -> int:
126
- if options.man or options.synopsis:
127
- manual_print(manual, shortOnly=not options.man)
128
- return 0
129
222
  return plugin_run(options, inputdir, outputdir)
130
223
 
131
224
  body.__name__ = spec.name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shexec
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Run a shell-type command over every file of an input tree: a ChRIS DS plugin on pfdo_run
5
5
  Home-page: https://github.com/FNNDSC/pl-shexec
6
6
  Author: FNNDSC
@@ -18,8 +18,8 @@ Requires-Python: >=3.11
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: chris_plugin>=0.4.0
21
- Requires-Dist: pfdo>=6.2.0
22
- Requires-Dist: pfdo_run>=4.2.0
21
+ Requires-Dist: pfdo>=6.3.0
22
+ Requires-Dist: pfdo_run>=4.3.0
23
23
  Requires-Dist: hostlib>=1.2.0
24
24
  Requires-Dist: pflog>=2.0.0
25
25
  Provides-Extra: dev
@@ -142,7 +142,7 @@ pl-shexec is a library as well as a plugin, in the same way pfdo_run is a librar
142
142
  ))
143
143
  main = PLUGIN.main
144
144
 
145
- ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
145
+ ``PluginSpec`` holds everything a plugin changes: its name and version, which become its command, its span and its ``--version``; its title, category and resource limits for ChRIS; the defaults of its flags, above all ``--exec``, which ChRIS shows as the defaults and the user can override; and its words on the manual page, over the sections every plugin shares. A plugin may also decline engine flags it should not offer: ``omit=("threads", "threadsSplit")`` removes them from its parser, its ChRIS description and its manual, and fixes their values, as pl-freesurfer does because two ``recon-all`` runs in one container exhaust its memory. ``plugin_build`` returns the parser with those defaults applied, the manual, and ``main`` already wrapped in pflog's ``timed`` and ``chris_plugin``, ready to be the console script. The engine, the flags, the records and the exit status are not in the spec, because they are the same for every plugin on the stack, and a fix to them reaches every plugin on its next build.
146
146
 
147
147
  The rest of pl-freesurfer is a ``pyproject.toml`` naming ``chris_plugin`` and ``shexec`` as its dependencies, the first so that ``chris_plugin_info`` finds the plugin, a ``setup.cfg``, a Dockerfile that starts from the FreeSurfer image and installs the package, tests, and docs. pl-shexec itself is the first plugin built this way; its ``__main__`` is the same call with an empty default command. The pf* stack's ``docs/plugin.adoc`` has the template.
148
148
 
@@ -159,7 +159,7 @@ From the command line, with the image:
159
159
  fnndsc/pl-shexec shexec --fileFilter txt \
160
160
  --exec "wc -l %{inputWorkingDir}/%{inputWorkingFile}" /incoming /outgoing
161
161
 
162
- docker run --rm fnndsc/pl-shexec shexec --man /tmp /tmp
162
+ docker run --rm fnndsc/pl-shexec shexec --man
163
163
 
164
164
  With Apptainer, ``apptainer exec docker://fnndsc/pl-shexec shexec ...``. The image carries the coreutils, zip and unzip, bc, perl, tar, ImageMagick and the DCMTK tools.
165
165
 
@@ -1,6 +1,6 @@
1
1
  chris_plugin>=0.4.0
2
- pfdo>=6.2.0
3
- pfdo_run>=4.2.0
2
+ pfdo>=6.3.0
3
+ pfdo_run>=4.3.0
4
4
  hostlib>=1.2.0
5
5
  pflog>=2.0.0
6
6
 
@@ -17,11 +17,15 @@ from shexec.plugin import synopsis_build
17
17
 
18
18
 
19
19
  def test_manual_andSynopsis(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
20
- assert main(parser.parse_args(["--man"]), tmp_path, tmp_path) == 0
20
+ with pytest.raises(SystemExit) as exc:
21
+ parser.parse_args(["--man"])
22
+ assert exc.value.code == 0
21
23
  out: str = capsys.readouterr().out
22
24
  body: str = out.split("DESCRIPTION", 1)[1]
23
25
  assert "`" not in body and "TAGS" in body and "<inputDir>" in body and "shexec" in body
24
- assert main(parser.parse_args(["--synopsis"]), tmp_path, tmp_path) == 0
26
+ with pytest.raises(SystemExit) as exc:
27
+ parser.parse_args(["--synopsis"])
28
+ assert exc.value.code == 0
25
29
  assert "TAGS" not in capsys.readouterr().out
26
30
 
27
31
 
@@ -62,3 +66,13 @@ def test_chris_plugin_info_describesThePlugin() -> None:
62
66
  assert description["documentation"] == "https://github.com/FNNDSC/pl-shexec"
63
67
  assert description["authors"] == "FNNDSC <dev@babyMRI.org>"
64
68
  assert any(p["name"] == "exec" for p in description["parameters"])
69
+
70
+
71
+ def test_consoleScript_manNeedsNoDirectories(
72
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
73
+ ) -> None:
74
+ monkeypatch.setattr("sys.argv", ["shexec", "--man"])
75
+ with pytest.raises(SystemExit) as exc:
76
+ main()
77
+ assert exc.value.code == 0
78
+ assert "TAGS" in capsys.readouterr().out
@@ -76,7 +76,9 @@ def test_derivedPlugin_describesItself_forChris() -> None:
76
76
 
77
77
  def test_derivedPlugin_manualAndVersion_areItsOwn(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
78
78
  plugin: Plugin = plugin_build(spec_make(), singleton=False)
79
- assert plugin.main(plugin.parser.parse_args(["--man"]), tmp_path, tmp_path) == 0
79
+ with pytest.raises(SystemExit) as exc:
80
+ plugin.parser.parse_args(["--man"])
81
+ assert exc.value.code == 0
80
82
  out: str = capsys.readouterr().out
81
83
  assert "linecount" in out and "Counts lines." in out and "9.9.9" in out and "TAGS" in out
82
84
  with pytest.raises(SystemExit):
@@ -90,3 +92,43 @@ def test_shexecItself_isBuiltByTheFactory() -> None:
90
92
  assert (
91
93
  SPEC.name == "shexec" and PLUGIN.parser.get_default("exec") == "" and PLUGIN.main.__name__ == "shexec"
92
94
  )
95
+
96
+
97
+ def test_omittedFlags_absentFromParserDescriptionAndManual_valuePinned(
98
+ capsys: pytest.CaptureFixture[str],
99
+ ) -> None:
100
+ plugin: Plugin = plugin_build(spec_make(omit=("threads", "threadsSplit")), singleton=False)
101
+ names: set[str] = {parameter["name"] for parameter in serialize(plugin.parser)}
102
+ assert "threads" not in names and "threadsSplit" not in names
103
+ assert "verbosity" in names
104
+ options: argparse.Namespace = plugin.parser.parse_args([])
105
+ assert options.threads == 0 and options.threadsSplit == 1
106
+ with pytest.raises(SystemExit) as raised:
107
+ plugin.parser.parse_args(["--threads", "4"])
108
+ assert raised.value.code == 2
109
+ capsys.readouterr()
110
+ with pytest.raises(SystemExit):
111
+ plugin.parser.parse_args(["--man"])
112
+ manual: str = capsys.readouterr().out
113
+ assert "[--threads" not in manual and "[--threadsSplit" not in manual
114
+ assert "[--verbosity" in manual
115
+
116
+
117
+ def test_omittedFlag_pinnedBySpecDefault() -> None:
118
+ plugin: Plugin = plugin_build(
119
+ spec_make(omit=("threads",), defaults={"exec": COUNT_LINES, "threads": 2}), singleton=False
120
+ )
121
+ assert plugin.parser.parse_args([]).threads == 2
122
+ assert "threads" not in {parameter["name"] for parameter in serialize(plugin.parser)}
123
+
124
+
125
+ def test_omittedFlag_unknownName_refused() -> None:
126
+ with pytest.raises(ValueError, match="threadz"):
127
+ plugin_build(spec_make(omit=("threadz",)), singleton=False)
128
+
129
+
130
+ def test_omittedFlags_derivedPluginStillRuns(tmp_path: Path) -> None:
131
+ inputDir, outputDir = tree_make(tmp_path)
132
+ plugin: Plugin = plugin_build(spec_make(omit=("threads", "threadsSplit")), singleton=False)
133
+ assert plugin.main(plugin.parser.parse_args(["--verbosity", "0"]), inputDir, outputDir) == 0
134
+ assert (outputDir / "sub-01" / "jobs.jsonl").exists()
File without changes
File without changes
File without changes
File without changes
File without changes