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.
Files changed (62) hide show
  1. {topmark-0.2.0/src/topmark.egg-info → topmark-0.2.1}/PKG-INFO +11 -2
  2. {topmark-0.2.0 → topmark-0.2.1}/README.md +10 -1
  3. {topmark-0.2.0 → topmark-0.2.1}/pyproject.toml +1 -1
  4. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/default.py +44 -52
  5. topmark-0.2.1/src/topmark/cli/commands/strip.py +331 -0
  6. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/main.py +28 -1
  7. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/utils.py +51 -40
  8. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/file_resolver.py +62 -24
  9. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/context.py +54 -8
  10. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/pipelines.py +13 -0
  11. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/base.py +194 -98
  12. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/pound.py +9 -11
  13. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/slash.py +9 -10
  14. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/xml.py +53 -26
  15. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/comparer.py +36 -4
  16. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/patcher.py +17 -77
  17. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/reader.py +18 -8
  18. topmark-0.2.1/src/topmark/pipeline/steps/stripper.py +76 -0
  19. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/updater.py +42 -55
  20. topmark-0.2.1/src/topmark/utils/diff.py +105 -0
  21. {topmark-0.2.0 → topmark-0.2.1/src/topmark.egg-info}/PKG-INFO +11 -2
  22. {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/SOURCES.txt +3 -0
  23. {topmark-0.2.0 → topmark-0.2.1}/LICENSE +0 -0
  24. {topmark-0.2.0 → topmark-0.2.1}/setup.cfg +0 -0
  25. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/__init__.py +0 -0
  26. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/__init__.py +0 -0
  27. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/cli_types.py +0 -0
  28. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/__init__.py +0 -0
  29. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/dump_config.py +0 -0
  30. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/filetypes.py +0 -0
  31. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/init_config.py +0 -0
  32. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/show_defaults.py +0 -0
  33. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/commands/version.py +0 -0
  34. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/config_resolver.py +0 -0
  35. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/cli/options.py +0 -0
  36. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/__init__.py +0 -0
  37. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/logging.py +0 -0
  38. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/config/topmark-default.toml +0 -0
  39. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/constants.py +0 -0
  40. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/__init__.py +0 -0
  41. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/base.py +0 -0
  42. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/instances.py +0 -0
  43. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/policy.py +0 -0
  44. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/filetypes/registry.py +0 -0
  45. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/__init__.py +0 -0
  46. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/contracts.py +0 -0
  47. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/processors/__init__.py +0 -0
  48. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/runner.py +0 -0
  49. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/__init__.py +0 -0
  50. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/builder.py +0 -0
  51. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/renderer.py +0 -0
  52. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/resolver.py +0 -0
  53. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/pipeline/steps/scanner.py +0 -0
  54. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/py.typed +0 -0
  55. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/rendering/api.py +0 -0
  56. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/rendering/formats.py +0 -0
  57. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/utils/__init__.py +0 -0
  58. {topmark-0.2.0 → topmark-0.2.1}/src/topmark/utils/file.py +0 -0
  59. {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/dependency_links.txt +0 -0
  60. {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/entry_points.txt +0 -0
  61. {topmark-0.2.0 → topmark-0.2.1}/src/topmark.egg-info/requires.txt +0 -0
  62. {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.0
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
- - Selective removal
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
- - Selective removal
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.0"
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
- This module implements the unified default behavior of TopMark’s CLI. It is
14
- invoked when no explicit subcommand is provided and processes the file paths
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
- When invoked via the root group, this command receives options parsed at the
116
- group level and reads positional paths from ``click.get_current_context().args``.
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: If True, read file paths from standard input (one per line).
120
- include_patterns: Glob patterns specifying additional files or directories to include.
121
- include_from: Paths to files that contain include glob patterns (one per line).
122
- exclude_patterns: Glob patterns specifying files or directories to exclude.
123
- exclude_from: Paths to files that contain exclude glob patterns (one per line).
124
- no_config: If True, ignore user/project configuration files and use defaults only.
125
- config_paths: Additional configuration file paths to load and merge.
126
- file_types: Restrict processing to the given TopMark file type identifiers.
127
- relative_to: Base directory used to compute relative paths in outputs.
128
- align_fields: If True, align header fields by the colon for readability.
129
- header_format: Optional override for the header output format used when rendering.
130
- apply_changes: If True, write changes to files; otherwise perform a dry run.
131
- diff: If True, show unified diffs of header changes (human output only).
132
- summary_mode: If True, print outcome counts instead of per-file details.
133
- skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
134
- skip_unsupported: If True, suppress unsupported file types.
135
- output_format: Output format to use (default, json, or ndjson).
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 (Black‑style preflight).
139
- SystemExit: With code 2 in check mode when changes would be required.
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
- # Use summary pipeline when --summary is set, else full (compare+update+patch)
204
- pipeline_name = "summary" if summary_mode else "apply"
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
- if diff:
295
- for r in view_results:
296
- if r.header_diff:
297
- click.echo(r.header_diff)
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. Auxiliary subcommands provide configuration and
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",