topmark 0.1.0__tar.gz → 0.2.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 (60) hide show
  1. {topmark-0.1.0/src/topmark.egg-info → topmark-0.2.0}/PKG-INFO +98 -6
  2. {topmark-0.1.0 → topmark-0.2.0}/README.md +93 -4
  3. {topmark-0.1.0 → topmark-0.2.0}/pyproject.toml +10 -3
  4. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/default.py +50 -9
  5. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/main.py +11 -0
  6. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/config/__init__.py +14 -9
  7. topmark-0.2.0/src/topmark/config/topmark-default.toml +75 -0
  8. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/constants.py +4 -1
  9. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/filetypes/base.py +18 -2
  10. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/filetypes/instances.py +144 -1
  11. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/context.py +2 -2
  12. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/processors/base.py +42 -6
  13. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/processors/pound.py +0 -43
  14. topmark-0.2.0/src/topmark/pipeline/processors/slash.py +84 -0
  15. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/processors/xml.py +5 -31
  16. topmark-0.2.0/src/topmark/pipeline/steps/resolver.py +142 -0
  17. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/updater.py +84 -29
  18. {topmark-0.1.0 → topmark-0.2.0/src/topmark.egg-info}/PKG-INFO +98 -6
  19. {topmark-0.1.0 → topmark-0.2.0}/src/topmark.egg-info/SOURCES.txt +2 -0
  20. topmark-0.1.0/src/topmark/pipeline/steps/resolver.py +0 -103
  21. {topmark-0.1.0 → topmark-0.2.0}/LICENSE +0 -0
  22. {topmark-0.1.0 → topmark-0.2.0}/setup.cfg +0 -0
  23. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/__init__.py +0 -0
  24. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/__init__.py +0 -0
  25. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/cli_types.py +0 -0
  26. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/__init__.py +0 -0
  27. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/dump_config.py +0 -0
  28. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/filetypes.py +0 -0
  29. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/init_config.py +0 -0
  30. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/show_defaults.py +0 -0
  31. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/commands/version.py +0 -0
  32. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/config_resolver.py +0 -0
  33. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/options.py +0 -0
  34. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/cli/utils.py +0 -0
  35. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/config/logging.py +0 -0
  36. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/file_resolver.py +0 -0
  37. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/filetypes/__init__.py +0 -0
  38. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/filetypes/policy.py +0 -0
  39. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/filetypes/registry.py +0 -0
  40. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/__init__.py +0 -0
  41. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/contracts.py +0 -0
  42. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/pipelines.py +0 -0
  43. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/processors/__init__.py +0 -0
  44. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/runner.py +0 -0
  45. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/__init__.py +0 -0
  46. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/builder.py +0 -0
  47. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/comparer.py +0 -0
  48. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/patcher.py +0 -0
  49. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/reader.py +0 -0
  50. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/renderer.py +0 -0
  51. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/pipeline/steps/scanner.py +0 -0
  52. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/py.typed +0 -0
  53. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/rendering/api.py +0 -0
  54. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/rendering/formats.py +0 -0
  55. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/utils/__init__.py +0 -0
  56. {topmark-0.1.0 → topmark-0.2.0}/src/topmark/utils/file.py +0 -0
  57. {topmark-0.1.0 → topmark-0.2.0}/src/topmark.egg-info/dependency_links.txt +0 -0
  58. {topmark-0.1.0 → topmark-0.2.0}/src/topmark.egg-info/entry_points.txt +0 -0
  59. {topmark-0.1.0 → topmark-0.2.0}/src/topmark.egg-info/requires.txt +0 -0
  60. {topmark-0.1.0 → topmark-0.2.0}/src/topmark.egg-info/top_level.txt +0 -0
@@ -1,10 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topmark
3
- Version: 0.1.0
4
- Summary: A tool to inspect, validate, and manage file headers in diverse codebases
3
+ Version: 0.2.0
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
7
7
  Project-URL: Homepage, https://github.com/shutterfreak/topmark
8
+ Project-URL: Documentation, https://topmark.readthedocs.io/en/latest/
9
+ Project-URL: Source, https://github.com/shutterfreak/topmark
10
+ Project-URL: Changelog, https://github.com/shutterfreak/topmark/releases
8
11
  Classifier: Programming Language :: Python :: 3.10
9
12
  Classifier: Programming Language :: Python :: 3.11
