topmark 0.6.2__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {topmark-0.6.2/src/topmark.egg-info → topmark-0.8.0}/PKG-INFO +72 -2
  2. {topmark-0.6.2 → topmark-0.8.0}/README.md +71 -1
  3. {topmark-0.6.2 → topmark-0.8.0}/pyproject.toml +1 -1
  4. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/__init__.py +2 -0
  5. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/api/__init__.py +57 -37
  6. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/__init__.py +1 -1
  7. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/cli_types.py +2 -2
  8. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/cmd_common.py +8 -5
  9. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/check.py +19 -16
  10. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/dump_config.py +14 -11
  11. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/filetypes.py +22 -17
  12. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/init_config.py +2 -2
  13. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/processors.py +26 -21
  14. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/show_defaults.py +2 -2
  15. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/strip.py +16 -13
  16. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/version.py +29 -9
  17. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/config_resolver.py +8 -8
  18. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/console_helpers.py +1 -1
  19. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/console_std.py +3 -3
  20. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/errors.py +2 -2
  21. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/io.py +14 -11
  22. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/main.py +8 -6
  23. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/options.py +17 -14
  24. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/utils.py +19 -14
  25. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli_shared/utils.py +18 -11
  26. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/config/__init__.py +23 -23
  27. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/config/io.py +4 -3
  28. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/config/logging.py +8 -7
  29. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/constants.py +4 -2
  30. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/file_resolver.py +18 -10
  31. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/filetypes/__init__.py +2 -0
  32. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/filetypes/base.py +13 -7
  33. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/filetypes/instances.py +91 -8
  34. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/filetypes/policy.py +7 -1
  35. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/filetypes/registry.py +5 -4
  36. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/__init__.py +2 -0
  37. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/context.py +8 -7
  38. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/contracts.py +2 -0
  39. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/processors/__init__.py +9 -6
  40. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/processors/base.py +66 -47
  41. topmark-0.8.0/src/topmark/pipeline/processors/cblock.py +119 -0
  42. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/processors/pound.py +20 -9
  43. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/processors/slash.py +5 -6
  44. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/processors/xml.py +27 -21
  45. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/runner.py +10 -7
  46. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/__init__.py +2 -0
  47. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/builder.py +9 -7
  48. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/comparer.py +6 -4
  49. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/patcher.py +5 -3
  50. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/reader.py +16 -8
  51. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/renderer.py +9 -5
  52. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/resolver.py +32 -22
  53. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/scanner.py +6 -4
  54. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/stripper.py +8 -5
  55. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/steps/updater.py +13 -9
  56. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/registry/__init__.py +1 -1
  57. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/registry/filetypes.py +3 -3
  58. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/registry/processors.py +2 -1
  59. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/rendering/api.py +10 -5
  60. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/rendering/formats.py +2 -0
  61. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/utils/__init__.py +2 -0
  62. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/utils/diff.py +6 -4
  63. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/utils/file.py +4 -4
  64. topmark-0.8.0/src/topmark/utils/version.py +83 -0
  65. {topmark-0.6.2 → topmark-0.8.0/src/topmark.egg-info}/PKG-INFO +72 -2
  66. {topmark-0.6.2 → topmark-0.8.0}/src/topmark.egg-info/SOURCES.txt +3 -1
  67. {topmark-0.6.2 → topmark-0.8.0}/LICENSE +0 -0
  68. {topmark-0.6.2 → topmark-0.8.0}/setup.cfg +0 -0
  69. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/api/public_types.py +0 -0
  70. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/api/types.py +0 -0
  71. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/commands/__init__.py +0 -0
  72. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli/console.py +0 -0
  73. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli_shared/__init__.py +0 -0
  74. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli_shared/console_api.py +0 -0
  75. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/cli_shared/exit_codes.py +0 -0
  76. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/config/topmark-default.toml +0 -0
  77. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/pipeline/pipelines.py +0 -0
  78. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/py.typed +0 -0
  79. {topmark-0.6.2 → topmark-0.8.0}/src/topmark/registry/registry.py +0 -0
  80. {topmark-0.6.2 → topmark-0.8.0}/src/topmark.egg-info/dependency_links.txt +0 -0
  81. {topmark-0.6.2 → topmark-0.8.0}/src/topmark.egg-info/entry_points.txt +0 -0
  82. {topmark-0.6.2 → topmark-0.8.0}/src/topmark.egg-info/requires.txt +0 -0
  83. {topmark-0.6.2 → topmark-0.8.0}/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.6.2
