sarj-python-lint 0.80.13__tar.gz → 0.80.15__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.80.13 → sarj_python_lint-0.80.15}/PKG-INFO +1 -1
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/pyproject.toml +1 -1
- sarj_python_lint-0.80.15/src/sarj_python_lint/__init__.py +2 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_ratchet_cli.py +3 -0
- sarj_python_lint-0.80.15/src/sarj_python_lint/rules/__init__.py +3 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_registry.py +2 -2
- sarj_python_lint-0.80.15/src/sarj_python_lint/rules/no_dunder_all.py +364 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/unused_mock_setup.py +202 -58
- sarj_python_lint-0.80.13/src/sarj_python_lint/__init__.py +0 -5
- sarj_python_lint-0.80.13/src/sarj_python_lint/rules/__init__.py +0 -6
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/.gitignore +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/LICENSE +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/README.md +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.80.
|
|
3
|
+
Version: 0.80.15
|
|
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/
|
{sarj_python_lint-0.80.13 → sarj_python_lint-0.80.15}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -33,6 +33,7 @@ from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInherit
|
|
|
33
33
|
from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
34
34
|
NoCorsWildcardWithCredentials,
|
|
35
35
|
)
|
|
36
|
+
from sarj_python_lint.rules.no_dunder_all import NoDunderAll
|
|
36
37
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
37
38
|
from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
|
|
38
39
|
from sarj_python_lint.rules.no_file_level_escape_hatch_suppression import (
|
|
@@ -224,6 +225,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
224
225
|
PreferSetIsdisjoint.id: PreferSetIsdisjoint,
|
|
225
226
|
PreferSelfDocumentingConstant.id: PreferSelfDocumentingConstant,
|
|
226
227
|
NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
|
|
228
|
+
NoDunderAll.id: NoDunderAll,
|
|
227
229
|
NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
|
|
228
230
|
RedundantClassDocstring.id: RedundantClassDocstring,
|
|
229
231
|
RedundantModuleDocstring.id: RedundantModuleDocstring,
|
|
@@ -250,5 +252,3 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
250
252
|
TypedErrorReasons.id: TypedErrorReasons,
|
|
251
253
|
}
|
|
252
254
|
)
|
|
253
|
-
|
|
254
|
-
__all__ = ["REGISTRY"]
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, NamedTuple, 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._paths import is_generated
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Location(NamedTuple):
|
|
26
|
+
line: int
|
|
27
|
+
column: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
class NoDunderAll(Rule):
|
|
32
|
+
id = "no-dunder-all"
|
|
33
|
+
code = "SARJ438"
|
|
34
|
+
documentation = RuleDocumentation(
|
|
35
|
+
summary="modules should not define or mutate `__all__`",
|
|
36
|
+
rationale=(
|
|
37
|
+
"`__all__` creates a second, mutable wildcard-import surface that can drift from runtime bindings; "
|
|
38
|
+
"Sarj code uses explicit imports and private-name conventions instead."
|
|
39
|
+
),
|
|
40
|
+
remediation=(
|
|
41
|
+
"Before deleting `__all__`, replace wildcard-import consumers with explicit imports, retain each intended "
|
|
42
|
+
"binding, and prefix implementation-only bindings with `_`. Use self-aliased imports to mark intentional "
|
|
43
|
+
"re-exports to static tooling."
|
|
44
|
+
),
|
|
45
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
46
|
+
autofix=AutofixPolicy.NONE,
|
|
47
|
+
limitations=(
|
|
48
|
+
(
|
|
49
|
+
"Reads, attributes on other objects, indirect mutation through arbitrary calls, and ordinary local "
|
|
50
|
+
"variables are not reported; the rule targets module-owned bindings and provable mutations."
|
|
51
|
+
),
|
|
52
|
+
"Writes inside a function or class are reported only when that lexical scope declares `global __all__`.",
|
|
53
|
+
"Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.",
|
|
54
|
+
"Dynamic construction through `globals()`, `setattr()`, or `exec()` is not inferred.",
|
|
55
|
+
(
|
|
56
|
+
"No autofix is offered because removal can change `module.__all__` introspection, wildcard imports, "
|
|
57
|
+
"underscore-name exports, ordering, and dynamic or conditional export behavior."
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
examples=(
|
|
61
|
+
RuleExample(
|
|
62
|
+
example_id="duplicated-export-list",
|
|
63
|
+
title="Do not repeat an explicit re-export list",
|
|
64
|
+
outcome=ExampleOutcome.MATCH,
|
|
65
|
+
files=(
|
|
66
|
+
ExampleFile.python(
|
|
67
|
+
"diagnostics/__init__.py",
|
|
68
|
+
'from .models import Diagnostic as Diagnostic\n\n__all__ = ["Diagnostic"]\n',
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
focus_path=PurePosixPath("diagnostics/__init__.py"),
|
|
72
|
+
expected_count=1,
|
|
73
|
+
public=True,
|
|
74
|
+
),
|
|
75
|
+
RuleExample(
|
|
76
|
+
example_id="explicit-re-export",
|
|
77
|
+
title="Make the re-export explicit in the import",
|
|
78
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
79
|
+
files=(
|
|
80
|
+
ExampleFile.python(
|
|
81
|
+
"diagnostics/__init__.py",
|
|
82
|
+
"from .models import Diagnostic as Diagnostic\n",
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
focus_path=PurePosixPath("diagnostics/__init__.py"),
|
|
86
|
+
expected_count=0,
|
|
87
|
+
public=True,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
description = documentation.summary
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
95
|
+
if path.suffix != ".py" or "__all__" not in source or is_generated(path, source):
|
|
96
|
+
return []
|
|
97
|
+
tree = parse_or_none(path, source)
|
|
98
|
+
if tree is None:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
findings: list[Diagnostic] = []
|
|
102
|
+
for statement in _module_statements(tree.body):
|
|
103
|
+
if (location := _dunder_all_location(statement)) is None:
|
|
104
|
+
continue
|
|
105
|
+
findings.append(
|
|
106
|
+
Diagnostic(
|
|
107
|
+
path=path,
|
|
108
|
+
line=location.line,
|
|
109
|
+
col=location.column,
|
|
110
|
+
code=self.code,
|
|
111
|
+
message="Remove the module-owned `__all__` binding or mutation; use explicit imports instead.",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.code))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _module_statements(statements: list[ast.stmt]) -> list[ast.stmt]:
|
|
118
|
+
result: list[ast.stmt] = []
|
|
119
|
+
pending = list(reversed(statements))
|
|
120
|
+
while pending:
|
|
121
|
+
statement = pending.pop()
|
|
122
|
+
result.append(statement)
|
|
123
|
+
nested = _module_control_flow_bodies(statement)
|
|
124
|
+
pending.extend(reversed(nested))
|
|
125
|
+
return result
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _module_control_flow_bodies(statement: ast.stmt) -> list[ast.stmt]:
|
|
129
|
+
match statement:
|
|
130
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
131
|
+
return [*statement.body, *statement.orelse]
|
|
132
|
+
case ast.With() | ast.AsyncWith():
|
|
133
|
+
return list(statement.body)
|
|
134
|
+
case ast.Try() | ast.TryStar():
|
|
135
|
+
return [
|
|
136
|
+
*statement.body,
|
|
137
|
+
*(nested for handler in statement.handlers for nested in handler.body),
|
|
138
|
+
*statement.orelse,
|
|
139
|
+
*statement.finalbody,
|
|
140
|
+
]
|
|
141
|
+
case ast.Match():
|
|
142
|
+
return [nested for match_case in statement.cases for nested in match_case.body]
|
|
143
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() if _declares_global_dunder_all(statement.body):
|
|
144
|
+
return list(statement.body)
|
|
145
|
+
case _:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dunder_all_location(statement: ast.stmt) -> _Location | None:
|
|
150
|
+
visitor = _DunderAllVisitor()
|
|
151
|
+
visitor.visit(statement)
|
|
152
|
+
return min(visitor.locations, default=None)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_MUTATING_METHODS = frozenset({"append", "clear", "extend", "insert", "pop", "remove", "reverse", "sort"})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class _ScopePruningVisitor(ast.NodeVisitor):
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
self.locations: list[_Location] = []
|
|
161
|
+
|
|
162
|
+
def _record(self, node: ast.expr | ast.stmt | ast.alias | ast.ExceptHandler | ast.pattern) -> None:
|
|
163
|
+
self.locations.append(_Location(node.lineno, node.col_offset + 1))
|
|
164
|
+
|
|
165
|
+
@override
|
|
166
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
167
|
+
if node.id == "__all__" and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
168
|
+
self._record(node)
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
def visit_alias(self, node: ast.alias) -> None:
|
|
172
|
+
bound_name = node.asname or node.name.split(".", maxsplit=1)[0]
|
|
173
|
+
if bound_name == "__all__":
|
|
174
|
+
self._record(node)
|
|
175
|
+
|
|
176
|
+
@override
|
|
177
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
178
|
+
if node.name == "__all__":
|
|
179
|
+
self._record(node)
|
|
180
|
+
self._visit_definition_header(node)
|
|
181
|
+
|
|
182
|
+
@override
|
|
183
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
184
|
+
if node.name == "__all__":
|
|
185
|
+
self._record(node)
|
|
186
|
+
self._visit_definition_header(node)
|
|
187
|
+
|
|
188
|
+
def _visit_definition_header(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
189
|
+
for decorator in node.decorator_list:
|
|
190
|
+
self.visit(decorator)
|
|
191
|
+
for default in (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None)):
|
|
192
|
+
self.visit(default)
|
|
193
|
+
for argument in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs):
|
|
194
|
+
if argument.annotation is not None:
|
|
195
|
+
self.visit(argument.annotation)
|
|
196
|
+
if node.args.vararg is not None and node.args.vararg.annotation is not None:
|
|
197
|
+
self.visit(node.args.vararg.annotation)
|
|
198
|
+
if node.args.kwarg is not None and node.args.kwarg.annotation is not None:
|
|
199
|
+
self.visit(node.args.kwarg.annotation)
|
|
200
|
+
if node.returns is not None:
|
|
201
|
+
self.visit(node.returns)
|
|
202
|
+
for type_param in node.type_params:
|
|
203
|
+
self.visit(type_param)
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
207
|
+
if node.name == "__all__":
|
|
208
|
+
self._record(node)
|
|
209
|
+
for decorator in node.decorator_list:
|
|
210
|
+
self.visit(decorator)
|
|
211
|
+
for base in node.bases:
|
|
212
|
+
self.visit(base)
|
|
213
|
+
for keyword in node.keywords:
|
|
214
|
+
self.visit(keyword.value)
|
|
215
|
+
for type_param in node.type_params:
|
|
216
|
+
self.visit(type_param)
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
220
|
+
del node
|
|
221
|
+
|
|
222
|
+
@override
|
|
223
|
+
def visit_If(self, node: ast.If) -> None:
|
|
224
|
+
self.visit(node.test)
|
|
225
|
+
|
|
226
|
+
@override
|
|
227
|
+
def visit_For(self, node: ast.For) -> None:
|
|
228
|
+
self.visit(node.target)
|
|
229
|
+
self.visit(node.iter)
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
|
|
233
|
+
self.visit(node.target)
|
|
234
|
+
self.visit(node.iter)
|
|
235
|
+
|
|
236
|
+
@override
|
|
237
|
+
def visit_While(self, node: ast.While) -> None:
|
|
238
|
+
self.visit(node.test)
|
|
239
|
+
|
|
240
|
+
@override
|
|
241
|
+
def visit_With(self, node: ast.With) -> None:
|
|
242
|
+
self._visit_with_items(node.items)
|
|
243
|
+
|
|
244
|
+
def _visit_with_items(self, items: list[ast.withitem]) -> None:
|
|
245
|
+
for item in items:
|
|
246
|
+
self.visit(item.context_expr)
|
|
247
|
+
if item.optional_vars is not None:
|
|
248
|
+
self.visit(item.optional_vars)
|
|
249
|
+
|
|
250
|
+
@override
|
|
251
|
+
def visit_AsyncWith(self, node: ast.AsyncWith) -> None:
|
|
252
|
+
self._visit_with_items(node.items)
|
|
253
|
+
|
|
254
|
+
@override
|
|
255
|
+
def visit_Try(self, node: ast.Try) -> None:
|
|
256
|
+
self._visit_handlers(node.handlers)
|
|
257
|
+
|
|
258
|
+
@override
|
|
259
|
+
def visit_TryStar(self, node: ast.TryStar) -> None:
|
|
260
|
+
self._visit_handlers(node.handlers)
|
|
261
|
+
|
|
262
|
+
def _visit_handlers(self, handlers: list[ast.ExceptHandler]) -> None:
|
|
263
|
+
for handler in handlers:
|
|
264
|
+
if handler.type is not None:
|
|
265
|
+
self.visit(handler.type)
|
|
266
|
+
if handler.name == "__all__":
|
|
267
|
+
self._record(handler)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class _DunderAllVisitor(_ScopePruningVisitor):
|
|
271
|
+
@override
|
|
272
|
+
def visit_Match(self, node: ast.Match) -> None:
|
|
273
|
+
self.visit(node.subject)
|
|
274
|
+
for match_case in node.cases:
|
|
275
|
+
self.visit(match_case.pattern)
|
|
276
|
+
if match_case.guard is not None:
|
|
277
|
+
self.visit(match_case.guard)
|
|
278
|
+
|
|
279
|
+
@override
|
|
280
|
+
def visit_MatchAs(self, node: ast.MatchAs) -> None:
|
|
281
|
+
if node.pattern is not None:
|
|
282
|
+
self.visit(node.pattern)
|
|
283
|
+
if node.name == "__all__":
|
|
284
|
+
self._record(node)
|
|
285
|
+
|
|
286
|
+
@override
|
|
287
|
+
def visit_MatchStar(self, node: ast.MatchStar) -> None:
|
|
288
|
+
if node.name == "__all__":
|
|
289
|
+
self._record(node)
|
|
290
|
+
|
|
291
|
+
@override
|
|
292
|
+
def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
|
|
293
|
+
for key in node.keys:
|
|
294
|
+
self.visit(key)
|
|
295
|
+
for pattern in node.patterns:
|
|
296
|
+
self.visit(pattern)
|
|
297
|
+
if node.rest == "__all__":
|
|
298
|
+
self._record(node)
|
|
299
|
+
|
|
300
|
+
@override
|
|
301
|
+
def visit_Subscript(self, node: ast.Subscript) -> None:
|
|
302
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)) and _is_dunder_all_root(node.value):
|
|
303
|
+
self._record(node.value)
|
|
304
|
+
self.generic_visit(node)
|
|
305
|
+
|
|
306
|
+
@override
|
|
307
|
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
308
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)) and _is_dunder_all_root(node.value):
|
|
309
|
+
self._record(node.value)
|
|
310
|
+
self.generic_visit(node)
|
|
311
|
+
|
|
312
|
+
@override
|
|
313
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
314
|
+
match node.func:
|
|
315
|
+
case ast.Attribute(value=ast.Name(id="__all__") as owner, attr=method) if method in _MUTATING_METHODS:
|
|
316
|
+
self._record(owner)
|
|
317
|
+
case _:
|
|
318
|
+
pass
|
|
319
|
+
self.generic_visit(node)
|
|
320
|
+
|
|
321
|
+
def _visit_comprehension(self, node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp) -> None:
|
|
322
|
+
if isinstance(node, ast.DictComp):
|
|
323
|
+
self.visit(node.key)
|
|
324
|
+
self.visit(node.value)
|
|
325
|
+
else:
|
|
326
|
+
self.visit(node.elt)
|
|
327
|
+
for generator in node.generators:
|
|
328
|
+
self.visit(generator.iter)
|
|
329
|
+
for condition in generator.ifs:
|
|
330
|
+
self.visit(condition)
|
|
331
|
+
|
|
332
|
+
@override
|
|
333
|
+
def visit_ListComp(self, node: ast.ListComp) -> None:
|
|
334
|
+
self._visit_comprehension(node)
|
|
335
|
+
|
|
336
|
+
@override
|
|
337
|
+
def visit_SetComp(self, node: ast.SetComp) -> None:
|
|
338
|
+
self._visit_comprehension(node)
|
|
339
|
+
|
|
340
|
+
@override
|
|
341
|
+
def visit_DictComp(self, node: ast.DictComp) -> None:
|
|
342
|
+
self._visit_comprehension(node)
|
|
343
|
+
|
|
344
|
+
@override
|
|
345
|
+
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
|
346
|
+
self._visit_comprehension(node)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _is_dunder_all_root(node: ast.expr) -> bool:
|
|
350
|
+
while isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
351
|
+
node = node.value
|
|
352
|
+
return isinstance(node, ast.Name) and node.id == "__all__"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _declares_global_dunder_all(statements: list[ast.stmt]) -> bool:
|
|
356
|
+
pending: list[ast.AST] = list(statements)
|
|
357
|
+
while pending:
|
|
358
|
+
node = pending.pop()
|
|
359
|
+
if isinstance(node, ast.Global) and "__all__" in node.names:
|
|
360
|
+
return True
|
|
361
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
362
|
+
continue
|
|
363
|
+
pending.extend(ast.iter_child_nodes(node))
|
|
364
|
+
return False
|