topmark 0.1.0__py3-none-any.whl
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/__init__.py +14 -0
- topmark/cli/__init__.py +23 -0
- topmark/cli/cli_types.py +299 -0
- topmark/cli/commands/__init__.py +11 -0
- topmark/cli/commands/default.py +295 -0
- topmark/cli/commands/dump_config.py +108 -0
- topmark/cli/commands/filetypes.py +39 -0
- topmark/cli/commands/init_config.py +60 -0
- topmark/cli/commands/show_defaults.py +54 -0
- topmark/cli/commands/version.py +32 -0
- topmark/cli/config_resolver.py +82 -0
- topmark/cli/main.py +464 -0
- topmark/cli/options.py +276 -0
- topmark/cli/utils.py +245 -0
- topmark/config/__init__.py +520 -0
- topmark/config/logging.py +148 -0
- topmark/constants.py +24 -0
- topmark/file_resolver.py +228 -0
- topmark/filetypes/__init__.py +16 -0
- topmark/filetypes/base.py +81 -0
- topmark/filetypes/instances.py +327 -0
- topmark/filetypes/policy.py +44 -0
- topmark/filetypes/registry.py +79 -0
- topmark/pipeline/__init__.py +25 -0
- topmark/pipeline/context.py +363 -0
- topmark/pipeline/contracts.py +22 -0
- topmark/pipeline/pipelines.py +93 -0
- topmark/pipeline/processors/__init__.py +96 -0
- topmark/pipeline/processors/base.py +754 -0
- topmark/pipeline/processors/pound.py +154 -0
- topmark/pipeline/processors/xml.py +278 -0
- topmark/pipeline/runner.py +49 -0
- topmark/pipeline/steps/__init__.py +11 -0
- topmark/pipeline/steps/builder.py +135 -0
- topmark/pipeline/steps/comparer.py +73 -0
- topmark/pipeline/steps/patcher.py +171 -0
- topmark/pipeline/steps/reader.py +173 -0
- topmark/pipeline/steps/renderer.py +64 -0
- topmark/pipeline/steps/resolver.py +103 -0
- topmark/pipeline/steps/scanner.py +146 -0
- topmark/pipeline/steps/updater.py +128 -0
- topmark/py.typed +1 -0
- topmark/rendering/api.py +80 -0
- topmark/rendering/formats.py +26 -0
- topmark/utils/__init__.py +11 -0
- topmark/utils/file.py +42 -0
- topmark-0.1.0.dist-info/METADATA +308 -0
- topmark-0.1.0.dist-info/RECORD +52 -0
- topmark-0.1.0.dist-info/WHEEL +5 -0
- topmark-0.1.0.dist-info/entry_points.txt +2 -0
- topmark-0.1.0.dist-info/licenses/LICENSE +21 -0
- topmark-0.1.0.dist-info/top_level.txt +1 -0
topmark/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : __init__.py
|
|
4
|
+
# file_relpath : src/topmark/__init__.py
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
"""TopMark package.
|
|
12
|
+
|
|
13
|
+
Provides tools and utilities for evaluating and benchmarking models and algorithms.
|
|
14
|
+
"""
|
topmark/cli/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : __init__.py
|
|
4
|
+
# file_relpath : src/topmark/cli/__init__.py
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
"""TopMark CLI package.
|
|
12
|
+
|
|
13
|
+
This package groups all Click command definitions and supporting utilities
|
|
14
|
+
for the TopMark command-line interface.
|
|
15
|
+
|
|
16
|
+
Typical usage:
|
|
17
|
+
The console script entry point is defined in ``pyproject.toml`` as::
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
topmark = "topmark.cli.main:cli"
|
|
21
|
+
|
|
22
|
+
All subcommands live in :mod:`topmark.cli.commands`.
|
|
23
|
+
"""
|
topmark/cli/cli_types.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : cli_types.py
|
|
4
|
+
# file_relpath : src/topmark/cli/cli_types.py
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
"""Shared CLI parameter types and argument parsing helpers for TopMark.
|
|
12
|
+
|
|
13
|
+
This module serves as the central container for shared CLI argument types, validators,
|
|
14
|
+
and helpers in TopMark. It defines the ArgsNamespace TypedDict for consistent
|
|
15
|
+
argument passing throughout the CLI, as well as reusable validator functions and
|
|
16
|
+
custom Click parameter types for argument parsing (such as file paths and globs).
|
|
17
|
+
These utilities ensure robust and uniform handling of CLI parameters
|
|
18
|
+
across all TopMark commands and subcommands.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import glob
|
|
22
|
+
import logging
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import (
|
|
26
|
+
TYPE_CHECKING,
|
|
27
|
+
Any,
|
|
28
|
+
Generic,
|
|
29
|
+
Iterable,
|
|
30
|
+
NoReturn,
|
|
31
|
+
Protocol,
|
|
32
|
+
TypedDict,
|
|
33
|
+
TypeVar,
|
|
34
|
+
cast,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
import click
|
|
38
|
+
|
|
39
|
+
from topmark.rendering.formats import HeaderOutputFormat
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
# Type-only import so we can reference the symbol in annotations
|
|
43
|
+
from click.shell_completion import CompletionItem as ClickCompletionItem
|
|
44
|
+
|
|
45
|
+
class ParamTypeBase(Protocol):
|
|
46
|
+
"""Typed base to avoid subclassing Any when Click lacks stubs."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
# At runtime, subclass the real Click type
|
|
52
|
+
ParamTypeBase = click.ParamType # type: ignore[assignment]
|
|
53
|
+
|
|
54
|
+
# Type variable bounded to Enum for generic EnumParam
|
|
55
|
+
E = TypeVar("E", bound=Enum)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ArgsNamespace(TypedDict, total=False):
|
|
59
|
+
"""Namespace for parsed CLI arguments and options in TopMark.
|
|
60
|
+
|
|
61
|
+
This TypedDict is used to represent all relevant CLI arguments and options
|
|
62
|
+
passed through TopMark commands and subcommands. It facilitates type-safe
|
|
63
|
+
access and transfer of parsed CLI state, including global options, config
|
|
64
|
+
file overrides, file selection, filtering, and formatting flags.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
log_level: Verbosity level for logging.
|
|
68
|
+
no_config: Whether to ignore local config files.
|
|
69
|
+
config_files: List of extra config file paths.
|
|
70
|
+
files: List of file paths to process.
|
|
71
|
+
stdin: Whether to read file paths from stdin.
|
|
72
|
+
include_patterns: Glob patterns of files to include.
|
|
73
|
+
include_from: Files containing include patterns.
|
|
74
|
+
exclude_patterns: Glob patterns of files to exclude.
|
|
75
|
+
exclude_from: Files containing exclude patterns.
|
|
76
|
+
file_types: Restrict to given file types.
|
|
77
|
+
relative_to: Root directory for relative paths.
|
|
78
|
+
align_fields: Align header fields with colons.
|
|
79
|
+
header_format: Header output format (file type aware, plain, or json).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Global options: retrieve from ctx.obj
|
|
83
|
+
log_level: int
|
|
84
|
+
|
|
85
|
+
# TopMark Command
|
|
86
|
+
# command: TopMarkCommand
|
|
87
|
+
|
|
88
|
+
# Command options: config
|
|
89
|
+
no_config: bool | None
|
|
90
|
+
config_files: list[str] | None
|
|
91
|
+
|
|
92
|
+
# Command arguments
|
|
93
|
+
files: list[str]
|
|
94
|
+
|
|
95
|
+
# Command options: common_file_and_filtering_options
|
|
96
|
+
stdin: bool | None
|
|
97
|
+
include_patterns: list[str] | None
|
|
98
|
+
include_from: list[str] | None
|
|
99
|
+
exclude_patterns: list[str] | None
|
|
100
|
+
exclude_from: list[str] | None
|
|
101
|
+
file_types: list[str] | None
|
|
102
|
+
relative_to: str | None
|
|
103
|
+
|
|
104
|
+
# Command options: formatting
|
|
105
|
+
align_fields: bool | None
|
|
106
|
+
header_format: HeaderOutputFormat | None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_args_namespace(
|
|
110
|
+
*,
|
|
111
|
+
log_level: int = logging.WARNING,
|
|
112
|
+
# command: TopMarkCommand = TopMarkCommand.NONE,
|
|
113
|
+
no_config: bool | None = None,
|
|
114
|
+
config_files: list[str] | None = None,
|
|
115
|
+
files: list[str] | None = None,
|
|
116
|
+
stdin: bool | None = None,
|
|
117
|
+
include_patterns: list[str] | None = None,
|
|
118
|
+
include_from: list[str] | None = None,
|
|
119
|
+
exclude_patterns: list[str] | None = None,
|
|
120
|
+
exclude_from: list[str] | None = None,
|
|
121
|
+
file_types: list[str] | None = None,
|
|
122
|
+
relative_to: str | None = None,
|
|
123
|
+
align_fields: bool | None = None,
|
|
124
|
+
header_format: HeaderOutputFormat | None = None,
|
|
125
|
+
) -> ArgsNamespace:
|
|
126
|
+
"""Build an ArgsNamespace dictionary for CLI argument passing.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
log_level: Verbosity level for logging.
|
|
130
|
+
no_config: Whether to ignore local config files.
|
|
131
|
+
config_files: List of extra config file paths.
|
|
132
|
+
files: List of file paths to process.
|
|
133
|
+
stdin: Whether to read file paths from stdin.
|
|
134
|
+
include_patterns: Glob patterns of files to include.
|
|
135
|
+
include_from: Files containing include patterns.
|
|
136
|
+
exclude_patterns: Glob patterns of files to exclude.
|
|
137
|
+
exclude_from: Files containing exclude patterns.
|
|
138
|
+
file_types: Restrict to given file types.
|
|
139
|
+
relative_to: Root directory for relative paths.
|
|
140
|
+
align_fields: Align header fields with colons.
|
|
141
|
+
header_format: Header output format (native, plain, or json).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary of CLI argument values for use throughout the CLI.
|
|
145
|
+
"""
|
|
146
|
+
return {
|
|
147
|
+
"log_level": log_level,
|
|
148
|
+
# "command": command,
|
|
149
|
+
"no_config": no_config,
|
|
150
|
+
"config_files": config_files,
|
|
151
|
+
"files": files if files is not None else [],
|
|
152
|
+
"stdin": stdin,
|
|
153
|
+
"include_patterns": include_patterns,
|
|
154
|
+
"include_from": include_from,
|
|
155
|
+
"exclude_patterns": exclude_patterns,
|
|
156
|
+
"exclude_from": exclude_from,
|
|
157
|
+
"file_types": file_types,
|
|
158
|
+
"relative_to": relative_to,
|
|
159
|
+
"align_fields": align_fields,
|
|
160
|
+
"header_format": header_format,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- Example custom Click parameter types and validators for TopMark CLI ---
|
|
165
|
+
|
|
166
|
+
# HeaderFormatParam: Click callback for validating header format option
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class EnumParam(ParamTypeBase, Generic[E]):
|
|
170
|
+
"""A Click parameter type that converts a string to a member of a given Enum."""
|
|
171
|
+
|
|
172
|
+
# Add instance variable annotations for maximum clarity
|
|
173
|
+
enum_cls: type[E]
|
|
174
|
+
name: str
|
|
175
|
+
choices: list[str]
|
|
176
|
+
|
|
177
|
+
def __init__(self, enum_cls: type[E]):
|
|
178
|
+
"""Initialize the EnumParam instance."""
|
|
179
|
+
self.enum_cls = enum_cls
|
|
180
|
+
self.name = self.enum_cls.__name__.lower()
|
|
181
|
+
# Assume the enum exposes string-valued members (e.g., HeaderOutputFormat)
|
|
182
|
+
self.choices = [cast(str, getattr(e, "value", str(e))) for e in self.enum_cls]
|
|
183
|
+
|
|
184
|
+
def _fail_noreturn(
|
|
185
|
+
self,
|
|
186
|
+
message: str,
|
|
187
|
+
param: click.Parameter | None,
|
|
188
|
+
ctx: click.Context | None,
|
|
189
|
+
) -> NoReturn:
|
|
190
|
+
"""Raise a BadParameter with a NoReturn signature (clear to type checkers)."""
|
|
191
|
+
raise click.BadParameter(message, param=param, ctx=ctx)
|
|
192
|
+
|
|
193
|
+
def convert(
|
|
194
|
+
self,
|
|
195
|
+
value: str | None,
|
|
196
|
+
param: click.Parameter | None,
|
|
197
|
+
ctx: click.Context | None,
|
|
198
|
+
) -> E | None:
|
|
199
|
+
"""Converts a string to a member of the Enum."""
|
|
200
|
+
if value is None:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
# Case-insensitive lookup by the enum's string value
|
|
204
|
+
lookup: dict[str, E] = {
|
|
205
|
+
cast(str, getattr(choice, "value", str(choice))).lower(): choice
|
|
206
|
+
for choice in cast(Iterable[E], self.enum_cls)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
key = value.lower()
|
|
210
|
+
if key in lookup:
|
|
211
|
+
return lookup[key]
|
|
212
|
+
|
|
213
|
+
# Raise a BadParameter exception for invalid input
|
|
214
|
+
self._fail_noreturn(
|
|
215
|
+
f"Invalid value '{value}'. Must be one of: {', '.join(self.choices)}",
|
|
216
|
+
param,
|
|
217
|
+
ctx,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def shell_complete(
|
|
221
|
+
self,
|
|
222
|
+
ctx: click.Context, # pylint: disable=unused-argument
|
|
223
|
+
param: click.Parameter, # pylint: disable=unused-argument
|
|
224
|
+
incomplete: str,
|
|
225
|
+
) -> list["ClickCompletionItem"]:
|
|
226
|
+
"""Tab completion for Click.
|
|
227
|
+
|
|
228
|
+
Bash: `eval "$(_TOPMARK_COMPLETE=bash_source topmark)"`
|
|
229
|
+
Zsh: `eval "$(_TOPMARK_COMPLETE=zsh_source topmark)"`
|
|
230
|
+
"""
|
|
231
|
+
# Runtime import to avoid import-time dependency for non-completion paths
|
|
232
|
+
from click.shell_completion import (
|
|
233
|
+
CompletionItem as RuntimeCompletionItem,
|
|
234
|
+
) # Click 8.x
|
|
235
|
+
|
|
236
|
+
prefix = (incomplete or "").lower()
|
|
237
|
+
items: list["ClickCompletionItem"] = []
|
|
238
|
+
for e in cast(Iterable[E], self.enum_cls):
|
|
239
|
+
val = str(getattr(e, "value", e))
|
|
240
|
+
if val.lower().startswith(prefix):
|
|
241
|
+
items.append(RuntimeCompletionItem(val))
|
|
242
|
+
return items
|
|
243
|
+
|
|
244
|
+
def __repr__(self) -> str:
|
|
245
|
+
"""Return a string representation."""
|
|
246
|
+
return f"EnumParam({self.enum_cls.__name__})"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def FileTypeParam(
|
|
250
|
+
ctx: click.Context, # pylint: disable=unused-argument
|
|
251
|
+
param: click.Parameter, # pylint: disable=unused-argument
|
|
252
|
+
value: Any,
|
|
253
|
+
) -> Path | None:
|
|
254
|
+
"""Validator: Ensure a CLI argument is a valid file path.
|
|
255
|
+
|
|
256
|
+
Validates and converts the CLI argument to a Path, raising an error if it does not exist
|
|
257
|
+
or is not a file.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
ctx: Click context.
|
|
261
|
+
param: The Click parameter.
|
|
262
|
+
value: The CLI argument value to validate.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
The validated file path, or None if value is None.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
click.BadParameter: If the file does not exist or is not a file.
|
|
269
|
+
"""
|
|
270
|
+
if value is None:
|
|
271
|
+
return None
|
|
272
|
+
path = Path(value)
|
|
273
|
+
# Check if the path exists and is a file.
|
|
274
|
+
if not path.exists() or not path.is_file():
|
|
275
|
+
raise click.BadParameter(f"File not found or not a file: {value}")
|
|
276
|
+
return path
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def GlobParam(
|
|
280
|
+
ctx: click.Context, # pylint: disable=unused-argument
|
|
281
|
+
param: click.Parameter, # pylint: disable=unused-argument
|
|
282
|
+
value: Any,
|
|
283
|
+
) -> list[Path]:
|
|
284
|
+
"""Validator: Expand a glob pattern CLI argument to a list of file paths.
|
|
285
|
+
|
|
286
|
+
Expands the given glob pattern string into a list of Path objects matching the pattern.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
ctx: Click context.
|
|
290
|
+
param: The Click parameter.
|
|
291
|
+
value: The glob pattern string.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of Path objects matching the glob pattern, or an empty list if value is None.
|
|
295
|
+
"""
|
|
296
|
+
if value is None:
|
|
297
|
+
return []
|
|
298
|
+
# Use glob.glob for shell-style wildcards; return as Path objects.
|
|
299
|
+
return [Path(p) for p in glob.glob(value, recursive=True)]
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# topmark:header:start
|
|
2
|
+
#
|
|
3
|
+
# file : default.py
|
|
4
|
+
# file_relpath : src/topmark/cli/commands/default.py
|
|
5
|
+
# project : TopMark
|
|
6
|
+
# license : MIT
|
|
7
|
+
# copyright : (c) 2025 Olivier Biot
|
|
8
|
+
#
|
|
9
|
+
# topmark:header:end
|
|
10
|
+
|
|
11
|
+
"""Default TopMark operation (check/apply).
|
|
12
|
+
|
|
13
|
+
This module implements the unified default behavior of TopMark’s CLI. It is
|
|
14
|
+
invoked when no explicit subcommand is provided and processes the file paths
|
|
15
|
+
captured by Click (``ctx.args``), performing a dry‑run **check** by default or
|
|
16
|
+
**applying** changes when ``--apply`` is given.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
Check files and print a human summary::
|
|
20
|
+
|
|
21
|
+
topmark --summary src
|
|
22
|
+
|
|
23
|
+
Emit per‑file objects in NDJSON (one per line)::
|
|
24
|
+
|
|
25
|
+
topmark --format=ndjson src pkg
|
|
26
|
+
|
|
27
|
+
Write changes and show diffs (human output only)::
|
|
28
|
+
|
|
29
|
+
topmark --apply --diff .
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
from yachalk import chalk
|
|
34
|
+
|
|
35
|
+
from topmark.cli.cli_types import EnumParam
|
|
36
|
+
from topmark.cli.config_resolver import resolve_config_from_click
|
|
37
|
+
from topmark.cli.options import (
|
|
38
|
+
common_config_options,
|
|
39
|
+
common_file_and_filtering_options,
|
|
40
|
+
common_header_formatting_options,
|
|
41
|
+
typed_option,
|
|
42
|
+
underscored_trap_option,
|
|
43
|
+
)
|
|
44
|
+
from topmark.cli.utils import (
|
|
45
|
+
OutputFormat,
|
|
46
|
+
count_by_outcome,
|
|
47
|
+
)
|
|
48
|
+
from topmark.config.logging import get_logger
|
|
49
|
+
from topmark.file_resolver import resolve_file_list
|
|
50
|
+
from topmark.pipeline import runner
|
|
51
|
+
from topmark.pipeline.context import ComparisonStatus, ProcessingContext
|
|
52
|
+
from topmark.pipeline.pipelines import get_pipeline
|
|
53
|
+
from topmark.rendering.formats import HeaderOutputFormat
|
|
54
|
+
|
|
55
|
+
logger = get_logger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@common_config_options
|
|
59
|
+
@common_file_and_filtering_options
|
|
60
|
+
@common_header_formatting_options
|
|
61
|
+
@typed_option(
|
|
62
|
+
"--apply", "apply_changes", is_flag=True, help="Write changes to files (off by default)."
|
|
63
|
+
)
|
|
64
|
+
@typed_option("--diff", is_flag=True, help="Show unified diffs (human output only).")
|
|
65
|
+
@typed_option(
|
|
66
|
+
"--summary",
|
|
67
|
+
"summary_mode",
|
|
68
|
+
is_flag=True,
|
|
69
|
+
help="Show outcome counts instead of per-file details.",
|
|
70
|
+
)
|
|
71
|
+
@typed_option(
|
|
72
|
+
"--skip-compliant",
|
|
73
|
+
"skip_compliant",
|
|
74
|
+
is_flag=True,
|
|
75
|
+
help="Hide files that are already up-to-date.",
|
|
76
|
+
)
|
|
77
|
+
@underscored_trap_option("--skip_compliant")
|
|
78
|
+
@typed_option(
|
|
79
|
+
"--format",
|
|
80
|
+
"output_format",
|
|
81
|
+
type=EnumParam(OutputFormat),
|
|
82
|
+
default=None,
|
|
83
|
+
help=f"Output format ({', '.join(v.value for v in OutputFormat)}).",
|
|
84
|
+
)
|
|
85
|
+
def default_command(
|
|
86
|
+
stdin: bool,
|
|
87
|
+
include_patterns: list[str],
|
|
88
|
+
include_from: list[str],
|
|
89
|
+
exclude_patterns: list[str],
|
|
90
|
+
exclude_from: list[str],
|
|
91
|
+
no_config: bool,
|
|
92
|
+
config_paths: list[str],
|
|
93
|
+
file_types: list[str],
|
|
94
|
+
relative_to: str | None,
|
|
95
|
+
align_fields: bool,
|
|
96
|
+
header_format: HeaderOutputFormat | None,
|
|
97
|
+
apply_changes: bool,
|
|
98
|
+
diff: bool,
|
|
99
|
+
summary_mode: bool,
|
|
100
|
+
skip_compliant: bool,
|
|
101
|
+
output_format: OutputFormat | None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Run the unified default command (check/apply).
|
|
104
|
+
|
|
105
|
+
When invoked via the root group, this command receives options parsed at the
|
|
106
|
+
group level and reads positional paths from ``click.get_current_context().args``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
stdin: If True, read file paths from standard input (one per line).
|
|
110
|
+
include_patterns: Glob patterns specifying additional files or directories to include.
|
|
111
|
+
include_from: Paths to files that contain include glob patterns (one per line).
|
|
112
|
+
exclude_patterns: Glob patterns specifying files or directories to exclude.
|
|
113
|
+
exclude_from: Paths to files that contain exclude glob patterns (one per line).
|
|
114
|
+
no_config: If True, ignore user/project configuration files and use defaults only.
|
|
115
|
+
config_paths: Additional configuration file paths to load and merge.
|
|
116
|
+
file_types: Restrict processing to the given TopMark file type identifiers.
|
|
117
|
+
relative_to: Base directory used to compute relative paths in outputs.
|
|
118
|
+
align_fields: If True, align header fields by the colon for readability.
|
|
119
|
+
header_format: Optional override for the header output format used when rendering.
|
|
120
|
+
apply_changes: If True, write changes to files; otherwise perform a dry run.
|
|
121
|
+
diff: If True, show unified diffs of header changes (human output only).
|
|
122
|
+
summary_mode: If True, print outcome counts instead of per-file details.
|
|
123
|
+
skip_compliant: If True, suppress files whose comparison status is UNCHANGED.
|
|
124
|
+
output_format: Output format to use (default, json, or ndjson).
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
click.UsageError: If no input is provided (Black‑style preflight).
|
|
128
|
+
SystemExit: With code 2 in check mode when changes would be required.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
None
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
Dry‑run (default)::
|
|
135
|
+
|
|
136
|
+
topmark src
|
|
137
|
+
|
|
138
|
+
Apply changes::
|
|
139
|
+
|
|
140
|
+
topmark --apply src
|
|
141
|
+
|
|
142
|
+
NDJSON per‑file output::
|
|
143
|
+
|
|
144
|
+
topmark --format=ndjson src
|
|
145
|
+
"""
|
|
146
|
+
ctx = click.get_current_context()
|
|
147
|
+
files = list(ctx.args)
|
|
148
|
+
|
|
149
|
+
# Before resolving config/files — block empty invocations (Black-style)
|
|
150
|
+
no_inputs = (
|
|
151
|
+
not files and not stdin and not include_patterns and not include_from and not config_paths
|
|
152
|
+
)
|
|
153
|
+
if no_inputs:
|
|
154
|
+
# Show a friendly usage error with guidance
|
|
155
|
+
raise click.UsageError(
|
|
156
|
+
"No input specified. Provide one or more paths, or use --stdin. "
|
|
157
|
+
"See --help for examples.",
|
|
158
|
+
ctx=ctx,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# === Primary TopMark operation (default: check, write with --apply) ===
|
|
162
|
+
config = resolve_config_from_click(
|
|
163
|
+
ctx=ctx,
|
|
164
|
+
files=list(files),
|
|
165
|
+
stdin=stdin,
|
|
166
|
+
include_patterns=list(include_patterns),
|
|
167
|
+
include_from=list(include_from),
|
|
168
|
+
exclude_patterns=list(exclude_patterns),
|
|
169
|
+
exclude_from=list(exclude_from),
|
|
170
|
+
file_types=list(file_types),
|
|
171
|
+
relative_to=relative_to,
|
|
172
|
+
no_config=no_config,
|
|
173
|
+
config_paths=list(config_paths),
|
|
174
|
+
align_fields=align_fields,
|
|
175
|
+
header_format=header_format,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
logger.trace("Config after merging args: %s", config)
|
|
179
|
+
|
|
180
|
+
# file_list = resolve_file_list(config)
|
|
181
|
+
# Use Click's text stream for stdin so CliRunner/invoke input is read reliably
|
|
182
|
+
stdin_stream = click.get_text_stream("stdin") if stdin else None
|
|
183
|
+
file_list = resolve_file_list(config, stdin_stream=stdin_stream)
|
|
184
|
+
if not file_list:
|
|
185
|
+
click.echo(chalk.blue("\nNo files.\n"))
|
|
186
|
+
raise SystemExit(0)
|
|
187
|
+
|
|
188
|
+
click.echo(chalk.blue(f"\nProcessing {len(file_list)} file(s):\n"))
|
|
189
|
+
click.echo(chalk.bold.underline("TopMark Results:"))
|
|
190
|
+
|
|
191
|
+
# Use summary pipeline when --summary is set, else full (compare+update+patch)
|
|
192
|
+
pipeline_name = "summary" if summary_mode else "apply"
|
|
193
|
+
steps = get_pipeline(pipeline_name)
|
|
194
|
+
|
|
195
|
+
results: list[ProcessingContext] = []
|
|
196
|
+
for path in file_list:
|
|
197
|
+
try:
|
|
198
|
+
ctx_obj: ProcessingContext = ProcessingContext.bootstrap(path=path, config=config)
|
|
199
|
+
ctx_obj = runner.run(ctx_obj, steps)
|
|
200
|
+
results.append(ctx_obj)
|
|
201
|
+
except (FileNotFoundError, PermissionError, IsADirectoryError) as e:
|
|
202
|
+
logger.error("Filesystem error while processing %s: %s", path, e)
|
|
203
|
+
click.echo(chalk.red(f"Filesystem error processing {path}: {e}"))
|
|
204
|
+
continue
|
|
205
|
+
except UnicodeDecodeError as e:
|
|
206
|
+
logger.error("Encoding error while reading %s: %s", path, e)
|
|
207
|
+
click.echo(chalk.red(f"Encoding error in {path}: {e}"))
|
|
208
|
+
continue
|
|
209
|
+
except Exception as e: # pragma: no cover - unexpected path
|
|
210
|
+
logger.exception("Unexpected error processing %s", path)
|
|
211
|
+
click.echo(
|
|
212
|
+
chalk.red(f"Unexpected error processing {path}: {e} (use -vv for traceback)")
|
|
213
|
+
)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Optional filtering
|
|
217
|
+
if skip_compliant:
|
|
218
|
+
view_results = [r for r in results if r.status.comparison is not ComparisonStatus.UNCHANGED]
|
|
219
|
+
else:
|
|
220
|
+
view_results = results
|
|
221
|
+
|
|
222
|
+
fmt: OutputFormat = output_format or OutputFormat.DEFAULT
|
|
223
|
+
# Machine formats first
|
|
224
|
+
if fmt is OutputFormat.NDJSON:
|
|
225
|
+
import json as _json
|
|
226
|
+
|
|
227
|
+
if summary_mode:
|
|
228
|
+
counts = count_by_outcome(view_results)
|
|
229
|
+
for key, (n, label, _color) in counts.items():
|
|
230
|
+
click.echo(_json.dumps({"key": key, "count": n, "label": label}))
|
|
231
|
+
else:
|
|
232
|
+
for r in view_results:
|
|
233
|
+
click.echo(_json.dumps(r.to_dict()))
|
|
234
|
+
elif fmt is OutputFormat.JSON:
|
|
235
|
+
import json as _json
|
|
236
|
+
|
|
237
|
+
if summary_mode:
|
|
238
|
+
counts = count_by_outcome(view_results)
|
|
239
|
+
data = {k: {"count": n, "label": label} for k, (n, label, _color) in counts.items()}
|
|
240
|
+
click.echo(_json.dumps(data, indent=2))
|
|
241
|
+
else:
|
|
242
|
+
payload = [r.to_dict() for r in view_results]
|
|
243
|
+
click.echo(_json.dumps(payload, indent=2))
|
|
244
|
+
else:
|
|
245
|
+
# Human output
|
|
246
|
+
if summary_mode:
|
|
247
|
+
click.echo()
|
|
248
|
+
click.echo(chalk.bold.underline("Summary by outcome:"))
|
|
249
|
+
|
|
250
|
+
counts = count_by_outcome(view_results)
|
|
251
|
+
label_width = max((len(v[1]) for v in counts.values()), default=0) + 1
|
|
252
|
+
num_width = len(str(len(file_list)))
|
|
253
|
+
for _key, (n, label, color) in counts.items():
|
|
254
|
+
click.echo(color(f" {label:<{label_width}}: {n:>{num_width}}"))
|
|
255
|
+
|
|
256
|
+
else:
|
|
257
|
+
for r in view_results:
|
|
258
|
+
click.echo(r.summary)
|
|
259
|
+
if diff:
|
|
260
|
+
for r in view_results:
|
|
261
|
+
if r.header_diff:
|
|
262
|
+
click.echo(r.header_diff)
|
|
263
|
+
|
|
264
|
+
# Writes (only when --apply is set)
|
|
265
|
+
if apply_changes:
|
|
266
|
+
written = 0
|
|
267
|
+
failed = 0
|
|
268
|
+
from pathlib import Path
|
|
269
|
+
|
|
270
|
+
for r in results:
|
|
271
|
+
try:
|
|
272
|
+
if r.updated_file_lines is not None and r.file_lines != r.updated_file_lines:
|
|
273
|
+
Path(str(r.path)).write_text("".join(r.updated_file_lines), encoding="utf-8")
|
|
274
|
+
written += 1
|
|
275
|
+
except Exception as e: # pragma: no cover
|
|
276
|
+
failed += 1
|
|
277
|
+
logger.error("Failed to write %s: %s", r.path, e)
|
|
278
|
+
if fmt is OutputFormat.DEFAULT:
|
|
279
|
+
click.echo(
|
|
280
|
+
f"\nApplied changes to {written} file(s)." if written else "\nNo changes to apply."
|
|
281
|
+
)
|
|
282
|
+
if failed:
|
|
283
|
+
raise SystemExit(1)
|
|
284
|
+
|
|
285
|
+
# Exit code policy: in check mode, non-zero if changes would be needed
|
|
286
|
+
if not apply_changes:
|
|
287
|
+
would_change = any(
|
|
288
|
+
(r.status.header.name == "MISSING") or (r.status.comparison.name == "CHANGED")
|
|
289
|
+
for r in results
|
|
290
|
+
)
|
|
291
|
+
if would_change:
|
|
292
|
+
raise SystemExit(2)
|
|
293
|
+
|
|
294
|
+
click.echo("All done!")
|
|
295
|
+
return
|