3
+ Version: 0.8.0
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
@@ -79,6 +79,73 @@ Full documentation is available on Read the Docs: <https://topmark.readthedocs.i
79
79
  This README is the canonical, complete introduction for GitHub/PyPI. The docs site provides a
80
80
  concise landing page and deep links into topics (install, usage, CI, API, etc.).
81
81
 
82
+ ## 📝 Example headers
83
+
84
+ TopMark inserts headers that are tailored to the comment style and conventions of each file type
85
+ (line comments like `#`/`//`, and block comments like `/* … */`). Here are examples of how TopMark
86
+ headers appear in different file formats:
87
+
88
+ ```bash
89
+ #!/bin/bash
90
+
91
+ # topmark:header:start
92
+ #
93
+ # project : TopMark
94
+ # file : script.sh
95
+ # license : MIT
96
+ # copyright : (c) 2025 Olivier Biot
97
+
98
+ # topmark:header:end
99
+
100
+ echo "Hello, World!"
101
+ ```
102
+
103
+ ```xml
104
+ <?xml version="1.0" encoding="UTF-8"?>
105
+ <!--
106
+ topmark:header:start
107
+
108
+ project : TopMark
109
+ file : config.xml
110
+ license : MIT
111
+ copyright : (c) 2025 Olivier Biot
112
+
113
+ topmark:header:end
114
+ -->
115
+
116
+ <configuration>
117
+ <!-- XML content here -->
118
+ </configuration>
119
+ ```
120
+
121
+ ```javascript
122
+ // topmark:header:start
123
+ //
124
+ // project : TopMark
125
+ // file : app.js
126
+ // license : MIT
127
+ // copyright : (c) 2025 Olivier Biot
128
+ //
129
+ // topmark:header:end
130
+
131
+ console.log("Hello, World!");
132
+ ```
133
+
134
+ ```css
135
+ /*
136
+ * topmark:header:start
137
+ *
138
+ * project : TopMark
139
+ * file : styles.css
140
+ * license : MIT
141
+ * copyright : (c) 2025 Olivier Biot
142
+ *
143
+ * topmark:header:end
144
+ */
145
+
146
+ body { margin: 0; }
147
+ ```
148
+
82
149
  ## ✨ Features
83
150
 
84
151
  - File header detection, insertion, and replacement
@@ -193,9 +260,12 @@ topmark show-defaults
193
260
  # Output a starter configuration to stdout
194
261
  topmark init-config
195
262
 
196
- # Show TopMark version in JSON format
263
+ # Output TopMark version (PEP 440) in JSON format
197
264
  topmark version --format json
198
265
 
266
+ # Output TopMark version (SemVer) to stdout
267
+ topmark version --semver
268
+
199
269
  # Apply changes to files in-place
200
270
  topmark check --apply src/
201
271
 
@@ -28,6 +28,73 @@ Full documentation is available on Read the Docs: <https://topmark.readthedocs.i
28
28
  This README is the canonical, complete introduction for GitHub/PyPI. The docs site provides a
29
29
  concise landing page and deep links into topics (install, usage, CI, API, etc.).
30
30
 
