topmark 0.3.2__tar.gz → 0.5.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.
- {topmark-0.3.2/src/topmark.egg-info → topmark-0.5.0}/PKG-INFO +19 -12
- {topmark-0.3.2 → topmark-0.5.0}/README.md +18 -11
- {topmark-0.3.2 → topmark-0.5.0}/pyproject.toml +1 -10
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/api/__init__.py +59 -20
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/api/public_types.py +24 -1
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/api/types.py +27 -3
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/cli_types.py +12 -10
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/cmd_common.py +27 -6
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/check.py +23 -15
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/dump_config.py +46 -18
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/filetypes.py +75 -37
- topmark-0.5.0/src/topmark/cli/commands/init_config.py +83 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/processors.py +88 -37
- topmark-0.5.0/src/topmark/cli/commands/show_defaults.py +83 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/strip.py +26 -17
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/version.py +21 -4
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/config_resolver.py +7 -1
- topmark-0.5.0/src/topmark/cli/console.py +111 -0
- topmark-0.5.0/src/topmark/cli/console_helpers.py +37 -0
- topmark-0.5.0/src/topmark/cli/console_std.py +51 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/errors.py +27 -10
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/main.py +22 -8
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/options.py +4 -22
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/utils.py +24 -31
- topmark-0.5.0/src/topmark/cli_shared/console_api.py +41 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli_shared/utils.py +1 -38
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/config/__init__.py +27 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/config/logging.py +35 -6
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/context.py +190 -5
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/pipelines.py +1 -2
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/processors/base.py +6 -8
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/builder.py +2 -4
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/reader.py +5 -3
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/resolver.py +3 -5
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/stripper.py +1 -6
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/updater.py +11 -8
- {topmark-0.3.2 → topmark-0.5.0/src/topmark.egg-info}/PKG-INFO +19 -12
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark.egg-info/SOURCES.txt +4 -0
- topmark-0.3.2/src/topmark/cli/commands/init_config.py +0 -63
- topmark-0.3.2/src/topmark/cli/commands/show_defaults.py +0 -58
- {topmark-0.3.2 → topmark-0.5.0}/LICENSE +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/setup.cfg +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/commands/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli/io.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli_shared/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/cli_shared/exit_codes.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/config/topmark-default.toml +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/constants.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/file_resolver.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/filetypes/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/filetypes/base.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/filetypes/instances.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/filetypes/policy.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/filetypes/registry.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/contracts.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/processors/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/processors/pound.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/processors/slash.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/processors/xml.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/runner.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/comparer.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/patcher.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/renderer.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/pipeline/steps/scanner.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/py.typed +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/registry/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/registry/filetypes.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/registry/processors.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/registry/registry.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/rendering/api.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/rendering/formats.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/utils/__init__.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/utils/diff.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark/utils/file.py +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark.egg-info/dependency_links.txt +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark.egg-info/entry_points.txt +0 -0
- {topmark-0.3.2 → topmark-0.5.0}/src/topmark.egg-info/requires.txt +0 -0
- {topmark-0.3.2 → topmark-0.5.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.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -97,6 +97,7 @@ concise landing page and deep links into topics (install, usage, CI, API, etc.).
|
|
|
97
97
|
- Full header removal (`topmark strip`)
|
|
98
98
|
- Preserves original newline style (LF/CRLF/CR) and BOM
|
|
99
99
|
- Idempotent updates (re-running does not change already-correct files)
|
|
100
|
+
- Structured diagnostics with severity levels (`info`, `warning`, `error`) and aggregate counts
|
|
100
101
|
|
|
101
102
|
## 🚀 Installation
|
|
102
103
|
|
|
@@ -120,19 +121,24 @@ pip install topmark
|
|
|
120
121
|
|
|
121
122
|
## ⚙️ Usage
|
|
122
123
|
|
|
124
|
+
> **Note**: In dry-run mode, TopMark shows summaries ending with `- previewed`. When run with `--apply`, summaries end with terminal statuses such as `- inserted`, `- replaced`, or `- removed`.
|
|
125
|
+
|
|
123
126
|
```bash
|
|
124
127
|
topmark [SUBCOMMAND] [OPTIONS] [PATHS]...
|
|
125
128
|
```
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
The `strip` subcommand is provided to remove entire headers.
|
|
130
|
+
The `check` and `strip` commands performs a dry‑run by default
|
|
131
|
+
and will only apply changes when `--apply` is provided.
|
|
131
132
|
|
|
132
133
|
Logging verbosity is controlled globally:
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
CLI verbosity has two layers:
|
|
136
|
+
|
|
137
|
+
- **Program output** (user-facing):
|
|
138
|
+
- `-v`, `--verbose`: Show more detail, including per-line diagnostics in summaries.
|
|
139
|
+
- `-q`, `--quiet`: Suppress most output (overrides `-v`).
|
|
140
|
+
- **Internal logging** (developer/machine):
|
|
141
|
+
- Also mapped by `-v`/`-q`, controls log level (`DEBUG` / `INFO` / `WARNING` / `ERROR`).
|
|
136
142
|
|
|
137
143
|
All other options are specific to individual subcommands.
|
|
138
144
|
|
|
@@ -142,6 +148,7 @@ ______________________________________________________________________
|
|
|
142
148
|
|
|
143
149
|
| Command | Description |
|
|
144
150
|
| --------------- | -------------------------------------------------- |
|
|
151
|
+
| `check` | Add / Update TopMark headers in files |
|
|
145
152
|
| `dump-config` | Show the resolved configuration in TOML format |
|
|
146
153
|
| `filetypes` | List supported file types and their comment styles |
|
|
147
154
|
| `processors` | List registered header processors and file types |
|
|
@@ -171,10 +178,10 @@ find . -name "*.py" | topmark check --file-type python --files-from - --summary
|
|
|
171
178
|
git ls-files -c -o --exclude-standard | sort -u | topmark check --files-from - --apply
|
|
172
179
|
|
|
173
180
|
# Read a single file's *content* from STDIN and render the resultng unified diff
|
|
174
|
-
cat README.md | topmark - --stdin-filename README.md --diff
|
|
181
|
+
cat README.md | topmark check - --stdin-filename README.md --diff
|
|
175
182
|
|
|
176
183
|
# Read a single file's *content* from STDIN and print the updated content to stdout
|
|
177
|
-
cat README.md | topmark - --stdin-filename README.md --apply
|
|
184
|
+
cat README.md | topmark check - --stdin-filename README.md --apply
|
|
178
185
|
|
|
179
186
|
# Dump the merged configuration (after loading all applicable config layers)
|
|
180
187
|
topmark dump-config --file-type python --exclude .venv --exclude-from .gitignore
|
|
@@ -200,8 +207,8 @@ topmark strip --apply src/
|
|
|
200
207
|
# CI-friendly summary: only show issues; ignore unsupported types
|
|
201
208
|
topmark check --skip-compliant --skip-unsupported src/
|
|
202
209
|
|
|
203
|
-
# Apply fixes, don't report unsupported files
|
|
204
|
-
topmark check --apply --skip-unsupported
|
|
210
|
+
# Apply fixes, don't report unsupported files; default output is terse (pre-commit friendly)
|
|
211
|
+
topmark check --apply --skip-unsupported
|
|
205
212
|
|
|
206
213
|
# Show supported file types in MarkDown format, providing detailed output:
|
|
207
214
|
topmark filetypes --format markdown --long
|
|
@@ -274,7 +281,7 @@ declaration for XML/HTML, etc.). Newline style and BOM are preserved; runs are i
|
|
|
274
281
|
|
|
275
282
|
```bash
|
|
276
283
|
# Preview (exit code 2 when changes are pending)
|
|
277
|
-
topmark src/
|
|
284
|
+
topmark check src/
|
|
278
285
|
|
|
279
286
|
# Apply in place
|
|
280
287
|
topmark check --apply src/
|
|
@@ -47,6 +47,7 @@ concise landing page and deep links into topics (install, usage, CI, API, etc.).
|
|
|
47
47
|
- Full header removal (`topmark strip`)
|
|
48
48
|
- Preserves original newline style (LF/CRLF/CR) and BOM
|
|
49
49
|
- Idempotent updates (re-running does not change already-correct files)
|
|
50
|
+
- Structured diagnostics with severity levels (`info`, `warning`, `error`) and aggregate counts
|
|
50
51
|
|
|
51
52
|
## 🚀 Installation
|
|
52
53
|
|
|
@@ -70,19 +71,24 @@ pip install topmark
|
|
|
70
71
|
|
|
71
72
|
## ⚙️ Usage
|
|
72
73
|
|
|
74
|
+
> **Note**: In dry-run mode, TopMark shows summaries ending with `- previewed`. When run with `--apply`, summaries end with terminal statuses such as `- inserted`, `- replaced`, or `- removed`.
|
|
75
|
+
|
|
73
76
|
```bash
|
|
74
77
|
topmark [SUBCOMMAND] [OPTIONS] [PATHS]...
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
The `strip` subcommand is provided to remove entire headers.
|
|
80
|
+
The `check` and `strip` commands performs a dry‑run by default
|
|
81
|
+
and will only apply changes when `--apply` is provided.
|
|
81
82
|
|
|
82
83
|
Logging verbosity is controlled globally:
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
CLI verbosity has two layers:
|
|
86
|
+
|
|
87
|
+
- **Program output** (user-facing):
|
|
88
|
+
- `-v`, `--verbose`: Show more detail, including per-line diagnostics in summaries.
|
|
89
|
+
- `-q`, `--quiet`: Suppress most output (overrides `-v`).
|
|
90
|
+
- **Internal logging** (developer/machine):
|
|
91
|
+
- Also mapped by `-v`/`-q`, controls log level (`DEBUG` / `INFO` / `WARNING` / `ERROR`).
|
|
86
92
|
|
|
87
93
|
All other options are specific to individual subcommands.
|
|
88
94
|
|
|
@@ -92,6 +98,7 @@ ______________________________________________________________________
|
|
|
92
98
|
|
|
93
99
|
| Command | Description |
|
|
94
100
|
| --------------- | -------------------------------------------------- |
|
|
101
|
+
| `check` | Add / Update TopMark headers in files |
|
|
95
102
|
| `dump-config` | Show the resolved configuration in TOML format |
|
|
96
103
|
| `filetypes` | List supported file types and their comment styles |
|
|
97
104
|
| `processors` | List registered header processors and file types |
|
|
@@ -121,10 +128,10 @@ find . -name "*.py" | topmark check --file-type python --files-from - --summary
|
|
|
121
128
|
git ls-files -c -o --exclude-standard | sort -u | topmark check --files-from - --apply
|
|
122
129
|
|
|
123
130
|
# Read a single file's *content* from STDIN and render the resultng unified diff
|
|
124
|
-
cat README.md | topmark - --stdin-filename README.md --diff
|
|
131
|
+
cat README.md | topmark check - --stdin-filename README.md --diff
|
|
125
132
|
|
|
126
133
|
# Read a single file's *content* from STDIN and print the updated content to stdout
|
|
127
|
-
cat README.md | topmark - --stdin-filename README.md --apply
|
|
134
|
+
cat README.md | topmark check - --stdin-filename README.md --apply
|
|
128
135
|
|
|
129
136
|
# Dump the merged configuration (after loading all applicable config layers)
|
|
130
137
|
topmark dump-config --file-type python --exclude .venv --exclude-from .gitignore
|
|
@@ -150,8 +157,8 @@ topmark strip --apply src/
|
|
|
150
157
|
# CI-friendly summary: only show issues; ignore unsupported types
|
|
151
158
|
topmark check --skip-compliant --skip-unsupported src/
|
|
152
159
|
|
|
153
|
-
# Apply fixes, don't report unsupported files
|
|
154
|
-
topmark check --apply --skip-unsupported
|
|
160
|
+
# Apply fixes, don't report unsupported files; default output is terse (pre-commit friendly)
|
|
161
|
+
topmark check --apply --skip-unsupported
|
|
155
162
|
|
|
156
163
|
# Show supported file types in MarkDown format, providing detailed output:
|
|
157
164
|
topmark filetypes --format markdown --long
|
|
@@ -224,7 +231,7 @@ declaration for XML/HTML, etc.). Newline style and BOM are preserved; runs are i
|
|
|
224
231
|
|
|
225
232
|
```bash
|
|
226
233
|
# Preview (exit code 2 when changes are pending)
|
|
227
|
-
topmark src/
|
|
234
|
+
topmark check src/
|
|
228
235
|
|
|
229
236
|
# Apply in place
|
|
230
237
|
topmark check --apply src/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "topmark"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.5.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"
|
|
@@ -113,15 +113,6 @@ include-package-data = true
|
|
|
113
113
|
[tool.setuptools.packages.find]
|
|
114
114
|
where = ["src"]
|
|
115
115
|
|
|
116
|
-
# Markdown formatting configuration (normative reference: .mdformat.toml)
|
|
117
|
-
# Aligns with Prettier in VS Code and the mdformat pre-commit hook
|
|
118
|
-
[tool.mdformat]
|
|
119
|
-
wrap = 100
|
|
120
|
-
extensions = ["tables"]
|
|
121
|
-
# Exclude auto-generated API page from formatting (Python 3.13)
|
|
122
|
-
# NOTE: Please use `make format-check` and `make format`
|
|
123
|
-
exclude = ["docs/api/public.md"]
|
|
124
|
-
|
|
125
116
|
|
|
126
117
|
# TOML formatting
|
|
127
118
|
[tool.taplo.format]
|
|
@@ -34,12 +34,11 @@ Notes:
|
|
|
34
34
|
- A FileType instance is **recognized** if it is in the FileTypeRegistry.
|
|
35
35
|
- A FileType instance is **supported** if it is recognized and is registered to
|
|
36
36
|
a HeaderProcessor instance in the HeaderProcessorRegistry.
|
|
37
|
+
- The API returns per-file diagnostics and aggregate counts; levels are "info", "warning", "error".
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
from __future__ import annotations
|
|
40
41
|
|
|
41
|
-
import logging
|
|
42
|
-
import os
|
|
43
42
|
from pathlib import Path
|
|
44
43
|
from typing import Any, Iterable, Mapping, Sequence
|
|
45
44
|
|
|
@@ -50,15 +49,18 @@ from topmark.cli.cmd_common import (
|
|
|
50
49
|
from topmark.cli.io import InputPlan
|
|
51
50
|
from topmark.cli_shared.utils import write_updates
|
|
52
51
|
from topmark.config import Config
|
|
53
|
-
from topmark.config.logging import get_logger
|
|
52
|
+
from topmark.config.logging import get_logger
|
|
54
53
|
from topmark.constants import TOPMARK_VERSION
|
|
55
|
-
from topmark.pipeline.context import
|
|
54
|
+
from topmark.pipeline.context import (
|
|
55
|
+
ComparisonStatus,
|
|
56
|
+
FileStatus,
|
|
57
|
+
ProcessingContext,
|
|
58
|
+
WriteStatus,
|
|
59
|
+
)
|
|
56
60
|
from topmark.registry import Registry
|
|
57
61
|
|
|
58
|
-
from .
|
|
59
|
-
|
|
60
|
-
if "TOPMARK_DEBUG" in os.environ:
|
|
61
|
-
setup_logging(level=logging.DEBUG)
|
|
62
|
+
from .public_types import PublicDiagnostic
|
|
63
|
+
from .types import DiagnosticTotals, FileResult, FileTypeInfo, Outcome, ProcessorInfo, RunResult
|
|
62
64
|
|
|
63
65
|
logger = get_logger(__name__)
|
|
64
66
|
|
|
@@ -75,6 +77,7 @@ __all__ = [
|
|
|
75
77
|
"get_filetype_info",
|
|
76
78
|
"get_processor_info",
|
|
77
79
|
"Registry",
|
|
80
|
+
"PublicDiagnostic",
|
|
78
81
|
]
|
|
79
82
|
|
|
80
83
|
|
|
@@ -153,6 +156,7 @@ def _run_pipeline(
|
|
|
153
156
|
paths: Iterable[Path | str],
|
|
154
157
|
base_config: Mapping[str, Any] | Config | None,
|
|
155
158
|
file_types: Sequence[str] | None,
|
|
159
|
+
apply_changes: bool,
|
|
156
160
|
) -> tuple[Config, list[Path], list[ProcessingContext], object | None]:
|
|
157
161
|
"""Resolve config + files, register processors, and run the pipeline.
|
|
158
162
|
|
|
@@ -168,6 +172,10 @@ def _run_pipeline(
|
|
|
168
172
|
cfg, file_list = _build_cfg_and_files_via_cli_helpers(
|
|
169
173
|
paths, base_config=base_config, file_types=file_types
|
|
170
174
|
)
|
|
175
|
+
|
|
176
|
+
# Set the 'apply' mode in the Config instance
|
|
177
|
+
cfg.apply_changes = apply_changes
|
|
178
|
+
|
|
171
179
|
if not file_list:
|
|
172
180
|
return cfg, file_list, [], None
|
|
173
181
|
|
|
@@ -200,24 +208,45 @@ def _apply_view_filter(
|
|
|
200
208
|
return view_results, skipped
|
|
201
209
|
|
|
202
210
|
|
|
203
|
-
def _collect_diagnostics(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
results: List of ProcessingContext results.
|
|
211
|
+
def _collect_diagnostics(
|
|
212
|
+
results: list[ProcessingContext],
|
|
213
|
+
) -> dict[str, list[PublicDiagnostic]]:
|
|
214
|
+
"""Collect per-file diagnostics as a mapping `{path: [diagnostics...]}`.
|
|
210
215
|
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
Diagnostics are returned in the *public* JSON-friendly shape and do not
|
|
217
|
+
expose internal classes or enums.
|
|
213
218
|
"""
|
|
214
|
-
diags: dict[str, list[
|
|
219
|
+
diags: dict[str, list[PublicDiagnostic]] = {}
|
|
215
220
|
for r in results:
|
|
216
221
|
if r.diagnostics:
|
|
217
|
-
diags[str(r.path)] =
|
|
222
|
+
diags[str(r.path)] = [
|
|
223
|
+
{"level": d.level.value, "message": d.message} for d in r.diagnostics
|
|
224
|
+
]
|
|
218
225
|
return diags
|
|
219
226
|
|
|
220
227
|
|
|
228
|
+
def _collect_diagnostic_totals(results: list[ProcessingContext]) -> DiagnosticTotals:
|
|
229
|
+
"""Return aggregate counts of diagnostics across the given results.
|
|
230
|
+
|
|
231
|
+
Counts reflect only the supplied `results` (typically the **view** after
|
|
232
|
+
filtering), matching what is returned to the caller.
|
|
233
|
+
"""
|
|
234
|
+
total_info = total_warn = total_error = 0
|
|
235
|
+
for r in results:
|
|
236
|
+
if not r.diagnostics:
|
|
237
|
+
continue
|
|
238
|
+
for d in r.diagnostics:
|
|
239
|
+
lv = d.level.value
|
|
240
|
+
if lv == "info":
|
|
241
|
+
total_info += 1
|
|
242
|
+
elif lv == "warning":
|
|
243
|
+
total_warn += 1
|
|
244
|
+
elif lv == "error":
|
|
245
|
+
total_error += 1
|
|
246
|
+
total = total_info + total_warn + total_error
|
|
247
|
+
return {"info": total_info, "warning": total_warn, "error": total_error, "total": total}
|
|
248
|
+
|
|
249
|
+
|
|
221
250
|
# ---------- public API ----------
|
|
222
251
|
|
|
223
252
|
|
|
@@ -269,7 +298,7 @@ def _build_cfg_and_files_via_cli_helpers(
|
|
|
269
298
|
# but without relying on Click. We construct the ArgsNamespace-equivalent
|
|
270
299
|
# mapping that `Config.load_merged()` expects.
|
|
271
300
|
args_mapping = {
|
|
272
|
-
"
|
|
301
|
+
"verbosity_level": None, # inherit program-output verbosity (tri-state)
|
|
273
302
|
"files": list(plan.paths),
|
|
274
303
|
"files_from": list(plan.files_from),
|
|
275
304
|
"stdin": False,
|
|
@@ -340,6 +369,7 @@ def check(
|
|
|
340
369
|
paths=paths,
|
|
341
370
|
base_config=config,
|
|
342
371
|
file_types=file_types,
|
|
372
|
+
apply_changes=apply,
|
|
343
373
|
)
|
|
344
374
|
if not file_list:
|
|
345
375
|
return RunResult(files=(), summary={}, had_errors=False)
|
|
@@ -354,6 +384,8 @@ def check(
|
|
|
354
384
|
encountered_error_code is not None
|
|
355
385
|
)
|
|
356
386
|
diagnostics = _collect_diagnostics(view_results)
|
|
387
|
+
diagnostic_totals = _collect_diagnostic_totals(view_results)
|
|
388
|
+
diagnostic_totals_all = _collect_diagnostic_totals(results)
|
|
357
389
|
|
|
358
390
|
written = failed = 0
|
|
359
391
|
if apply:
|
|
@@ -380,6 +412,8 @@ def check(
|
|
|
380
412
|
written=written,
|
|
381
413
|
failed=failed,
|
|
382
414
|
diagnostics=diagnostics,
|
|
415
|
+
diagnostic_totals=diagnostic_totals,
|
|
416
|
+
diagnostic_totals_all=diagnostic_totals_all,
|
|
383
417
|
)
|
|
384
418
|
|
|
385
419
|
|
|
@@ -418,6 +452,7 @@ def strip(
|
|
|
418
452
|
paths=paths,
|
|
419
453
|
base_config=config,
|
|
420
454
|
file_types=file_types,
|
|
455
|
+
apply_changes=apply,
|
|
421
456
|
)
|
|
422
457
|
if not file_list:
|
|
423
458
|
return RunResult(files=(), summary={}, had_errors=False)
|
|
@@ -432,6 +467,8 @@ def strip(
|
|
|
432
467
|
encountered_error_code is not None
|
|
433
468
|
)
|
|
434
469
|
diagnostics = _collect_diagnostics(view_results)
|
|
470
|
+
diagnostic_totals = _collect_diagnostic_totals(view_results)
|
|
471
|
+
diagnostic_totals_all = _collect_diagnostic_totals(results)
|
|
435
472
|
|
|
436
473
|
written = failed = 0
|
|
437
474
|
if apply:
|
|
@@ -452,6 +489,8 @@ def strip(
|
|
|
452
489
|
written=written,
|
|
453
490
|
failed=failed,
|
|
454
491
|
diagnostics=diagnostics,
|
|
492
|
+
diagnostic_totals=diagnostic_totals,
|
|
493
|
+
diagnostic_totals_all=diagnostic_totals_all,
|
|
455
494
|
)
|
|
456
495
|
|
|
457
496
|
|
|
@@ -21,11 +21,17 @@ Notes:
|
|
|
21
21
|
* Attribute names and types here are part of the public API and follow
|
|
22
22
|
semver. Adding optional attributes is allowed in minor versions; removing
|
|
23
23
|
or changing types is a breaking change.
|
|
24
|
+
|
|
25
|
+
Diagnostics
|
|
26
|
+
-----------
|
|
27
|
+
The public API uses JSON-friendly diagnostics with string literal severities.
|
|
28
|
+
See :class:`PublicDiagnostic` for the shape and :data:`DiagnosticLevelLiteral`
|
|
29
|
+
for the allowed values.
|
|
24
30
|
"""
|
|
25
31
|
|
|
26
32
|
from __future__ import annotations
|
|
27
33
|
|
|
28
|
-
from typing import TYPE_CHECKING, Protocol, Sequence
|
|
34
|
+
from typing import TYPE_CHECKING, Literal, Protocol, Sequence, TypedDict
|
|
29
35
|
|
|
30
36
|
if TYPE_CHECKING:
|
|
31
37
|
from collections.abc import Callable
|
|
@@ -96,3 +102,20 @@ class PublicHeaderProcessor(Protocol):
|
|
|
96
102
|
block_suffix: str
|
|
97
103
|
# registry binds at runtime
|
|
98
104
|
file_type: PublicFileType
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
"""Allowed diagnostic severity levels in the public API."""
|
|
108
|
+
DiagnosticLevelLiteral = Literal["info", "warning", "error"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PublicDiagnostic(TypedDict):
|
|
112
|
+
"""Stable, JSON-friendly diagnostic for API consumers.
|
|
113
|
+
|
|
114
|
+
Notes:
|
|
115
|
+
* Uses string literal levels for semver stability and easy serialization.
|
|
116
|
+
* Mirrors internal pipeline diagnostics but does not expose internals.
|
|
117
|
+
* This shape is stable for semver and safe to serialize to JSON/NDJSON.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
level: DiagnosticLevelLiteral
|
|
121
|
+
message: str
|
|
@@ -21,9 +21,28 @@ from dataclasses import dataclass
|
|
|
21
21
|
from enum import Enum
|
|
22
22
|
from typing import TYPE_CHECKING, Mapping, Sequence, TypedDict
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
class DiagnosticTotals(TypedDict):
|
|
26
|
+
"""Aggregate diagnostic counts across the returned *view*.
|
|
27
|
+
|
|
28
|
+
Keys:
|
|
29
|
+
info: Number of info diagnostics.
|
|
30
|
+
warning: Number of warning diagnostics.
|
|
31
|
+
error: Number of error diagnostics.
|
|
32
|
+
total: Sum of all diagnostics.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
info: int
|
|
36
|
+
warning: int
|
|
37
|
+
error: int
|
|
38
|
+
total: int
|
|
39
|
+
|
|
40
|
+
|
|
24
41
|
if TYPE_CHECKING:
|
|
25
42
|
from pathlib import Path
|
|
26
43
|
|
|
44
|
+
from .public_types import PublicDiagnostic
|
|
45
|
+
|
|
27
46
|
|
|
28
47
|
class Outcome(str, Enum):
|
|
29
48
|
"""Per-file outcome bucket.
|
|
@@ -72,8 +91,11 @@ class RunResult:
|
|
|
72
91
|
skipped: Number of items removed by view filters (e.g., `skip_compliant`).
|
|
73
92
|
written: Number of files successfully written (only in apply mode; otherwise 0).
|
|
74
93
|
failed: Number of files that failed to write (only in apply mode; otherwise 0).
|
|
75
|
-
diagnostics: Optional mapping from file path
|
|
76
|
-
|
|
94
|
+
diagnostics: Optional mapping from file path to a list of public diagnostics.
|
|
95
|
+
diagnostic_totals: Optional aggregate counts across the **returned view**
|
|
96
|
+
(not the entire run).
|
|
97
|
+
diagnostic_totals_all: Optional aggregate counts across the **entire run**
|
|
98
|
+
(pre view filtering).
|
|
77
99
|
"""
|
|
78
100
|
|
|
79
101
|
files: Sequence[FileResult]
|
|
@@ -82,7 +104,9 @@ class RunResult:
|
|
|
82
104
|
skipped: int = 0
|
|
83
105
|
written: int = 0
|
|
84
106
|
failed: int = 0
|
|
85
|
-
diagnostics: dict[str, list[
|
|
107
|
+
diagnostics: dict[str, list[PublicDiagnostic]] | None = None
|
|
108
|
+
diagnostic_totals: DiagnosticTotals | None = None
|
|
109
|
+
diagnostic_totals_all: DiagnosticTotals | None = None
|
|
86
110
|
|
|
87
111
|
|
|
88
112
|
# Tiny intent object used by check()/strip() to gate writes
|
|
@@ -19,7 +19,6 @@ across all TopMark commands and subcommands.
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
import glob
|
|
22
|
-
import logging
|
|
23
22
|
from enum import Enum
|
|
24
23
|
from pathlib import Path
|
|
25
24
|
from typing import (
|
|
@@ -63,7 +62,8 @@ class ArgsNamespace(TypedDict, total=False):
|
|
|
63
62
|
file overrides, file selection, filtering, and formatting flags.
|
|
64
63
|
|
|
65
64
|
Attributes:
|
|
66
|
-
|
|
65
|
+
verbosity_level: Program-output verbosity (0=terse, 1=verbose).
|
|
66
|
+
apply_changes: Whether to apply the changed (dry-run if not set ot False).
|
|
67
67
|
no_config: Whether to ignore local config files.
|
|
68
68
|
config_files: List of extra config file paths.
|
|
69
69
|
files: List of file paths to process.
|
|
@@ -79,10 +79,11 @@ class ArgsNamespace(TypedDict, total=False):
|
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
81
|
# Global options: retrieve from ctx.obj
|
|
82
|
-
|
|
82
|
+
verbosity_level: int | None
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
84
|
+
# Runtime intent: whether to actually write changes (apply) or preview only
|
|
85
|
+
# None = inherit/unspecified, False = dry-run/preview, True = apply
|
|
86
|
+
apply_changes: bool | None
|
|
86
87
|
|
|
87
88
|
# Command options: config
|
|
88
89
|
no_config: bool | None
|
|
@@ -108,8 +109,8 @@ class ArgsNamespace(TypedDict, total=False):
|
|
|
108
109
|
|
|
109
110
|
def build_args_namespace(
|
|
110
111
|
*,
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
verbosity_level: int | None = None,
|
|
113
|
+
apply_changes: bool | None = None,
|
|
113
114
|
no_config: bool | None = None,
|
|
114
115
|
config_files: list[str] | None = None,
|
|
115
116
|
files: list[str] | None = None,
|
|
@@ -127,7 +128,8 @@ def build_args_namespace(
|
|
|
127
128
|
"""Build an ArgsNamespace dictionary for CLI argument passing.
|
|
128
129
|
|
|
129
130
|
Args:
|
|
130
|
-
|
|
131
|
+
verbosity_level: Program-output verbosity (0=terse, 1=verbose).
|
|
132
|
+
apply_changes: Whether to apply the changed (dry-run if not set ot False).
|
|
131
133
|
no_config: Whether to ignore local config files.
|
|
132
134
|
config_files: List of extra config file paths.
|
|
133
135
|
files: List of file paths to process.
|
|
@@ -146,8 +148,8 @@ def build_args_namespace(
|
|
|
146
148
|
Dictionary of CLI argument values for use throughout the CLI.
|
|
147
149
|
"""
|
|
148
150
|
return {
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
+
"verbosity_level": verbosity_level,
|
|
152
|
+
"apply_changes": apply_changes,
|
|
151
153
|
"no_config": no_config,
|
|
152
154
|
"config_files": config_files,
|
|
153
155
|
"files": files if files is not None else [],
|
|
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
|
|
23
23
|
import click
|
|
24
24
|
|
|
25
25
|
from topmark.cli.config_resolver import resolve_config_from_click
|
|
26
|
+
from topmark.cli.console_helpers import get_console_safely
|
|
26
27
|
from topmark.cli_shared.exit_codes import ExitCode
|
|
27
28
|
from topmark.config.logging import get_logger
|
|
28
29
|
from topmark.file_resolver import resolve_file_list
|
|
@@ -38,6 +39,20 @@ if TYPE_CHECKING:
|
|
|
38
39
|
logger = get_logger(__name__)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def get_effective_verbosity(ctx: click.Context, config: "Config | None" = None) -> int:
|
|
43
|
+
"""Return the effective program-output verbosity for this command.
|
|
44
|
+
|
|
45
|
+
Resolution order (tri-state aware):
|
|
46
|
+
1. Config.verbosity_level if set (not None)
|
|
47
|
+
2. ctx.obj["verbosity_level"] if present
|
|
48
|
+
3. 0 (terse)
|
|
49
|
+
"""
|
|
50
|
+
cfg_level = getattr(config, "verbosity_level", None) if config else None
|
|
51
|
+
if cfg_level is not None:
|
|
52
|
+
return int(cfg_level)
|
|
53
|
+
return int(ctx.obj.get("verbosity_level", 0))
|
|
54
|
+
|
|
55
|
+
|
|
41
56
|
def build_file_list(config: Config, *, stdin_mode: bool, temp_path: Path | None) -> list[Path]:
|
|
42
57
|
"""Return the files to process, respecting STDIN content mode.
|
|
43
58
|
|
|
@@ -67,6 +82,7 @@ def run_steps_for_files(
|
|
|
67
82
|
ENCODING_ERROR → UnicodeDecodeError
|
|
68
83
|
PIPELINE_ERROR → any other unexpected exception
|
|
69
84
|
"""
|
|
85
|
+
console = get_console_safely()
|
|
70
86
|
steps = get_pipeline(pipeline_name)
|
|
71
87
|
results: list[ProcessingContext] = []
|
|
72
88
|
encountered_error_code: ExitCode | None = None
|
|
@@ -78,7 +94,7 @@ def run_steps_for_files(
|
|
|
78
94
|
results.append(ctx_obj)
|
|
79
95
|
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
|
|
80
96
|
logger.error("Filesystem error while processing %s: %s", path, e)
|
|
81
|
-
|
|
97
|
+
console.error(f"❌ Filesystem error processing {path}: {e}")
|
|
82
98
|
if isinstance(e, (FileNotFoundError, IsADirectoryError)):
|
|
83
99
|
logger.error("%s: %s", e, path)
|
|
84
100
|
encountered_error_code = encountered_error_code or ExitCode.FILE_NOT_FOUND
|
|
@@ -88,14 +104,12 @@ def run_steps_for_files(
|
|
|
88
104
|
continue
|
|
89
105
|
except UnicodeDecodeError as e:
|
|
90
106
|
logger.error("Encoding error while reading %s: %s", path, e)
|
|
91
|
-
|
|
107
|
+
console.error(f"🧵 Encoding error in {path}: {e}")
|
|
92
108
|
encountered_error_code = encountered_error_code or ExitCode.ENCODING_ERROR
|
|
93
109
|
continue
|
|
94
110
|
except Exception as e: # pragma: no cover
|
|
95
111
|
logger.exception("Unexpected error processing %s", path)
|
|
96
|
-
|
|
97
|
-
f"⚠️ Unexpected error processing {path}: {e} (use -vv for traceback)", fg="red"
|
|
98
|
-
)
|
|
112
|
+
console.error(f"⚠️ Unexpected error processing {path}: {e} (use -vv for traceback)")
|
|
99
113
|
encountered_error_code = encountered_error_code or ExitCode.PIPELINE_ERROR
|
|
100
114
|
continue
|
|
101
115
|
|
|
@@ -134,7 +148,8 @@ def filter_view_results(
|
|
|
134
148
|
def exit_if_no_files(file_list: list[Path]) -> bool:
|
|
135
149
|
"""Echo a friendly message and return True if there is nothing to process."""
|
|
136
150
|
if not file_list:
|
|
137
|
-
|
|
151
|
+
console = get_console_safely()
|
|
152
|
+
console.print(console.styled("\nℹ️ No files to process.\n", fg="blue"))
|
|
138
153
|
return True
|
|
139
154
|
return False
|
|
140
155
|
|
|
@@ -162,6 +177,8 @@ def build_config_common(
|
|
|
162
177
|
"""Materialize Config from an input plan (no file list resolution)."""
|
|
163
178
|
return resolve_config_from_click(
|
|
164
179
|
ctx=ctx,
|
|
180
|
+
verbosity_level=ctx.obj.get("verbosity_level"),
|
|
181
|
+
apply_changes=ctx.obj.get("apply_changes"),
|
|
165
182
|
files=plan.paths,
|
|
166
183
|
files_from=plan.files_from,
|
|
167
184
|
stdin=plan.stdin_mode,
|
|
@@ -193,6 +210,8 @@ def build_config_and_file_list(
|
|
|
193
210
|
if plan.stdin_mode:
|
|
194
211
|
config = resolve_config_from_click(
|
|
195
212
|
ctx=ctx,
|
|
213
|
+
verbosity_level=ctx.obj.get("verbosity_level"),
|
|
214
|
+
apply_changes=ctx.obj.get("apply_changes"),
|
|
196
215
|
files=plan.paths,
|
|
197
216
|
files_from=plan.files_from,
|
|
198
217
|
stdin=True,
|
|
@@ -211,6 +230,8 @@ def build_config_and_file_list(
|
|
|
211
230
|
else:
|
|
212
231
|
config = resolve_config_from_click(
|
|
213
232
|
ctx=ctx,
|
|
233
|
+
verbosity_level=ctx.obj.get("verbosity_level"),
|
|
234
|
+
apply_changes=ctx.obj.get("apply_changes"),
|
|
214
235
|
files=plan.paths,
|
|
215
236
|
files_from=plan.files_from,
|
|
216
237
|
stdin=False,
|