topmark 0.2.0__tar.gz → 0.2.1__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.
- {topmark-0.2.0/src/topmark.egg-info → topmark-0.2.1}/PKG-INFO +11 -2
- {topmark-0.2.0 → topmark-0.2.1}/README.md +10 -1
- {topmark-0.2.0 → topmark-0.2.1}/pyproject.toml +1 -1
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/default.py +44 -52
- topmark-0.2.1/src/topmark/cli/commands/strip.py +331 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/main.py +28 -1
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/utils.py +51 -40
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/file_resolver.py +62 -24
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/context.py +54 -8
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/pipelines.py +13 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/base.py +194 -98
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/pound.py +9 -11
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/slash.py +9 -10
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/xml.py +53 -26
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/comparer.py +36 -4
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/patcher.py +17 -77
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/reader.py +18 -8
- topmark-0.2.1/src/topmark/pipeline/steps/stripper.py +76 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/updater.py +42 -55
- topmark-0.2.1/src/topmark/utils/diff.py +105 -0
- {topmark-0.2.0 → topmark-0.2.1/src/topmark.egg-info}/PKG-INFO +11 -2
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/SOURCES.txt +3 -0
- {topmark-0.2.0 → topmark-0.2.1}/LICENSE +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/setup.cfg +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/cli_types.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/dump_config.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/filetypes.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/init_config.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/show_defaults.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/version.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/config_resolver.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/options.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/logging.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/topmark-default.toml +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/constants.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/base.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/instances.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/policy.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/registry.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/contracts.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/runner.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/builder.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/renderer.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/resolver.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/scanner.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/py.typed +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/rendering/api.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/rendering/formats.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/utils/__init__.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark/utils/file.py +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/dependency_links.txt +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/entry_points.txt +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/requires.txt +0 -0
- {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: topmark
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A Python CLI to inspect, validate, and manage license and copyright headers.
|
|
5
5
|
Author-email: Olivier Biot <olivier.biot@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -86,7 +86,7 @@ header formats, customizable fields, inclusion/exclusion rules, and dry-run safe
|
|
|
86
86
|
- CI-friendly design for safe automated use
|
|
87
87
|
- Strict static typing with mypy and Pyright, using PEP 604 union syntax
|
|
88
88
|
- Google-style docstrings without redundant type declarations
|
|
89
|
-
-
|
|
89
|
+
- Full header removal (`topmark strip`)
|
|
90
90
|
- Preserves original newline style (LF/CRLF/CR) and BOM
|
|
91
91
|
- Idempotent updates (re-running does not change already-correct files)
|
|
92
92
|
|
|
@@ -119,6 +119,8 @@ topmark [OPTIONS] [PATHS]...
|
|
|
119
119
|
TopMark uses Click 8.2 and supports shell completions. The base command performs a dry‑run *check*
|
|
120
120
|
by default and applies changes when `--apply` is provided.
|
|
121
121
|
|
|
122
|
+
The `strip` subcommand is provided to remove entire headers.
|
|
123
|
+
|
|
122
124
|
Logging verbosity is controlled globally:
|
|
123
125
|
|
|
124
126
|
- `-v`, `--verbose`: Increase verbosity (can be repeated)
|
|
@@ -169,6 +171,12 @@ topmark version
|
|
|
169
171
|
# Apply changes to files in-place
|
|
170
172
|
topmark --apply src/
|
|
171
173
|
|
|
174
|
+
# Remove headers from files (dry-run)
|
|
175
|
+
topmark strip src/
|
|
176
|
+
|
|
177
|
+
# Remove headers from files and apply changes
|
|
178
|
+
topmark strip --apply src/
|
|
179
|
+
|
|
172
180
|
# CI-friendly summary: only show issues; ignore unsupported types
|
|
173
181
|
topmark --skip-compliant --skip-unsupported src/
|
|
174
182
|
|
|
@@ -258,6 +266,7 @@ The following options can be used with most commands.
|
|
|
258
266
|
| --------------- | -------------------------------------------------- |
|
|
259
267
|
| `dump-config` | Show the resolved configuration in TOML format |
|
|
260
268
|
| `filetypes` | List supported file types and their comment styles |
|
|
269
|
+
| `strip` | Remove TopMark headers from files (destructive) |
|
|
261
270
|
| `version` | Print TopMark version |
|
|
262
271
|
| `show-defaults` | Show default config (without merging) |
|
|
263
272
|
| `init-config` | Output a starter configuration file |
|
|
@@ -37,7 +37,7 @@ header formats, customizable fields, inclusion/exclusion rules, and dry-run safe
|
|
|
37
37
|
- CI-friendly design for safe automated use
|
|
38
38
|
- Strict static typing with mypy and Pyright, using PEP 604 union syntax
|
|
39
39
|
- Google-style docstrings without redundant type declarations
|
|
40
|
-
-
|
|
40
|
+
- Full header removal (`topmark strip`)
|
|
41
41
|
- Preserves original newline style (LF/CRLF/CR) and BOM
|
|
42
42
|
- Idempotent updates (re-running does not change already-correct files)
|
|
43
43
|
|
|
@@ -70,6 +70,8 @@ topmark [OPTIONS] [PATHS]...
|
|
|
70
70
|
TopMark uses Click 8.2 and supports shell completions. The base command performs a dry‑run *check*
|
|
71
71
|
by default and applies changes when `--apply` is provided.
|
|
72
72
|
|
|
73
|
+
The `strip` subcommand is provided to remove entire headers.
|
|
74
|
+
|
|
73
75
|
Logging verbosity is controlled globally:
|
|
74
76
|
|
|
75
77
|
- `-v`, `--verbose`: Increase verbosity (can be repeated)
|
|
@@ -120,6 +122,12 @@ topmark version
|
|
|
120
122
|
# Apply changes to files in-place
|
|
121
123
|
topmark --apply src/
|
|
122
124
|
|
|
125
|
+
# Remove headers from files (dry-run)
|
|
126
|
+
topmark strip src/
|
|
127
|
+
|
|
128
|
+
# Remove headers from files and apply changes
|
|
129
|
+
topmark strip --apply src/
|
|
130
|
+
|
|
123
131
|
# CI-friendly summary: only show issues; ignore unsupported types
|
|
124
132
|
topmark --skip-compliant --skip-unsupported src/
|
|
125
133
|
|
|
@@ -209,6 +217,7 @@ The following options can be used with most commands.
|
|
|
209
217
|
| --------------- | -------------------------------------------------- |
|
|
210
218
|
| `dump-config` | Show the resolved configuration in TOML format |
|
|
211
219
|
| `filetypes` | List supported file types and their comment styles |
|
|
220
|
+
| `strip` | Remove TopMark headers from files (destructive) |
|
|
212
221
|
| `version` | Print TopMark version |
|
|
213
222
|
| `show-defaults` | Show default config (without merging) |
|
|
214
223
|
| `init-config` | Output a starter configuration file |
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "topmark"
|
|
13
|
-
version = "0.2.
|
|
13
|
+
version = "0.2.1"
|
|
14
14
|
description = "A Python CLI to inspect, validate, and manage license and copyright headers."
|
|
15
15
|
authors = [{ name = "Olivier Biot", email = "olivier.biot@gmail.com" }]
|
|
16
16
|
license = "MIT"
|
|
@@ -10,23 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
"""Default TopMark operation (check/apply).
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
captured by Click (``ctx.args``), performing a dry‑run **check** by default or
|
|
16
|
-
**applying** changes when ``--apply`` is given.
|
|
13
|
+
Invoked when no explicit subcommand is provided. Performs a dry‑run check by
|
|
14
|
+
default and applies changes when ``--apply`` is given.
|
|
17
15
|
|
|
18
16
|
Examples:
|
|
19
|
-
Check files and print a human summary
|
|
17
|
+
Check files and print a human summary:
|
|
20
18
|
|
|
21
|
-
topmark --summary src
|
|
19
|
+
$ topmark --summary src
|
|
22
20
|
|
|
23
|
-
Emit per‑file objects in NDJSON (one per line)
|
|
21
|
+
Emit per‑file objects in NDJSON (one per line):
|
|
24
22
|
|
|
25
|
-
topmark --format=ndjson src pkg
|
|
23
|
+
$ topmark --format=ndjson src pkg
|
|
26
24
|
|
|
27
|
-
Write changes and show diffs (human output only)
|
|
25
|
+
Write changes and show diffs (human output only):
|
|
28
26
|
|
|
29
|
-
topmark --apply --diff .
|
|
27
|
+
$ topmark --apply --diff .
|
|
30
28
|
"""
|
|
31
29
|
|
|
32
30
|
import logging
|
|
@@ -53,6 +51,7 @@ from topmark.pipeline import runner
|
|
|
53
51
|
from topmark.pipeline.context import ComparisonStatus, FileStatus, HeaderStatus, ProcessingContext
|
|
54
52
|
from topmark.pipeline.pipelines import get_pipeline
|
|
55
53
|
from topmark.rendering.formats import HeaderOutputFormat
|
|
54
|
+
from topmark.utils.diff import render_patch
|
|
56
55
|
|
|
57
56
|
logger = get_logger(__name__)
|
|
58
57
|
|
|
@@ -112,47 +111,39 @@ def default_command(
|
|
|
112
111
|
) -> None:
|
|
113
112
|
"""Run the unified default command (check/apply).
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
The command receives options parsed at the group level and reads positional
|
|
115
|
+
paths from ``click.get_current_context().args`` (Black‑style).
|
|
117
116
|
|
|
118
117
|
Args:
|
|
119
|
-
stdin:
|
|
120
|
-
include_patterns: Glob patterns specifying additional files or
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
stdin (bool): Read file paths from standard input (one per line).
|
|
119
|
+
include_patterns (list[str]): Glob patterns specifying additional files or
|
|
120
|
+
directories to include.
|
|
121
|
+
include_from (list[str]): Paths to files that contain include glob patterns
|
|
122
|
+
(one per line).
|
|
123
|
+
exclude_patterns (list[str]): Glob patterns specifying files or directories
|
|
124
|
+
to exclude.
|
|
125
|
+
exclude_from (list[str]): Paths to files that contain exclude glob patterns
|
|
126
|
+
(one per line).
|
|
127
|
+
no_config (bool): Ignore user/project configuration files and use defaults only.
|
|
128
|
+
config_paths (list[str]): Additional configuration file paths to load and merge.
|
|
129
|
+
file_types (list[str]): Restrict processing to the given TopMark file type identifiers.
|
|
130
|
+
relative_to (str | None): Base directory used to compute relative paths in outputs.
|
|
131
|
+
align_fields (bool): Align header fields by the colon for readability.
|
|
132
|
+
header_format (HeaderOutputFormat | None): Optional override for the header output format.
|
|
133
|
+
apply_changes (bool): Write changes to files; otherwise perform a dry run.
|
|
134
|
+
diff (bool): Show unified diffs of header changes (human output only).
|
|
135
|
+
summary_mode (bool): Show outcome counts instead of per‑file details.
|
|
136
|
+
skip_compliant (bool): Suppress files whose comparison status is UNCHANGED.
|
|
137
|
+
skip_unsupported (bool): Suppress unsupported file types.
|
|
138
|
+
output_format (OutputFormat | None): Output format to use
|
|
139
|
+
(``default``, ``json``, or ``ndjson``).
|
|
136
140
|
|
|
137
141
|
Raises:
|
|
138
|
-
click.UsageError: If no input is provided
|
|
139
|
-
SystemExit:
|
|
142
|
+
click.UsageError: If no input is provided.
|
|
143
|
+
SystemExit: Exit code 2 in dry‑run mode when changes would be required.
|
|
140
144
|
|
|
141
145
|
Returns:
|
|
142
|
-
None
|
|
143
|
-
|
|
144
|
-
Examples:
|
|
145
|
-
Dry‑run (default)::
|
|
146
|
-
|
|
147
|
-
topmark src
|
|
148
|
-
|
|
149
|
-
Apply changes::
|
|
150
|
-
|
|
151
|
-
topmark --apply src
|
|
152
|
-
|
|
153
|
-
NDJSON per‑file output::
|
|
154
|
-
|
|
155
|
-
topmark --format=ndjson src
|
|
146
|
+
None: This function terminates the process with the appropriate exit code.
|
|
156
147
|
"""
|
|
157
148
|
ctx = click.get_current_context()
|
|
158
149
|
files = list(ctx.args)
|
|
@@ -188,7 +179,6 @@ def default_command(
|
|
|
188
179
|
|
|
189
180
|
logger.trace("Config after merging args: %s", config)
|
|
190
181
|
|
|
191
|
-
# file_list = resolve_file_list(config)
|
|
192
182
|
# Use Click's text stream for stdin so CliRunner/invoke input is read reliably
|
|
193
183
|
stdin_stream = click.get_text_stream("stdin") if stdin else None
|
|
194
184
|
file_list = resolve_file_list(config, stdin_stream=stdin_stream)
|
|
@@ -200,8 +190,9 @@ def default_command(
|
|
|
200
190
|
click.echo(chalk.blue(f"\n🔍 Processing {len(file_list)} file(s):\n"))
|
|
201
191
|
click.echo(chalk.bold.underline("📋 TopMark Results:"))
|
|
202
192
|
|
|
203
|
-
#
|
|
204
|
-
|
|
193
|
+
# Always run the 'apply' pipeline when --apply is set so updated_file_lines are computed.
|
|
194
|
+
# The --summary flag only affects how we render output, not which steps run.
|
|
195
|
+
pipeline_name = "apply" if apply_changes else ("summary" if summary_mode else "apply")
|
|
205
196
|
steps = get_pipeline(pipeline_name)
|
|
206
197
|
|
|
207
198
|
results: list[ProcessingContext] = []
|
|
@@ -291,10 +282,11 @@ def default_command(
|
|
|
291
282
|
f" 🛠️ Run `topmark --apply {r.path}` to update this file."
|
|
292
283
|
)
|
|
293
284
|
)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
285
|
+
|
|
286
|
+
# Diff output
|
|
287
|
+
for r in view_results:
|
|
288
|
+
if diff and r.header_diff:
|
|
289
|
+
click.echo(render_patch(r.header_diff))
|
|
298
290
|
|
|
299
291
|
# Writes (only when --apply is set)
|
|
300
292
|
if apply_changes:
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : strip.py
|
|
4
|
+
# file_relpath : src/topmark/cli/commands/strip.py
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
"""TopMark ``strip`` command.
|
|
12
|
+
|
|
13
|
+
Removes the entire TopMark header from targeted files. By default this command
|
|
14
|
+
performs a dry run (no writes); pass ``--apply`` to write changes.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
Preview changes (dry run):
|
|
18
|
+
|
|
19
|
+
$ topmark strip src
|
|
20
|
+
|
|
21
|
+
Apply changes (write in place):
|
|
22
|
+
|
|
23
|
+
$ topmark strip --apply .
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
from yachalk import chalk
|
|
30
|
+
|
|
31
|
+
from topmark.cli.cli_types import EnumParam
|
|
32
|
+
from topmark.cli.config_resolver import resolve_config_from_click
|
|
33
|
+
from topmark.cli.options import (
|
|
34
|
+
common_config_options,
|
|
35
|
+
common_file_and_filtering_options,
|
|
36
|
+
common_header_formatting_options,
|
|
37
|
+
typed_option,
|
|
38
|
+
underscored_trap_option,
|
|
39
|
+
)
|
|
40
|
+
from topmark.cli.utils import (
|
|
41
|
+
OutputFormat,
|
|
42
|
+
count_by_outcome,
|
|
43
|
+
)
|
|
44
|
+
from topmark.config.logging import get_logger
|
|
45
|
+
from topmark.file_resolver import resolve_file_list
|
|
46
|
+
from topmark.pipeline import runner
|
|
47
|
+
from topmark.pipeline.context import ComparisonStatus, FileStatus, HeaderStatus, ProcessingContext
|
|
48
|
+
from topmark.pipeline.pipelines import get_pipeline
|
|
49
|
+
from topmark.rendering.formats import HeaderOutputFormat
|
|
50
|
+
from topmark.utils.diff import render_patch
|
|
51
|
+
|
|
52
|
+
logger = get_logger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@common_config_options
|
|
56
|
+
@common_file_and_filtering_options
|
|
57
|
+
@common_header_formatting_options
|
|
58
|
+
@typed_option(
|
|
59
|
+
"--apply", "apply_changes", is_flag=True, help="Write changes to files (off by default)."
|
|
60
|
+
)
|
|
61
|
+
@typed_option("--diff", is_flag=True, help="Show unified diffs (human output only).")
|
|
62
|
+
@typed_option(
|
|
63
|
+
"--summary",
|
|
64
|
+
"summary_mode",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
help="Show outcome counts instead of per-file details.",
|
|
67
|
+
)
|
|
68
|
+
@typed_option(
|
|
69
|
+
"--skip-compliant",
|
|
70
|
+
"skip_compliant",
|
|
71
|
+
is_flag=True,
|
|
72
|
+
help="Hide files that are already up-to-date.",
|
|
73
|
+
)
|
|
74
|
+
@underscored_trap_option("--skip_compliant")
|
|
75
|
+
@typed_option(
|
|
76
|
+
"--skip-unsupported",
|
|
77
|
+
"skip_unsupported",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
help="Hide unsupported file types.",
|
|
80
|
+
)
|
|
81
|
+
@underscored_trap_option("--skip_unsupported")
|
|
82
|
+
@typed_option(
|
|
83
|
+
"--format",
|
|
84
|
+
"output_format",
|
|
85
|
+
type=EnumParam(OutputFormat),
|
|
86
|
+
default=None,
|
|
87
|
+
help=f"Output format ({', '.join(v.value for v in OutputFormat)}).",
|
|
88
|
+
)
|
|
89
|
+
def strip_command(
|
|
90
|
+
*,
|
|
91
|
+
stdin: bool,
|
|
92
|
+
include_patterns: list[str],
|
|
93
|
+
include_from: list[str],
|
|
94
|
+
exclude_patterns: list[str],
|
|
95
|
+
exclude_from: list[str],
|
|
96
|
+
no_config: bool,
|
|
97
|
+
config_paths: list[str],
|
|
98
|
+
file_types: list[str],
|
|
99
|
+
relative_to: str | None,
|
|
100
|
+
align_fields: bool,
|
|
101
|
+
header_format: HeaderOutputFormat | None,
|
|
102
|
+
apply_changes: bool,
|
|
103
|
+
diff: bool,
|
|
104
|
+
summary_mode: bool,
|
|
105
|
+
skip_compliant: bool,
|
|
106
|
+
skip_unsupported: bool,
|
|
107
|
+
output_format: OutputFormat | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Remove the TopMark header block from targeted files.
|
|
110
|
+
|
|
111
|
+
This command reads positional paths from ``click.get_current_context().args``
|
|
112
|
+
(Black‑style) and reuses the standard config resolver and file selection logic
|
|
113
|
+
so options like ``--include``, ``--exclude``, ``--file-type`` and ``--stdin``
|
|
114
|
+
behave the same as the default command.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
stdin (bool): Read file paths from standard input (one per line).
|
|
118
|
+
include_patterns (list[str]): Glob patterns specifying additional files or
|
|
119
|
+
directories to include.
|
|
120
|
+
include_from (list[str]): Paths to files that contain include glob patterns
|
|
121
|
+
(one per line).
|
|
122
|
+
exclude_patterns (list[str]): Glob patterns specifying files or directories
|
|
123
|
+
to exclude.
|
|
124
|
+
exclude_from (list[str]): Paths to files that contain exclude glob patterns
|
|
125
|
+
(one per line).
|
|
126
|
+
no_config (bool): Ignore user/project configuration files and use defaults only.
|
|
127
|
+
config_paths (list[str]): Additional configuration file paths to load and merge.
|
|
128
|
+
file_types (list[str]): Restrict processing to the given TopMark file type identifiers.
|
|
129
|
+
relative_to (str | None): Base directory used to compute relative paths in outputs.
|
|
130
|
+
align_fields (bool): Align header fields by the colon for readability.
|
|
131
|
+
header_format (HeaderOutputFormat | None): Optional override for rendering format
|
|
132
|
+
(not used by ``strip`` logic, but accepted for parity with the default command).
|
|
133
|
+
apply_changes (bool): Write changes to files; otherwise perform a dry run.
|
|
134
|
+
diff (bool): Show unified diffs of header removals (human output only).
|
|
135
|
+
summary_mode (bool): Show outcome counts instead of per‑file details.
|
|
136
|
+
skip_compliant (bool): Suppress files whose comparison status is UNCHANGED.
|
|
137
|
+
skip_unsupported (bool): Suppress unsupported file types.
|
|
138
|
+
output_format (OutputFormat | None): Output format to use (``default``, ``json``,
|
|
139
|
+
or ``ndjson``).
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
click.UsageError: If no input is provided.
|
|
143
|
+
SystemExit: Exit code 2 in dry‑run mode when changes would be required.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
None: This function terminates the process with the appropriate exit code.
|
|
147
|
+
|
|
148
|
+
Exit Status:
|
|
149
|
+
0: Nothing to strip, or writes succeeded with ``--apply``.
|
|
150
|
+
2: Dry run detected files that would change.
|
|
151
|
+
1: Errors occurred while writing files with ``--apply``.
|
|
152
|
+
"""
|
|
153
|
+
ctx = click.get_current_context()
|
|
154
|
+
files = list(ctx.args)
|
|
155
|
+
|
|
156
|
+
# Before resolving config/files — block empty invocations (Black-style)
|
|
157
|
+
no_inputs = (
|
|
158
|
+
not files and not stdin and not include_patterns and not include_from and not config_paths
|
|
159
|
+
)
|
|
160
|
+
if no_inputs:
|
|
161
|
+
# Show a friendly usage error with guidance
|
|
162
|
+
raise click.UsageError(
|
|
163
|
+
"No input specified. Provide one or more paths, or use --stdin. "
|
|
164
|
+
"See --help for examples.",
|
|
165
|
+
ctx=ctx,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# === Primary TopMark operation (default: check, write with --apply) ===
|
|
169
|
+
config = resolve_config_from_click(
|
|
170
|
+
ctx=ctx,
|
|
171
|
+
files=list(files),
|
|
172
|
+
stdin=stdin,
|
|
173
|
+
include_patterns=list(include_patterns),
|
|
174
|
+
include_from=list(include_from),
|
|
175
|
+
exclude_patterns=list(exclude_patterns),
|
|
176
|
+
exclude_from=list(exclude_from),
|
|
177
|
+
file_types=list(file_types),
|
|
178
|
+
relative_to=relative_to,
|
|
179
|
+
no_config=no_config,
|
|
180
|
+
config_paths=list(config_paths),
|
|
181
|
+
align_fields=align_fields,
|
|
182
|
+
header_format=header_format,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
logger.trace("Config after merging args: %s", config)
|
|
186
|
+
|
|
187
|
+
# Use Click's text stream for stdin so CliRunner/invoke input is read reliably
|
|
188
|
+
stdin_stream = click.get_text_stream("stdin") if stdin else None
|
|
189
|
+
file_list = resolve_file_list(config, stdin_stream=stdin_stream)
|
|
190
|
+
if not file_list:
|
|
191
|
+
click.echo(chalk.blue("\nℹ️ No files to process.\n"))
|
|
192
|
+
raise SystemExit(0)
|
|
193
|
+
|
|
194
|
+
if logger.isEnabledFor(logging.INFO):
|
|
195
|
+
click.echo(chalk.blue(f"\n🔍 Processing {len(file_list)} file(s):\n"))
|
|
196
|
+
click.echo(chalk.bold.underline("📋 TopMark Results:"))
|
|
197
|
+
|
|
198
|
+
# Use summary pipeline when --summary is set, else full (compare+update+patch)
|
|
199
|
+
pipeline_name = "strip"
|
|
200
|
+
steps = get_pipeline(pipeline_name)
|
|
201
|
+
|
|
202
|
+
results: list[ProcessingContext] = []
|
|
203
|
+
for path in file_list:
|
|
204
|
+
try:
|
|
205
|
+
ctx_obj: ProcessingContext = ProcessingContext.bootstrap(path=path, config=config)
|
|
206
|
+
ctx_obj = runner.run(ctx_obj, steps)
|
|
207
|
+
results.append(ctx_obj)
|
|
208
|
+
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
|
|
209
|
+
logger.error("Filesystem error while processing %s: %s", path, e)
|
|
210
|
+
click.echo(chalk.red(f"❌ Filesystem error processing {path}: {e}"))
|
|
211
|
+
continue
|
|
212
|
+
except UnicodeDecodeError as e:
|
|
213
|
+
logger.error("Encoding error while reading %s: %s", path, e)
|
|
214
|
+
click.echo(chalk.red(f"🧵 Encoding error in {path}: {e}"))
|
|
215
|
+
continue
|
|
216
|
+
except Exception as e: # pragma: no cover - unexpected path
|
|
217
|
+
logger.exception("Unexpected error processing %s", path)
|
|
218
|
+
click.echo(
|
|
219
|
+
chalk.red(f"⚠️ Unexpected error processing {path}: {e} (use -vv for traceback)")
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Optional filtering
|
|
224
|
+
if skip_compliant:
|
|
225
|
+
view_results = [r for r in results if r.status.comparison is not ComparisonStatus.UNCHANGED]
|
|
226
|
+
else:
|
|
227
|
+
view_results = results
|
|
228
|
+
|
|
229
|
+
if skip_unsupported:
|
|
230
|
+
view_results = [
|
|
231
|
+
r
|
|
232
|
+
for r in view_results
|
|
233
|
+
if r.status.file
|
|
234
|
+
not in [
|
|
235
|
+
FileStatus.SKIPPED_UNSUPPORTED,
|
|
236
|
+
FileStatus.SKIPPED_KNOWN_NO_HEADERS,
|
|
237
|
+
]
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
fmt: OutputFormat = output_format or OutputFormat.DEFAULT
|
|
241
|
+
# Machine formats first
|
|
242
|
+
if fmt is OutputFormat.NDJSON:
|
|
243
|
+
import json as _json
|
|
244
|
+
|
|
245
|
+
if summary_mode:
|
|
246
|
+
counts = count_by_outcome(view_results)
|
|
247
|
+
for key, (n, label, _color) in counts.items():
|
|
248
|
+
click.echo(_json.dumps({"key": key, "count": n, "label": label}))
|
|
249
|
+
else:
|
|
250
|
+
for r in view_results:
|
|
251
|
+
click.echo(_json.dumps(r.to_dict()))
|
|
252
|
+
elif fmt is OutputFormat.JSON:
|
|
253
|
+
import json as _json
|
|
254
|
+
|
|
255
|
+
if summary_mode:
|
|
256
|
+
counts = count_by_outcome(view_results)
|
|
257
|
+
data = {k: {"count": n, "label": label} for k, (n, label, _color) in counts.items()}
|
|
258
|
+
click.echo(_json.dumps(data, indent=2))
|
|
259
|
+
else:
|
|
260
|
+
payload = [r.to_dict() for r in view_results]
|
|
261
|
+
click.echo(_json.dumps(payload, indent=2))
|
|
262
|
+
else:
|
|
263
|
+
# Human output
|
|
264
|
+
if summary_mode:
|
|
265
|
+
click.echo()
|
|
266
|
+
click.echo(chalk.bold.underline("Summary by outcome:"))
|
|
267
|
+
|
|
268
|
+
counts = count_by_outcome(view_results)
|
|
269
|
+
label_width = max((len(v[1]) for v in counts.values()), default=0) + 1
|
|
270
|
+
num_width = len(str(len(file_list)))
|
|
271
|
+
for _key, (n, label, color) in counts.items():
|
|
272
|
+
click.echo(color(f" {label:<{label_width}}: {n:>{num_width}}"))
|
|
273
|
+
|
|
274
|
+
else:
|
|
275
|
+
for r in view_results:
|
|
276
|
+
click.echo(r.summary)
|
|
277
|
+
if r.status.comparison == ComparisonStatus.CHANGED:
|
|
278
|
+
if apply_changes:
|
|
279
|
+
if r.status.header in [HeaderStatus.DETECTED, HeaderStatus.EMPTY]:
|
|
280
|
+
click.echo(chalk.yellow(f" 🧹 Stripping header in '{r.path}'"))
|
|
281
|
+
else:
|
|
282
|
+
click.echo(
|
|
283
|
+
chalk.yellow(
|
|
284
|
+
f" 🛠️ Run `topmark strip --apply {r.path}` to update this file."
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Diff output
|
|
289
|
+
import pprint
|
|
290
|
+
|
|
291
|
+
for r in view_results:
|
|
292
|
+
logger.debug("strip(): diff: %s, result: %s", diff, pprint.pformat(r, 2))
|
|
293
|
+
if diff and r.header_diff:
|
|
294
|
+
click.echo(render_patch(r.header_diff))
|
|
295
|
+
|
|
296
|
+
# Writes (only when --apply is set)
|
|
297
|
+
if apply_changes:
|
|
298
|
+
written = 0
|
|
299
|
+
failed = 0
|
|
300
|
+
from pathlib import Path
|
|
301
|
+
|
|
302
|
+
for r in results:
|
|
303
|
+
try:
|
|
304
|
+
if r.updated_file_lines is not None and r.file_lines != r.updated_file_lines:
|
|
305
|
+
Path(str(r.path)).write_text("".join(r.updated_file_lines), encoding="utf-8")
|
|
306
|
+
written += 1
|
|
307
|
+
except Exception as e: # pragma: no cover
|
|
308
|
+
failed += 1
|
|
309
|
+
logger.error("Failed to write %s: %s", r.path, e)
|
|
310
|
+
if fmt is OutputFormat.DEFAULT:
|
|
311
|
+
click.echo(
|
|
312
|
+
(
|
|
313
|
+
f"\n✅ Applied changes to {written} file(s)."
|
|
314
|
+
if written
|
|
315
|
+
else "\n✅ No changes to apply."
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
if failed:
|
|
319
|
+
raise SystemExit(1)
|
|
320
|
+
|
|
321
|
+
# Exit code policy for `strip`: non-zero only if a removal would occur.
|
|
322
|
+
# A missing header is *not* an error condition for `strip`.
|
|
323
|
+
if not apply_changes:
|
|
324
|
+
would_change = any((r.status.comparison == ComparisonStatus.CHANGED) for r in results)
|
|
325
|
+
if would_change:
|
|
326
|
+
raise SystemExit(2)
|
|
327
|
+
|
|
328
|
+
if logger.isEnabledFor(logging.INFO):
|
|
329
|
+
click.echo("✅ All done!")
|
|
330
|
+
|
|
331
|
+
return
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
This module wires the TopMark command-line interface (CLI) using Click 8.2.
|
|
14
14
|
The **base command** performs a check/dry‑run by default and **applies** changes
|
|
15
|
-
when ``--apply`` is specified.
|
|
15
|
+
when ``--apply`` is specified.
|
|
16
|
+
|
|
17
|
+
HEaders can be stripped from files with the ``strip`` subcommand.
|
|
18
|
+
|
|
19
|
+
Auxiliary subcommands provide configuration and
|
|
16
20
|
metadata functionality (e.g., ``dump-config``, ``filetypes``, ``show-defaults``,
|
|
17
21
|
``init-config``, ``version``).
|
|
18
22
|
|
|
@@ -33,6 +37,10 @@ Examples:
|
|
|
33
37
|
|
|
34
38
|
topmark --summary --format=json src
|
|
35
39
|
|
|
40
|
+
Remove headers (dry-run)::
|
|
41
|
+
|
|
42
|
+
topmark strip src
|
|
43
|
+
|
|
36
44
|
List supported file types::
|
|
37
45
|
|
|
38
46
|
topmark filetypes
|
|
@@ -54,6 +62,7 @@ from topmark.cli.commands.dump_config import dump_config_command
|
|
|
54
62
|
from topmark.cli.commands.filetypes import filetypes_command
|
|
55
63
|
from topmark.cli.commands.init_config import init_config_command
|
|
56
64
|
from topmark.cli.commands.show_defaults import show_defaults_command
|
|
65
|
+
from topmark.cli.commands.strip import strip_command
|
|
57
66
|
from topmark.cli.commands.version import version_command
|
|
58
67
|
from topmark.cli.options import (
|
|
59
68
|
common_config_options,
|
|
@@ -413,6 +422,24 @@ def ensure_commands_registered() -> None:
|
|
|
413
422
|
|
|
414
423
|
# Register other visible subcommands
|
|
415
424
|
|
|
425
|
+
typed_command_of(
|
|
426
|
+
cli,
|
|
427
|
+
"strip",
|
|
428
|
+
help="Remove the entire TopMark header from files.",
|
|
429
|
+
context_settings=CONTEXT_SETTINGS,
|
|
430
|
+
epilog="""
|
|
431
|
+
Removes the full TopMark header block (between 'topmark:header:start' and 'topmark:header:end')
|
|
432
|
+
from each targeted file. By default this is a dry run; use --apply to write changes.
|
|
433
|
+
|
|
434
|
+
Examples:
|
|
435
|
+
|
|
436
|
+
# Preview which files would change (dry-run)
|
|
437
|
+
topmark strip src
|
|
438
|
+
|
|
439
|
+
# Apply: remove headers in-place
|
|
440
|
+
topmark strip --apply .
|
|
441
|
+
""",
|
|
442
|
+
)(strip_command)
|
|
416
443
|
typed_command_of(
|
|
417
444
|
cli,
|
|
418
445
|
"version",
|