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.
- {shexec-2.2.0/shexec.egg-info → shexec-2.4.0}/PKG-INFO +5 -5
- {shexec-2.2.0 → shexec-2.4.0}/README.rst +2 -2
- {shexec-2.2.0 → shexec-2.4.0}/pyproject.toml +1 -1
- {shexec-2.2.0 → shexec-2.4.0}/shexec/__init__.py +1 -1
- {shexec-2.2.0 → shexec-2.4.0}/shexec/models.py +5 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec/plugin.py +101 -8
- {shexec-2.2.0 → shexec-2.4.0/shexec.egg-info}/PKG-INFO +5 -5
- {shexec-2.2.0 → shexec-2.4.0}/shexec.egg-info/requires.txt +2 -2
- {shexec-2.2.0 → shexec-2.4.0}/tests/test_cli.py +16 -2
- {shexec-2.2.0 → shexec-2.4.0}/tests/test_plugin.py +43 -1
- {shexec-2.2.0 → shexec-2.4.0}/LICENSE +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/setup.cfg +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec/__main__.py +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec/shexec.py +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec.egg-info/SOURCES.txt +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec.egg-info/dependency_links.txt +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec.egg-info/entry_points.txt +0 -0
- {shexec-2.2.0 → shexec-2.4.0}/shexec.egg-info/top_level.txt +0 -0
- {shexec-2.2.0 → shexec-2.4.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.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.
|
|
22
|
-
Requires-Dist: pfdo_run>=4.
|
|
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
|
|
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
|
|
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.
|
|
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",
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
22
|
-
Requires-Dist: pfdo_run>=4.
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|