galaxy-tool-refactor-cli 0.2.0__py3-none-any.whl
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.
- galaxy_tool_refactor_cli/__init__.py +22 -0
- galaxy_tool_refactor_cli/cli.py +1144 -0
- galaxy_tool_refactor_cli/py.typed +0 -0
- galaxy_tool_refactor_cli-0.2.0.dist-info/METADATA +90 -0
- galaxy_tool_refactor_cli-0.2.0.dist-info/RECORD +7 -0
- galaxy_tool_refactor_cli-0.2.0.dist-info/WHEEL +4 -0
- galaxy_tool_refactor_cli-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Top-level CLI app for the Galaxy tool refactoring framework.
|
|
2
|
+
|
|
3
|
+
Tier 4 (the app layer): a thin front-end over the registry facade. Orchestration
|
|
4
|
+
lives in the tier-3.6 facade (``galaxy-tool-refactor-registry``), which composes
|
|
5
|
+
the lower tiers; this package depends on that facade plus fmt's ``cli_support``
|
|
6
|
+
file-walking engine (tier 3) and tier-1 parsing — **not** on the codemod tier
|
|
7
|
+
directly (cli `docs/decisions.md` D4). It exposes the ``galaxy-tool-refactor`` CLI
|
|
8
|
+
with seven commands:
|
|
9
|
+
|
|
10
|
+
- ``format`` — structural canonicalisation + cosmetic formatting (safe,
|
|
11
|
+
idempotent; never changes ``profile=``).
|
|
12
|
+
- ``upgrade`` — opt-in repair + profile upgrade to the latest reachable profile.
|
|
13
|
+
- ``check`` — report-only linter over the selected rules' detect phases.
|
|
14
|
+
- ``find-references`` — read-only query: a parameter's Cheetah ``$var`` reference
|
|
15
|
+
sites across a tool's templated sections (cli §D8).
|
|
16
|
+
- ``rulesets`` / ``rules`` — introspect the baked-in rulesets and rules.
|
|
17
|
+
- ``normalize-macros`` — opt-in, repo-scoped: lowercase literal ``format``/``ftype``
|
|
18
|
+
in ``<macros>``-root files (never part of ``format``/``upgrade``; cli §D7).
|
|
19
|
+
|
|
20
|
+
Per dignified-python there are no re-exports; import from
|
|
21
|
+
``galaxy_tool_refactor_cli.cli`` directly.
|
|
22
|
+
"""
|
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
"""The ``galaxy-tool-refactor`` command-line interface.
|
|
2
|
+
|
|
3
|
+
Ten subcommands (including the two opt-in conversions, ``convert-help`` and
|
|
4
|
+
``tokenize-version``). ``format`` and ``upgrade`` share fmt's file-walking /
|
|
5
|
+
drift-detection engine (``galaxy_tool_fmt.cli_support``) and differ only in
|
|
6
|
+
which rules run before serialisation; ``check`` is a report-only linter that
|
|
7
|
+
mutates nothing; ``find-references`` is a read-only query for a parameter's Cheetah
|
|
8
|
+
``$var`` reference sites and ``rename-param`` is its mutating sibling (rename a
|
|
9
|
+
parameter across those sites); ``rules`` / ``rulesets`` print the available baked-in
|
|
10
|
+
rules and rulesets; ``normalize-macros`` is a separate, opt-in pass over macro-library
|
|
11
|
+
files. All rule orchestration is delegated to the tier-3.6 registry facade
|
|
12
|
+
(``galaxy_tool_refactor_registry``); this module only does CLI plumbing.
|
|
13
|
+
|
|
14
|
+
- ``format`` — apply a ruleset's (or a ``--select``/``--ignore`` selection's)
|
|
15
|
+
fixable rules then cosmetic formatting. Safe and idempotent; never changes
|
|
16
|
+
``profile=``. Default ruleset ``default`` reproduces the historical behaviour.
|
|
17
|
+
Macro-library files (``<macros>`` root) are also cosmetically formatted
|
|
18
|
+
(kind-applicable rules only — no codemods).
|
|
19
|
+
- ``upgrade`` — repair, then iteratively upgrade ``profile=`` toward the latest
|
|
20
|
+
(applying the registered migration each step), then format. Opt-in and
|
|
21
|
+
semantic; rulesets do not apply (``--select``/``--ignore`` adjust its rule set).
|
|
22
|
+
Also bumps an imported ``@PROFILE@`` token in place when every profile-using
|
|
23
|
+
importer in the run agrees on the target (else reports and skips); a
|
|
24
|
+
``profile="@TOKEN@"`` whose token is inline is handled per-file by GTR007.
|
|
25
|
+
- ``check`` — report where tools deviate from the selection, one
|
|
26
|
+
``file:line CODE message`` per finding, without changing anything. Fixable
|
|
27
|
+
findings fail the run; advisory (``detect_only``) findings are informational
|
|
28
|
+
unless ``--strict``. Macro files are checked for cosmetic (fixable) drift too.
|
|
29
|
+
- ``find-references`` — read-only query: print every Cheetah ``$NAME`` reference site
|
|
30
|
+
(``file:line [section] $ref``) across a tool's templated sections. Mutates nothing,
|
|
31
|
+
not a rule (no selection); the first read-only consumer of the Cheetah reference model
|
|
32
|
+
(``galaxy_tool_source.cheetah_refs``). See ``docs/decisions.md`` §D8.
|
|
33
|
+
- ``rename-param`` — the mutating sibling of ``find-references``: rename a parameter
|
|
34
|
+
OLD to NEW across every Cheetah section, by-name cross-reference attribute, and
|
|
35
|
+
``<tests>`` mirror, plus the definition. Atomic per file (rewrites everything or skips
|
|
36
|
+
with a reason); ``--check`` previews. Built on the faithful CDM lexer (M5.3); see
|
|
37
|
+
``docs/decisions.md`` §D9.
|
|
38
|
+
- ``rules`` / ``rulesets`` — introspection: the baked-in rules and the rulesets.
|
|
39
|
+
- ``normalize-macros`` — opt-in, repo-scoped: lowercase literal ``format`` /
|
|
40
|
+
``ftype`` in ``<macros>``-root files (the macro-library analog of the 24.2
|
|
41
|
+
normalization the per-tool ``upgrade`` cannot reach — a value defined in an
|
|
42
|
+
imported macro file). It rewrites files other than the one named (a shared
|
|
43
|
+
macro file affects every importer), so it is never folded into ``format`` /
|
|
44
|
+
``upgrade``; see ``galaxy-tool-codemod/docs/macro-aware-normalization.md``.
|
|
45
|
+
|
|
46
|
+
Selection (``--ruleset`` / ``--select`` / ``--ignore``) is shared by ``format``,
|
|
47
|
+
``upgrade`` (no ``--ruleset``), and ``check``; precedence is ruff-style
|
|
48
|
+
(``--ignore`` ▸ ``--select`` ▸ ``--ruleset``, where ``--ruleset`` unions the named
|
|
49
|
+
sets and ``--select`` replaces them).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import sys
|
|
55
|
+
from collections import defaultdict
|
|
56
|
+
from collections.abc import Mapping
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
import click
|
|
60
|
+
from galaxy_tool_fmt.cli_support import (
|
|
61
|
+
Action,
|
|
62
|
+
RunOptions,
|
|
63
|
+
TransformOutcome,
|
|
64
|
+
is_macros_root,
|
|
65
|
+
is_tool_root,
|
|
66
|
+
iter_targets,
|
|
67
|
+
make_backup,
|
|
68
|
+
run,
|
|
69
|
+
)
|
|
70
|
+
from galaxy_tool_fmt.detect import detect_macro_document
|
|
71
|
+
from galaxy_tool_fmt.format import format_macro_document
|
|
72
|
+
from galaxy_tool_refactor_registry import facade
|
|
73
|
+
from galaxy_tool_refactor_registry.bundle_rename import (
|
|
74
|
+
BundleRenameResult,
|
|
75
|
+
ConsensusRenameResult,
|
|
76
|
+
build_importer_map,
|
|
77
|
+
find_references_in_bundle,
|
|
78
|
+
rename_param_bundle,
|
|
79
|
+
rename_param_consensus,
|
|
80
|
+
)
|
|
81
|
+
from galaxy_tool_refactor_registry.errors import UnknownRuleCode, UnknownRuleset
|
|
82
|
+
from galaxy_tool_refactor_registry.macro_datatype import normalize_macro_files
|
|
83
|
+
from galaxy_tool_refactor_registry.macro_profile import (
|
|
84
|
+
apply_profile_token_plans,
|
|
85
|
+
plan_from_sites,
|
|
86
|
+
profile_token_site,
|
|
87
|
+
)
|
|
88
|
+
from galaxy_tool_refactor_registry.registry import display_code
|
|
89
|
+
from galaxy_tool_refactor_registry.resolve import (
|
|
90
|
+
resolve_codes,
|
|
91
|
+
resolve_upgrade_codes,
|
|
92
|
+
)
|
|
93
|
+
from galaxy_tool_source.binding import ToolXmlSyntaxError, load_macros, load_tool
|
|
94
|
+
from galaxy_tool_source.document import MacroDocument, ToolDocument
|
|
95
|
+
|
|
96
|
+
_PATH_ARGUMENT = click.argument(
|
|
97
|
+
"paths",
|
|
98
|
+
nargs=-1,
|
|
99
|
+
required=True,
|
|
100
|
+
type=click.Path(exists=True, path_type=Path),
|
|
101
|
+
)
|
|
102
|
+
_CHECK_OPTION = click.option(
|
|
103
|
+
"--check",
|
|
104
|
+
is_flag=True,
|
|
105
|
+
help="Don't write files. Exit non-zero if any file would change.",
|
|
106
|
+
)
|
|
107
|
+
_DIFF_OPTION = click.option(
|
|
108
|
+
"--diff",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
help="Don't write files. Print a unified diff of the rewrite to stdout.",
|
|
111
|
+
)
|
|
112
|
+
_QUIET_OPTION = click.option(
|
|
113
|
+
"-q",
|
|
114
|
+
"--quiet",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
help="Suppress per-file output; only errors and the summary are shown.",
|
|
117
|
+
)
|
|
118
|
+
_REPO_ROOT_OPTION = click.option(
|
|
119
|
+
"--repo-root",
|
|
120
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
121
|
+
default=None,
|
|
122
|
+
help=(
|
|
123
|
+
"Repo directory used to prove a macro file is sole-owned before a rename "
|
|
124
|
+
"edits it. Required only when a rename's references reach an imported macro."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
_BACKUP_OPTION = click.option(
|
|
128
|
+
"--backup",
|
|
129
|
+
is_flag=True,
|
|
130
|
+
help="Before overwriting a file, copy its current content to <file>.bak.",
|
|
131
|
+
)
|
|
132
|
+
_ACROSS_IMPORTERS_OPTION = click.option(
|
|
133
|
+
"--across-importers",
|
|
134
|
+
is_flag=True,
|
|
135
|
+
help=(
|
|
136
|
+
"When a rename reaches a macro shared by other tools, rename the parameter "
|
|
137
|
+
"across all of its importers in lockstep (only if they all agree). Needs "
|
|
138
|
+
"--repo-root."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
_STRICT_OPTION = click.option(
|
|
142
|
+
"--strict",
|
|
143
|
+
is_flag=True,
|
|
144
|
+
help="Also fail (exit non-zero) on advisory findings, not just fixable ones.",
|
|
145
|
+
)
|
|
146
|
+
_RULESET_OPTION = click.option(
|
|
147
|
+
"--ruleset",
|
|
148
|
+
"rulesets",
|
|
149
|
+
multiple=True,
|
|
150
|
+
metavar="NAME",
|
|
151
|
+
help="Rule-set(s) to apply/report — the UNION of the named sets "
|
|
152
|
+
"(cosmetic | default | iuc | strict). Repeatable or comma-separated, "
|
|
153
|
+
"e.g. --ruleset default,strict. Default: default. "
|
|
154
|
+
"See `galaxy-tool-refactor rulesets`.",
|
|
155
|
+
)
|
|
156
|
+
_SELECT_OPTION = click.option(
|
|
157
|
+
"--select",
|
|
158
|
+
"select",
|
|
159
|
+
multiple=True,
|
|
160
|
+
metavar="CODE",
|
|
161
|
+
help="Run only these rules — GTR codes or planemo linter names "
|
|
162
|
+
"(replaces the ruleset's set). Repeatable or comma-separated, "
|
|
163
|
+
"e.g. --select GTR001,HelpMissing.",
|
|
164
|
+
)
|
|
165
|
+
_IGNORE_OPTION = click.option(
|
|
166
|
+
"--ignore",
|
|
167
|
+
"ignore",
|
|
168
|
+
multiple=True,
|
|
169
|
+
metavar="CODE",
|
|
170
|
+
help="Drop these rules — GTR codes or planemo linter names — from the "
|
|
171
|
+
"selection. Repeatable or comma-separated.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _split_codes(values: tuple[str, ...]) -> tuple[str, ...]:
|
|
176
|
+
"""Flatten repeated / comma-separated select/ignore tokens, stripped.
|
|
177
|
+
|
|
178
|
+
Case is preserved (the resolver matches GTR codes and planemo linter names
|
|
179
|
+
case-insensitively) so an error message echoes the token the user typed.
|
|
180
|
+
"""
|
|
181
|
+
codes: list[str] = []
|
|
182
|
+
for value in values:
|
|
183
|
+
codes.extend(token.strip() for token in value.split(",") if token.strip())
|
|
184
|
+
return tuple(codes)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _split_names(values: tuple[str, ...]) -> tuple[str, ...]:
|
|
188
|
+
"""Flatten repeated / comma-separated ruleset names, lower-cased and stripped."""
|
|
189
|
+
names: list[str] = []
|
|
190
|
+
for value in values:
|
|
191
|
+
names.extend(
|
|
192
|
+
token.strip().lower() for token in value.split(",") if token.strip()
|
|
193
|
+
)
|
|
194
|
+
return tuple(names)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _resolve(
|
|
198
|
+
*, rulesets: tuple[str, ...], select: tuple[str, ...], ignore: tuple[str, ...]
|
|
199
|
+
) -> frozenset[str]:
|
|
200
|
+
"""Resolve a format/check selection, mapping facade errors to the CLI boundary."""
|
|
201
|
+
try:
|
|
202
|
+
return resolve_codes(
|
|
203
|
+
rulesets=_split_names(rulesets),
|
|
204
|
+
select=_split_codes(select),
|
|
205
|
+
ignore=_split_codes(ignore),
|
|
206
|
+
)
|
|
207
|
+
except (UnknownRuleset, UnknownRuleCode) as error:
|
|
208
|
+
raise click.BadParameter(str(error)) from error
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _resolve_upgrade(
|
|
212
|
+
*, select: tuple[str, ...], ignore: tuple[str, ...]
|
|
213
|
+
) -> frozenset[str]:
|
|
214
|
+
"""Resolve an upgrade selection (no ruleset), mapping facade errors to the CLI."""
|
|
215
|
+
try:
|
|
216
|
+
return resolve_upgrade_codes(
|
|
217
|
+
select=_split_codes(select), ignore=_split_codes(ignore)
|
|
218
|
+
)
|
|
219
|
+
except UnknownRuleCode as error:
|
|
220
|
+
raise click.BadParameter(str(error)) from error
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
224
|
+
def main() -> None:
|
|
225
|
+
"""Refactor Galaxy tool XML: structural codemods plus cosmetic formatting."""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@main.command(name="format")
|
|
229
|
+
@_PATH_ARGUMENT
|
|
230
|
+
@_CHECK_OPTION
|
|
231
|
+
@_DIFF_OPTION
|
|
232
|
+
@_QUIET_OPTION
|
|
233
|
+
@_BACKUP_OPTION
|
|
234
|
+
@_RULESET_OPTION
|
|
235
|
+
@_SELECT_OPTION
|
|
236
|
+
@_IGNORE_OPTION
|
|
237
|
+
def format_command(
|
|
238
|
+
paths: tuple[Path, ...],
|
|
239
|
+
check: bool,
|
|
240
|
+
diff: bool,
|
|
241
|
+
quiet: bool,
|
|
242
|
+
backup: bool,
|
|
243
|
+
rulesets: tuple[str, ...],
|
|
244
|
+
select: tuple[str, ...],
|
|
245
|
+
ignore: tuple[str, ...],
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Apply a ruleset's fixable rules then cosmetic formatting (never ``profile=``).
|
|
248
|
+
|
|
249
|
+
The default ruleset ``default`` applies the canonical codemods (typo repair,
|
|
250
|
+
attribute / element order) and the cosmetic rules — the historical ``format``
|
|
251
|
+
behaviour. Advisory rules in a selection (e.g. under ``--ruleset strict``) are
|
|
252
|
+
reported as notes but never change a file. Macro-library files (``<macros>``
|
|
253
|
+
root) are also **cosmetically** formatted (the kind-applicable rules — no
|
|
254
|
+
codemods, which are tool-only; rule selection governs tools). PATHS may be
|
|
255
|
+
files or directories (searched recursively for ``*.xml``); other XML is
|
|
256
|
+
skipped.
|
|
257
|
+
"""
|
|
258
|
+
codes = _resolve(rulesets=rulesets, select=select, ignore=ignore)
|
|
259
|
+
|
|
260
|
+
def transform(document: ToolDocument) -> TransformOutcome:
|
|
261
|
+
result = facade.run(document, codes=codes)
|
|
262
|
+
return TransformOutcome(result.formatted, notes=result.notes)
|
|
263
|
+
|
|
264
|
+
def macro_transform(document: MacroDocument) -> TransformOutcome:
|
|
265
|
+
# Macro files get cosmetic formatting only; codemods are tool-only.
|
|
266
|
+
return TransformOutcome(format_macro_document(document))
|
|
267
|
+
|
|
268
|
+
exit_code = run(
|
|
269
|
+
paths,
|
|
270
|
+
transform=transform,
|
|
271
|
+
action=Action(past="reformatted", conditional="would reformat"),
|
|
272
|
+
options=RunOptions(check=check, diff=diff, quiet=quiet, backup=backup),
|
|
273
|
+
macro_transform=macro_transform,
|
|
274
|
+
)
|
|
275
|
+
sys.exit(exit_code)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@main.command(name="upgrade")
|
|
279
|
+
@_PATH_ARGUMENT
|
|
280
|
+
@_CHECK_OPTION
|
|
281
|
+
@_DIFF_OPTION
|
|
282
|
+
@_QUIET_OPTION
|
|
283
|
+
@_BACKUP_OPTION
|
|
284
|
+
@_RULESET_OPTION
|
|
285
|
+
@_SELECT_OPTION
|
|
286
|
+
@_IGNORE_OPTION
|
|
287
|
+
def upgrade_command(
|
|
288
|
+
paths: tuple[Path, ...],
|
|
289
|
+
check: bool,
|
|
290
|
+
diff: bool,
|
|
291
|
+
quiet: bool,
|
|
292
|
+
backup: bool,
|
|
293
|
+
rulesets: tuple[str, ...],
|
|
294
|
+
select: tuple[str, ...],
|
|
295
|
+
ignore: tuple[str, ...],
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Repair and upgrade tools to the latest profile they can reach, then format.
|
|
298
|
+
|
|
299
|
+
Opt-in and semantic. The profile upgrade always runs; ``--select`` / ``--ignore``
|
|
300
|
+
adjust the *other* fixable rules (by default typo repair + cosmetic
|
|
301
|
+
formatting) — e.g. ``--ignore GTR006`` upgrades without typo repair. Rulesets
|
|
302
|
+
are a ``format``/``check`` concept and are **not** accepted here.
|
|
303
|
+
|
|
304
|
+
A ``profile="@PROFILE@"`` whose token lives in an *imported* macro file is
|
|
305
|
+
upgraded by bumping that token in place — but only when every profile-using
|
|
306
|
+
importer in this run agrees on the target profile; a macro file whose
|
|
307
|
+
importers disagree is reported and left untouched (no over-declaration). The
|
|
308
|
+
inline-token case is handled per-file by GTR007. The token value is the *only*
|
|
309
|
+
semantic edit, but the macro file it lives in **is** reserialised through fmt's
|
|
310
|
+
``format_macro_document`` when the token is bumped (so a bumped file is also
|
|
311
|
+
cosmetically normalised — GTR001/GTR004); ``upgrade`` runs no *separate*
|
|
312
|
+
cosmetic macro pass over un-bumped macro files the way ``format`` does. PATHS
|
|
313
|
+
may be files or directories.
|
|
314
|
+
|
|
315
|
+
The upgrade is structural, not behaviour-preserving: bumping ``profile=`` opts
|
|
316
|
+
the tool into newer Galaxy runtime defaults the XSD can't verify. When a bump
|
|
317
|
+
crosses such a boundary (e.g. ``set -e``, Python 3, optional-value templating),
|
|
318
|
+
a note lists the crossed versions to review — see ``docs/profile_upgrades.md``.
|
|
319
|
+
A few of those changes have a safe mechanical fix that is **applied
|
|
320
|
+
automatically** once the reached profile crosses them (e.g. stripping
|
|
321
|
+
whitespace from ``from_work_dir`` at 21.09); the rest are warn-only.
|
|
322
|
+
"""
|
|
323
|
+
if rulesets:
|
|
324
|
+
raise click.BadParameter(
|
|
325
|
+
"--ruleset is not applicable to 'upgrade'; rulesets govern "
|
|
326
|
+
"'format' / 'check'. Use --select / --ignore to adjust the rule set.",
|
|
327
|
+
param_hint="--ruleset",
|
|
328
|
+
)
|
|
329
|
+
codes = _resolve_upgrade(select=select, ignore=ignore)
|
|
330
|
+
|
|
331
|
+
# Whole-run phase first: bump imported @PROFILE@ tokens where every
|
|
332
|
+
# profile-using importer agrees on the target (the inline case is handled
|
|
333
|
+
# per-file by GTR007 in the transform below). This edits *macro* files, so it
|
|
334
|
+
# cannot ride the per-file tool transform.
|
|
335
|
+
macro_pending = _upgrade_macro_profile_tokens(
|
|
336
|
+
paths, check=check, diff=diff, quiet=quiet, backup=backup
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def transform(document: ToolDocument) -> TransformOutcome:
|
|
340
|
+
result = facade.upgrade(document, codes=codes)
|
|
341
|
+
return TransformOutcome(result.formatted, notes=result.notes)
|
|
342
|
+
|
|
343
|
+
exit_code = run(
|
|
344
|
+
paths,
|
|
345
|
+
transform=transform,
|
|
346
|
+
action=Action(past="upgraded", conditional="would upgrade"),
|
|
347
|
+
options=RunOptions(check=check, diff=diff, quiet=quiet, backup=backup),
|
|
348
|
+
)
|
|
349
|
+
# A pending macro-token bump is a "would change" under either preview mode
|
|
350
|
+
# (--check or --diff), so both must surface it in the exit code (cli D6).
|
|
351
|
+
sys.exit(exit_code or (1 if ((check or diff) and macro_pending) else 0))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _upgrade_macro_profile_tokens(
|
|
355
|
+
paths: tuple[Path, ...], *, check: bool, diff: bool, quiet: bool, backup: bool
|
|
356
|
+
) -> bool:
|
|
357
|
+
"""Upgrade imported ``@PROFILE@`` tokens across the run; return would-edit.
|
|
358
|
+
|
|
359
|
+
Walks the run's tool files, collects each one's imported-profile-token site,
|
|
360
|
+
and for every macro file whose profile-using importers agree on a target
|
|
361
|
+
bumps the ``<token>`` in place (writing unless ``check``/``diff``). A macro
|
|
362
|
+
file whose importers disagree is reported and left untouched. Returns whether
|
|
363
|
+
any macro file was (or, under preview, would be) edited — the caller folds
|
|
364
|
+
that into the ``--check`` exit code.
|
|
365
|
+
"""
|
|
366
|
+
sites = []
|
|
367
|
+
for path in iter_targets(paths):
|
|
368
|
+
try:
|
|
369
|
+
original = path.read_bytes()
|
|
370
|
+
except OSError:
|
|
371
|
+
continue
|
|
372
|
+
if not is_tool_root(original):
|
|
373
|
+
continue
|
|
374
|
+
try:
|
|
375
|
+
document = load_tool(path) # load from path so imports resolve
|
|
376
|
+
except ToolXmlSyntaxError:
|
|
377
|
+
continue # malformed tools are surfaced by the per-file run() below
|
|
378
|
+
site = profile_token_site(document)
|
|
379
|
+
if site is not None:
|
|
380
|
+
sites.append(site)
|
|
381
|
+
plans = plan_from_sites(sites)
|
|
382
|
+
result = apply_profile_token_plans(
|
|
383
|
+
plans, write=not (check or diff), backup=backup
|
|
384
|
+
)
|
|
385
|
+
if not quiet:
|
|
386
|
+
verb = "would upgrade" if (check or diff) else "upgraded"
|
|
387
|
+
for edit in result.edits:
|
|
388
|
+
click.echo(
|
|
389
|
+
f"{verb} {edit.token_name} {edit.old_value} -> {edit.new_value} "
|
|
390
|
+
f"in {edit.macro_file} ({len(edit.importers)} tool(s))"
|
|
391
|
+
)
|
|
392
|
+
for skip in result.skips:
|
|
393
|
+
click.echo(
|
|
394
|
+
f"skipped {skip.macro_file}: {skip.token_name} importers disagree "
|
|
395
|
+
f"on target profile ({len(skip.importers)} tool(s))"
|
|
396
|
+
)
|
|
397
|
+
return bool(result.edits)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _check_summary(
|
|
401
|
+
*,
|
|
402
|
+
fixable: int,
|
|
403
|
+
advisory: int,
|
|
404
|
+
flagged: int,
|
|
405
|
+
clean: int,
|
|
406
|
+
skipped: int,
|
|
407
|
+
errored: int,
|
|
408
|
+
) -> str:
|
|
409
|
+
"""Render the trailing summary line for ``check``."""
|
|
410
|
+
parts: list[str] = []
|
|
411
|
+
if fixable or advisory:
|
|
412
|
+
counts = []
|
|
413
|
+
if fixable:
|
|
414
|
+
counts.append(f"{fixable} fixable")
|
|
415
|
+
if advisory:
|
|
416
|
+
counts.append(f"{advisory} advisory")
|
|
417
|
+
parts.append(", ".join(counts) + f" finding(s) in {flagged} file(s)")
|
|
418
|
+
if clean:
|
|
419
|
+
parts.append(f"{clean} file(s) clean")
|
|
420
|
+
if skipped:
|
|
421
|
+
parts.append(f"{skipped} skipped (not a Galaxy tool or macro file)")
|
|
422
|
+
if errored:
|
|
423
|
+
parts.append(f"{errored} errored")
|
|
424
|
+
return "; ".join(parts) + "." if parts else "no files checked."
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@main.command(name="check")
|
|
428
|
+
@_PATH_ARGUMENT
|
|
429
|
+
@_QUIET_OPTION
|
|
430
|
+
@_STRICT_OPTION
|
|
431
|
+
@_RULESET_OPTION
|
|
432
|
+
@_SELECT_OPTION
|
|
433
|
+
@_IGNORE_OPTION
|
|
434
|
+
def check_command(
|
|
435
|
+
paths: tuple[Path, ...],
|
|
436
|
+
quiet: bool,
|
|
437
|
+
strict: bool,
|
|
438
|
+
rulesets: tuple[str, ...],
|
|
439
|
+
select: tuple[str, ...],
|
|
440
|
+
ignore: tuple[str, ...],
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Report where tools deviate from the selection, without changing them.
|
|
443
|
+
|
|
444
|
+
Runs the selected rules' detect phases (default ruleset ``default``): *fixable*
|
|
445
|
+
(GTR — what ``format`` would change) and, under ``--ruleset strict``, the
|
|
446
|
+
*advisory* IUC best-practice checks (marked ``(advisory)``). Prints one
|
|
447
|
+
``file:line CODE message`` per finding. Exits non-zero on any *fixable*
|
|
448
|
+
finding or error; advisory findings are informational unless ``--strict``.
|
|
449
|
+
Macro-library files (``<macros>`` root) are also checked, for cosmetic
|
|
450
|
+
(fixable) drift only — the selection governs tools; macro files get the
|
|
451
|
+
standard cosmetic checks. PATHS may be files or directories; other XML is
|
|
452
|
+
skipped.
|
|
453
|
+
"""
|
|
454
|
+
codes = _resolve(rulesets=rulesets, select=select, ignore=ignore)
|
|
455
|
+
fixable = advisory = flagged = clean = skipped = errored = 0
|
|
456
|
+
for target in iter_targets(paths):
|
|
457
|
+
try:
|
|
458
|
+
original = target.read_bytes()
|
|
459
|
+
except OSError as error:
|
|
460
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
461
|
+
errored += 1
|
|
462
|
+
continue
|
|
463
|
+
# Each finding is a (violation, is_advisory) pair. Tool files run the
|
|
464
|
+
# full selected detect (fixable GTR + advisory); macro files run the
|
|
465
|
+
# cosmetic macro rules only (all fixable). Other XML is skipped.
|
|
466
|
+
if is_tool_root(original):
|
|
467
|
+
try:
|
|
468
|
+
tool_document = load_tool(original)
|
|
469
|
+
except ToolXmlSyntaxError as error:
|
|
470
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
471
|
+
errored += 1
|
|
472
|
+
continue
|
|
473
|
+
if tool_document.root.tag != "tool":
|
|
474
|
+
skipped += 1
|
|
475
|
+
continue
|
|
476
|
+
result = facade.detect(tool_document, codes=codes)
|
|
477
|
+
findings = [(v, result.is_advisory(v)) for v in result.violations]
|
|
478
|
+
elif is_macros_root(original):
|
|
479
|
+
try:
|
|
480
|
+
macro_document = load_macros(original)
|
|
481
|
+
except ToolXmlSyntaxError as error:
|
|
482
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
483
|
+
errored += 1
|
|
484
|
+
continue
|
|
485
|
+
if macro_document.root.tag != "macros":
|
|
486
|
+
skipped += 1
|
|
487
|
+
continue
|
|
488
|
+
# Sort to match the tool path (facade.detect returns line-sorted
|
|
489
|
+
# violations), so `check` output ordering is consistent across kinds.
|
|
490
|
+
macro_violations = sorted(
|
|
491
|
+
detect_macro_document(macro_document),
|
|
492
|
+
key=lambda v: (v.sourceline, v.code),
|
|
493
|
+
)
|
|
494
|
+
findings = [(v, False) for v in macro_violations]
|
|
495
|
+
else:
|
|
496
|
+
skipped += 1
|
|
497
|
+
continue
|
|
498
|
+
if not findings:
|
|
499
|
+
clean += 1
|
|
500
|
+
continue
|
|
501
|
+
flagged += 1
|
|
502
|
+
for violation, is_advisory in findings:
|
|
503
|
+
if is_advisory:
|
|
504
|
+
advisory += 1
|
|
505
|
+
else:
|
|
506
|
+
fixable += 1
|
|
507
|
+
if not quiet:
|
|
508
|
+
suffix = " (advisory)" if is_advisory else ""
|
|
509
|
+
click.echo(
|
|
510
|
+
f"{target}:{violation.sourceline} "
|
|
511
|
+
f"{display_code(violation.code)} {violation.message}{suffix}"
|
|
512
|
+
)
|
|
513
|
+
if not quiet:
|
|
514
|
+
click.echo(
|
|
515
|
+
_check_summary(
|
|
516
|
+
fixable=fixable,
|
|
517
|
+
advisory=advisory,
|
|
518
|
+
flagged=flagged,
|
|
519
|
+
clean=clean,
|
|
520
|
+
skipped=skipped,
|
|
521
|
+
errored=errored,
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
fail = bool(errored or fixable or (strict and advisory))
|
|
525
|
+
sys.exit(1 if fail else 0)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@main.command(name="find-references")
|
|
529
|
+
@click.argument("name")
|
|
530
|
+
@_PATH_ARGUMENT
|
|
531
|
+
@_QUIET_OPTION
|
|
532
|
+
def find_references_command(
|
|
533
|
+
name: str, paths: tuple[Path, ...], quiet: bool
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Report every Cheetah $NAME reference across a tool **and its imported macros**.
|
|
536
|
+
|
|
537
|
+
Read-only query (mutates nothing). For each tool it scans the tool's own
|
|
538
|
+
``<command>``, inline ``<configfile>``\\ s, env vars, output labels and dynamic
|
|
539
|
+
options **plus every macro file it imports** (where a reference frequently lives),
|
|
540
|
+
and prints one ``file:line [section] $ref`` per occurrence whose identifier path
|
|
541
|
+
includes NAME (so ``$NAME``, ``$cond.NAME`` and ``$NAME.ext`` all match). PATHS may
|
|
542
|
+
be files or directories; non-tool XML is skipped. Occurrences are de-duplicated, so
|
|
543
|
+
a macro shared by several scanned tools is reported once. Conservative — may include
|
|
544
|
+
occurrences in comments/``#raw`` (see ``galaxy_tool_source.cheetah_refs``). Non-zero
|
|
545
|
+
exit on errors.
|
|
546
|
+
"""
|
|
547
|
+
total = scanned = skipped = errored = 0
|
|
548
|
+
seen: set[tuple[str, int, str, str]] = set()
|
|
549
|
+
for target in iter_targets(paths):
|
|
550
|
+
try:
|
|
551
|
+
original = target.read_bytes()
|
|
552
|
+
except OSError as error:
|
|
553
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
554
|
+
errored += 1
|
|
555
|
+
continue
|
|
556
|
+
if not is_tool_root(original):
|
|
557
|
+
skipped += 1
|
|
558
|
+
continue
|
|
559
|
+
try:
|
|
560
|
+
result = find_references_in_bundle(target, name=name)
|
|
561
|
+
except ToolXmlSyntaxError as error:
|
|
562
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
563
|
+
errored += 1
|
|
564
|
+
continue
|
|
565
|
+
scanned += 1
|
|
566
|
+
for ref in result.references:
|
|
567
|
+
key = (str(ref.path), ref.sourceline, ref.section, ref.reference)
|
|
568
|
+
if key in seen:
|
|
569
|
+
continue
|
|
570
|
+
seen.add(key)
|
|
571
|
+
total += 1
|
|
572
|
+
if not quiet:
|
|
573
|
+
click.echo(
|
|
574
|
+
f"{ref.path}:{ref.sourceline} [{ref.section}] {ref.reference}"
|
|
575
|
+
)
|
|
576
|
+
if not quiet:
|
|
577
|
+
click.echo(f"{total} reference(s) to '{name}' across {scanned} tool(s)")
|
|
578
|
+
sys.exit(1 if errored else 0)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _report_rename_skip(
|
|
582
|
+
result: BundleRenameResult, target: Path, *, quiet: bool
|
|
583
|
+
) -> None:
|
|
584
|
+
"""Print an informative skip line for a non-applied rename.
|
|
585
|
+
|
|
586
|
+
``not-found`` is the common case (the tool has no such param) — it stays silent.
|
|
587
|
+
"""
|
|
588
|
+
if result.reason == "not-found" or quiet:
|
|
589
|
+
return
|
|
590
|
+
if result.reason == "macro-edit-needs-repo-root":
|
|
591
|
+
click.echo(
|
|
592
|
+
f"skip {target}: '{result.old}' is referenced in an imported macro; "
|
|
593
|
+
"rerun with --repo-root DIR to prove the macro is sole-owned"
|
|
594
|
+
)
|
|
595
|
+
return
|
|
596
|
+
if result.reason == "macro-ownership-unprovable":
|
|
597
|
+
names = ", ".join(str(macro) for macro in result.unprovable)
|
|
598
|
+
click.echo(
|
|
599
|
+
f"skip {target}: cannot prove macro file(s) {names} are sole-owned within "
|
|
600
|
+
f"--repo-root (is {target} under the given --repo-root?)"
|
|
601
|
+
)
|
|
602
|
+
return
|
|
603
|
+
if result.reason == "shared-macro":
|
|
604
|
+
names = ", ".join(str(skip.macro_file) for skip in result.shared)
|
|
605
|
+
click.echo(
|
|
606
|
+
f"skip {target}: '{result.old}' is referenced in shared macro file(s) "
|
|
607
|
+
f"{names}; editing them would affect other tools (rename not applied)"
|
|
608
|
+
)
|
|
609
|
+
for skip in result.shared:
|
|
610
|
+
others = ", ".join(str(path) for path in skip.other_importers)
|
|
611
|
+
click.echo(f" {skip.macro_file} also imported by: {others}")
|
|
612
|
+
return
|
|
613
|
+
click.echo(f"skip {target}: {result.reason}")
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _report_consensus_skip(
|
|
617
|
+
result: ConsensusRenameResult, target: Path, *, quiet: bool
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Print an informative skip line for a non-applied consensus rename."""
|
|
620
|
+
if result.reason == "not-found" or quiet:
|
|
621
|
+
return
|
|
622
|
+
if result.reason == "no-consensus":
|
|
623
|
+
click.echo(
|
|
624
|
+
f"skip {target}: cannot rename '{result.old}' across importers — "
|
|
625
|
+
"these tools cannot rename it safely:"
|
|
626
|
+
)
|
|
627
|
+
for tool, reason in result.dissenting:
|
|
628
|
+
click.echo(f" {tool}: {reason}")
|
|
629
|
+
return
|
|
630
|
+
if result.reason == "macro-ownership-unprovable":
|
|
631
|
+
click.echo(
|
|
632
|
+
f"skip {target}: a shared macro is not covered by --repo-root; "
|
|
633
|
+
"point --repo-root at the repository that holds every importer"
|
|
634
|
+
)
|
|
635
|
+
return
|
|
636
|
+
click.echo(f"skip {target}: {result.reason}")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _run_consensus_rename(
|
|
640
|
+
paths: tuple[Path, ...],
|
|
641
|
+
*,
|
|
642
|
+
old: str,
|
|
643
|
+
new: str,
|
|
644
|
+
importers: Mapping[Path, frozenset[Path]],
|
|
645
|
+
check: bool,
|
|
646
|
+
backup: bool,
|
|
647
|
+
quiet: bool,
|
|
648
|
+
) -> tuple[int, int, int, int]:
|
|
649
|
+
"""Run the lockstep across-importers rename.
|
|
650
|
+
|
|
651
|
+
Returns the ``(renamed, would_change, skipped, errored)`` counts.
|
|
652
|
+
"""
|
|
653
|
+
processed: set[Path] = set()
|
|
654
|
+
renamed = would_change = skipped = errored = 0
|
|
655
|
+
for target in iter_targets(paths):
|
|
656
|
+
try:
|
|
657
|
+
original = target.read_bytes()
|
|
658
|
+
except OSError as error:
|
|
659
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
660
|
+
errored += 1
|
|
661
|
+
continue
|
|
662
|
+
if not is_tool_root(original):
|
|
663
|
+
skipped += 1
|
|
664
|
+
continue
|
|
665
|
+
if target.resolve() in processed:
|
|
666
|
+
continue # already rewritten as part of an earlier consensus group
|
|
667
|
+
try:
|
|
668
|
+
result = rename_param_consensus(
|
|
669
|
+
target, old=old, new=new, importers=importers,
|
|
670
|
+
write=not check, backup=backup,
|
|
671
|
+
)
|
|
672
|
+
except ToolXmlSyntaxError as error:
|
|
673
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
674
|
+
errored += 1
|
|
675
|
+
continue
|
|
676
|
+
processed.add(target.resolve())
|
|
677
|
+
processed.update(result.tools)
|
|
678
|
+
if not result.changed:
|
|
679
|
+
_report_consensus_skip(result, target, quiet=quiet)
|
|
680
|
+
skipped += 1
|
|
681
|
+
continue
|
|
682
|
+
sites = sum(edit.renamed for edit in result.edits)
|
|
683
|
+
summary = (
|
|
684
|
+
f"{len(result.tools)} tool(s), {len(result.edits)} file(s), {sites} site(s)"
|
|
685
|
+
)
|
|
686
|
+
if check:
|
|
687
|
+
would_change += 1
|
|
688
|
+
if not quiet:
|
|
689
|
+
click.echo(f"would rename across importers from {target}: {summary}")
|
|
690
|
+
else:
|
|
691
|
+
renamed += 1
|
|
692
|
+
if not quiet:
|
|
693
|
+
click.echo(f"renamed across importers from {target}: {summary}")
|
|
694
|
+
return renamed, would_change, skipped, errored
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
@main.command(name="rename-param")
|
|
698
|
+
@click.argument("old")
|
|
699
|
+
@click.argument("new")
|
|
700
|
+
@_PATH_ARGUMENT
|
|
701
|
+
@_REPO_ROOT_OPTION
|
|
702
|
+
@_ACROSS_IMPORTERS_OPTION
|
|
703
|
+
@_CHECK_OPTION
|
|
704
|
+
@_BACKUP_OPTION
|
|
705
|
+
@_QUIET_OPTION
|
|
706
|
+
def rename_param_command(
|
|
707
|
+
old: str,
|
|
708
|
+
new: str,
|
|
709
|
+
paths: tuple[Path, ...],
|
|
710
|
+
repo_root: Path | None,
|
|
711
|
+
across_importers: bool,
|
|
712
|
+
check: bool,
|
|
713
|
+
backup: bool,
|
|
714
|
+
quiet: bool,
|
|
715
|
+
) -> None:
|
|
716
|
+
"""Rename parameter OLD to NEW across a tool **and its imported macro files**.
|
|
717
|
+
|
|
718
|
+
The mutating sibling of ``find-references``. Rewrites every live ``$OLD`` reference
|
|
719
|
+
(``<command>`` / inline ``<configfile>`` via the faithful lexer, attribute-Cheetah,
|
|
720
|
+
by-name cross-reference attributes, and the ``<tests>`` mirrors) plus the
|
|
721
|
+
definition — across the tool **and every macro file it imports**, so a reference
|
|
722
|
+
that lives only in an imported macro is no longer left dangling.
|
|
723
|
+
|
|
724
|
+
Rename is **atomic across the bundle**: every member is rewritten or none is. A
|
|
725
|
+
tool is skipped with a reason when the rename cannot be proven safe (e.g. a ``#set``
|
|
726
|
+
local shadows OLD, a section is mixed-content, or an output ``<filter>`` references
|
|
727
|
+
OLD by bare Python name). Editing an imported macro
|
|
728
|
+
requires ``--repo-root`` to prove the macro is **sole-owned** (imported by no other
|
|
729
|
+
tool); a macro **shared** with another tool is reported and the rename is skipped —
|
|
730
|
+
unless ``--across-importers`` is given, which renames OLD across *every* importer of
|
|
731
|
+
the shared macro in lockstep (only when they all agree). PATHS may be files or
|
|
732
|
+
directories; non-tool XML is skipped. ``--check`` previews without writing and exits
|
|
733
|
+
non-zero if any file would change.
|
|
734
|
+
"""
|
|
735
|
+
if not old.isidentifier() or not new.isidentifier():
|
|
736
|
+
raise click.BadParameter("OLD and NEW must be valid identifiers")
|
|
737
|
+
if across_importers:
|
|
738
|
+
if repo_root is None:
|
|
739
|
+
raise click.BadParameter(
|
|
740
|
+
"--across-importers requires --repo-root to find every importer",
|
|
741
|
+
param_hint="--across-importers",
|
|
742
|
+
)
|
|
743
|
+
renamed, would_change, skipped, errored = _run_consensus_rename(
|
|
744
|
+
paths, old=old, new=new, importers=build_importer_map(repo_root),
|
|
745
|
+
check=check, backup=backup, quiet=quiet,
|
|
746
|
+
)
|
|
747
|
+
if not quiet:
|
|
748
|
+
done = would_change if check else renamed
|
|
749
|
+
verb = "would rename" if check else "renamed"
|
|
750
|
+
click.echo(f"{verb} {done} consensus group(s); skipped {skipped}")
|
|
751
|
+
sys.exit(1 if errored or (check and would_change) else 0)
|
|
752
|
+
importers = build_importer_map(repo_root) if repo_root is not None else None
|
|
753
|
+
renamed = would_change = skipped = errored = 0
|
|
754
|
+
for target in iter_targets(paths):
|
|
755
|
+
try:
|
|
756
|
+
original = target.read_bytes()
|
|
757
|
+
except OSError as error:
|
|
758
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
759
|
+
errored += 1
|
|
760
|
+
continue
|
|
761
|
+
if not is_tool_root(original):
|
|
762
|
+
skipped += 1
|
|
763
|
+
continue
|
|
764
|
+
try:
|
|
765
|
+
result = rename_param_bundle(
|
|
766
|
+
target,
|
|
767
|
+
old=old,
|
|
768
|
+
new=new,
|
|
769
|
+
importers=importers,
|
|
770
|
+
write=not check,
|
|
771
|
+
backup=backup,
|
|
772
|
+
)
|
|
773
|
+
except ToolXmlSyntaxError as error:
|
|
774
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
775
|
+
errored += 1
|
|
776
|
+
continue
|
|
777
|
+
if not result.changed:
|
|
778
|
+
_report_rename_skip(result, target, quiet=quiet)
|
|
779
|
+
skipped += 1
|
|
780
|
+
continue
|
|
781
|
+
sites = sum(edit.renamed for edit in result.edits)
|
|
782
|
+
files = len(result.edits)
|
|
783
|
+
if check:
|
|
784
|
+
would_change += 1
|
|
785
|
+
if not quiet:
|
|
786
|
+
click.echo(
|
|
787
|
+
f"would rename {target}: {sites} site(s) across {files} file(s)"
|
|
788
|
+
)
|
|
789
|
+
continue
|
|
790
|
+
renamed += 1
|
|
791
|
+
if not quiet:
|
|
792
|
+
click.echo(f"renamed {target}: {sites} site(s) across {files} file(s)")
|
|
793
|
+
if not quiet:
|
|
794
|
+
done = would_change if check else renamed
|
|
795
|
+
verb = "would rename" if check else "renamed"
|
|
796
|
+
click.echo(f"{verb} {done} tool(s); skipped {skipped}")
|
|
797
|
+
sys.exit(1 if errored or (check and would_change) else 0)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
@main.command(name="rulesets")
|
|
801
|
+
def rulesets_command() -> None:
|
|
802
|
+
"""List the available rulesets and the rule codes each one selects."""
|
|
803
|
+
for info in facade.list_rulesets():
|
|
804
|
+
default = " (default)" if info.is_default else ""
|
|
805
|
+
click.echo(f"{info.name}{default}: {info.description}")
|
|
806
|
+
click.echo(f" rules: {', '.join(info.codes)}")
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@main.command(name="rules")
|
|
810
|
+
@click.option(
|
|
811
|
+
"--include-upgrade",
|
|
812
|
+
is_flag=True,
|
|
813
|
+
help=(
|
|
814
|
+
"Also list the non-selectable codemods: the upgrade-pipeline steps and "
|
|
815
|
+
"the opt-in-command-only rules (e.g. GTR092, applied by convert-help)."
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
def rules_command(include_upgrade: bool) -> None:
|
|
819
|
+
"""List the baked-in rules: code, family, fixable/advisory, rulesets, planemo.
|
|
820
|
+
|
|
821
|
+
The ``planemo:`` field lists the planemo (``galaxy.tool_util.lint``) linter(s)
|
|
822
|
+
each rule covers — those names also work in ``--select`` / ``--ignore``.
|
|
823
|
+
"""
|
|
824
|
+
for info in facade.list_rules(include_upgrade=include_upgrade):
|
|
825
|
+
kind = "fixable" if info.fixable else "advisory"
|
|
826
|
+
in_rulesets = ",".join(info.rulesets) if info.rulesets else "-"
|
|
827
|
+
planemo = ",".join(info.planemo_linters) if info.planemo_linters else "-"
|
|
828
|
+
click.echo(
|
|
829
|
+
f"{info.code} [{info.family}/{kind}] rulesets:{in_rulesets} "
|
|
830
|
+
f"planemo:{planemo} {info.summary}"
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _collect_macro_files(paths: tuple[Path, ...], /) -> list[Path]:
|
|
835
|
+
"""Resolve *paths* (files and/or directories) to ``<macros>``-root files.
|
|
836
|
+
|
|
837
|
+
A directory is searched recursively for ``*.xml`` whose root opens ``<macros``;
|
|
838
|
+
a file is included only when it is itself a macro-library file. De-duplicated by
|
|
839
|
+
resolved path and returned in a stable (sorted) order for deterministic output.
|
|
840
|
+
"""
|
|
841
|
+
found: list[Path] = []
|
|
842
|
+
seen: set[Path] = set()
|
|
843
|
+
for path in paths:
|
|
844
|
+
candidates = sorted(path.rglob("*.xml")) if path.is_dir() else [path]
|
|
845
|
+
for candidate in candidates:
|
|
846
|
+
resolved = candidate.resolve()
|
|
847
|
+
if resolved in seen or not candidate.is_file():
|
|
848
|
+
continue
|
|
849
|
+
seen.add(resolved)
|
|
850
|
+
if is_macros_root(candidate.read_bytes()):
|
|
851
|
+
found.append(candidate)
|
|
852
|
+
return found
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@main.command(name="normalize-macros")
|
|
856
|
+
@click.argument(
|
|
857
|
+
"paths", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path)
|
|
858
|
+
)
|
|
859
|
+
@click.option(
|
|
860
|
+
"--check", is_flag=True, help="Report what would change and write nothing."
|
|
861
|
+
)
|
|
862
|
+
@_BACKUP_OPTION
|
|
863
|
+
def normalize_macros_command(
|
|
864
|
+
paths: tuple[Path, ...], check: bool, backup: bool
|
|
865
|
+
) -> None:
|
|
866
|
+
"""Normalize literal format/ftype in macro-library files (opt-in, repo-scoped).
|
|
867
|
+
|
|
868
|
+
Lowercases literal ``format`` / ``ftype`` datatype tokens (leaving ``@TOKEN@``
|
|
869
|
+
placeholders alone) in every ``<macros>``-root file found under PATHS — the
|
|
870
|
+
macro-library analog of the 24.2 normalization the per-tool ``upgrade`` cannot
|
|
871
|
+
reach (a value defined in an imported macro file). Unlike ``format`` / ``upgrade``
|
|
872
|
+
this rewrites files other than the one named — a shared macro file affects every
|
|
873
|
+
importer — so it is a deliberate, separate command, never part of ``format``.
|
|
874
|
+
"""
|
|
875
|
+
result = normalize_macro_files(
|
|
876
|
+
_collect_macro_files(paths), write=not check, backup=backup
|
|
877
|
+
)
|
|
878
|
+
verb = "would normalize" if check else "normalized"
|
|
879
|
+
for edit in result.edits:
|
|
880
|
+
click.echo(f"{verb} {edit.macro_file} ({edit.elements_changed} element(s))")
|
|
881
|
+
for bad in result.unparseable:
|
|
882
|
+
click.echo(f"skipped (could not parse): {bad}", err=True)
|
|
883
|
+
if not result.edits:
|
|
884
|
+
click.echo("no macro-library files needed normalization")
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
@main.command(name="convert-help")
|
|
888
|
+
@click.argument(
|
|
889
|
+
"paths", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path)
|
|
890
|
+
)
|
|
891
|
+
@click.option(
|
|
892
|
+
"--check", is_flag=True, help="Report what would convert and write nothing."
|
|
893
|
+
)
|
|
894
|
+
@_BACKUP_OPTION
|
|
895
|
+
def convert_help_command(paths: tuple[Path, ...], check: bool, backup: bool) -> None:
|
|
896
|
+
"""Convert RST <help> bodies to Markdown (opt-in, render-equivalence gated).
|
|
897
|
+
|
|
898
|
+
Rewrites a tool's reStructuredText ``<help>`` as Markdown and marks it
|
|
899
|
+
``format="markdown"`` — only when the conversion is *provable*: the tool's
|
|
900
|
+
profile must be >= 24.2 (``<help format=…>`` is not XSD-valid earlier — run
|
|
901
|
+
``upgrade`` first), and the markdown-it rendering must be semantically equal
|
|
902
|
+
to the docutils rendering (invalid RST is first passed through the GTR089.1
|
|
903
|
+
repair). Anything unprovable is skipped with the reason. This conversion
|
|
904
|
+
swaps Galaxy's rendering engine (server-side docutils -> client-side
|
|
905
|
+
markdown-it), which is why it is a deliberate, separate command — never part
|
|
906
|
+
of ``format``/``upgrade``. Needs the ``galaxy-tool-source[markdown]`` extra.
|
|
907
|
+
"""
|
|
908
|
+
converted = skipped = errored = 0
|
|
909
|
+
for target in iter_targets(paths):
|
|
910
|
+
try:
|
|
911
|
+
original = target.read_bytes()
|
|
912
|
+
except OSError as error:
|
|
913
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
914
|
+
errored += 1
|
|
915
|
+
continue
|
|
916
|
+
if not is_tool_root(original):
|
|
917
|
+
continue
|
|
918
|
+
try:
|
|
919
|
+
document = load_tool(original)
|
|
920
|
+
except ToolXmlSyntaxError as error:
|
|
921
|
+
click.echo(f"error: {target}: malformed XML: {error}", err=True)
|
|
922
|
+
errored += 1
|
|
923
|
+
continue
|
|
924
|
+
result = facade.convert_help(document)
|
|
925
|
+
if result.converted:
|
|
926
|
+
converted += 1
|
|
927
|
+
if not check:
|
|
928
|
+
if backup:
|
|
929
|
+
make_backup(target)
|
|
930
|
+
target.write_bytes(result.formatted)
|
|
931
|
+
click.echo(f"{'would convert' if check else 'converted'} {target}")
|
|
932
|
+
else:
|
|
933
|
+
skipped += 1
|
|
934
|
+
click.echo(f"skipped {target}: {result.skip_reason}")
|
|
935
|
+
click.echo(
|
|
936
|
+
f"{converted} converted, {skipped} skipped"
|
|
937
|
+
+ (f", {errored} error(s)" if errored else "")
|
|
938
|
+
)
|
|
939
|
+
if errored:
|
|
940
|
+
raise SystemExit(1)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
@main.command(name="tokenize-version")
|
|
944
|
+
@click.argument(
|
|
945
|
+
"paths", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path)
|
|
946
|
+
)
|
|
947
|
+
@click.option(
|
|
948
|
+
"--check", is_flag=True, help="Report what would tokenize and write nothing."
|
|
949
|
+
)
|
|
950
|
+
@click.option(
|
|
951
|
+
"--macros-file",
|
|
952
|
+
default=None,
|
|
953
|
+
metavar="NAME",
|
|
954
|
+
help=(
|
|
955
|
+
"Put the two tokens in a separate macros file NAME (e.g. macros.xml) the "
|
|
956
|
+
"tool imports, instead of an inline <macros> block (the default). NAME is "
|
|
957
|
+
"created when absent, or the tokens are merged into an existing NAME when "
|
|
958
|
+
"proven not to change any other importer; tools in a directory that share "
|
|
959
|
+
"NAME at the same version are tokenized together."
|
|
960
|
+
),
|
|
961
|
+
)
|
|
962
|
+
@click.option(
|
|
963
|
+
"--adopt-suffix",
|
|
964
|
+
is_flag=True,
|
|
965
|
+
help=(
|
|
966
|
+
"IDENTITY-CHANGING: for a tool whose bare version equals a package "
|
|
967
|
+
"<requirement> but has no +galaxy suffix, ADD +galaxy0 and tokenize. This "
|
|
968
|
+
"changes the published version (1.20 -> 1.20+galaxy0); use only when you are "
|
|
969
|
+
"intentionally adopting the convention. Inline only; not combinable with "
|
|
970
|
+
"--macros-file."
|
|
971
|
+
),
|
|
972
|
+
)
|
|
973
|
+
@_BACKUP_OPTION
|
|
974
|
+
def tokenize_version_command(
|
|
975
|
+
paths: tuple[Path, ...],
|
|
976
|
+
check: bool,
|
|
977
|
+
macros_file: str | None,
|
|
978
|
+
adopt_suffix: bool,
|
|
979
|
+
backup: bool,
|
|
980
|
+
) -> None:
|
|
981
|
+
"""Factor a literal version into @TOOL_VERSION@/@VERSION_SUFFIX@ (opt-in, gated).
|
|
982
|
+
|
|
983
|
+
Rewrites ``version="<base>+galaxy<suffix>"`` as
|
|
984
|
+
``@TOOL_VERSION@+galaxy@VERSION_SUFFIX@``, retargets the matching package
|
|
985
|
+
``<requirement>`` versions to ``@TOOL_VERSION@``, and defines the two
|
|
986
|
+
tokens in the tool's inline ``<macros>`` (or, with ``--macros-file``, in a
|
|
987
|
+
separate macros file the tool imports), only when *provable*: the
|
|
988
|
+
expansion-equality gate keeps the change solely when macro-expanding the
|
|
989
|
+
tokenized tool reproduces the original expansion byte-for-byte. Anything
|
|
990
|
+
unprovable is skipped with the reason. A multi-element style restructure,
|
|
991
|
+
which is why it is a deliberate, separate command, never part of
|
|
992
|
+
``format``/``upgrade``. Files are passed by path so imported macros resolve.
|
|
993
|
+
|
|
994
|
+
``--adopt-suffix`` is the **identity-changing** sibling: for a tool whose *bare*
|
|
995
|
+
version equals a package requirement, it adds ``+galaxy0`` (so ``1.20`` becomes
|
|
996
|
+
``1.20+galaxy0``) and tokenizes. The published version changes, so it is opt-in and
|
|
997
|
+
gated only on the controlled-change gate (the expansion changes solely in the
|
|
998
|
+
version attribute).
|
|
999
|
+
"""
|
|
1000
|
+
if adopt_suffix and macros_file is not None:
|
|
1001
|
+
click.echo(
|
|
1002
|
+
"error: --adopt-suffix cannot be combined with --macros-file", err=True
|
|
1003
|
+
)
|
|
1004
|
+
raise SystemExit(1)
|
|
1005
|
+
if adopt_suffix:
|
|
1006
|
+
_run_adopt_suffix(paths, check=check, backup=backup)
|
|
1007
|
+
return
|
|
1008
|
+
if macros_file is not None:
|
|
1009
|
+
_run_tokenize_shared(paths, macros_file=macros_file, check=check, backup=backup)
|
|
1010
|
+
return
|
|
1011
|
+
tokenized = skipped = errored = 0
|
|
1012
|
+
for target in iter_targets(paths):
|
|
1013
|
+
try:
|
|
1014
|
+
original = target.read_bytes()
|
|
1015
|
+
except OSError as error:
|
|
1016
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
1017
|
+
errored += 1
|
|
1018
|
+
continue
|
|
1019
|
+
if not is_tool_root(original):
|
|
1020
|
+
continue
|
|
1021
|
+
# Pass the PATH (not bytes): the expansion gate resolves <import>ed
|
|
1022
|
+
# macro files against the tool's own directory.
|
|
1023
|
+
result = facade.tokenize_version(target)
|
|
1024
|
+
if result.tokenized:
|
|
1025
|
+
tokenized += 1
|
|
1026
|
+
if not check:
|
|
1027
|
+
if backup:
|
|
1028
|
+
make_backup(target)
|
|
1029
|
+
target.write_bytes(result.formatted)
|
|
1030
|
+
verb = "would tokenize" if check else "tokenized"
|
|
1031
|
+
click.echo(f"{verb} {target}")
|
|
1032
|
+
else:
|
|
1033
|
+
skipped += 1
|
|
1034
|
+
click.echo(f"skipped {target}: {result.skip_reason}")
|
|
1035
|
+
click.echo(
|
|
1036
|
+
f"{tokenized} tokenized, {skipped} skipped"
|
|
1037
|
+
+ (f", {errored} error(s)" if errored else "")
|
|
1038
|
+
)
|
|
1039
|
+
if errored:
|
|
1040
|
+
raise SystemExit(1)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _run_adopt_suffix(
|
|
1044
|
+
paths: tuple[Path, ...], *, check: bool, backup: bool
|
|
1045
|
+
) -> None:
|
|
1046
|
+
"""``tokenize-version --adopt-suffix``: add +galaxy0 to a bare version, tokenize.
|
|
1047
|
+
|
|
1048
|
+
Identity-changing (the published version changes), so each applied tool is
|
|
1049
|
+
reported loudly. Gated per tool by the controlled-change gate.
|
|
1050
|
+
"""
|
|
1051
|
+
adopted = skipped = errored = 0
|
|
1052
|
+
for target in iter_targets(paths):
|
|
1053
|
+
try:
|
|
1054
|
+
original = target.read_bytes()
|
|
1055
|
+
except OSError as error:
|
|
1056
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
1057
|
+
errored += 1
|
|
1058
|
+
continue
|
|
1059
|
+
if not is_tool_root(original):
|
|
1060
|
+
continue
|
|
1061
|
+
result = facade.adopt_version_suffix(target)
|
|
1062
|
+
if result.tokenized:
|
|
1063
|
+
adopted += 1
|
|
1064
|
+
if not check:
|
|
1065
|
+
if backup:
|
|
1066
|
+
make_backup(target)
|
|
1067
|
+
target.write_bytes(result.formatted)
|
|
1068
|
+
verb = "would adopt" if check else "adopted"
|
|
1069
|
+
click.echo(f"{verb} +galaxy0 in {target} (published version changed)")
|
|
1070
|
+
else:
|
|
1071
|
+
skipped += 1
|
|
1072
|
+
click.echo(f"skipped {target}: {result.skip_reason}")
|
|
1073
|
+
click.echo(
|
|
1074
|
+
f"{adopted} adopted, {skipped} skipped"
|
|
1075
|
+
+ (f", {errored} error(s)" if errored else "")
|
|
1076
|
+
)
|
|
1077
|
+
if errored:
|
|
1078
|
+
raise SystemExit(1)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def _run_tokenize_shared(
|
|
1082
|
+
paths: tuple[Path, ...], *, macros_file: str, check: bool, backup: bool
|
|
1083
|
+
) -> None:
|
|
1084
|
+
"""``tokenize-version --macros-file``: group tools by directory, tokenize each set.
|
|
1085
|
+
|
|
1086
|
+
Each directory's target tools that share ``macros_file`` at the same version are
|
|
1087
|
+
tokenized together (consensus), defining the shared tokens once. See
|
|
1088
|
+
``galaxy_tool_refactor_registry.version_token_share``.
|
|
1089
|
+
"""
|
|
1090
|
+
if "/" in macros_file or "\\" in macros_file or macros_file in {"", ".", ".."}:
|
|
1091
|
+
click.echo(
|
|
1092
|
+
f"error: --macros-file must be a plain filename, not {macros_file!r}",
|
|
1093
|
+
err=True,
|
|
1094
|
+
)
|
|
1095
|
+
raise SystemExit(1)
|
|
1096
|
+
groups: dict[Path, list[Path]] = defaultdict(list)
|
|
1097
|
+
errored = 0
|
|
1098
|
+
for target in iter_targets(paths):
|
|
1099
|
+
try:
|
|
1100
|
+
raw = target.read_bytes()
|
|
1101
|
+
except OSError as error:
|
|
1102
|
+
click.echo(f"error: cannot read {target}: {error}", err=True)
|
|
1103
|
+
errored += 1
|
|
1104
|
+
continue
|
|
1105
|
+
if is_tool_root(raw):
|
|
1106
|
+
groups[target.parent].append(target)
|
|
1107
|
+
tokenized = skipped = 0
|
|
1108
|
+
verb = "would tokenize" if check else "tokenized"
|
|
1109
|
+
for directory, tools in sorted(groups.items()):
|
|
1110
|
+
plan = facade.tokenize_version_shared(
|
|
1111
|
+
directory / macros_file, target_tools=tools
|
|
1112
|
+
)
|
|
1113
|
+
for tool_path, reason in plan.skipped:
|
|
1114
|
+
click.echo(f"skipped {tool_path}: {reason}")
|
|
1115
|
+
skipped += 1
|
|
1116
|
+
if not plan.tool_edits:
|
|
1117
|
+
unreported = len(tools) - len(plan.skipped)
|
|
1118
|
+
if plan.skip_reason is not None and unreported > 0:
|
|
1119
|
+
click.echo(f"skipped {directory} ({macros_file}): {plan.skip_reason}")
|
|
1120
|
+
skipped += unreported
|
|
1121
|
+
continue
|
|
1122
|
+
if not check:
|
|
1123
|
+
if plan.macros_content is not None:
|
|
1124
|
+
if not plan.macros_created and backup:
|
|
1125
|
+
make_backup(plan.macros_path)
|
|
1126
|
+
plan.macros_path.write_bytes(plan.macros_content)
|
|
1127
|
+
for edit in plan.tool_edits:
|
|
1128
|
+
if backup:
|
|
1129
|
+
make_backup(edit.path)
|
|
1130
|
+
edit.path.write_bytes(edit.content)
|
|
1131
|
+
file_note = f"{'created' if plan.macros_created else 'updated'} {macros_file}"
|
|
1132
|
+
for edit in plan.tool_edits:
|
|
1133
|
+
click.echo(f"{verb} {edit.path} (-> {file_note})")
|
|
1134
|
+
tokenized += len(plan.tool_edits)
|
|
1135
|
+
click.echo(
|
|
1136
|
+
f"{tokenized} tokenized, {skipped} skipped"
|
|
1137
|
+
+ (f", {errored} error(s)" if errored else "")
|
|
1138
|
+
)
|
|
1139
|
+
if errored:
|
|
1140
|
+
raise SystemExit(1)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
if __name__ == "__main__":
|
|
1144
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: galaxy-tool-refactor-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Top-level CLI app that composes the Galaxy tool refactoring tiers (format + upgrade).
|
|
5
|
+
Author: Richard Burhans
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: click>=8
|
|
9
|
+
Requires-Dist: galaxy-tool-fmt==0.2.0
|
|
10
|
+
Requires-Dist: galaxy-tool-refactor-registry==0.2.0
|
|
11
|
+
Requires-Dist: galaxy-tool-source==0.2.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# galaxy-tool-refactor-cli
|
|
15
|
+
|
|
16
|
+
The **app tier** of the Galaxy tool refactoring framework — the user-facing
|
|
17
|
+
`galaxy-tool-refactor` CLI, a thin front-end over the tier-3.6 rule-registry
|
|
18
|
+
facade (`galaxy-tool-refactor-registry`).
|
|
19
|
+
|
|
20
|
+
| Tier | Layer | Package |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 0.5 | rule metadata | `galaxy-tool-refactor-rules` |
|
|
23
|
+
| 1 | parsing & validation | `galaxy-tool-source` |
|
|
24
|
+
| 2 | structure | `galaxy-tool-codemod` |
|
|
25
|
+
| 3 | formatting | `galaxy-tool-fmt` |
|
|
26
|
+
| 3.5 | advisory checks | `galaxy-tool-lint` |
|
|
27
|
+
| 3.6 | rule registry / rulesets | `galaxy-tool-refactor-registry` |
|
|
28
|
+
| 4 | **app / CLI** | `galaxy-tool-refactor-cli` *(this package)* |
|
|
29
|
+
|
|
30
|
+
Rule orchestration lives in the registry facade; this package depends on it
|
|
31
|
+
(plus fmt's `cli_support` engine and tier-1 parsing) and exposes ten commands
|
|
32
|
+
(`format`, `upgrade`, `check`, `find-references`, `rename-param`, `rulesets`, `rules`,
|
|
33
|
+
`normalize-macros`):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Safe, idempotent: apply a ruleset's fixable rules + cosmetic formatting.
|
|
37
|
+
# Default ruleset = structural canonicalisation + cosmetic; never profile=.
|
|
38
|
+
galaxy-tool-refactor format tool.xml
|
|
39
|
+
galaxy-tool-refactor format --ruleset cosmetic tool.xml # whitespace only
|
|
40
|
+
galaxy-tool-refactor format --ignore GTR002 tool.xml # all but param-reorder
|
|
41
|
+
galaxy-tool-refactor format tools/ # also formats <macros> files
|
|
42
|
+
|
|
43
|
+
# Opt-in, semantic: repair typos, then upgrade profile= to the latest reachable
|
|
44
|
+
# version (applying each step's structural migration), then format. Reports the
|
|
45
|
+
# steps applied and warns if a tool stalls. No --ruleset; --select/--ignore tune it.
|
|
46
|
+
galaxy-tool-refactor upgrade tool.xml
|
|
47
|
+
|
|
48
|
+
# Report-only linter: one `file:line CODE message` per finding, mutating
|
|
49
|
+
# nothing. The default ruleset reports the fixable GTR rules; `--ruleset strict` adds
|
|
50
|
+
# the advisory checks (marked `(advisory)`). Exits non-zero on any fixable
|
|
51
|
+
# finding; advisory findings are informational unless --strict.
|
|
52
|
+
galaxy-tool-refactor check tool.xml
|
|
53
|
+
galaxy-tool-refactor check --ruleset strict tool.xml
|
|
54
|
+
|
|
55
|
+
# Introspection.
|
|
56
|
+
galaxy-tool-refactor rulesets
|
|
57
|
+
galaxy-tool-refactor rules
|
|
58
|
+
|
|
59
|
+
# Opt-in, repo-scoped: lowercase literal format/ftype in <macros>-root files (the
|
|
60
|
+
# macro-library fix the per-tool `upgrade` can't reach). Rewrites files other than
|
|
61
|
+
# the one named, so it is a separate command — never part of format/upgrade.
|
|
62
|
+
galaxy-tool-refactor normalize-macros macros/ # --check to preview
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`format`/`upgrade`/`check` share rule selection — `--ruleset NAME`
|
|
66
|
+
(repeatable / comma-separated — the union of the named sets),
|
|
67
|
+
`--select CODE…`, `--ignore CODE…` (ruff-style precedence: `--ignore` ▸
|
|
68
|
+
`--select` ▸ `--ruleset`; `--select` replaces the rulesets' set; `upgrade` takes no
|
|
69
|
+
`--ruleset`). `format`/`upgrade` also honour `--check` (detect drift, exit
|
|
70
|
+
non-zero, don't write — distinct from the `check` *command*), `--diff`, and
|
|
71
|
+
`--quiet`; `check` honours `--quiet` and `--strict`. The typical modernization
|
|
72
|
+
flow is `upgrade` then `format`.
|
|
73
|
+
|
|
74
|
+
## Why a separate tier
|
|
75
|
+
|
|
76
|
+
Profile upgrade is semantic, fallible, and reports outcomes; canonicalisation +
|
|
77
|
+
formatting is safe and idempotent. Keeping them in separate, explicit commands
|
|
78
|
+
(rather than auto-upgrading inside "format my tool") lets users opt into
|
|
79
|
+
modernization deliberately. Rule orchestration sits *below* the CLI in the
|
|
80
|
+
registry facade — both because output is written via fmt's serializer (so the
|
|
81
|
+
orchestrator must sit above fmt) and so the MCP server reuses the same
|
|
82
|
+
core. See `docs/decisions.md` §D1 (the app tier), §D2 (`check`), §D3 (advisory
|
|
83
|
+
findings), §D4 (the registry facade + rule selection).
|
|
84
|
+
|
|
85
|
+
## Install / test
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv sync # from the workspace root
|
|
89
|
+
uv run --package galaxy-tool-refactor-cli pytest galaxy-tool-refactor-cli/tests/
|
|
90
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
galaxy_tool_refactor_cli/__init__.py,sha256=qxO1BbNJw4OJFxKNkst-q12J9QJe2-Jjfh4aNgluA2A,1253
|
|
2
|
+
galaxy_tool_refactor_cli/cli.py,sha256=Wv0XknNk4SjihrR-SOG5AR9n2CwUPGnMYPaCMIK2sik,45659
|
|
3
|
+
galaxy_tool_refactor_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
galaxy_tool_refactor_cli-0.2.0.dist-info/METADATA,sha256=sQTh6yDTtEeSGj9Avft-05QJs9ukTU5QDgAymeuZeJs,4182
|
|
5
|
+
galaxy_tool_refactor_cli-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
galaxy_tool_refactor_cli-0.2.0.dist-info/entry_points.txt,sha256=X003rXqXxy03bXBufCx2HIn655ONx5wUTqXf_Rqit2o,75
|
|
7
|
+
galaxy_tool_refactor_cli-0.2.0.dist-info/RECORD,,
|