sarj-python-lint 1.1.0__tar.gz → 1.2.0__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-1.1.0 → sarj_python_lint-1.2.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/__main__.py +42 -11
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_file_context.py +5 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_ast_index.py +15 -2
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.2.0/src/sarj_python_lint/rules/_test_composition.py +286 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +2 -2
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +15 -15
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +6 -9
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/mock_without_spec.py +2 -2
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +2 -2
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +1 -1
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +6 -10
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_select_star.py +5 -3
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +9 -11
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +6 -6
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +3 -5
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +6 -4
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +8 -10
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +6 -6
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +8 -6
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +157 -33
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +9 -8
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +5 -4
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +5 -4
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +2 -2
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +4 -4
- sarj_python_lint-1.2.0/src/sarj_python_lint/rules/repeated_test_composition.py +286 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +7 -5
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/.gitignore +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/LICENSE +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/README.md +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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/
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import Counter
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from fnmatch import fnmatch
|
|
6
7
|
import json
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import sys
|
|
@@ -66,19 +67,21 @@ def expand_paths(paths: list[Path]) -> list[Path]:
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def _python_files(p: Path) -> list[Path]:
|
|
70
|
+
if not SKIP_DIR_NAMES.isdisjoint(p.parts):
|
|
71
|
+
return []
|
|
69
72
|
out: list[Path] = []
|
|
70
|
-
for
|
|
71
|
-
if not
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
continue
|
|
75
|
-
try:
|
|
76
|
-
if child.stat().st_size > _MAX_FILE_BYTES:
|
|
73
|
+
for directory, directories, filenames in p.walk():
|
|
74
|
+
directories[:] = [name for name in directories if name not in SKIP_DIR_NAMES]
|
|
75
|
+
for name in filenames:
|
|
76
|
+
if not fnmatch(name, "*.py"):
|
|
77
77
|
continue
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
child = directory / name
|
|
79
|
+
try:
|
|
80
|
+
if child.is_file() and child.stat().st_size <= _MAX_FILE_BYTES:
|
|
81
|
+
out.append(child)
|
|
82
|
+
except OSError:
|
|
83
|
+
continue
|
|
84
|
+
return sorted(out)
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
@@ -157,6 +160,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
157
160
|
# still owns every fixed dictionary return when selected alone, while
|
|
158
161
|
# an all-rules run emits one actionable diagnostic for this overlap.
|
|
159
162
|
"SARJ447": frozenset({"SARJ008"}),
|
|
163
|
+
"SARJ457": frozenset({"SARJ045"}),
|
|
160
164
|
}
|
|
161
165
|
)
|
|
162
166
|
|
|
@@ -171,6 +175,7 @@ class _OwnerLocation(NamedTuple):
|
|
|
171
175
|
|
|
172
176
|
|
|
173
177
|
def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = None) -> list[Diagnostic]:
|
|
178
|
+
diags = _deduplicate_test_composition(diags, source)
|
|
174
179
|
codes = frozenset(diagnostic.code for diagnostic in diags)
|
|
175
180
|
needs_docstring_owners = ("SARJ092" in codes and not codes.isdisjoint(_DIAGNOSTIC_PRECEDENCE["SARJ092"])) or (
|
|
176
181
|
"SARJ420" in codes and not codes.isdisjoint(_DOCSTRING_PRECEDENCE_CODES)
|
|
@@ -218,6 +223,32 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
|
|
|
218
223
|
]
|
|
219
224
|
|
|
220
225
|
|
|
226
|
+
def _deduplicate_test_composition(diags: list[Diagnostic], source: str | None) -> list[Diagnostic]:
|
|
227
|
+
codes = {diagnostic.code for diagnostic in diags}
|
|
228
|
+
if source is None or not {"SARJ066", "SARJ457"} <= codes:
|
|
229
|
+
return diags
|
|
230
|
+
try:
|
|
231
|
+
tree = ast.parse(source)
|
|
232
|
+
except SyntaxError:
|
|
233
|
+
return diags
|
|
234
|
+
whole_tests = {
|
|
235
|
+
(diagnostic.path, diagnostic.line): diagnostic.severity for diagnostic in diags if diagnostic.code == "SARJ066"
|
|
236
|
+
}
|
|
237
|
+
functions = [node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
238
|
+
|
|
239
|
+
def redundant(diagnostic: Diagnostic) -> bool:
|
|
240
|
+
if diagnostic.code != "SARJ457":
|
|
241
|
+
return False
|
|
242
|
+
owners = [node for node in functions if node.lineno <= diagnostic.line <= (node.end_lineno or node.lineno)]
|
|
243
|
+
if not owners:
|
|
244
|
+
return False
|
|
245
|
+
owner = max(owners, key=lambda node: node.lineno)
|
|
246
|
+
severity = whole_tests.get((diagnostic.path, owner.lineno))
|
|
247
|
+
return severity is not None and (severity is Severity.ERROR or diagnostic.severity is Severity.WARNING)
|
|
248
|
+
|
|
249
|
+
return [diagnostic for diagnostic in diags if not redundant(diagnostic)]
|
|
250
|
+
|
|
251
|
+
|
|
221
252
|
def _suppressed_diagnostics(
|
|
222
253
|
present: dict[tuple[Path, int, int], dict[str, set[Severity]]],
|
|
223
254
|
) -> set[tuple[tuple[Path, int, int], str, Severity]]:
|
|
@@ -12,6 +12,7 @@ from sarj_python_lint.rules._paths import is_generated
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
|
|
@@ -48,6 +49,10 @@ class PythonFileContext:
|
|
|
48
49
|
def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
|
|
49
50
|
return [node for node in self.node_index.query(types) if isinstance(node, types)]
|
|
50
51
|
|
|
52
|
+
@property
|
|
53
|
+
def parents(self) -> Mapping[ast.AST, ast.AST]:
|
|
54
|
+
return self.node_index.parents
|
|
55
|
+
|
|
51
56
|
@cached_property
|
|
52
57
|
def imports(self) -> ImportIndex:
|
|
53
58
|
return ImportIndex.from_tree(self._valid_tree())
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
from types import MappingProxyType
|
|
6
7
|
from typing import TYPE_CHECKING, TypeIs, final
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from collections.abc import Iterator
|
|
11
|
+
from collections.abc import Iterator, Mapping
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
_AST = ast.AST
|
|
@@ -43,7 +44,7 @@ def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
|
|
|
43
44
|
|
|
44
45
|
@final
|
|
45
46
|
class NodeIndex:
|
|
46
|
-
__slots__ = ("_buckets", "_flat", "_queries", "tree")
|
|
47
|
+
__slots__ = ("_buckets", "_flat", "_parents", "_queries", "tree")
|
|
47
48
|
|
|
48
49
|
def __init__(self, tree: ast.AST) -> None:
|
|
49
50
|
self.tree = tree
|
|
@@ -62,8 +63,15 @@ class NodeIndex:
|
|
|
62
63
|
flat += children(current)
|
|
63
64
|
self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
|
|
64
65
|
self._flat: list[ast.AST] = flat
|
|
66
|
+
self._parents: Mapping[ast.AST, ast.AST] | None = None
|
|
65
67
|
self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
|
|
66
68
|
|
|
69
|
+
@property
|
|
70
|
+
def parents(self) -> Mapping[ast.AST, ast.AST]:
|
|
71
|
+
if self._parents is None:
|
|
72
|
+
self._parents = MappingProxyType({child: parent for parent in self._flat for child in children(parent)})
|
|
73
|
+
return self._parents
|
|
74
|
+
|
|
67
75
|
def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
|
|
68
76
|
hit = self._queries.get(types)
|
|
69
77
|
if hit is not None:
|
|
@@ -87,3 +95,8 @@ class NodeIndex:
|
|
|
87
95
|
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
|
|
88
96
|
selected = index if index is not None and index.tree is tree else NodeIndex(tree)
|
|
89
97
|
return [node for node in selected.query(types) if isinstance(node, types)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parent_map(tree: ast.AST, *, index: NodeIndex | None = None) -> Mapping[ast.AST, ast.AST]:
|
|
101
|
+
selected = index if index is not None and index.tree is tree else NodeIndex(tree)
|
|
102
|
+
return selected.parents
|
|
@@ -145,6 +145,7 @@ from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
|
145
145
|
from sarj_python_lint.rules.redundant_module_docstring import RedundantModuleDocstring
|
|
146
146
|
from sarj_python_lint.rules.repeated_kwarg_heavy_call_in_test import RepeatedKwargHeavyCallInTest
|
|
147
147
|
from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCallCases
|
|
148
|
+
from sarj_python_lint.rules.repeated_test_composition import RepeatedTestComposition
|
|
148
149
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
149
150
|
RequireKeywordOnlySwapProneParams,
|
|
150
151
|
)
|
|
@@ -225,6 +226,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
225
226
|
PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
|
|
226
227
|
StoreGetDelegatesToBulkRead.id: StoreGetDelegatesToBulkRead,
|
|
227
228
|
RepeatedKwargHeavyCallInTest.id: RepeatedKwargHeavyCallInTest,
|
|
229
|
+
RepeatedTestComposition.id: RepeatedTestComposition,
|
|
228
230
|
DefectXfailRequiresExplicitStrict.id: DefectXfailRequiresExplicitStrict,
|
|
229
231
|
NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
|
|
230
232
|
NoRestatedComment.id: NoRestatedComment,
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
|
|
14
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
type Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
18
|
+
_PORT_BASES = frozenset({"abc.ABC", "typing.Protocol", "typing_extensions.Protocol"})
|
|
19
|
+
_MIN_COLLABORATORS = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Constructor:
|
|
24
|
+
symbol: SymbolRef
|
|
25
|
+
positional: tuple[str, ...]
|
|
26
|
+
collaborators: frozenset[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class WiringScope:
|
|
31
|
+
parameters: frozenset[str]
|
|
32
|
+
aliases: dict[str, ast.expr]
|
|
33
|
+
blocked: frozenset[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def direct_nodes(node: ast.AST) -> Iterator[ast.AST]:
|
|
37
|
+
for child in ast.iter_child_nodes(node):
|
|
38
|
+
yield child
|
|
39
|
+
if not isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
40
|
+
yield from direct_nodes(child)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _class_owner(
|
|
44
|
+
context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet
|
|
45
|
+
) -> tuple[SourceUnit, ast.ClassDef] | None:
|
|
46
|
+
qualified = context.module_imports.resolved_qualified_name(expression)
|
|
47
|
+
unit = project.unit(context.path)
|
|
48
|
+
if qualified is not None:
|
|
49
|
+
module, _, name = qualified.rpartition(".")
|
|
50
|
+
unit = project.source_unit(module)
|
|
51
|
+
elif isinstance(expression, ast.Name):
|
|
52
|
+
name = expression.id
|
|
53
|
+
else:
|
|
54
|
+
return None
|
|
55
|
+
if unit is None or unit.tree is None or unit.module is None:
|
|
56
|
+
return None
|
|
57
|
+
owner = next((node for node in unit.tree.body if isinstance(node, ast.ClassDef) and node.name == name), None)
|
|
58
|
+
if owner is None:
|
|
59
|
+
return None
|
|
60
|
+
if any(
|
|
61
|
+
isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
62
|
+
for node in direct_nodes(unit.tree)
|
|
63
|
+
):
|
|
64
|
+
return None
|
|
65
|
+
return unit, owner
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def constructor(context: PythonFileContext, expression: ast.expr, project: ProjectIndexSet) -> Constructor | None:
|
|
69
|
+
resolved = _class_owner(context, expression, project)
|
|
70
|
+
if resolved is None:
|
|
71
|
+
return None
|
|
72
|
+
unit, owner = resolved
|
|
73
|
+
if unit.module is None:
|
|
74
|
+
return None
|
|
75
|
+
initializer = next(
|
|
76
|
+
(node for node in owner.body if isinstance(node, ast.FunctionDef) and node.name == "__init__"), None
|
|
77
|
+
)
|
|
78
|
+
if initializer is None:
|
|
79
|
+
return None
|
|
80
|
+
parameters = (*initializer.args.posonlyargs, *initializer.args.args, *initializer.args.kwonlyargs)
|
|
81
|
+
invoked = _invoked_fields(owner)
|
|
82
|
+
stored = _stored_parameters(initializer)
|
|
83
|
+
collaborators = frozenset(
|
|
84
|
+
parameter.arg
|
|
85
|
+
for parameter in parameters
|
|
86
|
+
if parameter.annotation is not None
|
|
87
|
+
and _is_port(project, unit, parameter.annotation)
|
|
88
|
+
and stored.get(parameter.arg) in invoked
|
|
89
|
+
)
|
|
90
|
+
if len(collaborators) < _MIN_COLLABORATORS:
|
|
91
|
+
return None
|
|
92
|
+
return Constructor(
|
|
93
|
+
SymbolRef(unit.module, owner.name),
|
|
94
|
+
tuple(arg.arg for arg in (*initializer.args.posonlyargs, *initializer.args.args)[1:]),
|
|
95
|
+
collaborators,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _is_port(project: ProjectIndexSet, unit: SourceUnit, annotation: ast.expr) -> bool:
|
|
100
|
+
if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
|
|
101
|
+
try:
|
|
102
|
+
annotation = ast.parse(annotation.value, mode="eval").body
|
|
103
|
+
except SyntaxError:
|
|
104
|
+
return False
|
|
105
|
+
summary = project.class_for(unit, annotation)
|
|
106
|
+
if summary is None:
|
|
107
|
+
return False
|
|
108
|
+
owner = project.source_unit(summary.symbol.module)
|
|
109
|
+
return owner is not None and project.class_inherits_from(owner, summary.symbol.name, _PORT_BASES)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _stored_parameters(initializer: Function) -> dict[str, str]:
|
|
113
|
+
stored: dict[str, str] = {}
|
|
114
|
+
writes = Counter(
|
|
115
|
+
node.attr
|
|
116
|
+
for node in direct_nodes(initializer)
|
|
117
|
+
if isinstance(node, ast.Attribute) and _is_self(node.value) and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
118
|
+
)
|
|
119
|
+
for statement in initializer.body:
|
|
120
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
|
|
121
|
+
target, value = statement.targets[0], statement.value
|
|
122
|
+
elif isinstance(statement, ast.AnnAssign):
|
|
123
|
+
target, value = statement.target, statement.value
|
|
124
|
+
else:
|
|
125
|
+
continue
|
|
126
|
+
if (
|
|
127
|
+
isinstance(value, ast.Name)
|
|
128
|
+
and isinstance(target, ast.Attribute)
|
|
129
|
+
and _is_self(target.value)
|
|
130
|
+
and writes[target.attr] == 1
|
|
131
|
+
):
|
|
132
|
+
stored[value.id] = target.attr
|
|
133
|
+
return stored
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _is_self(node: ast.expr) -> bool:
|
|
137
|
+
return isinstance(node, ast.Name) and node.id == "self"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _invoked_fields(owner: ast.ClassDef) -> frozenset[str]:
|
|
141
|
+
invoked: set[str] = set()
|
|
142
|
+
for method in owner.body:
|
|
143
|
+
if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)) and method.name != "__init__":
|
|
144
|
+
invoked.update(_method_fields(method))
|
|
145
|
+
return frozenset(invoked)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _method_fields(method: Function) -> Iterator[str]:
|
|
149
|
+
for node in direct_nodes(method):
|
|
150
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
151
|
+
continue
|
|
152
|
+
target = node.func.value
|
|
153
|
+
if isinstance(target, ast.Attribute) and _is_self(target.value):
|
|
154
|
+
yield target.attr
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def bindings(function: Function) -> dict[str, ast.expr]:
|
|
158
|
+
candidates: dict[str, list[ast.expr]] = {}
|
|
159
|
+
for statement in function.body:
|
|
160
|
+
if (
|
|
161
|
+
isinstance(statement, ast.Assign)
|
|
162
|
+
and len(statement.targets) == 1
|
|
163
|
+
and isinstance(statement.targets[0], ast.Name)
|
|
164
|
+
):
|
|
165
|
+
candidates.setdefault(statement.targets[0].id, []).append(statement.value)
|
|
166
|
+
elif (
|
|
167
|
+
isinstance(statement, ast.AnnAssign)
|
|
168
|
+
and isinstance(statement.target, ast.Name)
|
|
169
|
+
and statement.value is not None
|
|
170
|
+
):
|
|
171
|
+
candidates.setdefault(statement.target.id, []).append(statement.value)
|
|
172
|
+
writes = [
|
|
173
|
+
node.id
|
|
174
|
+
for node in direct_nodes(function)
|
|
175
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
176
|
+
]
|
|
177
|
+
return {name: values[0] for name, values in candidates.items() if len(values) == 1 and writes.count(name) == 1}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def local_names(function: Function) -> set[str]:
|
|
181
|
+
names: set[str] = set()
|
|
182
|
+
for node in direct_nodes(function):
|
|
183
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
184
|
+
names.add(node.id)
|
|
185
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
186
|
+
names.add(node.name)
|
|
187
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
188
|
+
names.update(alias.asname or alias.name.partition(".")[0] for alias in node.names)
|
|
189
|
+
return names
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def changed_before(function: Function, call: ast.Call) -> set[str]:
|
|
193
|
+
changed: set[str] = set()
|
|
194
|
+
for node in direct_nodes(function):
|
|
195
|
+
if not isinstance(node, (ast.Call, ast.Attribute)) or node.lineno >= call.lineno:
|
|
196
|
+
continue
|
|
197
|
+
target = node.func.value if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) else None
|
|
198
|
+
if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
199
|
+
target = node.value
|
|
200
|
+
if isinstance(target, ast.Name):
|
|
201
|
+
changed.add(target.id)
|
|
202
|
+
return changed
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def unalias(node: ast.expr, aliases: dict[str, ast.expr]) -> ast.expr:
|
|
206
|
+
seen: set[str] = set()
|
|
207
|
+
while isinstance(node, ast.Name) and node.id in aliases and node.id not in seen:
|
|
208
|
+
seen.add(node.id)
|
|
209
|
+
node = aliases[node.id]
|
|
210
|
+
return node
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def shadowed(node: ast.expr, names: frozenset[str]) -> bool:
|
|
214
|
+
while isinstance(node, ast.Attribute):
|
|
215
|
+
node = node.value
|
|
216
|
+
return isinstance(node, ast.Name) and node.id in names
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def wiring(
|
|
220
|
+
context: PythonFileContext,
|
|
221
|
+
project: ProjectIndexSet,
|
|
222
|
+
call: ast.Call,
|
|
223
|
+
spec: Constructor,
|
|
224
|
+
scope: WiringScope,
|
|
225
|
+
) -> tuple[tuple[str, str], ...] | None:
|
|
226
|
+
if any(isinstance(arg, ast.Starred) for arg in call.args) or any(keyword.arg is None for keyword in call.keywords):
|
|
227
|
+
return None
|
|
228
|
+
if len(call.args) > len(spec.positional):
|
|
229
|
+
return None
|
|
230
|
+
values = dict(zip(spec.positional, call.args, strict=False))
|
|
231
|
+
for keyword in call.keywords:
|
|
232
|
+
if keyword.arg is None or keyword.arg in values:
|
|
233
|
+
return None
|
|
234
|
+
values[keyword.arg] = keyword.value
|
|
235
|
+
result: list[tuple[str, str]] = []
|
|
236
|
+
for name in sorted(spec.collaborators):
|
|
237
|
+
value = values.get(name)
|
|
238
|
+
if value is None or not isinstance(unalias(value, scope.aliases), (ast.Name, ast.Call)):
|
|
239
|
+
return None
|
|
240
|
+
identity = _value_key(context, project, value, scope)
|
|
241
|
+
if identity is None:
|
|
242
|
+
return None
|
|
243
|
+
result.append((name, identity))
|
|
244
|
+
return tuple(result)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _value_key(
|
|
248
|
+
context: PythonFileContext,
|
|
249
|
+
project: ProjectIndexSet,
|
|
250
|
+
value: ast.expr,
|
|
251
|
+
scope: WiringScope,
|
|
252
|
+
seen: frozenset[str] = frozenset(),
|
|
253
|
+
) -> str | None:
|
|
254
|
+
if isinstance(value, ast.Name):
|
|
255
|
+
if value.id in seen or value.id in scope.blocked:
|
|
256
|
+
return None
|
|
257
|
+
if value.id in scope.aliases:
|
|
258
|
+
return _value_key(
|
|
259
|
+
context,
|
|
260
|
+
project,
|
|
261
|
+
scope.aliases[value.id],
|
|
262
|
+
scope,
|
|
263
|
+
seen=seen | {value.id},
|
|
264
|
+
)
|
|
265
|
+
return f"fixture:{value.id}" if value.id in scope.parameters else None
|
|
266
|
+
if isinstance(value, ast.Constant):
|
|
267
|
+
return ast.dump(value)
|
|
268
|
+
if not isinstance(value, ast.Call):
|
|
269
|
+
return None
|
|
270
|
+
callee = unalias(value.func, scope.aliases)
|
|
271
|
+
if shadowed(callee, scope.parameters | scope.blocked):
|
|
272
|
+
return None
|
|
273
|
+
owner = _class_owner(context, callee, project)
|
|
274
|
+
if owner is None or any(keyword.arg is None for keyword in value.keywords):
|
|
275
|
+
return None
|
|
276
|
+
positional = [_value_key(context, project, argument, scope, seen=seen) for argument in value.args]
|
|
277
|
+
keywords = [
|
|
278
|
+
(
|
|
279
|
+
keyword.arg,
|
|
280
|
+
_value_key(context, project, keyword.value, scope, seen=seen),
|
|
281
|
+
)
|
|
282
|
+
for keyword in value.keywords
|
|
283
|
+
]
|
|
284
|
+
if any(key is None for key in positional) or any(key is None for _, key in keywords):
|
|
285
|
+
return None
|
|
286
|
+
return f"{owner[0].module}.{owner[1].name}:{positional!r}:{sorted(keywords)!r}"
|
|
@@ -69,7 +69,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
69
69
|
id = "async-mock-call-without-await-assertion"
|
|
70
70
|
code = "SARJ456"
|
|
71
71
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
72
|
-
default_level=Severity.
|
|
72
|
+
default_level=Severity.ERROR,
|
|
73
73
|
summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
|
|
74
74
|
rationale=(
|
|
75
75
|
"Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
|
|
@@ -150,7 +150,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
150
150
|
line=call.lineno,
|
|
151
151
|
col=call.col_offset + 1,
|
|
152
152
|
code=self.code,
|
|
153
|
-
severity=Severity.
|
|
153
|
+
severity=Severity.ERROR,
|
|
154
154
|
message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
|
|
155
155
|
)
|
|
156
156
|
for test in _collected_tests(tree.body)
|
|
@@ -23,7 +23,7 @@ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import a
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from collections.abc import Callable
|
|
26
|
+
from collections.abc import Callable, Mapping
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
from sqlglot import exp
|
|
@@ -83,18 +83,18 @@ def _docstring_node_ids(tree: ast.AST, *, node_index: NodeIndex | None = None) -
|
|
|
83
83
|
return result
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _is_query_context(node: ast.expr, parents:
|
|
86
|
+
def _is_query_context(node: ast.expr, parents: Mapping[ast.AST, ast.AST], roots: frozenset[int]) -> bool:
|
|
87
87
|
current: ast.AST = node
|
|
88
88
|
while True:
|
|
89
89
|
if id(current) in roots:
|
|
90
90
|
return True
|
|
91
|
-
if (parent := parents.get(
|
|
91
|
+
if (parent := parents.get(current)) is None:
|
|
92
92
|
return False
|
|
93
93
|
current = parent
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def _query_roots(
|
|
97
|
-
tree: ast.Module, parents:
|
|
97
|
+
tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
|
|
98
98
|
) -> frozenset[int]:
|
|
99
99
|
binding_counts: dict[tuple[int, str], int] = {}
|
|
100
100
|
binding_values: dict[tuple[int, str], ast.expr] = {}
|
|
@@ -133,10 +133,10 @@ def _query_roots(
|
|
|
133
133
|
return frozenset(roots)
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
def _scope(node: ast.AST, parents:
|
|
136
|
+
def _scope(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> ast.AST:
|
|
137
137
|
current = node
|
|
138
138
|
while not isinstance(current, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)):
|
|
139
|
-
parent = parents.get(
|
|
139
|
+
parent = parents.get(current)
|
|
140
140
|
if parent is None:
|
|
141
141
|
return current
|
|
142
142
|
current = parent
|
|
@@ -169,7 +169,7 @@ def _call_name(function: ast.expr) -> str:
|
|
|
169
169
|
|
|
170
170
|
|
|
171
171
|
def _postgres_sql_constructors(
|
|
172
|
-
tree: ast.Module, parents:
|
|
172
|
+
tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
|
|
173
173
|
) -> frozenset[_ConstructorImport]:
|
|
174
174
|
constructors: set[_ConstructorImport] = set()
|
|
175
175
|
for node in nodes(tree, ast.Import, ast.ImportFrom, index=node_index):
|
|
@@ -204,7 +204,7 @@ def _qualified_name(node: ast.expr) -> str:
|
|
|
204
204
|
def _visible_constructors(
|
|
205
205
|
owner: ast.AST,
|
|
206
206
|
imports: frozenset[_ConstructorImport],
|
|
207
|
-
parents:
|
|
207
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
208
208
|
binding_counts: dict[tuple[int, str], int],
|
|
209
209
|
wildcard_scopes: set[int],
|
|
210
210
|
) -> frozenset[str]:
|
|
@@ -212,7 +212,7 @@ def _visible_constructors(
|
|
|
212
212
|
current = owner
|
|
213
213
|
while True:
|
|
214
214
|
scope_chain.append(id(current))
|
|
215
|
-
parent = parents.get(
|
|
215
|
+
parent = parents.get(current)
|
|
216
216
|
if parent is None:
|
|
217
217
|
break
|
|
218
218
|
current = _scope(parent, parents)
|
|
@@ -380,7 +380,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
380
380
|
return []
|
|
381
381
|
|
|
382
382
|
docstrings = _docstring_node_ids(tree, node_index=context.node_index)
|
|
383
|
-
parents =
|
|
383
|
+
parents = context.parents
|
|
384
384
|
query_roots = _query_roots(tree, parents, node_index=context.node_index)
|
|
385
385
|
diagnostics: list[Diagnostic] = []
|
|
386
386
|
consumed: set[int] = set()
|
|
@@ -417,7 +417,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
417
417
|
|
|
418
418
|
def _count_query_bindings(
|
|
419
419
|
tree: ast.Module,
|
|
420
|
-
parents:
|
|
420
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
421
421
|
count: Callable[[ast.AST, str], None],
|
|
422
422
|
wildcard_scopes: set[int],
|
|
423
423
|
*,
|
|
@@ -430,7 +430,7 @@ def _count_query_bindings(
|
|
|
430
430
|
count(_scope(argument, parents), argument.arg)
|
|
431
431
|
_count_import_bindings(tree, parents, count, wildcard_scopes, node_index=node_index)
|
|
432
432
|
for definition in nodes(tree, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, index=node_index):
|
|
433
|
-
parent = parents.get(
|
|
433
|
+
parent = parents.get(definition)
|
|
434
434
|
if parent is not None:
|
|
435
435
|
count(_scope(parent, parents), definition.name)
|
|
436
436
|
for handler in nodes(tree, ast.ExceptHandler, index=node_index):
|
|
@@ -447,7 +447,7 @@ def _count_query_bindings(
|
|
|
447
447
|
|
|
448
448
|
def _query_binding_values(
|
|
449
449
|
tree: ast.Module,
|
|
450
|
-
parents:
|
|
450
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
451
451
|
binding_values: dict[tuple[int, str], ast.expr],
|
|
452
452
|
*,
|
|
453
453
|
node_index: NodeIndex | None = None,
|
|
@@ -461,7 +461,7 @@ def _query_binding_values(
|
|
|
461
461
|
if not isinstance(target, ast.Name) or assignment.value is None:
|
|
462
462
|
continue
|
|
463
463
|
owner = _scope(assignment, parents)
|
|
464
|
-
if parents.get(
|
|
464
|
+
if parents.get(assignment) is owner:
|
|
465
465
|
binding_values[id(owner), target.id] = assignment.value
|
|
466
466
|
|
|
467
467
|
|
|
@@ -591,7 +591,7 @@ def _maximum_query_joins(statement: exp.Query) -> int:
|
|
|
591
591
|
|
|
592
592
|
def _count_import_bindings(
|
|
593
593
|
tree: ast.Module,
|
|
594
|
-
parents:
|
|
594
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
595
595
|
count: Callable[[ast.AST, str], None],
|
|
596
596
|
wildcard_scopes: set[int],
|
|
597
597
|
*,
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rules._paths import is_test_path
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Mapping
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
25
26
|
from sarj_python_lint._file_context import PythonFileContext
|
|
@@ -92,7 +93,7 @@ class FastapiClassRouterContract(Rule):
|
|
|
92
93
|
if tree is None:
|
|
93
94
|
return []
|
|
94
95
|
index = context.fastapi
|
|
95
|
-
parents =
|
|
96
|
+
parents = context.parents
|
|
96
97
|
findings = _router_construction_findings(path, tree, index, parents, node_index=context.node_index)
|
|
97
98
|
for function in (
|
|
98
99
|
node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
@@ -109,7 +110,7 @@ def _router_construction_findings(
|
|
|
109
110
|
path: Path,
|
|
110
111
|
tree: ast.Module,
|
|
111
112
|
index: FastapiIndex,
|
|
112
|
-
parents:
|
|
113
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
113
114
|
*,
|
|
114
115
|
node_index: NodeIndex | None = None,
|
|
115
116
|
) -> list[Diagnostic]:
|
|
@@ -187,16 +188,12 @@ def _is_collection_or_scalar(annotation: ast.expr) -> bool:
|
|
|
187
188
|
return flat_name(target) in _TOP_LEVEL_COLLECTIONS | _SCALARS
|
|
188
189
|
|
|
189
190
|
|
|
190
|
-
def _parents(tree: ast.Module, *, node_index: NodeIndex | None = None) -> dict[int, ast.AST]:
|
|
191
|
-
return {id(child): parent for parent in walk_ast(tree, index=node_index) for child in ast.iter_child_nodes(parent)}
|
|
192
|
-
|
|
193
|
-
|
|
194
191
|
def _ancestor(
|
|
195
|
-
node: ast.AST, parents:
|
|
192
|
+
node: ast.AST, parents: Mapping[ast.AST, ast.AST], kind: type[ast.AST] | tuple[type[ast.AST], ...]
|
|
196
193
|
) -> ast.AST | None:
|
|
197
|
-
current = parents.get(
|
|
194
|
+
current = parents.get(node)
|
|
198
195
|
while current is not None:
|
|
199
196
|
if isinstance(current, kind):
|
|
200
197
|
return current
|
|
201
|
-
current = parents.get(
|
|
198
|
+
current = parents.get(current)
|
|
202
199
|
return None
|
{sarj_python_lint-1.1.0 → sarj_python_lint-1.2.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
@@ -121,7 +121,7 @@ class MockWithoutSpec(Rule):
|
|
|
121
121
|
id: str = "mock-without-spec"
|
|
122
122
|
code: str = "SARJ040"
|
|
123
123
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
124
|
-
default_level=Severity.
|
|
124
|
+
default_level=Severity.ERROR,
|
|
125
125
|
summary="Unrestricted mock permits attributes outside the collaborator contract.",
|
|
126
126
|
rationale=(
|
|
127
127
|
"A mock without a spec permits removed or misspelled attributes; a callable mock without autospec may "
|
|
@@ -234,7 +234,7 @@ class MockWithoutSpec(Rule):
|
|
|
234
234
|
col=node.col_offset + 1,
|
|
235
235
|
code=self.code,
|
|
236
236
|
message=_diagnostic_message(label),
|
|
237
|
-
severity=Severity.
|
|
237
|
+
severity=Severity.ERROR,
|
|
238
238
|
)
|
|
239
239
|
for node, label in _unspecced_calls(
|
|
240
240
|
tree, names, pytest_mocker, _FileFacts.from_tree(tree), node_index=context.node_index
|