sarj-python-lint 0.81.2__tar.gz → 0.81.3__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.
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/PKG-INFO +1 -1
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/pyproject.toml +1 -1
- sarj_python_lint-0.81.3/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +203 -0
- sarj_python_lint-0.81.2/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -190
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/.gitignore +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/LICENSE +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/README.md +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.81.
|
|
3
|
+
Version: 0.81.3
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
parse_or_none,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_SYS_SOURCES = frozenset({"sys"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@final
|
|
30
|
+
class NoRedundantModuleAliasExports(Rule):
|
|
31
|
+
id: str = "no-redundant-module-alias-exports"
|
|
32
|
+
code: str = "SARJ440"
|
|
33
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
34
|
+
summary="Do not replace the current module through `sys.modules`.",
|
|
35
|
+
rationale=(
|
|
36
|
+
"Replacing the current module mutates import identity at runtime in a way static tools cannot model. "
|
|
37
|
+
"It makes introspection, reloads, circular imports, state, and monkeypatch behavior surprising."
|
|
38
|
+
),
|
|
39
|
+
remediation=(
|
|
40
|
+
"Import the canonical path inside maintained code. If a compatibility module is still required, expose "
|
|
41
|
+
"its supported names with explicit same-name imports grouped by source module."
|
|
42
|
+
),
|
|
43
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
44
|
+
autofix=AutofixPolicy.NONE,
|
|
45
|
+
limitations=(
|
|
46
|
+
"Only module-scope assignments through an unconditional, unshadowed stdlib `sys` import are checked.",
|
|
47
|
+
"Assignments inside functions, classes, and `TYPE_CHECKING` branches are excluded.",
|
|
48
|
+
"Literal-key registrations, child-module registrations, method calls, reads, and arbitrary registries are excluded.",
|
|
49
|
+
"Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
|
|
50
|
+
"No autofix is offered because the intended compatibility surface and identity requirements need review.",
|
|
51
|
+
),
|
|
52
|
+
examples=(
|
|
53
|
+
RuleExample(
|
|
54
|
+
example_id="current-module-replacement",
|
|
55
|
+
title="Do not replace a compatibility module at runtime",
|
|
56
|
+
outcome=ExampleOutcome.MATCH,
|
|
57
|
+
files=(
|
|
58
|
+
ExampleFile.python(
|
|
59
|
+
"legacy/settings.py",
|
|
60
|
+
"import sys\n\n"
|
|
61
|
+
"from canonical import settings as _canonical\n\n\n"
|
|
62
|
+
"sys.modules[__name__] = _canonical\n",
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
focus_path=PurePosixPath("legacy/settings.py"),
|
|
66
|
+
expected_count=1,
|
|
67
|
+
public=True,
|
|
68
|
+
),
|
|
69
|
+
RuleExample(
|
|
70
|
+
example_id="explicit-compatibility-exports",
|
|
71
|
+
title="Expose the supported compatibility surface explicitly",
|
|
72
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
73
|
+
files=(
|
|
74
|
+
ExampleFile.python(
|
|
75
|
+
"legacy/settings.py",
|
|
76
|
+
"from canonical.settings import (\n Settings as Settings,\n load as load,\n)\n",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
focus_path=PurePosixPath("legacy/settings.py"),
|
|
80
|
+
expected_count=0,
|
|
81
|
+
public=True,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
description: str = documentation.summary
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
89
|
+
if path.suffix != ".py" or "modules" not in source or "__name__" not in source or is_generated(path, source):
|
|
90
|
+
return []
|
|
91
|
+
tree = parse_or_none(path, source)
|
|
92
|
+
if tree is None:
|
|
93
|
+
return []
|
|
94
|
+
imports = ImportIndex.from_tree(tree, module_scope_only=True)
|
|
95
|
+
if not imports.builtin_is_unshadowed("__name__"):
|
|
96
|
+
return []
|
|
97
|
+
direct_sys_bindings = _direct_sys_bindings(tree)
|
|
98
|
+
findings = [
|
|
99
|
+
Diagnostic(
|
|
100
|
+
path=path,
|
|
101
|
+
line=target.lineno,
|
|
102
|
+
col=target.col_offset + 1,
|
|
103
|
+
code=self.code,
|
|
104
|
+
message=(
|
|
105
|
+
"Do not replace the current module through `sys.modules[__name__]`; expose compatibility names "
|
|
106
|
+
"with explicit imports and update maintained callers to the canonical module."
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
for statement in _module_statements(tree.body)
|
|
110
|
+
for target in _assignment_targets(statement)
|
|
111
|
+
if _is_current_module_target(target, imports, direct_sys_bindings)
|
|
112
|
+
]
|
|
113
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.code))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _direct_sys_bindings(tree: ast.Module) -> frozenset[str]:
|
|
117
|
+
bindings: set[str] = set()
|
|
118
|
+
for statement in tree.body:
|
|
119
|
+
match statement:
|
|
120
|
+
case ast.Import(names=names):
|
|
121
|
+
bindings.update(alias.asname or "sys" for alias in names if alias.name == "sys")
|
|
122
|
+
case ast.ImportFrom(module="sys", level=0, names=names):
|
|
123
|
+
bindings.update(alias.asname or "modules" for alias in names if alias.name == "modules")
|
|
124
|
+
case _:
|
|
125
|
+
pass
|
|
126
|
+
return frozenset(bindings)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _module_statements(statements: list[ast.stmt]) -> list[ast.stmt]:
|
|
130
|
+
result: list[ast.stmt] = []
|
|
131
|
+
pending = list(reversed(statements))
|
|
132
|
+
while pending:
|
|
133
|
+
statement = pending.pop()
|
|
134
|
+
result.append(statement)
|
|
135
|
+
pending.extend(reversed(_module_control_flow_bodies(statement)))
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _module_control_flow_bodies(statement: ast.stmt) -> list[ast.stmt]:
|
|
140
|
+
match statement:
|
|
141
|
+
case ast.If() if _is_type_checking_guard(statement.test):
|
|
142
|
+
return list(statement.orelse)
|
|
143
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
144
|
+
return [*statement.body, *statement.orelse]
|
|
145
|
+
case ast.With() | ast.AsyncWith():
|
|
146
|
+
return list(statement.body)
|
|
147
|
+
case ast.Try() | ast.TryStar():
|
|
148
|
+
return [
|
|
149
|
+
*statement.body,
|
|
150
|
+
*(nested for handler in statement.handlers for nested in handler.body),
|
|
151
|
+
*statement.orelse,
|
|
152
|
+
*statement.finalbody,
|
|
153
|
+
]
|
|
154
|
+
case ast.Match():
|
|
155
|
+
return [nested for match_case in statement.cases for nested in match_case.body]
|
|
156
|
+
case _:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _assignment_targets(statement: ast.stmt) -> list[ast.expr]:
|
|
161
|
+
match statement:
|
|
162
|
+
case ast.Assign(targets=targets):
|
|
163
|
+
return [nested for target in targets for nested in _nested_targets(target)]
|
|
164
|
+
case ast.AnnAssign(target=target):
|
|
165
|
+
return _nested_targets(target)
|
|
166
|
+
case _:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _nested_targets(target: ast.expr) -> list[ast.expr]:
|
|
171
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
172
|
+
return [nested for element in target.elts for nested in _nested_targets(element)]
|
|
173
|
+
return [target]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_current_module_target(
|
|
177
|
+
target: ast.expr,
|
|
178
|
+
imports: ImportIndex,
|
|
179
|
+
direct_sys_bindings: frozenset[str],
|
|
180
|
+
) -> bool:
|
|
181
|
+
match target:
|
|
182
|
+
case ast.Subscript(
|
|
183
|
+
value=value,
|
|
184
|
+
slice=ast.Name(id="__name__", ctx=ast.Load()),
|
|
185
|
+
ctx=ast.Store(),
|
|
186
|
+
):
|
|
187
|
+
root = value.value if isinstance(value, ast.Attribute) else value
|
|
188
|
+
return (
|
|
189
|
+
isinstance(root, ast.Name)
|
|
190
|
+
and root.id in direct_sys_bindings
|
|
191
|
+
and imports.resolves(value, sources=_SYS_SOURCES, symbol="modules")
|
|
192
|
+
)
|
|
193
|
+
case _:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _is_type_checking_guard(node: ast.expr) -> bool:
|
|
198
|
+
return (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or (
|
|
199
|
+
isinstance(node, ast.Attribute)
|
|
200
|
+
and isinstance(node.value, ast.Name)
|
|
201
|
+
and node.value.id in {"typing", "typing_extensions"}
|
|
202
|
+
and node.attr == "TYPE_CHECKING"
|
|
203
|
+
)
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
from pathlib import PurePosixPath
|
|
5
|
-
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
-
|
|
7
|
-
from sarj_python_lint.rule_base import (
|
|
8
|
-
AutofixPolicy,
|
|
9
|
-
Diagnostic,
|
|
10
|
-
ExampleFile,
|
|
11
|
-
ExampleOutcome,
|
|
12
|
-
Rule,
|
|
13
|
-
RuleCategory,
|
|
14
|
-
RuleDocumentation,
|
|
15
|
-
RuleExample,
|
|
16
|
-
Severity,
|
|
17
|
-
parse_or_none,
|
|
18
|
-
)
|
|
19
|
-
from sarj_python_lint.rules._paths import is_generated
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
_MIN_ALIAS_STATEMENTS = 4
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@final
|
|
30
|
-
class NoRedundantModuleAliasExports(Rule):
|
|
31
|
-
id: str = "no-redundant-module-alias-exports"
|
|
32
|
-
code: str = "SARJ440"
|
|
33
|
-
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
34
|
-
summary="Do not copy names before replacing a compatibility module.",
|
|
35
|
-
rationale=(
|
|
36
|
-
"A compatibility module that replaces itself in `sys.modules` already exposes the canonical module. "
|
|
37
|
-
"Copying every public attribute first duplicates the API surface and invites drift."
|
|
38
|
-
),
|
|
39
|
-
remediation=(
|
|
40
|
-
"Delete the forwarding assignments and keep the canonical module import plus the final "
|
|
41
|
-
"`sys.modules[__name__] = alias` replacement. Import the canonical path inside maintained code."
|
|
42
|
-
),
|
|
43
|
-
category=RuleCategory.MAINTAINABILITY,
|
|
44
|
-
autofix=AutofixPolicy.NONE,
|
|
45
|
-
limitations=(
|
|
46
|
-
"Only otherwise-pure compatibility modules with a final, unconditional self-replacement are checked.",
|
|
47
|
-
"Forwarding must use exact same-name assignments such as `Thing = canonical.Thing`.",
|
|
48
|
-
"Conditional, nested, computed, annotated, chained, destructured, or mixed-purpose modules are excluded.",
|
|
49
|
-
"Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
|
|
50
|
-
"No autofix is offered because attribute access can have side effects and circular imports require review.",
|
|
51
|
-
),
|
|
52
|
-
examples=(
|
|
53
|
-
RuleExample(
|
|
54
|
-
example_id="self-replacing-compatibility-module",
|
|
55
|
-
title="Remove bindings that the canonical module replacement supersedes",
|
|
56
|
-
outcome=ExampleOutcome.MATCH,
|
|
57
|
-
files=(
|
|
58
|
-
ExampleFile.python(
|
|
59
|
-
"legacy/settings.py",
|
|
60
|
-
"import sys\n\n"
|
|
61
|
-
"from canonical import settings as _canonical\n\n\n"
|
|
62
|
-
"Settings = _canonical.Settings\n"
|
|
63
|
-
"load = _canonical.load\n"
|
|
64
|
-
"sys.modules[__name__] = _canonical\n",
|
|
65
|
-
),
|
|
66
|
-
),
|
|
67
|
-
focus_path=PurePosixPath("legacy/settings.py"),
|
|
68
|
-
expected_count=1,
|
|
69
|
-
public=True,
|
|
70
|
-
),
|
|
71
|
-
RuleExample(
|
|
72
|
-
example_id="pure-module-alias",
|
|
73
|
-
title="Replace the compatibility module without copying its attributes",
|
|
74
|
-
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
-
files=(
|
|
76
|
-
ExampleFile.python(
|
|
77
|
-
"legacy/settings.py",
|
|
78
|
-
"import sys\n\n"
|
|
79
|
-
"from canonical import settings as _canonical\n\n\n"
|
|
80
|
-
"sys.modules[__name__] = _canonical\n",
|
|
81
|
-
),
|
|
82
|
-
),
|
|
83
|
-
focus_path=PurePosixPath("legacy/settings.py"),
|
|
84
|
-
expected_count=0,
|
|
85
|
-
public=True,
|
|
86
|
-
),
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
description: str = documentation.summary
|
|
90
|
-
|
|
91
|
-
@override
|
|
92
|
-
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
93
|
-
if path.suffix != ".py" or "sys.modules" not in source or is_generated(path, source):
|
|
94
|
-
return []
|
|
95
|
-
tree = parse_or_none(path, source)
|
|
96
|
-
if tree is None:
|
|
97
|
-
return []
|
|
98
|
-
first_forward = _redundant_forward(tree)
|
|
99
|
-
if first_forward is None:
|
|
100
|
-
return []
|
|
101
|
-
return [
|
|
102
|
-
Diagnostic(
|
|
103
|
-
path=path,
|
|
104
|
-
line=first_forward.lineno,
|
|
105
|
-
col=first_forward.col_offset + 1,
|
|
106
|
-
code=self.code,
|
|
107
|
-
severity=Severity.ERROR,
|
|
108
|
-
message=(
|
|
109
|
-
"Delete the forwarding assignments; this compatibility module already replaces itself with "
|
|
110
|
-
"the canonical module."
|
|
111
|
-
),
|
|
112
|
-
)
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _redundant_forward(tree: ast.Module) -> ast.Assign | None:
|
|
117
|
-
body = list(tree.body)
|
|
118
|
-
if body and _is_docstring(body[0]):
|
|
119
|
-
body.pop(0)
|
|
120
|
-
while body and isinstance(body[0], ast.ImportFrom) and body[0].module == "__future__":
|
|
121
|
-
body.pop(0)
|
|
122
|
-
if len(body) < _MIN_ALIAS_STATEMENTS or not _imports_sys(body[0]):
|
|
123
|
-
return None
|
|
124
|
-
alias = _canonical_alias(body[1])
|
|
125
|
-
if alias is None or not _replaces_current_module(body[-1], alias):
|
|
126
|
-
return None
|
|
127
|
-
forwards = body[2:-1]
|
|
128
|
-
if not forwards:
|
|
129
|
-
return None
|
|
130
|
-
for statement in forwards:
|
|
131
|
-
if not _is_same_name_forward(statement, alias):
|
|
132
|
-
return None
|
|
133
|
-
return forwards[0] if isinstance(forwards[0], ast.Assign) else None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _is_docstring(statement: ast.stmt) -> bool:
|
|
137
|
-
return (
|
|
138
|
-
isinstance(statement, ast.Expr)
|
|
139
|
-
and isinstance(statement.value, ast.Constant)
|
|
140
|
-
and isinstance(statement.value.value, str)
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _imports_sys(statement: ast.stmt) -> bool:
|
|
145
|
-
return (
|
|
146
|
-
isinstance(statement, ast.Import)
|
|
147
|
-
and len(statement.names) == 1
|
|
148
|
-
and statement.names[0].name == "sys"
|
|
149
|
-
and (statement.names[0].asname is None)
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _canonical_alias(statement: ast.stmt) -> str | None:
|
|
154
|
-
match statement:
|
|
155
|
-
case ast.Import(names=[ast.alias(asname=alias)]) if alias is not None:
|
|
156
|
-
return alias
|
|
157
|
-
case ast.ImportFrom(module=module, names=[ast.alias(name=name, asname=alias)]) if (
|
|
158
|
-
module is not None and name != "*" and alias is not None
|
|
159
|
-
):
|
|
160
|
-
return alias
|
|
161
|
-
case _:
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _is_same_name_forward(statement: ast.stmt, alias: str) -> bool:
|
|
166
|
-
match statement:
|
|
167
|
-
case ast.Assign(
|
|
168
|
-
targets=[ast.Name(id=target, ctx=ast.Store())],
|
|
169
|
-
value=ast.Attribute(value=ast.Name(id=owner, ctx=ast.Load()), attr=attribute, ctx=ast.Load()),
|
|
170
|
-
):
|
|
171
|
-
return owner == alias and target == attribute and target not in {"sys", "__name__", alias}
|
|
172
|
-
case _:
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _replaces_current_module(statement: ast.stmt, alias: str) -> bool:
|
|
177
|
-
match statement:
|
|
178
|
-
case ast.Assign(
|
|
179
|
-
targets=[
|
|
180
|
-
ast.Subscript(
|
|
181
|
-
value=ast.Attribute(value=ast.Name(id="sys", ctx=ast.Load()), attr="modules", ctx=ast.Load()),
|
|
182
|
-
slice=ast.Name(id="__name__", ctx=ast.Load()),
|
|
183
|
-
ctx=ast.Store(),
|
|
184
|
-
)
|
|
185
|
-
],
|
|
186
|
-
value=ast.Name(id=replacement, ctx=ast.Load()),
|
|
187
|
-
):
|
|
188
|
-
return replacement == alias
|
|
189
|
-
case _:
|
|
190
|
-
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.2 → sarj_python_lint-0.81.3}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|