topmark 0.1.1__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 (59) hide show
  1. {topmark-0.1.1/src/topmark.egg-info → topmark-0.2.0}/PKG-INFO +89 -5
  2. {topmark-0.1.1 → topmark-0.2.0}/README.md +88 -4
  3. {topmark-0.1.1 → topmark-0.2.0}/pyproject.toml +3 -1
  4. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/default.py +50 -9
  5. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/main.py +11 -0
  6. {topmark-0.1.1 → 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.1 → topmark-0.2.0}/src/topmark/constants.py +4 -1
  9. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/context.py +2 -2
  10. {topmark-0.1.1 → topmark-0.2.0/src/topmark.egg-info}/PKG-INFO +89 -5
  11. {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/SOURCES.txt +1 -0
  12. {topmark-0.1.1 → topmark-0.2.0}/LICENSE +0 -0
  13. {topmark-0.1.1 → topmark-0.2.0}/setup.cfg +0 -0
  14. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/__init__.py +0 -0
  15. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/__init__.py +0 -0
  16. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/cli_types.py +0 -0
  17. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/__init__.py +0 -0
  18. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/dump_config.py +0 -0
  19. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/filetypes.py +0 -0
  20. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/init_config.py +0 -0
  21. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/show_defaults.py +0 -0
  22. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/commands/version.py +0 -0
  23. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/config_resolver.py +0 -0
  24. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/options.py +0 -0
  25. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/cli/utils.py +0 -0
  26. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/config/logging.py +0 -0
  27. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/file_resolver.py +0 -0
  28. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/__init__.py +0 -0
  29. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/base.py +0 -0
  30. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/instances.py +0 -0
  31. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/policy.py +0 -0
  32. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/filetypes/registry.py +0 -0
  33. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/__init__.py +0 -0
  34. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/contracts.py +0 -0
  35. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/pipelines.py +0 -0
  36. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/__init__.py +0 -0
  37. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/base.py +0 -0
  38. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/pound.py +0 -0
  39. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/slash.py +0 -0
  40. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/processors/xml.py +0 -0
  41. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/runner.py +0 -0
  42. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/__init__.py +0 -0
  43. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/builder.py +0 -0
  44. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/comparer.py +0 -0
  45. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/patcher.py +0 -0
  46. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/reader.py +0 -0
  47. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/renderer.py +0 -0
  48. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/resolver.py +0 -0
  49. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/scanner.py +0 -0
  50. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/pipeline/steps/updater.py +0 -0
  51. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/py.typed +0 -0
  52. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/rendering/api.py +0 -0
  53. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/rendering/formats.py +0 -0
  54. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/utils/__init__.py +0 -0
  55. {topmark-0.1.1 → topmark-0.2.0}/src/topmark/utils/file.py +0 -0
  56. {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/dependency_links.txt +0 -0
  57. {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/entry_points.txt +0 -0
  58. {topmark-0.1.1 → topmark-0.2.0}/src/topmark.egg-info/requires.txt +0 -0
  59. {topmark-0.1.1 → topmark-0.2.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.1.1
3
+ Version: 0.2.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
@@ -127,6 +127,15 @@ Logging verbosity is controlled globally:
127
127
  All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
128
128
  subcommands like `check` or `apply`.
129
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
+
130
139
  ### Examples
131
140
 
132
141
  ```bash
@@ -159,6 +168,12 @@ topmark version
159
168
 
160
169
  # Apply changes to files in-place
161
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
162
177
  ```
163
178
 
164
179
  ## 📐 Header placement rules
@@ -249,10 +264,79 @@ The following options can be used with most commands.
249
264
 
250
265
  ## 🧩 Supported file types
251
266
 
252
- | Processor | File types (examples) |
253
- | -------------------- | -------------------------------------------------------------------------- |
254
- | PoundHeaderProcessor | python, shell, ruby, r, julia, perl, makefile, dockerfile, yaml, toml, env |
255
- | 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.
256
340
 
257
341
  ## 🛠 Configuration
258
342
 
@@ -78,6 +78,15 @@ Logging verbosity is controlled globally:
78
78
  All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
79
79
  subcommands like `check` or `apply`.
80
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
+
81
90
  ### Examples
82
91
 
83
92
  ```bash
@@ -110,6 +119,12 @@ topmark version
110
119
 
111
120
  # Apply changes to files in-place
112
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
113
128
  ```
114
129
 
115
130
  ## 📐 Header placement rules
@@ -200,10 +215,79 @@ The following options can be used with most commands.
200
215
 
201
216
  ## 🧩 Supported file types
202
217
 
203
- | Processor | File types (examples) |
204
- | -------------------- | -------------------------------------------------------------------------- |
205
- | PoundHeaderProcessor | python, shell, ruby, r, julia, perl, makefile, dockerfile, yaml, toml, env |
206
- | 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.
207
291
 
208
292
  ## 🛠 Configuration
209
293
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  [project]
12
12
  name = "topmark"
13
- version = "0.1.1"
13
+ version = "0.2.0"
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"
@@ -102,9 +102,11 @@ typing = [
102
102
 
103
103
  [tool.setuptools]
104
104
  package-dir = { "" = "src" }
105
+ include-package-data = true
105
106
 
106
107
  [tool.setuptools.package-data]
107
108
  "topmark" = ["py.typed"]
109
+ "topmark.config" = ["topmark-default.toml"]
108
110
 
109
111
  [tool.setuptools.packages.find]
110
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"
@@ -186,9 +186,9 @@ class ComparisonStatus(BaseStatus):
186
186
  Callable[[str], str],
187
187
  {
188
188
  ComparisonStatus.PENDING: chalk.gray,
189
- ComparisonStatus.CHANGED: chalk.yellow_bright,
189
+ ComparisonStatus.CHANGED: chalk.red,
190
190
  ComparisonStatus.UNCHANGED: chalk.green,
191
- ComparisonStatus.CANNOT_COMPARE: chalk.red,
191
+ ComparisonStatus.CANNOT_COMPARE: chalk.yellow_bright,
192
192
  }[self],
193
193
  )
194
194
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topmark
3
- Version: 0.1.1
3
+ Version: 0.2.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
@@ -127,6 +127,15 @@ Logging verbosity is controlled globally:
127
127
  All other options, such as `--stdin`, `--file-type`, and path filters, are specific to individual
128
128
  subcommands like `check` or `apply`.
129
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
+
130
139
  ### Examples
131
140
 
132
141
  ```bash
@@ -159,6 +168,12 @@ topmark version
159
168
 
160
169
  # Apply changes to files in-place
161
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
162
177
  ```
163
178
 
164
179
  ## 📐 Header placement rules
@@ -249,10 +264,79 @@ The following options can be used with most commands.
249
264
 
250
265
  ## 🧩 Supported file types
251
266
 
252
- | Processor | File types (examples) |
253
- | -------------------- | -------------------------------------------------------------------------- |
254
- | PoundHeaderProcessor | python, shell, ruby, r, julia, perl, makefile, dockerfile, yaml, toml, env |
255
- | 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.
256
340
 
257
341
  ## 🛠 Configuration
258
342
 
@@ -26,6 +26,7 @@ src/topmark/cli/commands/show_defaults.py
26
26
  src/topmark/cli/commands/version.py
27
27
  src/topmark/config/__init__.py
28
28
  src/topmark/config/logging.py
29
+ src/topmark/config/topmark-default.toml
29
30
  src/topmark/filetypes/__init__.py
30
31
  src/topmark/filetypes/base.py
31
32
  src/topmark/filetypes/instances.py
File without changes
File without changes
File without changes
File without changes