sarj-python-lint 1.31.0__tar.gz → 1.32.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.31.0 → sarj_python_lint-1.32.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_file_context.py +5 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_python_target.py +39 -8
- sarj_python_lint-1.32.0/src/sarj_python_lint/rules/_closed_helpers.py +185 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_copy_on_write.py +136 -19
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_namespace_access.py +6 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_project_index.py +624 -5
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +56 -12
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_resource_provenance.py +63 -12
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +2 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +5 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +5 -6
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +9 -5
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +71 -3
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +6 -5
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +64 -27
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +25 -8
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +20 -11
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +83 -24
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +63 -14
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +2 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +58 -5
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +4 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +3 -1
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +3 -1
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +2 -2
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +130 -25
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +17 -4
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +17 -4
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +67 -11
- sarj_python_lint-1.31.0/src/sarj_python_lint/rules/_closed_helpers.py +0 -100
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/.gitignore +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/LICENSE +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/README.md +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/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: 1.
|
|
3
|
+
Version: 1.32.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/
|
|
@@ -9,6 +9,7 @@ from sarj_python_lint._source import symbol_prefilter_source
|
|
|
9
9
|
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
10
10
|
from sarj_python_lint.rules._fastapi import FastapiIndex
|
|
11
11
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
12
|
+
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
12
13
|
from sarj_python_lint.rules._paths import is_generated
|
|
13
14
|
|
|
14
15
|
|
|
@@ -90,6 +91,10 @@ class PythonFileContext:
|
|
|
90
91
|
def module_imports(self) -> ImportIndex:
|
|
91
92
|
return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
|
|
92
93
|
|
|
94
|
+
@cached_property
|
|
95
|
+
def dynamic_namespace(self) -> bool:
|
|
96
|
+
return dynamic_namespace_access(self.nodes(ast.AST), self.imports, self.parents)
|
|
97
|
+
|
|
93
98
|
@cached_property
|
|
94
99
|
def fastapi(self) -> FastapiIndex:
|
|
95
100
|
return FastapiIndex(self._valid_tree(), path=self.path)
|
|
@@ -23,25 +23,38 @@ class PythonTargetFacts:
|
|
|
23
23
|
self._projects: dict[Path, str | None] = {}
|
|
24
24
|
self._installed: dict[Path, dict[Path, str | None]] = {}
|
|
25
25
|
self._witnesses: dict[tuple[str, tuple[int, int]], bool] = {}
|
|
26
|
+
self._minimums: dict[tuple[str, tuple[int, int]], bool] = {}
|
|
26
27
|
|
|
27
28
|
def has_declared_support_before(self, path: Path, minimum: tuple[int, int]) -> bool:
|
|
29
|
+
declaration = self._declared_target(path)
|
|
30
|
+
if not declaration:
|
|
31
|
+
return False
|
|
32
|
+
key = declaration, minimum
|
|
33
|
+
if key not in self._witnesses:
|
|
34
|
+
self._witnesses[key] = _has_legacy_witness(declaration, minimum)
|
|
35
|
+
return self._witnesses[key]
|
|
36
|
+
|
|
37
|
+
def has_declared_minimum(self, path: Path, minimum: tuple[int, int]) -> bool:
|
|
38
|
+
declaration = self._declared_target(path)
|
|
39
|
+
if not declaration:
|
|
40
|
+
return False
|
|
41
|
+
key = declaration, minimum
|
|
42
|
+
if key not in self._minimums:
|
|
43
|
+
self._minimums[key] = _proves_minimum(declaration, minimum)
|
|
44
|
+
return self._minimums[key]
|
|
45
|
+
|
|
46
|
+
def _declared_target(self, path: Path) -> str | None:
|
|
28
47
|
try:
|
|
29
48
|
absolute = path.absolute()
|
|
30
49
|
resolved = path.resolve()
|
|
31
50
|
except OSError, RuntimeError:
|
|
32
|
-
return
|
|
51
|
+
return None
|
|
33
52
|
if absolute not in self._declarations:
|
|
34
53
|
try:
|
|
35
54
|
self._declarations[absolute] = self._declaration(absolute, resolved)
|
|
36
55
|
except OSError, RuntimeError:
|
|
37
56
|
self._declarations[absolute] = None
|
|
38
|
-
|
|
39
|
-
if not declaration:
|
|
40
|
-
return False
|
|
41
|
-
key = declaration, minimum
|
|
42
|
-
if key not in self._witnesses:
|
|
43
|
-
self._witnesses[key] = _has_legacy_witness(declaration, minimum)
|
|
44
|
-
return self._witnesses[key]
|
|
57
|
+
return self._declarations[absolute]
|
|
45
58
|
|
|
46
59
|
def _declaration(self, absolute: Path, resolved: Path) -> str | None:
|
|
47
60
|
ancestors = [*list(absolute.parents)[:_MAX_ANCESTORS], *list(resolved.parents)[:_MAX_ANCESTORS]]
|
|
@@ -142,3 +155,21 @@ def _has_legacy_witness(declaration: str, minimum: tuple[int, int]) -> bool:
|
|
|
142
155
|
and specifiers.contains(candidate, prereleases=False)
|
|
143
156
|
for candidate in candidates
|
|
144
157
|
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _proves_minimum(declaration: str, minimum: tuple[int, int]) -> bool:
|
|
161
|
+
try:
|
|
162
|
+
specifiers = SpecifierSet(declaration)
|
|
163
|
+
except InvalidSpecifier:
|
|
164
|
+
return False
|
|
165
|
+
threshold = Version(".".join(map(str, minimum)))
|
|
166
|
+
for specifier in specifiers:
|
|
167
|
+
if specifier.operator not in {">=", ">", "==", "~="}:
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
boundary = Version(specifier.version.removesuffix(".*"))
|
|
171
|
+
except InvalidVersion:
|
|
172
|
+
continue
|
|
173
|
+
if not boundary.epoch and boundary >= threshold:
|
|
174
|
+
return True
|
|
175
|
+
return False
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sarj_python_lint.rules._ast_index import nodes
|
|
7
|
+
from sarj_python_lint.rules._pydantic_field_contract import (
|
|
8
|
+
inert_annotation,
|
|
9
|
+
inert_class_value,
|
|
10
|
+
inert_signature,
|
|
11
|
+
literal_value,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
17
|
+
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
18
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_DYNAMIC_NAMES = frozenset(
|
|
22
|
+
{"globals", "locals", "vars", "eval", "exec", "getattr", "setattr", "delattr", "__import__", "__all__"}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dynamic_namespace(context: PythonFileContext) -> bool:
|
|
27
|
+
return (
|
|
28
|
+
context.dynamic_namespace
|
|
29
|
+
or any(node.id in _DYNAMIC_NAMES for node in context.nodes(ast.Name))
|
|
30
|
+
or any(node.attr in _DYNAMIC_NAMES | {"__dict__"} for node in context.nodes(ast.Attribute))
|
|
31
|
+
or any(node.name == "*" for node in context.nodes(ast.alias))
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def executable_body(function: ast.FunctionDef) -> list[ast.stmt]:
|
|
36
|
+
body = function.body
|
|
37
|
+
return (
|
|
38
|
+
body[1:]
|
|
39
|
+
if body
|
|
40
|
+
and isinstance(body[0], ast.Expr)
|
|
41
|
+
and isinstance(body[0].value, ast.Constant)
|
|
42
|
+
and isinstance(body[0].value.value, str)
|
|
43
|
+
else body
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def closed_references(
|
|
48
|
+
context: PythonFileContext, function: ast.FunctionDef, provenance: ResourceProvenance
|
|
49
|
+
) -> list[ast.Name] | None:
|
|
50
|
+
if (
|
|
51
|
+
context.parents.get(function) is not context.tree
|
|
52
|
+
or not function.name.startswith("_")
|
|
53
|
+
or function.name.startswith("__")
|
|
54
|
+
or function.decorator_list
|
|
55
|
+
or function.type_params
|
|
56
|
+
):
|
|
57
|
+
return None
|
|
58
|
+
if has_member_import(context.tree, function.name, node_index=context.node_index):
|
|
59
|
+
return None
|
|
60
|
+
if (
|
|
61
|
+
function.args.vararg is not None
|
|
62
|
+
or function.args.kwarg is not None
|
|
63
|
+
or dynamic_namespace(context)
|
|
64
|
+
or any(node.attr == function.name for node in context.nodes(ast.Attribute))
|
|
65
|
+
or any(
|
|
66
|
+
isinstance(node.value, str) and function.name in node.value.split(".")
|
|
67
|
+
for node in context.nodes(ast.Constant)
|
|
68
|
+
)
|
|
69
|
+
):
|
|
70
|
+
return None
|
|
71
|
+
references = [node for node in context.nodes(ast.Name) if node.id == function.name]
|
|
72
|
+
if not references or any(
|
|
73
|
+
not isinstance(node.ctx, ast.Load) or provenance.binding(node.id, node) is not function for node in references
|
|
74
|
+
):
|
|
75
|
+
return None
|
|
76
|
+
return references
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def has_member_import(tree: ast.Module | None, name: str, *, node_index: NodeIndex | None = None) -> bool:
|
|
80
|
+
return tree is None or any(
|
|
81
|
+
alias.name == name for node in nodes(tree, ast.ImportFrom, index=node_index) for alias in node.names
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def direct_call(reference: ast.Name, context: PythonFileContext) -> ast.Call | None:
|
|
86
|
+
parent = context.parents.get(reference)
|
|
87
|
+
if not isinstance(parent, ast.Call) or parent.func is not reference:
|
|
88
|
+
return None
|
|
89
|
+
if any(isinstance(argument, ast.Starred) for argument in parent.args) or any(
|
|
90
|
+
keyword.arg is None for keyword in parent.keywords
|
|
91
|
+
):
|
|
92
|
+
return None
|
|
93
|
+
return parent
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def call_matches(function: ast.FunctionDef, call: ast.Call) -> bool:
|
|
97
|
+
positional = [*function.args.posonlyargs, *function.args.args]
|
|
98
|
+
keywords = {arg.arg for arg in (*function.args.args, *function.args.kwonlyargs)}
|
|
99
|
+
required = {arg.arg for arg in positional[: len(positional) - len(function.args.defaults)]}
|
|
100
|
+
required.update(
|
|
101
|
+
arg.arg
|
|
102
|
+
for arg, default in zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True)
|
|
103
|
+
if default is None
|
|
104
|
+
)
|
|
105
|
+
supplied = {arg.arg for arg in positional[: len(call.args)]}
|
|
106
|
+
if len(call.args) > len(positional):
|
|
107
|
+
return False
|
|
108
|
+
for keyword in call.keywords:
|
|
109
|
+
if keyword.arg not in keywords or keyword.arg in supplied:
|
|
110
|
+
return False
|
|
111
|
+
supplied.add(keyword.arg)
|
|
112
|
+
return required <= supplied
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def stable_class(context: PythonFileContext, declaration: ast.ClassDef, provenance: ResourceProvenance) -> bool:
|
|
116
|
+
if declaration.decorator_list or declaration.keywords or context.parents.get(declaration) is not context.tree:
|
|
117
|
+
return False
|
|
118
|
+
return provenance.bindings.get(context.tree, {}).get(declaration.name) == [declaration]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def plain_class_members(
|
|
122
|
+
context: PythonFileContext,
|
|
123
|
+
declaration: ast.ClassDef,
|
|
124
|
+
provenance: ResourceProvenance,
|
|
125
|
+
*,
|
|
126
|
+
pydantic_fields: bool = False,
|
|
127
|
+
) -> bool:
|
|
128
|
+
return all(
|
|
129
|
+
_plain_class_member(context, member, provenance, pydantic_fields=pydantic_fields) for member in declaration.body
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _plain_class_member(
|
|
134
|
+
context: PythonFileContext, member: ast.stmt, provenance: ResourceProvenance, *, pydantic_fields: bool
|
|
135
|
+
) -> bool:
|
|
136
|
+
match member:
|
|
137
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
138
|
+
if member.name.startswith("__"):
|
|
139
|
+
return False
|
|
140
|
+
return inert_signature(member, context) and all(
|
|
141
|
+
_plain_decorator(context, decorator, provenance, pydantic_fields=pydantic_fields)
|
|
142
|
+
for decorator in member.decorator_list
|
|
143
|
+
)
|
|
144
|
+
case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
|
|
145
|
+
return inert_annotation(annotation, context) and (
|
|
146
|
+
value is None or inert_class_value(context, value, provenance, pydantic_fields=pydantic_fields)
|
|
147
|
+
)
|
|
148
|
+
case ast.Assign(targets=targets, value=value):
|
|
149
|
+
return all(isinstance(target, ast.Name) for target in targets) and inert_class_value(
|
|
150
|
+
context, value, provenance, pydantic_fields=pydantic_fields
|
|
151
|
+
)
|
|
152
|
+
case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
|
|
153
|
+
return True
|
|
154
|
+
case _:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _plain_decorator(
|
|
159
|
+
context: PythonFileContext, decorator: ast.expr, provenance: ResourceProvenance, *, pydantic_fields: bool
|
|
160
|
+
) -> bool:
|
|
161
|
+
if isinstance(decorator, ast.Name) and decorator.id in {"classmethod", "staticmethod", "property"}:
|
|
162
|
+
return context.imports.builtin_is_unshadowed(decorator.id) and provenance.builtin_namespace_untouched
|
|
163
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
164
|
+
qualified = context.module_imports.resolved_qualified_name(target)
|
|
165
|
+
if qualified in {"typing.final", "typing_extensions.final"} and not isinstance(decorator, ast.Call):
|
|
166
|
+
return provenance.imported(target, qualified)
|
|
167
|
+
if (
|
|
168
|
+
not pydantic_fields
|
|
169
|
+
or not isinstance(decorator, ast.Call)
|
|
170
|
+
or qualified
|
|
171
|
+
not in {
|
|
172
|
+
"pydantic.model_validator",
|
|
173
|
+
"pydantic.field_validator",
|
|
174
|
+
"pydantic.functional_validators.model_validator",
|
|
175
|
+
"pydantic.functional_validators.field_validator",
|
|
176
|
+
}
|
|
177
|
+
or not provenance.imported(target, qualified)
|
|
178
|
+
):
|
|
179
|
+
return False
|
|
180
|
+
return all(literal_value(argument) for argument in decorator.args) and all(
|
|
181
|
+
keyword.arg is not None
|
|
182
|
+
and literal_value(keyword.value)
|
|
183
|
+
and not (keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "wrap")
|
|
184
|
+
for keyword in decorator.keywords
|
|
185
|
+
)
|
{sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
@@ -2,23 +2,28 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from dataclasses import dataclass, replace
|
|
5
|
+
from functools import cached_property
|
|
5
6
|
from types import MappingProxyType
|
|
6
7
|
from typing import TYPE_CHECKING, final
|
|
7
8
|
|
|
9
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
10
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
8
11
|
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
12
|
+
from sarj_python_lint.rules._closed_helpers import plain_class_members
|
|
9
13
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
10
|
-
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
14
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef, imports_outside
|
|
15
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
if TYPE_CHECKING:
|
|
14
19
|
from collections.abc import Mapping
|
|
15
|
-
|
|
16
|
-
from sarj_python_lint._file_context import PythonFileContext
|
|
20
|
+
from pathlib import Path
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
type _Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
20
24
|
_MODEL_BASES = frozenset({"pydantic.BaseModel", "pydantic.main.BaseModel"})
|
|
21
25
|
_PYDANTIC = frozenset({"pydantic", "pydantic.functional_validators"})
|
|
26
|
+
_MAX_BEFORE_VALIDATOR_DECORATORS = 2
|
|
22
27
|
_TYPING = frozenset({"typing", "typing_extensions"})
|
|
23
28
|
_COLLECTIONS = frozenset({"collections.abc", "typing", "typing_extensions"})
|
|
24
29
|
_TWO_ARGUMENTS = 2
|
|
@@ -62,6 +67,12 @@ class Mutation:
|
|
|
62
67
|
before_validator: bool
|
|
63
68
|
|
|
64
69
|
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class _ValidationOwner:
|
|
72
|
+
context: PythonFileContext
|
|
73
|
+
provenance: ResourceProvenance
|
|
74
|
+
|
|
75
|
+
|
|
65
76
|
@final
|
|
66
77
|
class _Models:
|
|
67
78
|
def __init__(self, context: PythonFileContext) -> None:
|
|
@@ -81,11 +92,28 @@ class _Models:
|
|
|
81
92
|
},
|
|
82
93
|
)
|
|
83
94
|
)
|
|
95
|
+
if (
|
|
96
|
+
(unit is None or unit.module is None)
|
|
97
|
+
and self.unit.tree is not None
|
|
98
|
+
and any(isinstance(node, ast.ClassDef) for node in self.unit.tree.body)
|
|
99
|
+
):
|
|
100
|
+
self.indexes = self.indexes.with_unit(self.unit)
|
|
101
|
+
self._validation_session = AnalysisSession()
|
|
102
|
+
self._validation_session.project = self.indexes
|
|
103
|
+
self._validation_owners: dict[Path, _ValidationOwner] = {}
|
|
84
104
|
self._classes: dict[SymbolRef, tuple[SourceUnit, ast.ClassDef] | None] = {}
|
|
85
105
|
self._ancestry: dict[SymbolRef, bool] = {}
|
|
86
106
|
self._fields: dict[tuple[SymbolRef, str], _Shape | None] = {}
|
|
87
107
|
self._annotations: dict[str | None, AnnotationSemantics] = {}
|
|
88
108
|
|
|
109
|
+
def validation_owner(self, unit: SourceUnit) -> _ValidationOwner:
|
|
110
|
+
if unit.path not in self._validation_owners:
|
|
111
|
+
context = PythonFileContext(unit.path, unit.source, self._validation_session)
|
|
112
|
+
self._validation_owners[unit.path] = _ValidationOwner(
|
|
113
|
+
context, ResourceProvenance(context, require_static_namespace=True)
|
|
114
|
+
)
|
|
115
|
+
return self._validation_owners[unit.path]
|
|
116
|
+
|
|
89
117
|
def annotations(self, unit: SourceUnit) -> AnnotationSemantics:
|
|
90
118
|
if unit.module not in self._annotations:
|
|
91
119
|
tree = unit.tree if unit.tree is not None else ast.Module(body=[], type_ignores=[])
|
|
@@ -180,6 +208,39 @@ class _Models:
|
|
|
180
208
|
for base in bases
|
|
181
209
|
)
|
|
182
210
|
|
|
211
|
+
def plain_validation(self, symbol: SymbolRef, seen: frozenset[SymbolRef] = frozenset()) -> bool:
|
|
212
|
+
if (
|
|
213
|
+
symbol in seen
|
|
214
|
+
or self.indexes.nominal_membership_is_dynamic(symbol)
|
|
215
|
+
or (resolved := self.class_node(symbol)) is None
|
|
216
|
+
):
|
|
217
|
+
return False
|
|
218
|
+
unit, node = resolved
|
|
219
|
+
if node.decorator_list or (
|
|
220
|
+
unit.module == "__copy_source__" and imports_outside(unit.tree, _PYDANTIC | _TYPING)
|
|
221
|
+
):
|
|
222
|
+
return False
|
|
223
|
+
owner = self.validation_owner(unit)
|
|
224
|
+
if not plain_class_members(owner.context, node, owner.provenance, pydantic_fields=True):
|
|
225
|
+
return False
|
|
226
|
+
if any(
|
|
227
|
+
name
|
|
228
|
+
in {
|
|
229
|
+
"__init_subclass__",
|
|
230
|
+
"__pydantic_init_subclass__",
|
|
231
|
+
"__pydantic_on_complete__",
|
|
232
|
+
"__get_pydantic_core_schema__",
|
|
233
|
+
}
|
|
234
|
+
for name in scope_bound_names(node.body)
|
|
235
|
+
):
|
|
236
|
+
return False
|
|
237
|
+
bases = [self.symbol(base, unit) for base in node.bases]
|
|
238
|
+
return bool(bases) and all(
|
|
239
|
+
base is not None
|
|
240
|
+
and (f"{base.module}.{base.name}" in _MODEL_BASES or self.plain_validation(base, seen | {symbol}))
|
|
241
|
+
for base in bases
|
|
242
|
+
)
|
|
243
|
+
|
|
183
244
|
def shape(
|
|
184
245
|
self,
|
|
185
246
|
annotation: ast.expr | None,
|
|
@@ -314,6 +375,10 @@ class CopyOnWriteAnalysis:
|
|
|
314
375
|
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
315
376
|
self._function(function)
|
|
316
377
|
|
|
378
|
+
@cached_property
|
|
379
|
+
def _validator_provider(self) -> ResourceProvenance:
|
|
380
|
+
return ResourceProvenance(self.context, require_static_namespace=True)
|
|
381
|
+
|
|
317
382
|
def _function(self, function: _Function) -> None:
|
|
318
383
|
parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
|
|
319
384
|
names = {parameter.arg for parameter in parameters}
|
|
@@ -343,9 +408,11 @@ class CopyOnWriteAnalysis:
|
|
|
343
408
|
receiver = self._receiver(function, parameters)
|
|
344
409
|
for parameter in parameters:
|
|
345
410
|
shape = self.models.shape(parameter.annotation, blocked=self._blocked)
|
|
346
|
-
if
|
|
411
|
+
if parameter.arg == before_input:
|
|
412
|
+
bindings[parameter.arg] = _Value(shape, borrowed=True, shared_children=True)
|
|
413
|
+
elif receiver is not None and parameter is parameters[0]:
|
|
347
414
|
bindings[parameter.arg] = receiver
|
|
348
|
-
elif _contains_model(shape)
|
|
415
|
+
elif _contains_model(shape):
|
|
349
416
|
bindings[parameter.arg] = _Value(shape, borrowed=True, shared_children=True)
|
|
350
417
|
return bindings
|
|
351
418
|
|
|
@@ -362,26 +429,76 @@ class CopyOnWriteAnalysis:
|
|
|
362
429
|
|
|
363
430
|
def _validator_input(self, function: _Function) -> str | None:
|
|
364
431
|
parent = self.context.parents.get(function)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
in {"model_validator", "field_validator"}
|
|
371
|
-
and any(
|
|
372
|
-
keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "before"
|
|
373
|
-
for keyword in decorator.keywords
|
|
374
|
-
)
|
|
375
|
-
for decorator in function.decorator_list
|
|
432
|
+
if (
|
|
433
|
+
not isinstance(parent, ast.ClassDef)
|
|
434
|
+
or self.context.parents.get(parent) is not self.context.tree
|
|
435
|
+
or not function.decorator_list
|
|
436
|
+
or self.context.dynamic_namespace
|
|
376
437
|
):
|
|
377
438
|
return None
|
|
439
|
+
decorator = self._before_decorator(function)
|
|
440
|
+
if decorator is None:
|
|
441
|
+
return None
|
|
442
|
+
symbol = self.models.symbol(ast.Name(id=parent.name))
|
|
443
|
+
if symbol is None or not self.models.plain_validation(symbol):
|
|
444
|
+
return None
|
|
445
|
+
if function.name in scope_bound_names([member for member in parent.body if member is not function]):
|
|
446
|
+
return None
|
|
447
|
+
class_bindings = scope_bound_names(parent.body)
|
|
448
|
+
if any(isinstance(node, ast.Name) and node.id in class_bindings for node in ast.walk(decorator.func)):
|
|
449
|
+
return None
|
|
450
|
+
descriptor = self._validator_descriptor(function, class_bindings)
|
|
451
|
+
if descriptor is None:
|
|
452
|
+
return None
|
|
378
453
|
parameters = [*function.args.posonlyargs, *function.args.args]
|
|
379
|
-
|
|
380
|
-
|
|
454
|
+
qualified = self.context.module_imports.resolved_qualified_name(decorator.func)
|
|
455
|
+
if qualified is None or not self._validator_provider.imported(decorator.func, qualified):
|
|
456
|
+
return None
|
|
457
|
+
offset = int(
|
|
458
|
+
descriptor == "builtins.classmethod"
|
|
459
|
+
or (descriptor == "auto" and bool(parameters and parameters[0].arg == "cls"))
|
|
381
460
|
)
|
|
382
|
-
offset = int(is_classmethod or bool(parameters and parameters[0].arg == "cls"))
|
|
383
461
|
return parameters[offset].arg if len(parameters) > offset else None
|
|
384
462
|
|
|
463
|
+
def _before_decorator(self, function: _Function) -> ast.Call | None:
|
|
464
|
+
decorator = function.decorator_list[0]
|
|
465
|
+
if not isinstance(decorator, ast.Call) or len(function.decorator_list) > _MAX_BEFORE_VALIDATOR_DECORATORS:
|
|
466
|
+
return None
|
|
467
|
+
if self.context.module_imports.resolved_symbol(decorator.func, sources=_PYDANTIC) not in {
|
|
468
|
+
"model_validator",
|
|
469
|
+
"field_validator",
|
|
470
|
+
}:
|
|
471
|
+
return None
|
|
472
|
+
return (
|
|
473
|
+
decorator
|
|
474
|
+
if any(
|
|
475
|
+
keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and keyword.value.value == "before"
|
|
476
|
+
for keyword in decorator.keywords
|
|
477
|
+
)
|
|
478
|
+
else None
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def _validator_descriptor(self, function: _Function, class_bindings: set[str]) -> str | None:
|
|
482
|
+
if not self._validator_provider.builtin_namespace_untouched:
|
|
483
|
+
return None
|
|
484
|
+
if len(function.decorator_list) == _MAX_BEFORE_VALIDATOR_DECORATORS:
|
|
485
|
+
inner = function.decorator_list[1]
|
|
486
|
+
qualified = self.context.module_imports.resolved_qualified_name(inner)
|
|
487
|
+
if (
|
|
488
|
+
isinstance(inner, ast.Name)
|
|
489
|
+
and inner.id in {"classmethod", "staticmethod"}
|
|
490
|
+
and (
|
|
491
|
+
inner.id not in class_bindings
|
|
492
|
+
and inner.id not in self.context.module_imports.shadowed_names
|
|
493
|
+
and inner.id not in self.context.module_imports.bindings
|
|
494
|
+
)
|
|
495
|
+
):
|
|
496
|
+
qualified = f"builtins.{inner.id}"
|
|
497
|
+
if qualified not in {"builtins.classmethod", "builtins.staticmethod"}:
|
|
498
|
+
return None
|
|
499
|
+
return qualified
|
|
500
|
+
return "auto"
|
|
501
|
+
|
|
385
502
|
def _value(self, node: ast.expr, bindings: Mapping[str, _Value]) -> _Value | None:
|
|
386
503
|
match node:
|
|
387
504
|
case ast.Name(id=name):
|
{sarj_python_lint-1.31.0 → sarj_python_lint-1.32.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, Final
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
@@ -27,11 +27,13 @@ _DYNAMIC_MEMBERS = frozenset(
|
|
|
27
27
|
"f_builtins",
|
|
28
28
|
}
|
|
29
29
|
)
|
|
30
|
-
_MODULE_APIS = {
|
|
30
|
+
_MODULE_APIS: Final[dict[str, frozenset[str]]] = {
|
|
31
31
|
"sys": frozenset({"modules", "_getframe"}),
|
|
32
|
+
"inspect": frozenset({"currentframe"}),
|
|
32
33
|
"importlib": frozenset({"import_module"}),
|
|
33
34
|
"builtins": _NAMESPACE_APIS,
|
|
34
35
|
}
|
|
36
|
+
_RESOLVED_MEMBERS = _NAMESPACE_APIS | {"modules", "import_module"}
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def dynamic_namespace_access(
|
|
@@ -72,6 +74,8 @@ def _namespace_reference(
|
|
|
72
74
|
if isinstance(node, ast.Attribute):
|
|
73
75
|
if node.attr in _DYNAMIC_MEMBERS:
|
|
74
76
|
return True
|
|
77
|
+
if node.attr not in _RESOLVED_MEMBERS:
|
|
78
|
+
return False
|
|
75
79
|
qualified = imports.resolved_qualified_name(node)
|
|
76
80
|
return qualified in {"sys.modules", "importlib.import_module"} or (
|
|
77
81
|
imports.resolved_symbol(node, sources=frozenset({"builtins"})) in _NAMESPACE_APIS
|