10
13
  Classifier: Programming Language :: Python :: 3.12
@@ -58,6 +61,11 @@ topmark:header:end
58
61
 
59
62
  # TopMark
60
63
 
64
+ [![PyPI version](https://img.shields.io/pypi/v/topmark.svg)](https://pypi.org/project/topmark/)
65
+ [![Documentation Status](https://readthedocs.org/projects/topmark/badge/?version=latest)](https://topmark.readthedocs.io/en/latest/?badge=latest)
66
+ [![Downloads](https://static.pepy.tech/badge/topmark)](https://pepy.tech/project/topmark)
67
+ [![GitHub release](https://img.shields.io/github/v/release/shutterfreak/topmark)](https://github.com/shutterfreak/topmark/releases)
68
+
61
69
  **TopMark** is a command-line tool to inspect, validate, and manage file headers in diverse
62
70
  codebases. It helps maintain consistent header metadata across projects by supporting per-file-type
63
71
  header formats, customizable fields, inclusion/exclusion rules, and dry-run safety.
@@ -119,6 +127,15 @@ Logging verbosity is controlled globally:
119
127
  All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
120
128
  subcommands like `check` or `apply`.
121
129
 
130
+ ### Skipping and filtering helpers
131
+
132
+ These switches help keep CI output meaningful and fast:
133
+
134
+ - `--skip-compliant` — hide files that are already compliant; only show items that need attention.
135
+ - `--skip-unsupported` — hide unsupported file types and files matched by known types that
136
+ intentionally **do not** support headers (e.g., strict JSON). They remain **recognized** but are
137
+ not listed in results.
138
+
122
139
  ### Examples
123
140
 
124
141
  ```bash
@@ -151,6 +168,12 @@ topmark version
151
168
 
152
169
  # Apply changes to files in-place
153
170
  topmark --apply src/
171
+
172
+ # CI-friendly summary: only show issues; ignore unsupported types
173
+ topmark --skip-compliant --skip-unsupported src/
174
+
175
+ # Apply fixes for only the changed files passed by pre-commit
176
+ topmark --apply --apply --skip-unsupported --quiet
154
177
  ```
155
178
 
156
179
  ## 📐 Header placement rules
@@ -241,10 +264,79 @@ The following options can be used with most commands.
241
264
 
242
265
  ## 🧩 Supported file types
243
266
 
244
- | Processor | File types (examples) |
245
- | -------------------- | -------------------------------------------------------------------------- |
246
- | PoundHeaderProcessor | python, shell, ruby, r, julia, perl, makefile, dockerfile, yaml, toml, env |
247
- | XmlHeaderProcessor | xml, xhtml, html, svg, xsl/xslt, vue, svelte, markdown |
267
+ | Processor | File types (examples) |
268
+ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
269
+ | PoundHeaderProcessor | dockerfile, env, git-meta, ini, julia, makefile, perl, python, python-requirements, r, ruby, shell, toml, yaml |
270
+ | SlashHeaderProcessor | c, cpp, cs, go, java, javascript, kotlin, rust, swift, typescript, vscode-jsonc |
271
+ | XmlHeaderProcessor | html, markdown, svelte, svg, vue, xhtml, xml, xsl, xslt, yaml |
272
+
273
+ Some formats (e.g., strict JSON) are **recognized but intentionally skipped** because they lack a
274
+ safe comment syntax. Use `--skip-unsupported` to hide them from the report while keeping safety.
275
+
276
+ For a complete list, please run:
277
+
278
+ ```sh
279
+ topmark filetypes
280
+ ```
281
+
282
+ ### How TopMark resolves file types (specificity & safety)
283
+
284
+ TopMark may have multiple `FileType` definitions that **match** a given path. The resolver now:
285
+
286
+ - evaluates **all** matching file types and scores them by **specificity**,
287
+ - prefers **explicit filenames / tail subpaths** (e.g., `.vscode/settings.json`) over patterns, and
288
+ **patterns** over simple **extensions**,
289
+ - breaks ties in favor of **headerable** types (those without `skip_processing=True`).
290
+
291
+ **Tail subpath matching.** `FileType.filenames` entries that contain a path separator (e.g.,
292
+ `".vscode/settings.json"`) are matched as **path suffixes** against `path.as_posix()`; plain names
293
+ still match the **basename** only.
294
+
295
+ **JSON vs JSONC.** Generic `json` is recognized but marked `skip_processing=True` (no comments in
296
+ strict JSON), while `vscode-jsonc` is a safe, **narrow** opt‑in that uses `//` headers. If you need
297
+ more JSON-with-comments files, add them via a dedicated `FileType` or an explicit allow‑list in
298
+ config.
299
+
300
+ **Shebang‑aware insertion.** The default insertion logic is policy‑driven and shebang‑aware (insert
301
+ after `#!` and optional encoding line). For formats like XML that need character‑precise placement,
302
+ processors provide a text‑offset path; `XmlHeaderProcessor` uses this and signals **no line
303
+ anchor**.
304
+
305
+ ## 🪝 Pre-commit integration
306
+
307
+ TopMark provides first-class pre-commit hooks. A minimal consumer configuration:
308
+
309
+ ```yaml
310
+ # .pre-commit-config.yaml (in a consuming repository)
311
+ repos:
312
+ - repo: https://github.com/shutterfreak/topmark
313
+ rev: v0.2.0 # pin to a released tag
314
+ hooks:
315
+ - id: topmark-check
316
+ # Optional: limit scope to supported text types
317
+ # files: '\\.(py|md|toml|ya?ml|sh|Makefile)$'
318
+ ```
319
+
320
+ Hooks shipped by this repo:
321
+
322
+ - **`topmark-check`** — non-destructive validation. Recommended on `pre-commit` / `pre-push`.
323
+ - **`topmark-apply`** — destructive, requires `--apply`. Marked `manual` so it only runs when you
324
+ call it explicitly.
325
+
326
+ **Run the manual hook locally:**
327
+
328
+ ```bash
329
+ # On the whole repo
330
+ pre-commit run topmark-apply --all-files --hook-stage manual
331
+
332
+ # On specific files
333
+ pre-commit run topmark-apply --files path/to/file1 path/to/file2 --hook-stage manual
334
+ ```
335
+
336
+ **Why does the hook seem to run multiple times?** Pre-commit batches filenames to avoid OS
337
+ argument-length limits. You may see repeated banners (e.g., “Processing N file(s)”) as the hook runs
338
+ once per batch. To run **once** per repo, set `pass_filenames: false` in the hook manifest and let
339
+ TopMark discover files itself.
248
340
 
249
341
  ## 🛠 Configuration
250
342
 
@@ -12,6 +12,11 @@ topmark:header:end
12
12
 
13
13
  # TopMark
14
14
 
15
+ [![PyPI version](https://img.shields.io/pypi/v/topmark.svg)](https://pypi.org/project/topmark/)
16
+ [![Documentation Status](https://readthedocs.org/projects/topmark/badge/?version=latest)](https://topmark.readthedocs.io/en/latest/?badge=latest)
17
+ [![Downloads](https://static.pepy.tech/badge/topmark)](https://pepy.tech/project/topmark)
18
+ [![GitHub release](https://img.shields.io/github/v/release/shutterfreak/topmark)](https://github.com/shutterfreak/topmark/releases)
19
+
15
20
  **TopMark** is a command-line tool to inspect, validate, and manage file headers in diverse
16
21
  codebases. It helps maintain consistent header metadata across projects by supporting per-file-type
17
22
  header formats, customizable fields, inclusion/exclusion rules, and dry-run safety.
@@ -73,6 +78,15 @@ Logging verbosity is controlled globally:
73
78
  All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
74
79
  subcommands like `check` or `apply`.
75
80
 
81
+ ### Skipping and filtering helpers
82
+
83
+ These switches help keep CI output meaningful and fast:
84
+
85
+ - `--skip-compliant` — hide files that are already compliant; only show items that need attention.
86
+ - `--skip-unsupported` — hide unsupported file types and files matched by known types that
87
+ intentionally **do not** support headers (e.g., strict JSON). They remain **recognized** but are
88
+ not listed in results.
89
+
76
90
  ### Examples
77
91
 
78
92
  ```bash
@@ -105,6 +119,12 @@ topmark version
105
119
 
106
120
  # Apply changes to files in-place
107
121
  topmark --apply src/
122
+
123
+ # CI-friendly summary: only show issues; ignore unsupported types
124
+ topmark --skip-compliant --skip-unsupported src/
125
+
126
+ # Apply fixes for only the changed files passed by pre-commit
127
+ topmark --apply --apply --skip-unsupported --quiet
108
128
  ```
109
129
 
110
130
  ## 📐 Header placement rules
@@ -195,10 +215,79 @@ The following options can be used with most commands.
195
215
 
196
216
  ## 🧩 Supported file types
197
217
 
198
- | Processor | File types (examples) |
199
- | -------------------- | -------------------------------------------------------------------------- |
200
- | PoundHeaderProcessor | python, shell, ruby, r, julia, perl, makefile, dockerfile, yaml, toml, env |
201
- | XmlHeaderProcessor | xml, xhtml, html, svg, xsl/xslt, vue, svelte, markdown |
218
+ | Processor | File types (examples) |
219
+ | -------------------- | -------------------------------------------------------------------------------------------------------------- |
220
+ | PoundHeaderProcessor | dockerfile, env, git-meta, ini, julia, makefile, perl, python, python-requirements, r, ruby, shell, toml, yaml |
221
+ | SlashHeaderProcessor | c, cpp, cs, go, java, javascript, kotlin, rust, swift, typescript, vscode-jsonc |
222
+ | XmlHeaderProcessor | html, markdown, svelte, svg, vue, xhtml, xml, xsl, xslt, yaml |
223
+
224
+ Some formats (e.g., strict JSON) are **recognized but intentionally skipped** because they lack a
225
+ safe comment syntax. Use `--skip-unsupported` to hide them from the report while keeping safety.
226
+
227
+ For a complete list, please run:
228
+
229
+ ```sh
230
+ topmark filetypes
231
+ ```
232
+
233
+ ### How TopMark resolves file types (specificity & safety)
234
+
235
+ TopMark may have multiple `FileType` definitions that **match** a given path. The resolver now:
236
+
237
+ - evaluates **all** matching file types and scores them by **specificity**,
238
+ - prefers **explicit filenames / tail subpaths** (e.g., `.vscode/settings.json`) over patterns, and
239
+ **patterns** over simple **extensions**,
240
+ - breaks ties in favor of **headerable** types (those without `skip_processing=True`).
241
+
242
+ **Tail subpath matching.** `FileType.filenames` entries that contain a path separator (e.g.,
243
+ `".vscode/settings.json"`) are matched as **path suffixes** against `path.as_posix()`; plain names
244
+ still match the **basename** only.
245
+
246
+ **JSON vs JSONC.** Generic `json` is recognized but marked `skip_processing=True` (no comments in
247
+ strict JSON), while `vscode-jsonc` is a safe, **narrow** opt‑in that uses `//` headers. If you need
248
+ more JSON-with-comments files, add them via a dedicated `FileType` or an explicit allow‑list in
249
+ config.
250
+
251
+ **Shebang‑aware insertion.** The default insertion logic is policy‑driven and shebang‑aware (insert
252
+ after `#!` and optional encoding line). For formats like XML that need character‑precise placement,
253
+ processors provide a text‑offset path; `XmlHeaderProcessor` uses this and signals **no line
254
+ anchor**.
255
+
256
+ ## 🪝 Pre-commit integration
257
+
258
+ TopMark provides first-class pre-commit hooks. A minimal consumer configuration:
259
+
260
+ ```yaml
261
+ # .pre-commit-config.yaml (in a consuming repository)
262
+ repos:
263
+ - repo: https://github.com/shutterfreak/topmark
264
+ rev: v0.2.0 # pin to a released tag
265
+ hooks:
266
+ - id: topmark-check
267
+ # Optional: limit scope to supported text types
268
+ # files: '\\.(py|md|toml|ya?ml|sh|Makefile)$'
269
+ ```
270
+
271
+ Hooks shipped by this repo:
272
+
273
+ - **`topmark-check`** — non-destructive validation. Recommended on `pre-commit` / `pre-push`.
274
+ - **`topmark-apply`** — destructive, requires `--apply`. Marked `manual` so it only runs when you
275
+ call it explicitly.
276
+
277
+ **Run the manual hook locally:**
278
+
279
+ ```bash
280
+ # On the whole repo
281
+ pre-commit run topmark-apply --all-files --hook-stage manual
282
+
283
+ # On specific files
284
+ pre-commit run topmark-apply --files path/to/file1 path/to/file2 --hook-stage manual
285
+ ```
286
+
287
+ **Why does the hook seem to run multiple times?** Pre-commit batches filenames to avoid OS
288
+ argument-length limits. You may see repeated banners (e.g., “Processing N file(s)”) as the hook runs
289
+ once per batch. To run **once** per repo, set `pass_filenames: false` in the hook manifest and let
290
+ TopMark discover files itself.
202
291
 
203
292
  ## 🛠 Configuration
204
293
 
@@ -10,10 +10,9 @@
10
10
 
11
11
  [project]
12
12
  name = "topmark"
13
- version = "0.1.0"
14
- description = "A tool to inspect, validate, and manage file headers in diverse codebases"
13
+ version = "0.2.0"
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
- urls = { "Homepage" = "https://github.com/shutterfreak/topmark" }
17
16
  license = "MIT"
18
17
  readme = "README.md"
19
18
  dependencies = [
@@ -40,6 +39,12 @@ classifiers = [
40
39
  [project.scripts]
41
40
  topmark = "topmark.cli.main:cli"
42
41
 
42
+ [project.urls]
43
+ Homepage = "https://github.com/shutterfreak/topmark"
44
+ Documentation = "https://topmark.readthedocs.io/en/latest/"
45
+ Source = "https://github.com/shutterfreak/topmark"
46
+ Changelog = "https://github.com/shutterfreak/topmark/releases"
47
+
43
48
  [build-system]
44
49
  requires = ["setuptools>=61.0", "wheel"]
45
50
  build-backend = "setuptools.build_meta"
@@ -97,9 +102,11 @@ typing = [
97
102
 
98
103
  [tool.setuptools]
99
104
  package-dir = { "" = "src" }
105
+ include-package-data = true
100
106
 
101
107
  [tool.setuptools.package-data]
102
108
  "topmark" = ["py.typed"]
109
+ "topmark.config" = ["topmark-default.toml"]
103
110
 
104
111
  [tool.setuptools.packages.find]
105
112
  where = ["src"]
@@ -29,6 +29,8 @@ Examples:
29
29
  topmark --apply --diff .
30
30
  """
31
31
 
32
+ import logging
33
+
32
34
  import click
33
35
  from yachalk import chalk
34
36
 
@@ -48,7 +50,7 @@ from topmark.cli.utils import (
48
50
  from topmark.config.logging import get_logger
49
51
  from topmark.file_resolver import resolve_file_list
50
52
  from topmark.pipeline import runner
51
- from topmark.pipeline.context import ComparisonStatus, ProcessingContext
53
+ from topmark.pipeline.context import ComparisonStatus, FileStatus, HeaderStatus, ProcessingContext
52
54
  from topmark.pipeline.pipelines import get_pipeline
53
55
  from topmark.rendering.formats import HeaderOutputFormat
54
56
 
@@ -75,6 +77,13 @@ logger = get_logger(__name__)
75
77
  help="Hide files that are already up-to-date.",
76
78
  )
77
79
  @underscored_trap_option("--skip_compliant")
80
+ @typed_option(
81
+ "--skip-unsupported",
82
+ "skip_unsupported",
83
+ is_flag=True,
84
+ help="Hide unsupported file types.",
85
+ )
86
+ @underscored_trap_option("--skip_unsupported")
78
87
  @typed_option(
79
88
  "--format",
80
89
  "output_format",
@@ -98,6 +107,7 @@ def default_command(
98
107
  diff: bool,
99
108
  summary_mode: bool,
100
109
  skip_compliant: bool,
110
+ skip_unsupported: bool,
101
111
  output_format: OutputFormat | None,
102
112
  ) -> None:
103
113
  """Run the unified default command (check/apply).
@@ -121,6 +131,7 @@ def default_command(
121
131
  diff: If True, show unified diffs of header changes (human output only).
122
132
  summary_mode: If True, print outcome counts instead of per-file details.
123
133
  skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
134
+ skip_unsupported: If True, suppress unsupported file types.
124
135
  output_format: Output format to use (default, json, or ndjson).
125
136
 
126
137
  Raises:
@@ -182,11 +193,12 @@ def default_command(
182
193
  stdin_stream = click.get_text_stream("stdin") if stdin else None
183
194
  file_list = resolve_file_list(config, stdin_stream=stdin_stream)
184
195
  if not file_list:
185
- click.echo(chalk.blue("\nNo files.\n"))
196
+ click.echo(chalk.blue("\nℹ️ No files to process.\n"))
186
197
  raise SystemExit(0)
187
198
 
188
- click.echo(chalk.blue(f"\nProcessing {len(file_list)} file(s):\n"))
189
- click.echo(chalk.bold.underline("TopMark Results:"))
199
+ if logger.isEnabledFor(logging.INFO):
200
+ click.echo(chalk.blue(f"\n🔍 Processing {len(file_list)} file(s):\n"))
201
+ click.echo(chalk.bold.underline("📋 TopMark Results:"))
190
202
 
191
203
  # Use summary pipeline when --summary is set, else full (compare+update+patch)
192
204
  pipeline_name = "summary" if summary_mode else "apply"
@@ -200,16 +212,16 @@ def default_command(
200
212
  results.append(ctx_obj)
201
213
  except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
202
214
  logger.error("Filesystem error while processing %s: %s", path, e)
203
- click.echo(chalk.red(f"Filesystem error processing {path}: {e}"))
215
+ click.echo(chalk.red(f"❌ Filesystem error processing {path}: {e}"))
204
216
  continue
205
217
  except UnicodeDecodeError as e:
206
218
  logger.error("Encoding error while reading %s: %s", path, e)
207
- click.echo(chalk.red(f"Encoding error in {path}: {e}"))
219
+ click.echo(chalk.red(f"🧵 Encoding error in {path}: {e}"))
208
220
  continue
209
221
  except Exception as e: # pragma: no cover - unexpected path
210
222
  logger.exception("Unexpected error processing %s", path)
211
223
  click.echo(
212
- chalk.red(f"Unexpected error processing {path}: {e} (use -vv for traceback)")
224
+ chalk.red(f"⚠️ Unexpected error processing {path}: {e} (use -vv for traceback)")
213
225
  )
214
226
  continue
215
227
 
@@ -219,6 +231,17 @@ def default_command(
219
231
  else:
220
232
  view_results = results
221
233
 
234
+ if skip_unsupported:
235
+ view_results = [
236
+ r
237
+ for r in view_results
238
+ if r.status.file
239
+ not in [
240
+ FileStatus.SKIPPED_UNSUPPORTED,
241
+ FileStatus.SKIPPED_KNOWN_NO_HEADERS,
242
+ ]
243
+ ]
244
+
222
245
  fmt: OutputFormat = output_format or OutputFormat.DEFAULT
223
246
  # Machine formats first
224
247
  if fmt is OutputFormat.NDJSON:
@@ -256,6 +279,18 @@ def default_command(
256
279
  else:
257
280
  for r in view_results:
258
281
  click.echo(r.summary)
282
+ if r.status.comparison == ComparisonStatus.CHANGED:
283
+ if apply_changes:
284
+ if r.status.header == HeaderStatus.MISSING:
285
+ click.echo(chalk.yellow(f" ➕ Adding header for '{r.path}'"))
286
+ else:
287
+ click.echo(chalk.yellow(f" ✏️ Updating header for '{r.path}'"))
288
+ else:
289
+ click.echo(
290
+ chalk.yellow(
291
+ f" 🛠️ Run `topmark --apply {r.path}` to update this file."
292
+ )
293
+ )
259
294
  if diff:
260
295
  for r in view_results:
261
296
  if r.header_diff:
@@ -277,7 +312,11 @@ def default_command(
277
312
  logger.error("Failed to write %s: %s", r.path, e)
278
313
  if fmt is OutputFormat.DEFAULT:
279
314
  click.echo(
280
- f"\nApplied changes to {written} file(s)." if written else "\nNo changes to apply."
315
+ (
316
+ f"\n✅ Applied changes to {written} file(s)."
317
+ if written
318
+ else "\n✅ No changes to apply."
319
+ )
281
320
  )
282
321
  if failed:
283
322
  raise SystemExit(1)
@@ -291,5 +330,7 @@ def default_command(
291
330
  if would_change:
292
331
  raise SystemExit(2)
293
332
 
294
- click.echo("All done!")
333
+ if logger.isEnabledFor(logging.INFO):
334
+ click.echo("✅ All done!")
335
+
295
336
  return
@@ -184,6 +184,13 @@ CONTEXT_SETTINGS = dict(
184
184
  help="Hide files that are already up-to-date.",
185
185
  )
186
186
  @underscored_trap_option("--skip_compliant")
187
+ @typed_option(
188
+ "--skip-unsupported",
189
+ "skip_unsupported",
190
+ is_flag=True,
191
+ help="Hide unsupported file types.",
192
+ )
193
+ @underscored_trap_option("--skip_unsupported")
187
194
  @typed_option(
188
195
  "--format",
189
196
  "output_format",
@@ -227,6 +234,7 @@ def cli(
227
234
  diff: bool,
228
235
  summary_mode: bool,
229
236
  skip_compliant: bool,
237
+ skip_unsupported: bool,
230
238
  output_format: OutputFormat | None,
231
239
  ) -> None:
232
240
  """
@@ -257,6 +265,7 @@ def cli(
257
265
  diff: If True, show unified diffs of header changes (human output only).
258
266
  summary_mode: If True, print outcome counts instead of per-file details.
259
267
  skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
268
+ skip_unsupported: If True, suppress unsupported file types.
260
269
  output_format: Output format to use (default, json, or ndjson).
261
270
 
262
271
  Notes:
@@ -331,6 +340,7 @@ def cli(
331
340
  diff=diff,
332
341
  summary_mode=summary_mode,
333
342
  skip_compliant=skip_compliant,
343
+ skip_unsupported=skip_unsupported,
334
344
  output_format=output_format,
335
345
  )
336
346
  return
@@ -395,6 +405,7 @@ def ensure_commands_registered() -> None:
395
405
  diff=p.get("diff", False),
396
406
  summary_mode=p.get("summary_mode", False),
397
407
  skip_compliant=p.get("skip_compliant", False),
408
+ skip_unsupported=p.get("skip_unsupported", False),
398
409
  output_format=p.get("output_format"),
399
410
  )
400
411
 
@@ -21,17 +21,17 @@ from topmark.toml or pyproject.toml.
21
21
  import functools
22
22
  from dataclasses import dataclass, field
23
23
  from datetime import datetime
24
+ from importlib.resources import files
24
25
  from pathlib import Path
25
26
  from typing import Any, TypeGuard
26
27
 
27
28
  import toml
28
29
 
29
30
  from topmark.cli.cli_types import ArgsNamespace
30
- from topmark.constants import DEFAULT_TOML_CONFIG_PATH, VALUE_NOT_SET
31
+ from topmark.config.logging import get_logger
32
+ from topmark.constants import DEFAULT_TOML_CONFIG_RESOURCE, VALUE_NOT_SET
31
33
  from topmark.rendering.formats import HeaderOutputFormat
32
34
 
33
- from .logging import get_logger
34
-
35
35
  logger = get_logger(__name__)
36
36
 
37
37
 
@@ -103,8 +103,9 @@ class Config:
103
103
  Returns:
104
104
  str: The contents of the default TOML configuration file.
105
105
  """
106
- logger.debug("Loading defaults from %s", DEFAULT_TOML_CONFIG_PATH)
107
- return DEFAULT_TOML_CONFIG_PATH.read_text(encoding="utf-8")
106
+ logger.debug("Loading defaults from package resource: %s", DEFAULT_TOML_CONFIG_RESOURCE)
107
+ resource = files("topmark.config") / DEFAULT_TOML_CONFIG_RESOURCE
108
+ return resource.read_text(encoding="utf-8")
108
109
 
109
110
  @classmethod
110
111
  def to_cleaned_toml(cls, toml_doc: str) -> str:
@@ -122,7 +123,7 @@ class Config:
122
123
  return toml.dumps(parsed)
123
124
 
124
125
  @classmethod
125
- @functools.cache # Cache to avoid re-parsing defaults multiple times
126
+ @functools.cache
126
127
  def from_defaults(cls) -> "Config":
127
128
  """
128
129
  Load the default configuration from the bundled topmark-default.toml file.
@@ -130,12 +131,16 @@ class Config:
130
131
  Returns:
131
132
  Config: A Config instance populated with default values.
132
133
  """
133
- default_toml_data = toml.load(DEFAULT_TOML_CONFIG_PATH)
134
+ resource = files("topmark.config") / DEFAULT_TOML_CONFIG_RESOURCE
135
+ default_toml_text = resource.read_text(encoding="utf-8")
136
+ default_toml_data = toml.loads(default_toml_text)
134
137
  logger.debug("Default Config: %s", default_toml_data)
135
- # When loading defaults, explicitly include defaults flag to allow special handling
138
+
139
+ # Note: `config_file` is set to None because this is a package resource,
140
+ # not a user-specified filesystem path.
136
141
  return cls.from_toml_dict(
137
142
  default_toml_data,
138
- config_file=DEFAULT_TOML_CONFIG_PATH,
143
+ config_file=None,
139
144
  use_defaults=True, # We ONLY include defaults when loading from defaults!
140
145
  )
141
146
 
@@ -0,0 +1,75 @@
1
+ # topmark:header:start
2
+ #
3
+ # file : topmark-default.toml
4
+ # file_relpath : src/topmark/config/topmark-default.toml
5
+ # project : TopMark
6
+ # license : MIT
7
+ # copyright : (c) 2025 Olivier Biot
8
+ #
9
+ # topmark:header:end
10
+
11
+ #########################
12
+ # TopMark configuration #
13
+ #########################
14
+ # This file is used to define the default configuration for TopMark.
15
+
16
+
17
+ # ==========================
18
+ # Header field configuration
19
+ # ==========================
20
+
21
+ [header]
22
+ # Configuration for header fields and metadata to include in generated headers
23
+
24
+ # List of fields to include in the header.
25
+ # Available fields include 'file', 'file_relpath', 'author', 'date', 'license', etc.
26
+ # You can define your own fields as well.
27
+
28
+ # TODO: review the fields: builtin field types and custom fields
29
+
30
+ fields = ["file", "file_relpath"]
31
+
32
+
33
+ # ========================
34
+ # TopMark field definition
35
+ # ========================
36
+
37
+ [fields]
38
+
39
+ # Project name to include in the header
40
+ # project = "TopMark"
41
+
42
+ # License to include in the header. Example values: 'MIT', 'GPL-3.0', 'Apache-2.0'
43
+ # license = "MIT"
44
+
45
+ # Copyright statement to include in the header.
46
+ # copyright = "(c) 2025 Your Name" # Update according to your needs
47
+
48
+
49
+ # ========================
50
+ # Formatting configuration
51
+ # ========================
52
+
53
+ [formatting]
54
+ # Formatting options for header rendering.
55
+
56
+ # Align the fields (colons) in the header for better readability. Values: true or false.
57
+ align_fields = true
58
+
59
+ # File header format (default, native, plain, json)
60
+ # header_format = "native"
61
+
62
+
63
+ # ===================
64
+ # Files configuration
65
+ # ===================
66
+
67
+ [files]
68
+
69
+ # Specify (filter) file types:
70
+ # file_types = "markdown"
71
+
72
+
73
+ # ============================
74
+ # END of TopMark Configuration
75
+ # ============================
@@ -16,7 +16,10 @@ from pathlib import Path
16
16
  TOPMARK_VERSION = get_version("topmark")
17
17
 
18
18
  PYPROJECT_TOML_PATH = Path(__file__).parent.parent / "pyproject.toml"
19
- DEFAULT_TOML_CONFIG_PATH: Path = Path(__file__).parent / "config" / "topmark-default.toml"
19
+
20
+ # Name of the bundled default config inside the package `topmark.config`.
21
+ # This is used with importlib.resources in config/__init__.py
22
+ DEFAULT_TOML_CONFIG_RESOURCE: str = "topmark-default.toml"
20
23
 
21
24
  TOPMARK_START_MARKER: str = "topmark:header:start"
22
25
  TOPMARK_END_MARKER: str = "topmark:header:end"
@@ -66,10 +66,26 @@ class FileType:
66
66
  Returns:
67
67
  True if the file matches this file type, False otherwise.
68
68
  """
69
+ # Extension match (simple suffix)
69
70
  if path.suffix in self.extensions:
70
71
  return True
71
- if path.name in self.filenames:
72
- return True
72
+
73
+ # Filenames: support exact basename or tail subpath matches
74
+ # Examples:
75
+ # - "settings.json" matches only if basename == "settings.json"
76
+ # - ".vscode/settings.json" matches if path.as_posix().endswith(".vscode/settings.json")
77
+ if self.filenames:
78
+ basename = path.name
79
+ posix = path.as_posix()
80
+ for fname in self.filenames:
81
+ if "/" in fname or "\\" in fname:
82
+ if posix.endswith(fname):
83
+ return True
84
+ else:
85
+ if basename == fname:
86
+ return True
87
+
88
+ # Regex patterns against basename
73
89
  for pattern in self.patterns:
74
90
  if re.fullmatch(pattern, path.name):
75
91
  return True