py-wlcli 0.8.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.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""Lint reporter module.
|
|
2
|
+
Handles report generation for lint command.
|
|
3
|
+
|
|
4
|
+
Rendering only (#170): :meth:`LintReporter.render` reads a finished
|
|
5
|
+
:class:`~wl_cli.commands.lint.lint_run.LintRun` and writes the report files.
|
|
6
|
+
It starts no child process -- the executor has already run each tool exactly
|
|
7
|
+
once -- so the content cannot disagree with the verdict, and the number of
|
|
8
|
+
executions is visible in one place.
|
|
9
|
+
|
|
10
|
+
Freshness contract (#200): every report file :meth:`LintReporter.render`
|
|
11
|
+
writes carries a ``- Generated at: <ISO-8601>`` line naming the run that wrote
|
|
12
|
+
it. Two rounds over the same scope with the same findings therefore differ in
|
|
13
|
+
that line even though every other byte is identical -- before this, a report
|
|
14
|
+
could only be shown to be "this run's" by asserting the run wrote it, and two
|
|
15
|
+
such reports compared byte-for-byte. The stamp is computed once per
|
|
16
|
+
:meth:`render` call, so every file of one run names the same moment. It is a
|
|
17
|
+
timestamp rather than a random id so the report also answers *when* it was
|
|
18
|
+
produced; two runs inside the same microsecond would share it, which is the
|
|
19
|
+
one residual ambiguity in the contract.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from ...utils.logging import log_info_pair
|
|
27
|
+
from ...utils.pre_commit import PreCommitRun
|
|
28
|
+
from .lint_run import LintRun, RuffBlock, display_path
|
|
29
|
+
|
|
30
|
+
#: The mypy scopes ``error.md`` reports on separately. One mypy run covers
|
|
31
|
+
#: both; the output is split by these path prefixes.
|
|
32
|
+
_MYPY_SCOPES = ("src/", "tests/")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run_stamp(generated_at: datetime | None = None) -> str:
|
|
36
|
+
"""The run stamp a report carries: ``generated_at`` or the current moment.
|
|
37
|
+
|
|
38
|
+
Microseconds, not seconds, are what makes two consecutive runs
|
|
39
|
+
distinguishable; the offset keeps the stamp unambiguous when the report
|
|
40
|
+
travels between machines (#200).
|
|
41
|
+
|
|
42
|
+
The only clock read this module performs: every report of one ``render``
|
|
43
|
+
call is passed the string it returns, so the files cannot disagree about
|
|
44
|
+
the moment. A naive ``generated_at`` is read as local wall time and given
|
|
45
|
+
the local offset (LOW-1) rather than emitting an offset-less ISO string
|
|
46
|
+
that reads as UTC to a distant reader.
|
|
47
|
+
"""
|
|
48
|
+
moment = generated_at if generated_at is not None else datetime.now().astimezone()
|
|
49
|
+
if moment.tzinfo is None:
|
|
50
|
+
moment = moment.astimezone()
|
|
51
|
+
return moment.isoformat(timespec="microseconds")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LintReporter:
|
|
55
|
+
"""Reporter for lint command."""
|
|
56
|
+
|
|
57
|
+
def render(self, run: LintRun, *, generated_at: datetime | None = None) -> None:
|
|
58
|
+
"""Write every lint report from *run*.
|
|
59
|
+
|
|
60
|
+
The files and their locations are unchanged: ``todos/lint_report.md``,
|
|
61
|
+
``todos/error.md``, and ``todos/to_packaging_modularization.md`` when
|
|
62
|
+
oversized modules exist.
|
|
63
|
+
|
|
64
|
+
#200: each file names this run with a ``- Generated at:`` line. The
|
|
65
|
+
moment is read once here, so all files of one run agree; callers pass
|
|
66
|
+
nothing in production. ``generated_at`` exists for tests: the committed
|
|
67
|
+
snapshots pin everything except the clock, which would otherwise make
|
|
68
|
+
them unrecordable and would hide the very line the freshness contract
|
|
69
|
+
is about.
|
|
70
|
+
"""
|
|
71
|
+
stamp = _run_stamp(generated_at)
|
|
72
|
+
|
|
73
|
+
# Create todos directory in current working directory
|
|
74
|
+
todos_dir = Path("todos")
|
|
75
|
+
todos_dir.mkdir(exist_ok=True)
|
|
76
|
+
|
|
77
|
+
report_file = todos_dir / "lint_report.md"
|
|
78
|
+
error_file = todos_dir / "error.md"
|
|
79
|
+
|
|
80
|
+
paths = list(run.paths) or None
|
|
81
|
+
large_files = self._find_large_python_files(run.project_root, paths)
|
|
82
|
+
|
|
83
|
+
# Generate main lint report content
|
|
84
|
+
lint_report_content = self._generate_lint_report(run, large_files, stamp=stamp)
|
|
85
|
+
self._write_report_file(report_file, lint_report_content)
|
|
86
|
+
|
|
87
|
+
# Generate error report content
|
|
88
|
+
error_report_content = self._generate_error_report(run, stamp=stamp)
|
|
89
|
+
self._write_report_file(error_file, error_report_content)
|
|
90
|
+
|
|
91
|
+
# Handle large files report if needed
|
|
92
|
+
if large_files:
|
|
93
|
+
self._generate_packaging_todo(
|
|
94
|
+
large_files, run.project_root, run.quiet, stamp=stamp
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Log report generation messages
|
|
98
|
+
if not run.quiet:
|
|
99
|
+
log_info_pair(
|
|
100
|
+
f"Lint report generated at {report_file}",
|
|
101
|
+
f"静态检查报告已生成到 {report_file}",
|
|
102
|
+
)
|
|
103
|
+
log_info_pair(
|
|
104
|
+
f"Error report generated at {error_file}",
|
|
105
|
+
f"错误报告已生成到 {error_file}",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def issue_text(self, run: LintRun) -> str:
|
|
109
|
+
"""The ruff findings as terminal text, exactly as ruff rendered them.
|
|
110
|
+
|
|
111
|
+
The one ruff run is asked for ``--output-format full`` (#170 review
|
|
112
|
+
F1), so its stdout is already the text the old reporter printed. A
|
|
113
|
+
previous cut captured JSON here and re-rendered one concise line per
|
|
114
|
+
finding; that is what shrank the report (and this terminal view) away
|
|
115
|
+
from ruff's snippets, carets and ``help:`` lines.
|
|
116
|
+
"""
|
|
117
|
+
return run.ruff.stdout
|
|
118
|
+
|
|
119
|
+
def _render_pre_commit(self, pre_commit: PreCommitRun | None) -> str:
|
|
120
|
+
"""Render the pre-commit section from the already-finished run.
|
|
121
|
+
|
|
122
|
+
The verdict, the blocks and the filtered text all come from
|
|
123
|
+
:class:`PreCommitRun` (#165); this method only chooses the wording.
|
|
124
|
+
"""
|
|
125
|
+
if pre_commit is None:
|
|
126
|
+
return "pre-commit was not run."
|
|
127
|
+
if pre_commit.result.launch_error is not None:
|
|
128
|
+
# ``launch_error`` covers every way the launcher can raise -- a
|
|
129
|
+
# missing executable, but also a malformed environment or an
|
|
130
|
+
# undecodable stream -- so name the failure instead of guessing
|
|
131
|
+
# "not installed" (#165/#172).
|
|
132
|
+
return f"pre-commit could not be started: {pre_commit.result.launch_error}"
|
|
133
|
+
|
|
134
|
+
# A non-zero exit with no hook verdict is pre-commit itself failing
|
|
135
|
+
# (e.g. an invalid config), not "nothing to report" (#165 patch 4).
|
|
136
|
+
filtered = pre_commit.filtered_output
|
|
137
|
+
if filtered:
|
|
138
|
+
return filtered
|
|
139
|
+
if pre_commit.hooks_failed:
|
|
140
|
+
return pre_commit.combined_output.strip()
|
|
141
|
+
return "No pre-commit issues found."
|
|
142
|
+
|
|
143
|
+
def _render_ruff_sections(self, run: LintRun) -> list[str]:
|
|
144
|
+
"""Render the per-directory ruff sections from the one verbatim run.
|
|
145
|
+
|
|
146
|
+
Each diagnostic block is ruff's own text; only its placement changes.
|
|
147
|
+
The run's tail -- ruff's summary, with its own ``[*]`` fixable count
|
|
148
|
+
and ``--unsafe-fixes`` hint -- is rendered once, after every section: a
|
|
149
|
+
single invocation can only produce one summary (#170 review F2/F5).
|
|
150
|
+
"""
|
|
151
|
+
sections = self._existing_sections(run.project_root, list(run.paths) or None)
|
|
152
|
+
parsed = run.ruff_output
|
|
153
|
+
if parsed.empty:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
grouped: dict[str, list[RuffBlock]] = {title: [] for title, _ in sections}
|
|
157
|
+
ungrouped: list[RuffBlock] = []
|
|
158
|
+
for block in parsed.blocks:
|
|
159
|
+
title = _enclosing_section(block.path, sections, run.project_root)
|
|
160
|
+
if title is None:
|
|
161
|
+
ungrouped.append(block)
|
|
162
|
+
else:
|
|
163
|
+
grouped[title].append(block)
|
|
164
|
+
|
|
165
|
+
lines: list[str] = []
|
|
166
|
+
for title, _ in sections:
|
|
167
|
+
lines.extend(self._render_ruff_section(title, grouped[title]))
|
|
168
|
+
# Findings outside every section (a root-level module, say) used to be
|
|
169
|
+
# dropped from the report while still failing the run. Keep them, under
|
|
170
|
+
# the same "." the header uses for the default scope.
|
|
171
|
+
if ungrouped:
|
|
172
|
+
lines.extend(self._render_ruff_section(".", ungrouped))
|
|
173
|
+
if parsed.tail:
|
|
174
|
+
lines.extend(self._render_ruff_summary(parsed.tail))
|
|
175
|
+
return lines
|
|
176
|
+
|
|
177
|
+
def _render_ruff_section(self, title: str, blocks: list[RuffBlock]) -> list[str]:
|
|
178
|
+
"""One ``## Issues in <title>`` fence, holding ruff's blocks verbatim."""
|
|
179
|
+
if not blocks:
|
|
180
|
+
return []
|
|
181
|
+
body = "\n\n".join(block.text for block in blocks)
|
|
182
|
+
return [
|
|
183
|
+
f"## Issues in {title}\n\n",
|
|
184
|
+
"```\n",
|
|
185
|
+
body + "\n",
|
|
186
|
+
"```\n\n",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def _render_ruff_summary(self, tail: tuple[str, ...]) -> list[str]:
|
|
190
|
+
"""ruff's own closing lines: totals, fixable count, unsafe hint."""
|
|
191
|
+
return [
|
|
192
|
+
"## Ruff Summary\n\n",
|
|
193
|
+
"```\n",
|
|
194
|
+
"\n".join(tail) + "\n",
|
|
195
|
+
"```\n\n",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _start_section(lines: list[str], heading: str) -> None:
|
|
200
|
+
"""Append *heading* with exactly one blank line before it (#170 F10)."""
|
|
201
|
+
while lines and lines[-1] == "":
|
|
202
|
+
lines.pop()
|
|
203
|
+
if lines:
|
|
204
|
+
text = "".join(lines)
|
|
205
|
+
if not text.endswith("\n\n"):
|
|
206
|
+
lines.append("\n" if text.endswith("\n") else "\n\n")
|
|
207
|
+
lines.append(heading)
|
|
208
|
+
|
|
209
|
+
def _existing_sections(
|
|
210
|
+
self, project_root: Path, paths: list[str] | None
|
|
211
|
+
) -> list[tuple[str, Path]]:
|
|
212
|
+
return [
|
|
213
|
+
(title, path)
|
|
214
|
+
for title, path in self._resolve_sections(project_root, paths)
|
|
215
|
+
if path.exists()
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
def _find_large_python_files(
|
|
219
|
+
self, project_root: Path, paths: list[str] | None = None
|
|
220
|
+
) -> list[tuple[Path, int]]:
|
|
221
|
+
search_paths = self._build_search_paths(project_root, paths)
|
|
222
|
+
results: list[tuple[Path, int]] = []
|
|
223
|
+
for sp in search_paths:
|
|
224
|
+
for py_file in self._iter_python_files(sp):
|
|
225
|
+
count = self._count_lines(py_file)
|
|
226
|
+
if count is not None and count > 200:
|
|
227
|
+
results.append((py_file, count))
|
|
228
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
229
|
+
return results
|
|
230
|
+
|
|
231
|
+
def _build_search_paths(
|
|
232
|
+
self, project_root: Path, paths: list[str] | None
|
|
233
|
+
) -> list[Path]:
|
|
234
|
+
if paths:
|
|
235
|
+
resolved: list[Path] = []
|
|
236
|
+
for p in paths:
|
|
237
|
+
path = Path(p)
|
|
238
|
+
if not path.is_absolute():
|
|
239
|
+
path = project_root / path
|
|
240
|
+
if path.exists():
|
|
241
|
+
resolved.append(path)
|
|
242
|
+
return resolved
|
|
243
|
+
base = [project_root / "src"]
|
|
244
|
+
t = project_root / "tools"
|
|
245
|
+
e = project_root / "examples"
|
|
246
|
+
if t.exists():
|
|
247
|
+
base.append(t)
|
|
248
|
+
if e.exists():
|
|
249
|
+
base.append(e)
|
|
250
|
+
return base
|
|
251
|
+
|
|
252
|
+
def _iter_python_files(self, path: Path):
|
|
253
|
+
if not path.exists():
|
|
254
|
+
return []
|
|
255
|
+
if path.is_file() and path.suffix == ".py":
|
|
256
|
+
return [path]
|
|
257
|
+
if path.is_dir():
|
|
258
|
+
return [p for p in path.rglob("*.py") if "__pycache__" not in str(p)]
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
def _count_lines(self, file_path: Path) -> int | None:
|
|
262
|
+
try:
|
|
263
|
+
with open(file_path, encoding="utf-8") as f:
|
|
264
|
+
return sum(1 for _ in f)
|
|
265
|
+
except Exception:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def _build_header(
|
|
269
|
+
self,
|
|
270
|
+
quiet: bool,
|
|
271
|
+
fix: bool,
|
|
272
|
+
paths: list[str] | None,
|
|
273
|
+
*,
|
|
274
|
+
stamp: str,
|
|
275
|
+
) -> list[str]:
|
|
276
|
+
"""The report's header. ``stamp`` is required: rendering a file never
|
|
277
|
+
reads the clock itself, it only carries the moment :meth:`render`
|
|
278
|
+
settled on (LOW-2 -- the ``stamp or _run_stamp()`` fallback let a
|
|
279
|
+
private helper start a second clock inside one run)."""
|
|
280
|
+
lines: list[str] = []
|
|
281
|
+
lines.append("# Lint Report\n\n")
|
|
282
|
+
lines.append(f"- Generated at: {stamp}\n\n")
|
|
283
|
+
lines.append("## Configuration\n\n")
|
|
284
|
+
lines.append(f"- Quiet mode: {quiet}\n")
|
|
285
|
+
lines.append(f"- Fix enabled: {fix}\n")
|
|
286
|
+
lines.append(f"- Paths: {', '.join(paths) if paths else '.'}\n\n")
|
|
287
|
+
return lines
|
|
288
|
+
|
|
289
|
+
def _resolve_sections(
|
|
290
|
+
self, project_root: Path, paths: list[str] | None
|
|
291
|
+
) -> list[tuple[str, Path]]:
|
|
292
|
+
"""The report's per-directory sections, in the order they render.
|
|
293
|
+
|
|
294
|
+
Section rule (#172): the user's ``paths`` are the sections when any
|
|
295
|
+
were given -- each title is the path exactly as typed. With no paths
|
|
296
|
+
the default scope is ``src/``, ``tools/`` and ``examples/``. A finding
|
|
297
|
+
that belongs to no section -- ``tests/`` under that default scope, or
|
|
298
|
+
a root-level module -- is rendered under ``.`` by
|
|
299
|
+
:meth:`_render_ruff_sections`. :meth:`_existing_sections` then drops
|
|
300
|
+
sections whose directory does not exist, so a fresh project with no
|
|
301
|
+
``tools/`` renders no empty section.
|
|
302
|
+
"""
|
|
303
|
+
if paths:
|
|
304
|
+
items: list[tuple[str, Path]] = []
|
|
305
|
+
for p in paths:
|
|
306
|
+
path = Path(p)
|
|
307
|
+
if not path.is_absolute():
|
|
308
|
+
path = project_root / path
|
|
309
|
+
items.append((p, path))
|
|
310
|
+
return items
|
|
311
|
+
return [
|
|
312
|
+
("src/", project_root / "src"),
|
|
313
|
+
("tools/", project_root / "tools"),
|
|
314
|
+
("examples/", project_root / "examples"),
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
def _generate_lint_report(
|
|
318
|
+
self,
|
|
319
|
+
run: LintRun,
|
|
320
|
+
large_files: list[tuple[Path, int]],
|
|
321
|
+
*,
|
|
322
|
+
stamp: str,
|
|
323
|
+
) -> list[str]:
|
|
324
|
+
"""Generate the main lint report content from the run's one stamp."""
|
|
325
|
+
paths = list(run.paths) or None
|
|
326
|
+
lines = self._build_header(run.quiet, run.fix, paths, stamp=stamp)
|
|
327
|
+
|
|
328
|
+
lines.extend(self._render_ruff_sections(run))
|
|
329
|
+
parsed_ruff = run.ruff_output
|
|
330
|
+
|
|
331
|
+
# Pre-commit results, from the run the executor already finished
|
|
332
|
+
self._start_section(lines, "## Pre-commit Dry Run Results\n\n")
|
|
333
|
+
lines.append("```\n")
|
|
334
|
+
lines.append(self._render_pre_commit(run.pre_commit))
|
|
335
|
+
lines.append("```\n")
|
|
336
|
+
|
|
337
|
+
# MyPy results (one run over ``src/ tests/``)
|
|
338
|
+
self._start_section(lines, "## MyPy Results\n\n")
|
|
339
|
+
mypy_result = run.mypy
|
|
340
|
+
if mypy_result is None:
|
|
341
|
+
lines.append("mypy was not run.\n")
|
|
342
|
+
elif mypy_result.stdout:
|
|
343
|
+
lines.append("```\n")
|
|
344
|
+
lines.append(mypy_result.stdout)
|
|
345
|
+
lines.append("```\n")
|
|
346
|
+
if run.mypy_failure is not None:
|
|
347
|
+
# ``rc >= 2`` can print partial diagnostics before dying; the
|
|
348
|
+
# fenced stdout alone would read as an ordinary type-error
|
|
349
|
+
# listing, so keep the verdict next to it (#172 r1 review F1).
|
|
350
|
+
lines.append(run.mypy_failure + "\n")
|
|
351
|
+
elif run.mypy_failure is not None:
|
|
352
|
+
# A mypy that never produced a verdict is a failure, not "clean":
|
|
353
|
+
# the old shape answered "No MyPy issues found." to a run that
|
|
354
|
+
# could not even find its config (#170 review F4).
|
|
355
|
+
lines.append(run.mypy_failure + "\n")
|
|
356
|
+
else:
|
|
357
|
+
lines.append("No MyPy issues found.\n")
|
|
358
|
+
if mypy_result is not None and mypy_result.stderr:
|
|
359
|
+
self._start_section(lines, "## MyPy Errors\n\n")
|
|
360
|
+
lines.append("```\n")
|
|
361
|
+
lines.append(mypy_result.stderr)
|
|
362
|
+
lines.append("```\n")
|
|
363
|
+
|
|
364
|
+
mypy_output = "" if mypy_result is None else (mypy_result.stdout or "")
|
|
365
|
+
if (
|
|
366
|
+
not run.issues_found
|
|
367
|
+
and not large_files
|
|
368
|
+
and not mypy_output
|
|
369
|
+
and run.mypy_failure is None
|
|
370
|
+
):
|
|
371
|
+
lines.append("No lint issues found.\n")
|
|
372
|
+
# ruff's stderr used to be rendered only when there were no findings at
|
|
373
|
+
# all, so a run that emitted diagnostics *and* a warning lost the
|
|
374
|
+
# warning (#170 review F8). Keep it whenever ruff actually produced
|
|
375
|
+
# findings or produced nothing at all; a clean run's config warnings
|
|
376
|
+
# stay out of the report, as they did before.
|
|
377
|
+
if run.ruff.stderr.strip() and (parsed_ruff.blocks or parsed_ruff.empty):
|
|
378
|
+
self._start_section(lines, "## Errors\n\n")
|
|
379
|
+
lines.append("```\n")
|
|
380
|
+
lines.append(run.ruff.stderr)
|
|
381
|
+
lines.append("```\n")
|
|
382
|
+
|
|
383
|
+
return lines
|
|
384
|
+
|
|
385
|
+
def _generate_error_report(self, run: LintRun, *, stamp: str) -> list[str]:
|
|
386
|
+
"""Generate the per-scope mypy error report from the single run.
|
|
387
|
+
|
|
388
|
+
``src/`` and ``tests/`` used to be two mypy invocations; now the one
|
|
389
|
+
run's output is split by path prefix. Only scopes that carry output get
|
|
390
|
+
a section -- the old report printed a section only when that scope's
|
|
391
|
+
run said something, so an empty scope never rendered an "No issues
|
|
392
|
+
found." placeholder (#170 review F7). The one run's totals stay in a
|
|
393
|
+
tail block because they cannot be attributed to either scope.
|
|
394
|
+
"""
|
|
395
|
+
error_lines = ["# MyPy Error Report\n\n"]
|
|
396
|
+
error_lines.append(f"- Generated at: {stamp}\n\n")
|
|
397
|
+
mypy_result = run.mypy
|
|
398
|
+
if mypy_result is None:
|
|
399
|
+
error_lines.append("mypy was not run.\n")
|
|
400
|
+
return error_lines
|
|
401
|
+
|
|
402
|
+
failure = run.mypy_failure
|
|
403
|
+
if failure is not None:
|
|
404
|
+
error_lines.append(f"{failure}\n\n")
|
|
405
|
+
|
|
406
|
+
grouped, tail = _split_mypy_output(mypy_result.stdout or "", run.project_root)
|
|
407
|
+
|
|
408
|
+
for scope in _MYPY_SCOPES:
|
|
409
|
+
body = grouped[scope]
|
|
410
|
+
if not body:
|
|
411
|
+
continue
|
|
412
|
+
error_lines.append(f"## MyPy Results for {scope}\n\n")
|
|
413
|
+
error_lines.append("```\n")
|
|
414
|
+
error_lines.append("\n".join(body) + "\n")
|
|
415
|
+
error_lines.append("```\n\n")
|
|
416
|
+
|
|
417
|
+
# The one run's summary ("Found N errors in M files ...") cannot be
|
|
418
|
+
# attributed to either scope; keep it rather than dropping the totals.
|
|
419
|
+
if tail:
|
|
420
|
+
error_lines.append("```\n")
|
|
421
|
+
error_lines.append("\n".join(tail) + "\n")
|
|
422
|
+
error_lines.append("```\n")
|
|
423
|
+
|
|
424
|
+
if mypy_result.stderr:
|
|
425
|
+
self._start_section(error_lines, "## MyPy Errors\n\n")
|
|
426
|
+
error_lines.append("```\n")
|
|
427
|
+
error_lines.append(mypy_result.stderr)
|
|
428
|
+
error_lines.append("```\n")
|
|
429
|
+
|
|
430
|
+
return error_lines
|
|
431
|
+
|
|
432
|
+
def _write_report_file(self, file_path: Path, content: list[str]) -> None:
|
|
433
|
+
"""Write report content to the specified file."""
|
|
434
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
435
|
+
f.writelines(content)
|
|
436
|
+
|
|
437
|
+
def _generate_packaging_todo(
|
|
438
|
+
self,
|
|
439
|
+
large_files: list[tuple[Path, int]],
|
|
440
|
+
project_root: Path,
|
|
441
|
+
quiet: bool,
|
|
442
|
+
*,
|
|
443
|
+
stamp: str,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""
|
|
446
|
+
生成用于打包模块化的待办事项报告(``stamp`` 必填:一次 render 只读一次时钟)
|
|
447
|
+
"""
|
|
448
|
+
# Create todos directory in current working directory
|
|
449
|
+
todos_dir = Path("todos")
|
|
450
|
+
todos_dir.mkdir(exist_ok=True)
|
|
451
|
+
|
|
452
|
+
packaging_report_file = todos_dir / "to_packaging_modularization.md"
|
|
453
|
+
|
|
454
|
+
lines: list[str] = []
|
|
455
|
+
lines.append("# To Packaging Modularization\n\n")
|
|
456
|
+
lines.append(f"- Generated at: {stamp}\n\n")
|
|
457
|
+
lines.append("## Large Python Files (>200 lines)\n\n")
|
|
458
|
+
lines.append(
|
|
459
|
+
"These files are candidates for modularization due to their size.\n\n"
|
|
460
|
+
)
|
|
461
|
+
lines.append("| File Path | Line Count |\n")
|
|
462
|
+
lines.append("|-----------|------------|\n")
|
|
463
|
+
for file_path, line_count in large_files:
|
|
464
|
+
# 将路径转换为相对于项目根目录的路径
|
|
465
|
+
relative_path = file_path.relative_to(project_root)
|
|
466
|
+
lines.append(f"| {relative_path} | {line_count} |\n")
|
|
467
|
+
lines.append("\n")
|
|
468
|
+
|
|
469
|
+
with open(packaging_report_file, "w", encoding="utf-8") as f:
|
|
470
|
+
f.writelines(lines)
|
|
471
|
+
|
|
472
|
+
if not quiet:
|
|
473
|
+
log_info_pair(
|
|
474
|
+
f"Packaging todo list generated at {packaging_report_file}",
|
|
475
|
+
f"打包模块化待办列表已生成到 {packaging_report_file}",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _enclosing_section(
|
|
480
|
+
path: str,
|
|
481
|
+
sections: list[tuple[str, Path]],
|
|
482
|
+
project_root: Path,
|
|
483
|
+
) -> str | None:
|
|
484
|
+
"""The title of the section *path* belongs to, or ``None``.
|
|
485
|
+
|
|
486
|
+
``None`` means the finding is outside every section; the caller renders
|
|
487
|
+
those under ``.`` -- which is why a ``tests/`` finding (not one of the
|
|
488
|
+
default sections, see :meth:`LintReporter._resolve_sections`) still
|
|
489
|
+
appears in the report instead of being dropped. When sections overlap
|
|
490
|
+
(``src`` and ``src/pkg``) the longest matching prefix wins, so each
|
|
491
|
+
finding is shown once, in its most specific section. Both sides are
|
|
492
|
+
resolved for the same symlinked-root reason as :func:`display_path`.
|
|
493
|
+
"""
|
|
494
|
+
candidate = Path(path)
|
|
495
|
+
if not candidate.is_absolute():
|
|
496
|
+
candidate = project_root / candidate
|
|
497
|
+
try:
|
|
498
|
+
resolved = candidate.resolve()
|
|
499
|
+
except OSError:
|
|
500
|
+
resolved = candidate
|
|
501
|
+
|
|
502
|
+
best: tuple[str, Path] | None = None
|
|
503
|
+
for title, section in sections:
|
|
504
|
+
try:
|
|
505
|
+
section_resolved = section.resolve()
|
|
506
|
+
except OSError:
|
|
507
|
+
section_resolved = section
|
|
508
|
+
if resolved == section_resolved or section_resolved in resolved.parents:
|
|
509
|
+
if best is None or len(section_resolved.parts) > len(best[1].parts):
|
|
510
|
+
best = (title, section_resolved)
|
|
511
|
+
return None if best is None else best[0]
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
#: mypy's diagnostic prefix: ``path:line: error:``, ``path:line:col: error:``
|
|
515
|
+
#: or ``path: note:``. Diagnostic lines start at column 0; continuation lines
|
|
516
|
+
#: (wrapped message, snippet, caret) are indented, which is what keeps a source
|
|
517
|
+
#: line that happens to contain ``error:`` from being read as a new diagnostic.
|
|
518
|
+
_MYPY_DIAGNOSTIC = re.compile(
|
|
519
|
+
r"^(?P<path>\S.*?):(?:(?P<row>\d+):(?:(?P<column>\d+):)?)?\s*"
|
|
520
|
+
r"(?P<severity>error|note|warning):"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _split_mypy_output(
|
|
525
|
+
output: str, project_root: Path
|
|
526
|
+
) -> tuple[dict[str, list[str]], list[str]]:
|
|
527
|
+
"""Split one mypy run's output into the ``src/`` and ``tests/`` scopes.
|
|
528
|
+
|
|
529
|
+
mypy prints one diagnostic per prefixed line, followed by continuation
|
|
530
|
+
lines -- wrapped messages, source snippets, carets -- that carry no prefix
|
|
531
|
+
of their own. Those belong to the diagnostic above them, so the split
|
|
532
|
+
tracks the target of the last *diagnostic* line rather than classifying
|
|
533
|
+
every line on its own.
|
|
534
|
+
|
|
535
|
+
A diagnostic whose path is not under either scope (``tools/helper.py``,
|
|
536
|
+
an absolute path outside the project, or mypy's own ``mypy: error: ...``)
|
|
537
|
+
starts a tail target of its own. The previous version only looked at the
|
|
538
|
+
last scope it had seen, so such a line was appended to the *previous*
|
|
539
|
+
scope's section -- and an absolute path under ``src/`` was classified as
|
|
540
|
+
"outside", leaving both sections saying "No issues found." while the
|
|
541
|
+
finding hid in the tail (#170 review F6).
|
|
542
|
+
"""
|
|
543
|
+
grouped: dict[str, list[str]] = {scope: [] for scope in _MYPY_SCOPES}
|
|
544
|
+
tail: list[str] = []
|
|
545
|
+
current: list[str] | None = None
|
|
546
|
+
for line in (output or "").splitlines():
|
|
547
|
+
if line.startswith(("Found ", "Success:")):
|
|
548
|
+
current = tail
|
|
549
|
+
tail.append(line)
|
|
550
|
+
continue
|
|
551
|
+
match = _MYPY_DIAGNOSTIC.match(line)
|
|
552
|
+
if match is not None:
|
|
553
|
+
path = match.group("path")
|
|
554
|
+
scope = _mypy_scope(path, project_root)
|
|
555
|
+
current = tail if scope is None else grouped[scope]
|
|
556
|
+
current.append(line)
|
|
557
|
+
continue
|
|
558
|
+
if current is None:
|
|
559
|
+
tail.append(line)
|
|
560
|
+
else:
|
|
561
|
+
current.append(line)
|
|
562
|
+
return grouped, tail
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _mypy_scope(path: str, project_root: Path) -> str | None:
|
|
566
|
+
"""The scope whose path prefix covers *path*, or ``None``.
|
|
567
|
+
|
|
568
|
+
Absolute paths are expressed relative to *project_root* first (with both
|
|
569
|
+
sides resolved, so a symlinked root such as ``/tmp`` still matches), then
|
|
570
|
+
the same prefix rule the report uses applies to either form.
|
|
571
|
+
"""
|
|
572
|
+
shown = display_path(path, project_root).replace("\\", "/")
|
|
573
|
+
while shown.startswith("./"):
|
|
574
|
+
shown = shown[2:]
|
|
575
|
+
for scope in _MYPY_SCOPES:
|
|
576
|
+
name = scope.rstrip("/")
|
|
577
|
+
if shown == name or shown.startswith(scope):
|
|
578
|
+
return scope
|
|
579
|
+
return None
|