31
+ ## 📝 Example headers
32
+
33
+ TopMark inserts headers that are tailored to the comment style and conventions of each file type
34
+ (line comments like `#`/`//`, and block comments like `/* … */`). Here are examples of how TopMark
35
+ headers appear in different file formats:
36
+
37
+ ```bash
38
+ #!/bin/bash
39
+
40
+ # topmark:header:start
41
+ #
42
+ # project : TopMark
43
+ # file : script.sh
44
+ # license : MIT
45
+ # copyright : (c) 2025 Olivier Biot
46
+
47
+ # topmark:header:end
48
+
49
+ echo "Hello, World!"
50
+ ```
51
+
52
+ ```xml
53
+ <?xml version="1.0" encoding="UTF-8"?>
54
+ <!--
55
+ topmark:header:start
56
+
57
+ project : TopMark
58
+ file : config.xml
59
+ license : MIT
60
+ copyright : (c) 2025 Olivier Biot
61
+
62
+ topmark:header:end
63
+ -->
64
+
65
+ <configuration>
66
+ <!-- XML content here -->
67
+ </configuration>
68
+ ```
69
+
70
+ ```javascript
71
+ // topmark:header:start
72
+ //
73
+ // project : TopMark
74
+ // file : app.js
75
+ // license : MIT
76
+ // copyright : (c) 2025 Olivier Biot
77
+ //
78
+ // topmark:header:end
79
+
80
+ console.log("Hello, World!");
81
+ ```
82
+
83
+ ```css
84
+ /*
85
+ * topmark:header:start
86
+ *
87
+ * project : TopMark
88
+ * file : styles.css
89
+ * license : MIT
90
+ * copyright : (c) 2025 Olivier Biot
91
+ *
92
+ * topmark:header:end
93
+ */
94
+
95
+ body { margin: 0; }
96
+ ```
97
+
31
98
  ## ✨ Features
32
99
 
33
100
  - File header detection, insertion, and replacement
@@ -142,9 +209,12 @@ topmark show-defaults
142
209
  # Output a starter configuration to stdout
143
210
  topmark init-config
144
211
 
145
- # Show TopMark version in JSON format
212
+ # Output TopMark version (PEP 440) in JSON format
146
213
  topmark version --format json
147
214
 
215
+ # Output TopMark version (SemVer) to stdout
216
+ topmark version --semver
217
+
148
218
  # Apply changes to files in-place
149
219
  topmark check --apply src/
150
220
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  [project]
12
12
  name = "topmark"
13
- version = "0.6.2"
13
+ version = "0.8.0" # PEP 440: "X.Y.Z.devN", "X.Y.ZrcN", or "X.Y.Z"
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"
@@ -12,3 +12,5 @@
12
12
 
13
13
  Provides tools and utilities for evaluating and benchmarking models and algorithms.
14
14
  """
15
+
16
+ from __future__ import annotations
@@ -65,7 +65,7 @@ from __future__ import annotations
65
65
 
66
66
  from dataclasses import replace
67
67
  from pathlib import Path
68
- from typing import TYPE_CHECKING, Any, Iterable, Sequence, cast
68
+ from typing import TYPE_CHECKING, Any, Iterable, Mapping, Sequence, cast
69
69
 
70
70
  from topmark.cli.cmd_common import (
71
71
  filter_view_results,
@@ -74,7 +74,7 @@ from topmark.cli.cmd_common import (
74
74
  from topmark.cli.io import InputPlan
75
75
  from topmark.cli_shared.utils import write_updates
76
76
  from topmark.config import Config, MutableConfig
77
- from topmark.config.logging import get_logger
77
+ from topmark.config.logging import TopmarkLogger, get_logger
78
78
  from topmark.constants import TOPMARK_VERSION
79
79
  from topmark.pipeline.context import (
80
80
  ComparisonStatus,
@@ -90,10 +90,12 @@ from .types import DiagnosticTotals, FileResult, FileTypeInfo, Outcome, Processo
90
90
  if TYPE_CHECKING:
91
91
  from collections.abc import Mapping, Sequence
92
92
 
93
- logger = get_logger(__name__)
93
+ from topmark.cli_shared.exit_codes import ExitCode
94
94
 
95
+ logger: TopmarkLogger = get_logger(__name__)
95
96
 
96
- __all__ = [
97
+
98
+ __all__: list[str] = [
97
99
  "Outcome",
98
100
  "FileResult",
99
101
  "RunResult",
@@ -174,7 +176,7 @@ def _map_outcome(r: ProcessingContext, *, apply: bool) -> Outcome:
174
176
  """
175
177
  if r.status.file is not FileStatus.RESOLVED:
176
178
  # Treat unsupported/matched-but-unhandled types as non-errors for API consumers.
