shexec 2.2.0__tar.gz → 2.3.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.
- {shexec-2.2.0/shexec.egg-info → shexec-2.3.0}/PKG-INFO +2 -2
- {shexec-2.2.0 → shexec-2.3.0}/README.rst +1 -1
- {shexec-2.2.0 → shexec-2.3.0}/shexec/__init__.py +1 -1
- {shexec-2.2.0 → shexec-2.3.0}/shexec/models.py +5 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec/plugin.py +98 -4
- {shexec-2.2.0 → shexec-2.3.0/shexec.egg-info}/PKG-INFO +2 -2
- {shexec-2.2.0 → shexec-2.3.0}/tests/test_plugin.py +39 -0
- {shexec-2.2.0 → shexec-2.3.0}/LICENSE +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/pyproject.toml +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/setup.cfg +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec/__main__.py +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec/shexec.py +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec.egg-info/SOURCES.txt +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec.egg-info/dependency_links.txt +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec.egg-info/entry_points.txt +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec.egg-info/requires.txt +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/shexec.egg-info/top_level.txt +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/tests/test_cli.py +0 -0
- {shexec-2.2.0 → shexec-2.3.0}/tests/test_shexec.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shexec
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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,7 +31,8 @@ from __future__ import annotations
|
|
|
30
31
|
|
|
31
32
|
import argparse
|
|
32
33
|
import inspect
|
|
33
|
-
|
|
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
|
|
@@ -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
|
|
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,13 +142,57 @@ 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.
|
|
100
193
|
"""
|
|
101
194
|
parser: argparse.ArgumentParser = parser_build(standalone=False, command=spec.name, version=spec.version)
|
|
195
|
+
parser_omit(parser, spec.omit)
|
|
102
196
|
parser.set_defaults(**spec.defaults)
|
|
103
197
|
return parser
|
|
104
198
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shexec
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -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
|
|
|
@@ -90,3 +90,42 @@ def test_shexecItself_isBuiltByTheFactory() -> None:
|
|
|
90
90
|
assert (
|
|
91
91
|
SPEC.name == "shexec" and PLUGIN.parser.get_default("exec") == "" and PLUGIN.main.__name__ == "shexec"
|
|
92
92
|
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_omittedFlags_absentFromParserDescriptionAndManual_valuePinned(
|
|
96
|
+
capsys: pytest.CaptureFixture[str],
|
|
97
|
+
) -> None:
|
|
98
|
+
plugin: Plugin = plugin_build(spec_make(omit=("threads", "threadsSplit")), singleton=False)
|
|
99
|
+
names: set[str] = {parameter["name"] for parameter in serialize(plugin.parser)}
|
|
100
|
+
assert "threads" not in names and "threadsSplit" not in names
|
|
101
|
+
assert "verbosity" in names
|
|
102
|
+
options: argparse.Namespace = plugin.parser.parse_args([])
|
|
103
|
+
assert options.threads == 0 and options.threadsSplit == 1
|
|
104
|
+
with pytest.raises(SystemExit) as raised:
|
|
105
|
+
plugin.parser.parse_args(["--threads", "4"])
|
|
106
|
+
assert raised.value.code == 2
|
|
107
|
+
capsys.readouterr()
|
|
108
|
+
assert plugin.main(plugin.parser.parse_args(["--man"]), Path("/tmp"), Path("/tmp")) == 0
|
|
109
|
+
manual: str = capsys.readouterr().out
|
|
110
|
+
assert "[--threads" not in manual and "[--threadsSplit" not in manual
|
|
111
|
+
assert "[--verbosity" in manual
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_omittedFlag_pinnedBySpecDefault() -> None:
|
|
115
|
+
plugin: Plugin = plugin_build(
|
|
116
|
+
spec_make(omit=("threads",), defaults={"exec": COUNT_LINES, "threads": 2}), singleton=False
|
|
117
|
+
)
|
|
118
|
+
assert plugin.parser.parse_args([]).threads == 2
|
|
119
|
+
assert "threads" not in {parameter["name"] for parameter in serialize(plugin.parser)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_omittedFlag_unknownName_refused() -> None:
|
|
123
|
+
with pytest.raises(ValueError, match="threadz"):
|
|
124
|
+
plugin_build(spec_make(omit=("threadz",)), singleton=False)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_omittedFlags_derivedPluginStillRuns(tmp_path: Path) -> None:
|
|
128
|
+
inputDir, outputDir = tree_make(tmp_path)
|
|
129
|
+
plugin: Plugin = plugin_build(spec_make(omit=("threads", "threadsSplit")), singleton=False)
|
|
130
|
+
assert plugin.main(plugin.parser.parse_args(["--verbosity", "0"]), inputDir, outputDir) == 0
|
|
131
|
+
assert (outputDir / "sub-01" / "jobs.jsonl").exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|