sarj-python-lint 0.80.6__tar.gz → 0.80.10__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.6 → sarj_python_lint-0.80.10}/PKG-INFO +1 -1
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/pyproject.toml +1 -1
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/_fixed_record.py +146 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/_imports.py +177 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_prose_budget.py +7 -1
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_registry.py +8 -16
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_sql.py +17 -1
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +75 -2
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/iac_source_coupled_test.py +14 -5
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +3 -12
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/mock_without_spec.py +3 -7
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +2 -3
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +7 -14
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +913 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +804 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +912 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_redundant_literal_description.py +1 -3
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_repeated_string_literal.py +12 -4
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +25 -22
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +4 -4
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/no_vague_suppression_description.py +138 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +298 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/over_mocked_test.py +214 -51
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/phase_label_comment.py +29 -16
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_class_row.py +430 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +508 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +244 -87
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +130 -26
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_library_fake.py +251 -89
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_match_assert_never.py +426 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +439 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_module_level_constant.py +254 -110
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +202 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_nominal_id_types.py +546 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +332 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_or_pattern.py +72 -34
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +206 -34
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_self_type_annotation.py +649 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_str_enum.py +298 -128
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +366 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +265 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +508 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +253 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/production_derived_test_cases.py +162 -44
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/pydantic_at_boundaries.py +299 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/redundant_class_docstring.py +204 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/redundant_docstring.py +261 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/redundant_module_docstring.py +79 -41
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/repeated_static_call_cases.py +512 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +219 -18
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +151 -51
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/require_port_for_service.py +250 -51
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1208 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +615 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/restated_test_docstring.py +548 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +315 -0
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/_imports.py +0 -96
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -107
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -209
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_class_row.py +0 -231
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -332
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -440
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -978
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -493
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -475
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -383
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -175
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -250
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -131
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -669
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -171
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -298
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -149
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -138
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/preserve_enum_types.py +0 -131
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -374
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -121
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/redundant_docstring.py +0 -171
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -305
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -601
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -166
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -159
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/restated_test_docstring.py +0 -248
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/source_coupled_test.py +0 -519
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -202
- sarj_python_lint-0.80.6/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -148
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/.gitignore +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/LICENSE +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/README.md +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/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.80.
|
|
3
|
+
Version: 0.80.10
|
|
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,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from sarj_python_lint.rules._ast_index import children
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_RECORD_MUTATOR_METHODS = frozenset({"clear", "pop", "popitem", "setdefault", "update"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def builds_fixed_record(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
12
|
+
returned: list[ast.expr] = []
|
|
13
|
+
record_names = {
|
|
14
|
+
target.id
|
|
15
|
+
for current in _owned_nodes(node)
|
|
16
|
+
if isinstance(current, ast.Assign) and _is_record_literal(current.value)
|
|
17
|
+
for target in current.targets
|
|
18
|
+
if isinstance(target, ast.Name)
|
|
19
|
+
}
|
|
20
|
+
record_names.update(
|
|
21
|
+
current.target.id
|
|
22
|
+
for current in _owned_nodes(node)
|
|
23
|
+
if isinstance(current, ast.AnnAssign)
|
|
24
|
+
and isinstance(current.target, ast.Name)
|
|
25
|
+
and current.value is not None
|
|
26
|
+
and _is_record_literal(current.value)
|
|
27
|
+
)
|
|
28
|
+
invalidated_names: set[str] = set()
|
|
29
|
+
aliases: dict[str, str] = {}
|
|
30
|
+
stack: list[ast.AST] = list(node.body)
|
|
31
|
+
while stack:
|
|
32
|
+
current = stack.pop()
|
|
33
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
34
|
+
continue
|
|
35
|
+
if isinstance(current, ast.Return) and current.value is not None:
|
|
36
|
+
returned.append(current.value)
|
|
37
|
+
elif isinstance(current, ast.Assign) and _is_record_literal(current.value):
|
|
38
|
+
for target in current.targets:
|
|
39
|
+
if isinstance(target, ast.Subscript):
|
|
40
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
41
|
+
elif isinstance(current, ast.Assign):
|
|
42
|
+
if isinstance(current.value, ast.Name):
|
|
43
|
+
for target in current.targets:
|
|
44
|
+
if not isinstance(target, ast.Name):
|
|
45
|
+
continue
|
|
46
|
+
if target.id in record_names:
|
|
47
|
+
invalidated_names.add(target.id)
|
|
48
|
+
else:
|
|
49
|
+
aliases[target.id] = current.value.id
|
|
50
|
+
else:
|
|
51
|
+
invalidated_names.update(target.id for target in current.targets if isinstance(target, ast.Name))
|
|
52
|
+
for target in current.targets:
|
|
53
|
+
if isinstance(target, ast.Subscript):
|
|
54
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
55
|
+
elif (
|
|
56
|
+
isinstance(current, ast.AnnAssign)
|
|
57
|
+
and isinstance(current.target, ast.Name)
|
|
58
|
+
and current.value is not None
|
|
59
|
+
and _is_record_literal(current.value)
|
|
60
|
+
):
|
|
61
|
+
pass
|
|
62
|
+
elif (
|
|
63
|
+
isinstance(current, ast.AnnAssign)
|
|
64
|
+
and isinstance(current.target, ast.Name)
|
|
65
|
+
and isinstance(current.value, ast.Name)
|
|
66
|
+
):
|
|
67
|
+
if current.target.id in record_names:
|
|
68
|
+
invalidated_names.add(current.target.id)
|
|
69
|
+
else:
|
|
70
|
+
aliases[current.target.id] = current.value.id
|
|
71
|
+
elif isinstance(current, (ast.AnnAssign, ast.AugAssign)):
|
|
72
|
+
invalidated_names.update(_mutated_record_roots(current.target))
|
|
73
|
+
elif isinstance(current, ast.Delete):
|
|
74
|
+
for target in current.targets:
|
|
75
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
76
|
+
elif (
|
|
77
|
+
isinstance(current, ast.Call)
|
|
78
|
+
and isinstance(current.func, ast.Attribute)
|
|
79
|
+
and current.func.attr in _RECORD_MUTATOR_METHODS
|
|
80
|
+
and isinstance(current.func.value, ast.Name)
|
|
81
|
+
):
|
|
82
|
+
invalidated_names.add(current.func.value.id)
|
|
83
|
+
elif isinstance(current, ast.Call):
|
|
84
|
+
invalidated_names.update(
|
|
85
|
+
argument.id
|
|
86
|
+
for argument in (*current.args, *(keyword.value for keyword in current.keywords))
|
|
87
|
+
if isinstance(argument, ast.Name)
|
|
88
|
+
)
|
|
89
|
+
stack.extend(children(current))
|
|
90
|
+
|
|
91
|
+
changed = True
|
|
92
|
+
while changed:
|
|
93
|
+
changed = False
|
|
94
|
+
for alias, original in aliases.items():
|
|
95
|
+
if alias in invalidated_names and original not in invalidated_names:
|
|
96
|
+
invalidated_names.add(original)
|
|
97
|
+
changed = True
|
|
98
|
+
if original in invalidated_names and alias not in invalidated_names:
|
|
99
|
+
invalidated_names.add(alias)
|
|
100
|
+
changed = True
|
|
101
|
+
|
|
102
|
+
def original_name(name: str) -> str:
|
|
103
|
+
seen: set[str] = set()
|
|
104
|
+
while name in aliases and name not in seen:
|
|
105
|
+
seen.add(name)
|
|
106
|
+
name = aliases[name]
|
|
107
|
+
return name
|
|
108
|
+
|
|
109
|
+
intact_record_names = record_names - invalidated_names
|
|
110
|
+
if any(
|
|
111
|
+
isinstance(value, ast.Name) and original_name(value.id) in record_names & invalidated_names
|
|
112
|
+
for value in returned
|
|
113
|
+
):
|
|
114
|
+
return False
|
|
115
|
+
return any(
|
|
116
|
+
_is_record_literal(value) or (isinstance(value, ast.Name) and original_name(value.id) in intact_record_names)
|
|
117
|
+
for value in returned
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _owned_nodes(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.AST]:
|
|
122
|
+
owned: list[ast.AST] = []
|
|
123
|
+
stack: list[ast.AST] = list(node.body)
|
|
124
|
+
while stack:
|
|
125
|
+
current = stack.pop()
|
|
126
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
127
|
+
continue
|
|
128
|
+
owned.append(current)
|
|
129
|
+
stack.extend(children(current))
|
|
130
|
+
return owned
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _is_record_literal(node: ast.expr) -> bool:
|
|
134
|
+
if isinstance(node, ast.List):
|
|
135
|
+
return bool(node.elts) and all(_is_record_literal(element) for element in node.elts)
|
|
136
|
+
if isinstance(node, ast.ListComp):
|
|
137
|
+
return _is_record_literal(node.elt)
|
|
138
|
+
if not isinstance(node, ast.Dict):
|
|
139
|
+
return False
|
|
140
|
+
return bool(node.keys) and all(isinstance(key, ast.Constant) and isinstance(key.value, str) for key in node.keys)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _mutated_record_roots(target: ast.AST) -> set[str]:
|
|
144
|
+
while isinstance(target, ast.Subscript):
|
|
145
|
+
target = target.value
|
|
146
|
+
return {target.id} if isinstance(target, ast.Name) else set()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class _ImportTarget:
|
|
12
|
+
module: str
|
|
13
|
+
symbol: str | None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ImportIndex:
|
|
18
|
+
bindings: Mapping[str, _ImportTarget]
|
|
19
|
+
shadowed_names: frozenset[str]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_tree(cls, tree: ast.Module, *, module_scope_only: bool = False) -> Self:
|
|
23
|
+
candidates: dict[str, set[_ImportTarget]] = {}
|
|
24
|
+
statements = _module_import_statements(tree) if module_scope_only else tree.body
|
|
25
|
+
for statement in statements:
|
|
26
|
+
match statement:
|
|
27
|
+
case ast.Import(names=names):
|
|
28
|
+
for alias in names:
|
|
29
|
+
local = alias.asname or alias.name.partition(".")[0]
|
|
30
|
+
module = alias.name if alias.asname else alias.name.partition(".")[0]
|
|
31
|
+
candidates.setdefault(local, set()).add(_ImportTarget(module, None))
|
|
32
|
+
case ast.ImportFrom(module=str(module), names=names, level=0):
|
|
33
|
+
for alias in names:
|
|
34
|
+
if alias.name == "*":
|
|
35
|
+
continue
|
|
36
|
+
local = alias.asname or alias.name
|
|
37
|
+
candidates.setdefault(local, set()).add(_ImportTarget(module, alias.name))
|
|
38
|
+
case _:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
imported_names = frozenset(candidates)
|
|
42
|
+
non_import_bindings = _module_non_import_bindings(tree) if module_scope_only else _non_import_bindings(tree)
|
|
43
|
+
rebound = non_import_bindings & imported_names
|
|
44
|
+
bindings = {
|
|
45
|
+
local: next(iter(targets))
|
|
46
|
+
for local, targets in candidates.items()
|
|
47
|
+
if len(targets) == 1 and local not in rebound
|
|
48
|
+
}
|
|
49
|
+
return cls(MappingProxyType(bindings), frozenset(non_import_bindings))
|
|
50
|
+
|
|
51
|
+
def resolves(self, node: ast.expr, *, sources: frozenset[str], symbol: str) -> bool:
|
|
52
|
+
resolved = self._resolve(node)
|
|
53
|
+
return resolved is not None and resolved.module in sources and resolved.symbol == symbol
|
|
54
|
+
|
|
55
|
+
def resolved_symbol(self, node: ast.expr, *, sources: frozenset[str]) -> str | None:
|
|
56
|
+
resolved = self._resolve(node)
|
|
57
|
+
if resolved is None or resolved.module not in sources:
|
|
58
|
+
return None
|
|
59
|
+
return resolved.symbol
|
|
60
|
+
|
|
61
|
+
def builtin_is_unshadowed(self, name: str) -> bool:
|
|
62
|
+
return name not in self.shadowed_names and name not in self.bindings
|
|
63
|
+
|
|
64
|
+
def _resolve(self, node: ast.expr) -> _ImportTarget | None:
|
|
65
|
+
match node:
|
|
66
|
+
case ast.Name(id=local):
|
|
67
|
+
target = self.bindings.get(local)
|
|
68
|
+
return target if target is not None and target.symbol is not None else None
|
|
69
|
+
case ast.Attribute():
|
|
70
|
+
parts: list[str] = []
|
|
71
|
+
current: ast.expr = node
|
|
72
|
+
while isinstance(current, ast.Attribute):
|
|
73
|
+
parts.append(current.attr)
|
|
74
|
+
current = current.value
|
|
75
|
+
if not isinstance(current, ast.Name):
|
|
76
|
+
return None
|
|
77
|
+
target = self.bindings.get(current.id)
|
|
78
|
+
if target is None or target.symbol is not None or not parts:
|
|
79
|
+
return None
|
|
80
|
+
ordered = list(reversed(parts))
|
|
81
|
+
return _ImportTarget(".".join((target.module, *ordered[:-1])), ordered[-1])
|
|
82
|
+
case _:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _non_import_bindings(tree: ast.Module) -> set[str]:
|
|
87
|
+
names: set[str] = set()
|
|
88
|
+
for node in ast.walk(tree):
|
|
89
|
+
match node:
|
|
90
|
+
case ast.Name(id=name, ctx=(ast.Store() | ast.Del())) | ast.arg(arg=name):
|
|
91
|
+
names.add(name)
|
|
92
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
93
|
+
names.add(node.name)
|
|
94
|
+
case ast.alias():
|
|
95
|
+
# Imports are the candidate bindings, not shadows of themselves.
|
|
96
|
+
continue
|
|
97
|
+
case _:
|
|
98
|
+
pass
|
|
99
|
+
return names
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _module_non_import_bindings(tree: ast.Module) -> set[str]:
|
|
103
|
+
names: set[str] = set()
|
|
104
|
+
for statement in tree.body:
|
|
105
|
+
match statement:
|
|
106
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
107
|
+
names.add(statement.name)
|
|
108
|
+
case ast.Assign(targets=targets):
|
|
109
|
+
for target in targets:
|
|
110
|
+
names.update(_target_names(target))
|
|
111
|
+
case ast.AnnAssign(target=target) | ast.AugAssign(target=target):
|
|
112
|
+
names.update(_target_names(target))
|
|
113
|
+
case ast.TypeAlias(name=ast.Name(id=name)):
|
|
114
|
+
names.add(name)
|
|
115
|
+
case ast.Import() | ast.ImportFrom():
|
|
116
|
+
continue
|
|
117
|
+
case _:
|
|
118
|
+
# Module control flow can still bind names in the module namespace.
|
|
119
|
+
_collect_module_bindings(statement, names)
|
|
120
|
+
return names
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _target_names(node: ast.expr) -> set[str]:
|
|
124
|
+
if isinstance(node, ast.Name):
|
|
125
|
+
return {node.id}
|
|
126
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
127
|
+
return {name for element in node.elts for name in _target_names(element)}
|
|
128
|
+
return set()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _collect_module_bindings(node: ast.AST, names: set[str]) -> None:
|
|
132
|
+
match node:
|
|
133
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
134
|
+
names.add(node.name)
|
|
135
|
+
return
|
|
136
|
+
case ast.ListComp() | ast.SetComp() | ast.GeneratorExp():
|
|
137
|
+
_collect_module_bindings(node.elt, names)
|
|
138
|
+
_collect_comprehension_bindings(node.generators, names)
|
|
139
|
+
return
|
|
140
|
+
case ast.DictComp():
|
|
141
|
+
_collect_module_bindings(node.key, names)
|
|
142
|
+
_collect_module_bindings(node.value, names)
|
|
143
|
+
_collect_comprehension_bindings(node.generators, names)
|
|
144
|
+
return
|
|
145
|
+
case ast.Lambda():
|
|
146
|
+
return
|
|
147
|
+
case ast.Name(id=name, ctx=(ast.Store() | ast.Del())):
|
|
148
|
+
names.add(name)
|
|
149
|
+
case _:
|
|
150
|
+
pass
|
|
151
|
+
for child in ast.iter_child_nodes(node):
|
|
152
|
+
_collect_module_bindings(child, names)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _collect_comprehension_bindings(generators: list[ast.comprehension], names: set[str]) -> None:
|
|
156
|
+
for generator in generators:
|
|
157
|
+
_collect_module_bindings(generator.iter, names)
|
|
158
|
+
for condition in generator.ifs:
|
|
159
|
+
_collect_module_bindings(condition, names)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _module_import_statements(tree: ast.Module) -> list[ast.stmt]:
|
|
163
|
+
statements = list(tree.body)
|
|
164
|
+
for statement in tree.body:
|
|
165
|
+
if not isinstance(statement, ast.If) or not _is_type_checking_guard(statement.test):
|
|
166
|
+
continue
|
|
167
|
+
statements.extend(child for child in statement.body if isinstance(child, (ast.Import, ast.ImportFrom)))
|
|
168
|
+
return statements
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_type_checking_guard(node: ast.expr) -> bool:
|
|
172
|
+
return (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or (
|
|
173
|
+
isinstance(node, ast.Attribute)
|
|
174
|
+
and isinstance(node.value, ast.Name)
|
|
175
|
+
and node.value.id in {"typing", "typing_extensions"}
|
|
176
|
+
and node.attr == "TYPE_CHECKING"
|
|
177
|
+
)
|
{sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
@@ -428,7 +428,13 @@ def _yielded_type(node: ast.expr) -> str | None:
|
|
|
428
428
|
node = parsed
|
|
429
429
|
if not isinstance(node, ast.Subscript):
|
|
430
430
|
return None
|
|
431
|
-
name =
|
|
431
|
+
name = (
|
|
432
|
+
node.value.attr
|
|
433
|
+
if isinstance(node.value, ast.Attribute)
|
|
434
|
+
else node.value.id
|
|
435
|
+
if isinstance(node.value, ast.Name)
|
|
436
|
+
else ""
|
|
437
|
+
)
|
|
432
438
|
if name not in {"AsyncGenerator", "AsyncIterable", "AsyncIterator", "Generator", "Iterable", "Iterator"}:
|
|
433
439
|
return None
|
|
434
440
|
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
{sarj_python_lint-0.80.6 → sarj_python_lint-0.80.10}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -50,8 +50,13 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
|
50
50
|
)
|
|
51
51
|
from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
|
|
52
52
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
53
|
+
from sarj_python_lint.rules.no_positional_psycopg_row_escape import NoPositionalPsycopgRowEscape
|
|
54
|
+
from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import (
|
|
55
|
+
NoPsycopgExecutionOutsideInjectedOwner,
|
|
56
|
+
)
|
|
53
57
|
from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
|
|
54
58
|
from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
|
|
59
|
+
from sarj_python_lint.rules.no_raw_source_text_test_oracle import NoRawSourceTextTestOracle
|
|
55
60
|
from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
|
|
56
61
|
from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
|
|
57
62
|
from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
|
|
@@ -96,7 +101,6 @@ from sarj_python_lint.rules.prefer_nominal_id_types import PreferNominalIdTypes
|
|
|
96
101
|
from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
97
102
|
PreferNonNullableCollection,
|
|
98
103
|
)
|
|
99
|
-
from sarj_python_lint.rules.prefer_one_for_required_row import PreferOneForRequiredRow
|
|
100
104
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
101
105
|
from sarj_python_lint.rules.prefer_self_documenting_constant import (
|
|
102
106
|
PreferSelfDocumentingConstant,
|
|
@@ -107,17 +111,12 @@ from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
|
107
111
|
from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
108
112
|
PreferStructOverNamedtuple,
|
|
109
113
|
)
|
|
110
|
-
from sarj_python_lint.rules.prefer_timedelta_for_durations import (
|
|
111
|
-
PreferTimedeltaForDurations,
|
|
112
|
-
)
|
|
113
114
|
from sarj_python_lint.rules.prefer_walrus_awaited_none_guard import PreferWalrusAwaitedNoneGuard
|
|
114
115
|
from sarj_python_lint.rules.prefer_walrus_comprehension_filter import (
|
|
115
116
|
PreferWalrusComprehensionFilter,
|
|
116
117
|
)
|
|
117
118
|
from sarj_python_lint.rules.prefer_walrus_regex_match import PreferWalrusRegexMatch
|
|
118
119
|
from sarj_python_lint.rules.prefer_walrus_stream_loop import PreferWalrusStreamLoop
|
|
119
|
-
from sarj_python_lint.rules.preserve_declared_nominal_id import PreserveDeclaredNominalId
|
|
120
|
-
from sarj_python_lint.rules.preserve_enum_types import PreserveEnumTypes
|
|
121
120
|
from sarj_python_lint.rules.production_derived_test_cases import ProductionDerivedTestCases
|
|
122
121
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
123
122
|
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import PytestFixtureReturnsBareTuple
|
|
@@ -139,10 +138,7 @@ from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
|
139
138
|
from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
|
|
140
139
|
RequirePydanticOrdinalLowerBound,
|
|
141
140
|
)
|
|
142
|
-
from sarj_python_lint.rules.require_validated_row_factory import RequireValidatedRowFactory
|
|
143
141
|
from sarj_python_lint.rules.restated_test_docstring import RestatedTestDocstring
|
|
144
|
-
from sarj_python_lint.rules.source_coupled_test import SourceCoupledTest
|
|
145
|
-
from sarj_python_lint.rules.sql_requires_injected_pool_owner import SqlRequiresInjectedPoolOwner
|
|
146
142
|
from sarj_python_lint.rules.stepdown import Stepdown
|
|
147
143
|
from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDelegatesToBulkRead
|
|
148
144
|
from sarj_python_lint.rules.store_insert_requires_on_conflict import (
|
|
@@ -179,12 +175,11 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
179
175
|
FakesInSharedLocation.id: FakesInSharedLocation,
|
|
180
176
|
PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
|
|
181
177
|
NoSecretInLog.id: NoSecretInLog,
|
|
182
|
-
PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
|
|
183
178
|
PreferStructOverNamedtuple.id: PreferStructOverNamedtuple,
|
|
184
179
|
NoCommentCruft.id: NoCommentCruft,
|
|
185
180
|
NoConftestTestModuleImport.id: NoConftestTestModuleImport,
|
|
186
181
|
StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
|
|
187
|
-
|
|
182
|
+
NoRawSourceTextTestOracle.id: NoRawSourceTextTestOracle,
|
|
188
183
|
IacSourceCoupledTest.id: IacSourceCoupledTest,
|
|
189
184
|
NoRawConnectionInTests.id: NoRawConnectionInTests,
|
|
190
185
|
NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
|
|
@@ -201,7 +196,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
201
196
|
OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
|
|
202
197
|
PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
|
|
203
198
|
StoreGetDelegatesToBulkRead.id: StoreGetDelegatesToBulkRead,
|
|
204
|
-
PreferOneForRequiredRow.id: PreferOneForRequiredRow,
|
|
205
199
|
RepeatedKwargHeavyCallInTest.id: RepeatedKwargHeavyCallInTest,
|
|
206
200
|
DefectXfailRequiresExplicitStrict.id: DefectXfailRequiresExplicitStrict,
|
|
207
201
|
NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
|
|
@@ -249,10 +243,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
249
243
|
ProductionDerivedTestCases.id: ProductionDerivedTestCases,
|
|
250
244
|
UncontrolledRandomnessInTest.id: UncontrolledRandomnessInTest,
|
|
251
245
|
RepeatedStaticCallCases.id: RepeatedStaticCallCases,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
PreserveDeclaredNominalId.id: PreserveDeclaredNominalId,
|
|
255
|
-
PreserveEnumTypes.id: PreserveEnumTypes,
|
|
246
|
+
NoPositionalPsycopgRowEscape.id: NoPositionalPsycopgRowEscape,
|
|
247
|
+
NoPsycopgExecutionOutsideInjectedOwner.id: NoPsycopgExecutionOutsideInjectedOwner,
|
|
256
248
|
RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
|
|
257
249
|
RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
|
|
258
250
|
NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
|
|
@@ -39,12 +39,28 @@ def sql_string_value(node: ast.expr, *, interpolation_placeholder: str = " ") ->
|
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def strip_sql_noise(
|
|
42
|
+
def strip_sql_noise(
|
|
43
|
+
text: str,
|
|
44
|
+
*,
|
|
45
|
+
mask_dollar_quotes: bool = True,
|
|
46
|
+
mask_double_quotes: bool = True,
|
|
47
|
+
) -> str:
|
|
43
48
|
out = list(text)
|
|
44
49
|
n = len(text)
|
|
45
50
|
i = 0
|
|
46
51
|
while i < n:
|
|
47
52
|
ch = text[i]
|
|
53
|
+
if ch == '"' and not mask_double_quotes:
|
|
54
|
+
i += 1
|
|
55
|
+
while i < n:
|
|
56
|
+
if text[i] == '"':
|
|
57
|
+
i += 1
|
|
58
|
+
if i < n and text[i] == '"':
|
|
59
|
+
i += 1
|
|
60
|
+
continue
|
|
61
|
+
break
|
|
62
|
+
i += 1
|
|
63
|
+
continue
|
|
48
64
|
if ch in {"'", '"'}:
|
|
49
65
|
out[i] = " "
|
|
50
66
|
i += 1
|
|
@@ -26,6 +26,7 @@ from sarj_python_lint.rules._fastapi import (
|
|
|
26
26
|
Route,
|
|
27
27
|
flat_name,
|
|
28
28
|
)
|
|
29
|
+
from sarj_python_lint.rules._fixed_record import builds_fixed_record
|
|
29
30
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
30
31
|
|
|
31
32
|
|
|
@@ -152,6 +153,42 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
152
153
|
scenario="schema-integrity",
|
|
153
154
|
public=True,
|
|
154
155
|
),
|
|
156
|
+
RuleExample(
|
|
157
|
+
example_id="unannotated-record-response",
|
|
158
|
+
title="Unnamed record leaves the response schema implicit",
|
|
159
|
+
outcome=ExampleOutcome.MATCH,
|
|
160
|
+
files=(
|
|
161
|
+
ExampleFile.python(
|
|
162
|
+
"api.py",
|
|
163
|
+
"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
|
|
164
|
+
"@router.get('/health', status_code=200)\n"
|
|
165
|
+
"async def health():\n return {'status': 'ok'}\n",
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
focus_path=PurePosixPath("api.py"),
|
|
169
|
+
expected_count=1,
|
|
170
|
+
scenario="unannotated-record",
|
|
171
|
+
public=True,
|
|
172
|
+
),
|
|
173
|
+
RuleExample(
|
|
174
|
+
example_id="named-record-response",
|
|
175
|
+
title="Named response type documents the record",
|
|
176
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
177
|
+
files=(
|
|
178
|
+
ExampleFile.python(
|
|
179
|
+
"api.py",
|
|
180
|
+
"from fastapi import APIRouter\nfrom pydantic import BaseModel\n\n"
|
|
181
|
+
"class HealthResponse(BaseModel):\n status: str\n\n"
|
|
182
|
+
"router = APIRouter()\n\n@router.get('/health', status_code=200)\n"
|
|
183
|
+
"async def health() -> HealthResponse:\n"
|
|
184
|
+
" return HealthResponse(status='ok')\n",
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
focus_path=PurePosixPath("api.py"),
|
|
188
|
+
expected_count=0,
|
|
189
|
+
scenario="unannotated-record",
|
|
190
|
+
public=True,
|
|
191
|
+
),
|
|
155
192
|
),
|
|
156
193
|
)
|
|
157
194
|
description: str = documentation.summary
|
|
@@ -386,10 +423,26 @@ def _check_return(
|
|
|
386
423
|
) -> list[_Finding]:
|
|
387
424
|
annotation = index.resolve_annotation(function.returns)
|
|
388
425
|
if annotation is None:
|
|
426
|
+
if not builds_fixed_record(function):
|
|
427
|
+
return []
|
|
428
|
+
for route in _operations(routes):
|
|
429
|
+
if route.has_unpack:
|
|
430
|
+
continue
|
|
431
|
+
response_model = route.keywords.get("response_model")
|
|
432
|
+
status = _status_code(route.keywords.get("status_code"), index) or HTTPStatus.OK
|
|
433
|
+
if _concrete_response_model(response_model, index) or any(
|
|
434
|
+
_documents_response_model(node, status, index) for node in _route_response_nodes(route)
|
|
435
|
+
):
|
|
436
|
+
continue
|
|
437
|
+
return [
|
|
438
|
+
_Finding(
|
|
439
|
+
function,
|
|
440
|
+
"[return] fixed-shape dictionary requires a named return type or concrete response_model.",
|
|
441
|
+
)
|
|
442
|
+
]
|
|
389
443
|
return []
|
|
390
444
|
problems: set[str] = set()
|
|
391
|
-
|
|
392
|
-
problems.add("response annotation erases its OpenAPI schema; define an output model")
|
|
445
|
+
annotation_erases_schema = _schema_erasing(annotation, index)
|
|
393
446
|
is_response = index.is_response(annotation)
|
|
394
447
|
is_none = _is_none_annotation(annotation, index)
|
|
395
448
|
for route in _operations(routes):
|
|
@@ -398,6 +451,8 @@ def _check_return(
|
|
|
398
451
|
keywords = route.keywords
|
|
399
452
|
response_model = keywords.get("response_model")
|
|
400
453
|
response_model_none = response_model is not None and _literal_none(response_model)
|
|
454
|
+
if annotation_erases_schema and not _concrete_response_model(response_model, index):
|
|
455
|
+
problems.add("response annotation erases its OpenAPI schema; define an output model")
|
|
401
456
|
status = _status_code(keywords.get("status_code"), index)
|
|
402
457
|
response_class = keywords.get("response_class") or route.inherited_response_class
|
|
403
458
|
if is_response and status not in _NO_CONTENT_STATUSES and response_class is None:
|
|
@@ -464,6 +519,24 @@ def _documents_response_content(node: ast.expr | None, status: int | None, index
|
|
|
464
519
|
return False
|
|
465
520
|
|
|
466
521
|
|
|
522
|
+
def _documents_response_model(node: ast.expr | None, status: int | None, index: FastapiIndex) -> bool:
|
|
523
|
+
if not isinstance(node, ast.Dict):
|
|
524
|
+
return False
|
|
525
|
+
for key, value in zip(node.keys, node.values, strict=True):
|
|
526
|
+
if key is None or not isinstance(value, ast.Dict):
|
|
527
|
+
continue
|
|
528
|
+
if status is not None and status not in _response_codes(ast.Dict(keys=[key], values=[value]), index):
|
|
529
|
+
continue
|
|
530
|
+
for entry_key, entry_value in zip(value.keys, value.values, strict=True):
|
|
531
|
+
if (
|
|
532
|
+
isinstance(entry_key, ast.Constant)
|
|
533
|
+
and entry_key.value == "model"
|
|
534
|
+
and _concrete_response_model(entry_value, index)
|
|
535
|
+
):
|
|
536
|
+
return True
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
|
|
467
540
|
def _route_response_nodes(route: Route) -> tuple[ast.expr, ...]:
|
|
468
541
|
return tuple(node for node in (route.inherited_responses, route.keywords.get("responses")) if node is not None)
|
|
469
542
|
|
|
@@ -15,8 +15,9 @@ from sarj_python_lint.rule_base import (
|
|
|
15
15
|
Severity,
|
|
16
16
|
parse_or_none,
|
|
17
17
|
)
|
|
18
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
18
19
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
19
|
-
from sarj_python_lint.rules.
|
|
20
|
+
from sarj_python_lint.rules.no_raw_source_text_test_oracle import FunctionAnalyzer, top_level_test_functions
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
@@ -45,7 +46,7 @@ class IacSourceCoupledTest(Rule):
|
|
|
45
46
|
"The rule follows local aliases, path collections, context-managed reads, and common normalization; interprocedural flows remain unreported.",
|
|
46
47
|
"Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
|
|
47
48
|
"The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.",
|
|
48
|
-
"
|
|
49
|
+
"Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.",
|
|
49
50
|
),
|
|
50
51
|
examples=(
|
|
51
52
|
RuleExample(
|
|
@@ -72,7 +73,7 @@ class IacSourceCoupledTest(Rule):
|
|
|
72
73
|
files=(
|
|
73
74
|
ExampleFile.python(
|
|
74
75
|
"tests/test_policy.py",
|
|
75
|
-
"
|
|
76
|
+
"from pathlib import Path\n\ndef test_policy():\n source = Path('main.tf').read_text()\n assert 'prevent_destroy = true' in source\n",
|
|
76
77
|
),
|
|
77
78
|
),
|
|
78
79
|
focus_path=PurePosixPath("tests/test_policy.py"),
|
|
@@ -90,10 +91,18 @@ class IacSourceCoupledTest(Rule):
|
|
|
90
91
|
tree = parse_or_none(path, source)
|
|
91
92
|
if not isinstance(tree, ast.Module):
|
|
92
93
|
return []
|
|
94
|
+
imports = ImportIndex.from_tree(tree, module_scope_only=True)
|
|
95
|
+
source_lines = source.splitlines()
|
|
93
96
|
assertions = [
|
|
94
97
|
assertion
|
|
95
|
-
for function, unittest_style in top_level_test_functions(tree)
|
|
96
|
-
for assertion in FunctionAnalyzer(
|
|
98
|
+
for function, unittest_style in top_level_test_functions(tree, imports)
|
|
99
|
+
for assertion in FunctionAnalyzer(
|
|
100
|
+
IAC_SOURCE_SUFFIXES,
|
|
101
|
+
imports=imports,
|
|
102
|
+
suppression_code=self.code,
|
|
103
|
+
suppression_lines=source_lines,
|
|
104
|
+
unittest_style=unittest_style,
|
|
105
|
+
).analyze(function)
|
|
97
106
|
]
|
|
98
107
|
return [
|
|
99
108
|
Diagnostic(
|
|
@@ -181,9 +181,7 @@ class InvalidPydanticFieldDefault(Rule):
|
|
|
181
181
|
files=(
|
|
182
182
|
ExampleFile.python(
|
|
183
183
|
"app/models.py",
|
|
184
|
-
"from pydantic import BaseModel\n\n"
|
|
185
|
-
"class User(BaseModel):\n"
|
|
186
|
-
" display_name: str = None\n",
|
|
184
|
+
"from pydantic import BaseModel\n\nclass User(BaseModel):\n display_name: str = None\n",
|
|
187
185
|
),
|
|
188
186
|
),
|
|
189
187
|
focus_path=PurePosixPath("app/models.py"),
|
|
@@ -231,11 +229,7 @@ class InvalidPydanticFieldDefault(Rule):
|
|
|
231
229
|
if statement.target.id.startswith("_"):
|
|
232
230
|
continue
|
|
233
231
|
contract = _annotation_contract(path, tree, statement.annotation)
|
|
234
|
-
if
|
|
235
|
-
contract.transforms_default
|
|
236
|
-
or transformers.all_fields
|
|
237
|
-
or statement.target.id in transformers.fields
|
|
238
|
-
):
|
|
232
|
+
if contract.transforms_default or transformers.all_fields or statement.target.id in transformers.fields:
|
|
239
233
|
continue
|
|
240
234
|
assignment_field = (
|
|
241
235
|
statement.value
|
|
@@ -276,10 +270,7 @@ def _is_direct_base_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
|
276
270
|
return False
|
|
277
271
|
return all(
|
|
278
272
|
imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel")
|
|
279
|
-
or (
|
|
280
|
-
isinstance(base, ast.Subscript)
|
|
281
|
-
and imports.resolves(base.value, sources=_TYPING_SOURCES, symbol="Generic")
|
|
282
|
-
)
|
|
273
|
+
or (isinstance(base, ast.Subscript) and imports.resolves(base.value, sources=_TYPING_SOURCES, symbol="Generic"))
|
|
283
274
|
for base in node.bases
|
|
284
275
|
)
|
|
285
276
|
|