177
- unsupported = {
179
+ unsupported: set[FileStatus] = {
178
180
  FileStatus.SKIPPED_UNSUPPORTED,
179
181
  FileStatus.SKIPPED_KNOWN_NO_HEADERS,
180
182
  }
@@ -194,8 +196,8 @@ def _map_outcome(r: ProcessingContext, *, apply: bool) -> Outcome:
194
196
  def _to_file_result(r: ProcessingContext, *, apply: bool) -> FileResult:
195
197
  """Convert a ProcessingContext into a FileResult."""
196
198
  # Prefer a unified diff when available; otherwise None (human views may omit diffs).
197
- diff = r.header_diff or None
198
- message = getattr(r, "summary", None)
199
+ diff: str | None = r.header_diff or None
200
+ message: str | None = r.summary or None
199
201
  return FileResult(
200
202
  path=Path(str(r.path)), outcome=_map_outcome(r, apply=apply), diff=diff, message=message
201
203
  )
@@ -216,7 +218,7 @@ def _run_pipeline(
216
218
  base_config: Mapping[str, Any] | Config | None,
217
219
  file_types: Sequence[str] | None,
218
220
  apply_changes: bool,
219
- ) -> tuple[Config, list[Path], list[ProcessingContext], object | None]:
221
+ ) -> tuple[Config, list[Path], list[ProcessingContext], ExitCode | None]:
220
222
  """Resolve config + files, register processors, and run the pipeline.
221
223
 
222
224
  - Uses the same planner/resolver as the CLI (via `build_config_and_file_list`)
@@ -232,14 +234,16 @@ def _run_pipeline(
232
234
  apply_changes (bool): True if changes should be applied, False if dry-run.
233
235
 
234
236
  Returns:
235
- tuple[Config, list[Path], list[ProcessingContext], object | None]: Tuple containing:
237
+ tuple[Config, list[Path], list[ProcessingContext], ExitCode | None]: Tuple containing:
236
238
  - the resulting Config object;
237
239
  - the resolved (optionally filtered if `file_types` was specified) list of paths
238
240
  to process;
239
241
  - the results of running the pipeline as list of `ProcessingContext` instances;
240
- - the encountered error code
242
+ - the encountered `ExitCode` error code
241
243
  """
242
244
  logger.info("Building config and file list for paths: %s", paths)
245
+ cfg: Config
246
+ file_list: list[Path]
243
247
  cfg, file_list = _build_cfg_and_files_via_cli_helpers(
244
248
  paths, base_config=base_config, file_types=file_types
245
249
  )
@@ -253,8 +257,10 @@ def _run_pipeline(
253
257
  Registry.ensure_processors_registered()
254
258
 
255
259
  # Use a snapshot that carries the apply intent
256
- cfg_for_run = replace(cfg, apply_changes=apply_changes)
260
+ cfg_for_run: Config = replace(cfg, apply_changes=apply_changes)
257
261
 
262
+ results: list[ProcessingContext] = []
263
+ encountered_error_code: ExitCode | None
258
264
  results, encountered_error_code = run_steps_for_files(
259
265
  file_list, pipeline_name=pipeline_name, config=cfg_for_run
260
266
  )
@@ -275,10 +281,10 @@ def _apply_view_filter(
275
281
  `skipped_count` equals `len(results) - len(filtered_results)` and reflects only
276
282
  view-level filtering (not internal SKIPPED statuses).
277
283
  """
278
- view_results = filter_view_results(
284
+ view_results: list[ProcessingContext] = filter_view_results(
279
285
  results, skip_compliant=skip_compliant, skip_unsupported=skip_unsupported
280
286
  )
281
- skipped = len(results) - len(view_results)
287
+ skipped: int = len(results) - len(view_results)
282
288
  return view_results, skipped
283
289
 
284
290
 
@@ -305,19 +311,21 @@ def _collect_diagnostic_totals(results: list[ProcessingContext]) -> DiagnosticTo
305
311
  Counts reflect only the supplied `results` (typically the **view** after
306
312
  filtering), matching what is returned to the caller.
307
313
  """
308
- total_info = total_warn = total_error = 0
314
+ total_info: int = 0
315
+ total_warn: int = 0
316
+ total_error: int = 0
309
317
  for r in results:
310
318
  if not r.diagnostics:
311
319
  continue
312
320
  for d in r.diagnostics:
313
- lv = d.level.value
321
+ lv: str = d.level.value
314
322
  if lv == "info":
315
323
  total_info += 1
316
324
  elif lv == "warning":
317
325
  total_warn += 1
318
326
  elif lv == "error":
319
327
  total_error += 1
320
- total = total_info + total_warn + total_error
328
+ total: int = total_info + total_warn + total_error
321
329
  return {"info": total_info, "warning": total_warn, "error": total_error, "total": total}
322
330
 
323
331
 
@@ -362,7 +370,7 @@ def _build_cfg_and_files_via_cli_helpers(
362
370
  # normalization of paths.
363
371
  if base_config is not None:
364
372
  # Start from defaults so that header fields/values are present unless overridden
365
- draft_defaults = MutableConfig.from_defaults()
373
+ draft_defaults: MutableConfig = MutableConfig.from_defaults()
366
374
  draft = draft_defaults.merge_with(
367
375
  draft
368
376
  ) # 'draft' came from _ensure_mutable_config(base_config)
@@ -372,8 +380,8 @@ def _build_cfg_and_files_via_cli_helpers(
372
380
  logger.debug("Found %d input paths", len(draft.files))
373
381
  if file_types:
374
382
  draft.file_types = set(file_types)
375
- cfg = draft.freeze()
376
- file_list = resolve_file_list(cfg)
383
+ cfg: Config = draft.freeze()
384
+ file_list: list[Path] = resolve_file_list(cfg)
377
385
  logger.debug("Files found: %s", len(file_list))
378
386
  return cfg, file_list
379
387
 
@@ -405,7 +413,7 @@ def _build_cfg_and_files_via_cli_helpers(
405
413
  local_toml = Path(local_toml_file)
406
414
  if local_toml.exists():
407
415
  logger.info("Loading local config from: %s", local_toml)
408
- local_config = MutableConfig.from_toml_file(local_toml)
416
+ local_config: MutableConfig | None = MutableConfig.from_toml_file(local_toml)
409
417
  if local_config is not None:
410
418
  draft = draft.merge_with(local_config)
411
419
  break # only first one
@@ -415,11 +423,11 @@ def _build_cfg_and_files_via_cli_helpers(
415
423
  "Sequence[str | Path]", args_mapping.get("config_files", ())
416
424
  )
417
425
  for entry in config_files_seq:
418
- p = entry if isinstance(entry, Path) else Path(entry)
426
+ p: Path = entry if isinstance(entry, Path) else Path(entry)
419
427
  logger.debug("Adding config file '%s' (type=%s)", entry, type(entry).__name__)
420
428
  if p.exists():
421
429
  logger.info("Loading config override from: %s", p)
422
- extra = MutableConfig.from_toml_file(p)
430
+ extra: MutableConfig | None = MutableConfig.from_toml_file(p)
423
431
  if extra is not None:
424
432
  draft = draft.merge_with(extra)
425
433
  else:
@@ -475,6 +483,9 @@ def check(
475
483
  raise ValueError("Options add_only and update_only are mutually exclusive.")
476
484
 
477
485
  # Always use 'apply' pipeline to compute updated content even in dry-run
486
+ file_list: list[Path] = []
487
+ results: list[ProcessingContext] = []
488
+ encountered_error_code: ExitCode | None = None
478
489
  _, file_list, results, encountered_error_code = _run_pipeline(
479
490
  pipeline_name="apply",
480
491
  paths=paths,
@@ -485,20 +496,23 @@ def check(
485
496
  if not file_list:
486
497
  return RunResult(files=(), summary={}, had_errors=False)
487
498
 
499
+ view_results: list[ProcessingContext]
500
+ skipped: int
488
501
  view_results, skipped = _apply_view_filter(
489
502
  results, skip_compliant=skip_compliant, skip_unsupported=skip_unsupported
490
503
  )
491
504
 
492
- files = tuple(_to_file_result(r, apply=apply) for r in view_results)
493
- summary = _summarize(files)
494
- had_errors = any(_map_outcome(r, apply=apply) is Outcome.ERROR for r in results) or (
505
+ files: tuple[FileResult, ...] = tuple(_to_file_result(r, apply=apply) for r in view_results)
506
+ summary: Mapping[str, int] = _summarize(files)
507
+ had_errors: bool = any(_map_outcome(r, apply=apply) is Outcome.ERROR for r in results) or (
495
508
  encountered_error_code is not None
496
509
  )
497
- diagnostics = _collect_diagnostics(view_results)
498
- diagnostic_totals = _collect_diagnostic_totals(view_results)
499
- diagnostic_totals_all = _collect_diagnostic_totals(results)
510
+ diagnostics: dict[str, list[PublicDiagnostic]] = _collect_diagnostics(view_results)
511
+ diagnostic_totals: DiagnosticTotals = _collect_diagnostic_totals(view_results)
512
+ diagnostic_totals_all: DiagnosticTotals = _collect_diagnostic_totals(results)
500
513
 
501
- written = failed = 0
514
+ written: int = 0
515
+ failed: int = 0
502
516
  if apply:
503
517
 
504
518
  def _should_write_check(r: ProcessingContext) -> bool:
@@ -559,6 +573,9 @@ def strip(
559
573
  pipeline write decisions.
560
574
  """
561
575
  # The 'strip' pipeline computes removal-ready updates even in dry-run
576
+ file_list: list[Path] = []
577
+ results: list[ProcessingContext] = []
578
+ encountered_error_code: ExitCode | None = None
562
579
  _, file_list, results, encountered_error_code = _run_pipeline(
563
580
  pipeline_name="strip",
564
581
  paths=paths,
@@ -569,20 +586,23 @@ def strip(
569
586
  if not file_list:
570
587
  return RunResult(files=(), summary={}, had_errors=False)
571
588
 
589
+ view_results: list[ProcessingContext]
590
+ skipped: int = 0
572
591
  view_results, skipped = _apply_view_filter(
573
592
  results, skip_compliant=skip_compliant, skip_unsupported=skip_unsupported
574
593
  )
575
594
 
576
- files = tuple(_to_file_result(r, apply=apply) for r in view_results)
577
- summary = _summarize(files)
578
- had_errors = any(_map_outcome(r, apply=apply) is Outcome.ERROR for r in results) or (
595
+ files: tuple[FileResult, ...] = tuple(_to_file_result(r, apply=apply) for r in view_results)
596
+ summary: Mapping[str, int] = _summarize(files)
597
+ had_errors: bool = any(_map_outcome(r, apply=apply) is Outcome.ERROR for r in results) or (
579
598
  encountered_error_code is not None
580
599
  )
581
- diagnostics = _collect_diagnostics(view_results)
582
- diagnostic_totals = _collect_diagnostic_totals(view_results)
583
- diagnostic_totals_all = _collect_diagnostic_totals(results)
600
+ diagnostics: dict[str, list[PublicDiagnostic]] = _collect_diagnostics(view_results)
601
+ diagnostic_totals: DiagnosticTotals = _collect_diagnostic_totals(view_results)
602
+ diagnostic_totals_all: DiagnosticTotals = _collect_diagnostic_totals(results)
584
603
 
585
- written = failed = 0
604
+ written: int = 0
605
+ failed: int = 0
586
606
  if apply:
587
607
 
588
608
  def _should_write_strip(r: ProcessingContext) -> bool:
@@ -625,7 +645,7 @@ def get_filetype_info(long: bool = False) -> list[FileTypeInfo]:
625
645
  for name, ft in Registry.filetypes().items():
626
646
  processor = proc_reg.get(name, None) if proc_reg else None
627
647
  supported = bool(processor)
628
- processor_name = processor.__class__.__name__ if processor else None
648
+ processor_name: str | None = processor.__class__.__name__ if processor else None
629
649
  info: FileTypeInfo = {
630
650
  "name": name,
631
651
  "description": getattr(ft, "description", ""),
@@ -26,4 +26,4 @@ from __future__ import annotations
26
26
 
27
27
  from .main import cli
28
28
 
29
- __all__ = ["cli"]
29
+ __all__: list[str] = ["cli"]
@@ -213,7 +213,7 @@ class EnumChoiceParam(ParamTypeBase, Generic[E]):
213
213
  for choice in cast("Iterable[E]", self.enum_cls)
214
214
  }
215
215
 
216
- key = value.lower()
216
+ key: str = value.lower()
217
217
  if key in lookup:
218
218
  return lookup[key]
219
219
 
@@ -240,7 +240,7 @@ class EnumChoiceParam(ParamTypeBase, Generic[E]):
240
240
  CompletionItem as RuntimeCompletionItem,
241
241
  ) # Click 8.x
242
242
 
243
- prefix = (incomplete or "").lower()
243
+ prefix: str = (incomplete or "").lower()
244
244
  items: list["ClickCompletionItem"] = []
245
245
  for e in cast("Iterable[E]", self.enum_cls):
246
246
  val = str(getattr(e, "value", e))
@@ -24,20 +24,23 @@ import click
24
24
  from topmark.cli.config_resolver import resolve_config_from_click
25
25
  from topmark.cli.console_helpers import get_console_safely
26
26
  from topmark.cli_shared.exit_codes import ExitCode
27
- from topmark.config.logging import get_logger
27
+ from topmark.config.logging import TopmarkLogger, get_logger
28
28
  from topmark.file_resolver import resolve_file_list
29
29
  from topmark.pipeline import runner
30
30
  from topmark.pipeline.context import ComparisonStatus, FileStatus, ProcessingContext
31
31
  from topmark.pipeline.pipelines import get_pipeline
32
32
 
33
33
  if TYPE_CHECKING:
34
+ from collections.abc import Sequence
34
35
  from pathlib import Path
35
36
 
36
37
  from topmark.cli.io import InputPlan
38
+ from topmark.cli_shared.console_api import ConsoleLike
37
39
  from topmark.config import Config, MutableConfig
40
+ from topmark.pipeline.contracts import Step
38
41
  from topmark.rendering.formats import HeaderOutputFormat
39
42
 
40
- logger = get_logger(__name__)
43
+ logger: TopmarkLogger = get_logger(__name__)
41
44
 
42
45
 
43
46
  def get_effective_verbosity(ctx: click.Context, config: "Config | None" = None) -> int:
@@ -48,7 +51,7 @@ def get_effective_verbosity(ctx: click.Context, config: "Config | None" = None)
48
51
  2. ctx.obj["verbosity_level"] if present
49
52
  3. 0 (terse)
50
53
  """
51
- cfg_level = getattr(config, "verbosity_level", None) if config else None
54
+ cfg_level: int | None = config.verbosity_level if config else None
52
55
  if cfg_level is not None:
53
56
  return int(cfg_level)
54
57
  return int(ctx.obj.get("verbosity_level", 0))
@@ -83,8 +86,8 @@ def run_steps_for_files(
83
86
  ENCODING_ERROR → UnicodeDecodeError
84
87
  PIPELINE_ERROR → any other unexpected exception
85
88
  """
86
- console = get_console_safely()
87
- steps = get_pipeline(pipeline_name)
89
+ console: ConsoleLike = get_console_safely()
90
+ steps: Sequence[Step] = get_pipeline(pipeline_name)
88
91
  results: list[ProcessingContext] = []
89
92
  encountered_error_code: ExitCode | None = None
90
93
 
@@ -58,7 +58,7 @@ from topmark.cli.cmd_common import (
58
58
  run_steps_for_files,
59
59
  )
60
60
  from topmark.cli.errors import TopmarkIOError, TopmarkUsageError
61
- from topmark.cli.io import plan_cli_inputs
61
+ from topmark.cli.io import InputPlan, plan_cli_inputs
62
62
  from topmark.cli.options import (
63
63
  CONTEXT_SETTINGS,
64
64
  common_config_options,
@@ -80,7 +80,7 @@ from topmark.cli_shared.utils import (
80
80
  safe_unlink,
81
81
  write_updates,
82
82
  )
83
- from topmark.config.logging import get_logger
83
+ from topmark.config.logging import TopmarkLogger, get_logger
84
84
  from topmark.pipeline.context import (
85
85
  ComparisonStatus,
86
86
  FileStatus,
@@ -90,10 +90,13 @@ from topmark.pipeline.context import (
90
90
  )
91
91
 
92
92
  if TYPE_CHECKING:
93
+ from pathlib import Path
94
+
93
95
  from topmark.cli_shared.console_api import ConsoleLike
96
+ from topmark.config import Config, MutableConfig
94
97
  from topmark.rendering.formats import HeaderOutputFormat
95
98
 
96
- logger = get_logger(__name__)
99
+ logger: TopmarkLogger = get_logger(__name__)
97
100
 
98
101
 
99
102
  @click.command(
@@ -236,7 +239,7 @@ def check_command(
236
239
  PIPELINE_ERROR (70): An internal processing step failed.
237
240
  UNEXPECTED_ERROR (255): An unhandled error occurred.
238
241
  """
239
- ctx = click.get_current_context()
242
+ ctx: click.Context = click.get_current_context()
240
243
  ctx.ensure_object(dict)
241
244
  console: ConsoleLike = ctx.obj["console"]
242
245
 
@@ -253,7 +256,7 @@ def check_command(
253
256
  )
254
257
 
255
258
  # === Build Config and file list ===
256
- plan = plan_cli_inputs(
259
+ plan: InputPlan = plan_cli_inputs(
257
260
  ctx=ctx,
258
261
  files_from=files_from,
259
262
  include_from=include_from,
@@ -262,7 +265,7 @@ def check_command(
262
265
  exclude_patterns=exclude_patterns,
263
266
  stdin_filename=stdin_filename,
264
267
  )
265
- draft_config = build_config_common(
268
+ draft_config: MutableConfig = build_config_common(
266
269
  ctx=ctx,
267
270
  plan=plan,
268
271
  no_config=no_config,
@@ -276,18 +279,18 @@ def check_command(
276
279
  # Propagate runtime intent for updater (terminal vs preview write status)
277
280
  draft_config.apply_changes = bool(apply_changes)
278
281
 
279
- config = draft_config.freeze()
282
+ config: Config = draft_config.freeze()
280
283
 
281
- temp_path = plan.temp_path # for cleanup/STDIN-apply branch
282
- stdin_mode = plan.stdin_mode
283
- file_list = build_file_list(
284
+ temp_path: Path | None = plan.temp_path # for cleanup/STDIN-apply branch
285
+ stdin_mode: bool = plan.stdin_mode
286
+ file_list: list[Path] = build_file_list(
284
287
  config,
285
288
  stdin_mode=stdin_mode,
286
289
  temp_path=temp_path,
287
290
  )
288
291
 
289
292
  # Determine effective program-output verbosity for gating extra details
290
- vlevel = get_effective_verbosity(ctx, config)
293
+ vlevel: int = get_effective_verbosity(ctx, config)
291
294
 
292
295
  logger.trace("Config after merging args and resolving file list: %s", config)
293
296
 
@@ -304,7 +307,7 @@ def check_command(
304
307
 
305
308
  # Always run the 'apply' pipeline when --apply is set so updated_file_lines are computed.
306
309
  # The --summary flag only affects how we render output, not which steps run.
307
- pipeline_name = "apply" if apply_changes else ("summary" if summary_mode else "apply")
310
+ pipeline_name: str = "apply" if apply_changes else ("summary" if summary_mode else "apply")
308
311
  results: list[ProcessingContext] = []
309
312
  encountered_error_code: ExitCode | None = None
310
313
 
@@ -313,7 +316,7 @@ def check_command(
313
316
  )
314
317
 
315
318
  # Optional filtering
316
- view_results = filter_view_results(
319
+ view_results: list[ProcessingContext] = filter_view_results(
317
320
  results, skip_compliant=skip_compliant, skip_unsupported=skip_unsupported
318
321
  )
319
322
 
@@ -358,8 +361,8 @@ def check_command(
358
361
  return
359
362
 
360
363
  if apply_changes:
361
- written = 0
362
- failed = 0
364
+ written: int = 0
365
+ failed: int = 0
363
366
 
364
367
  def _should_write_check(r: ProcessingContext) -> bool:
365
368
  """Determine whether to write this file in check mode."""
@@ -376,7 +379,7 @@ def check_command(
376
379
  written, failed = write_updates(results, should_write=_should_write_check)
377
380
 
378
381
  if fmt == OutputFormat.DEFAULT:
379
- msg = (
382
+ msg: str = (
380
383
  f"\n✅ Applied changes to {written} file(s)."
381
384
  if written
382
385
  else "\n✅ No changes to apply."