sarj-python-lint 1.28.6__tar.gz → 1.29.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.28.6 → sarj_python_lint-1.29.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__main__.py +55 -21
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_namespace_access.py +84 -0
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/_pydantic_field_contract.py +285 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_registry.py +6 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +49 -19
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +148 -0
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +244 -0
- sarj_python_lint-1.29.0/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +163 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/.gitignore +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/LICENSE +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/README.md +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.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.29.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/
|
|
@@ -177,6 +177,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
177
177
|
"SARJ447": frozenset({"SARJ008"}),
|
|
178
178
|
"SARJ457": frozenset({"SARJ045"}),
|
|
179
179
|
"SARJ465": frozenset({"SARJ071"}),
|
|
180
|
+
"SARJ491": frozenset({"SARJ400"}),
|
|
180
181
|
}
|
|
181
182
|
)
|
|
182
183
|
|
|
@@ -190,6 +191,33 @@ class _OwnerLocation(NamedTuple):
|
|
|
190
191
|
column: int
|
|
191
192
|
|
|
192
193
|
|
|
194
|
+
@dataclass(frozen=True, slots=True)
|
|
195
|
+
class _DiagnosticOwners:
|
|
196
|
+
docstrings: dict[int, _OwnerLocation]
|
|
197
|
+
signatures: dict[int, _OwnerLocation]
|
|
198
|
+
fields: tuple[ast.AnnAssign, ...]
|
|
199
|
+
|
|
200
|
+
def location(self, diagnostic: Diagnostic) -> _OwnerLocation:
|
|
201
|
+
if diagnostic.code in {"SARJ400", "SARJ491"}:
|
|
202
|
+
return _field_owner_location(diagnostic, self.fields)
|
|
203
|
+
owners = (
|
|
204
|
+
self.signatures
|
|
205
|
+
if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ094", "SARJ447"}
|
|
206
|
+
else self.docstrings
|
|
207
|
+
)
|
|
208
|
+
return _OwnerLocation(*owners.get(diagnostic.line, (diagnostic.line, diagnostic.col)))
|
|
209
|
+
|
|
210
|
+
def presence(self, diags: list[Diagnostic]) -> dict[tuple[Path, int, int], dict[str, set[Severity]]]:
|
|
211
|
+
present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
|
|
212
|
+
for diagnostic in diags:
|
|
213
|
+
if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
|
|
214
|
+
continue
|
|
215
|
+
location = self.location(diagnostic)
|
|
216
|
+
by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
|
|
217
|
+
by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
|
|
218
|
+
return present
|
|
219
|
+
|
|
220
|
+
|
|
193
221
|
def deduplicate_diagnostics(
|
|
194
222
|
diags: list[Diagnostic], *, source: str | None = None, context: PythonFileContext | None = None
|
|
195
223
|
) -> list[Diagnostic]:
|
|
@@ -203,30 +231,20 @@ def deduplicate_diagnostics(
|
|
|
203
231
|
or ("SARJ094" in codes and not codes.isdisjoint(_DIAGNOSTIC_PRECEDENCE["SARJ094"]))
|
|
204
232
|
)
|
|
205
233
|
needs_composition_owners = {"SARJ066", "SARJ457"} <= codes
|
|
234
|
+
needs_field_owners = {"SARJ400", "SARJ491"} <= codes
|
|
206
235
|
tree = (
|
|
207
236
|
_diagnostic_owner_tree(source, context)
|
|
208
|
-
if needs_docstring_owners or needs_signature_owners or needs_composition_owners
|
|
237
|
+
if needs_docstring_owners or needs_signature_owners or needs_composition_owners or needs_field_owners
|
|
209
238
|
else None
|
|
210
239
|
)
|
|
211
240
|
diags = _deduplicate_test_composition(diags, tree)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
|
|
220
|
-
return _OwnerLocation(line, column)
|
|
221
|
-
|
|
222
|
-
present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
|
|
223
|
-
for diagnostic in diags:
|
|
224
|
-
if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
|
|
225
|
-
continue
|
|
226
|
-
location = owner_location(diagnostic)
|
|
227
|
-
by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
|
|
228
|
-
by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
|
|
229
|
-
suppressed = _suppressed_diagnostics(present)
|
|
241
|
+
owners = _DiagnosticOwners(
|
|
242
|
+
docstrings=_docstring_owner_locations(tree) if needs_docstring_owners else {},
|
|
243
|
+
signatures=_function_signature_owner_locations(tree) if needs_signature_owners else {},
|
|
244
|
+
fields=_field_owners(tree) if needs_field_owners else (),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
suppressed = _suppressed_diagnostics(owners.presence(diags))
|
|
230
248
|
return [
|
|
231
249
|
diagnostic
|
|
232
250
|
for diagnostic in diags
|
|
@@ -234,8 +252,8 @@ def deduplicate_diagnostics(
|
|
|
234
252
|
(
|
|
235
253
|
(
|
|
236
254
|
diagnostic.path,
|
|
237
|
-
|
|
238
|
-
|
|
255
|
+
owners.location(diagnostic).line,
|
|
256
|
+
owners.location(diagnostic).column,
|
|
239
257
|
),
|
|
240
258
|
diagnostic.code,
|
|
241
259
|
diagnostic.severity,
|
|
@@ -255,6 +273,22 @@ def _diagnostic_owner_tree(source: str | None, context: PythonFileContext | None
|
|
|
255
273
|
return None
|
|
256
274
|
|
|
257
275
|
|
|
276
|
+
def _field_owners(tree: ast.Module | None) -> tuple[ast.AnnAssign, ...]:
|
|
277
|
+
if tree is None:
|
|
278
|
+
return ()
|
|
279
|
+
return tuple(node for node in ast.walk(tree) if isinstance(node, ast.AnnAssign))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _field_owner_location(diagnostic: Diagnostic, fields: tuple[ast.AnnAssign, ...]) -> _OwnerLocation:
|
|
283
|
+
location = (diagnostic.line, diagnostic.col - 1)
|
|
284
|
+
for field in fields:
|
|
285
|
+
start = (field.lineno, field.col_offset)
|
|
286
|
+
end = (field.end_lineno or field.lineno, field.end_col_offset or field.col_offset)
|
|
287
|
+
if start <= location < end:
|
|
288
|
+
return _OwnerLocation(field.lineno, field.col_offset + 1)
|
|
289
|
+
return _OwnerLocation(diagnostic.line, diagnostic.col)
|
|
290
|
+
|
|
291
|
+
|
|
258
292
|
def _deduplicate_test_composition(diags: list[Diagnostic], tree: ast.Module | None) -> list[Diagnostic]:
|
|
259
293
|
codes = {diagnostic.code for diagnostic in diags}
|
|
260
294
|
if tree is None or not {"SARJ066", "SARJ457"} <= codes:
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterable, Mapping
|
|
9
|
+
|
|
10
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_NAMESPACE_APIS = frozenset({"globals", "locals", "vars", "exec", "eval", "__import__"})
|
|
14
|
+
_DYNAMIC_MEMBERS = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"__dict__",
|
|
17
|
+
"__globals__",
|
|
18
|
+
"__code__",
|
|
19
|
+
"__setattr__",
|
|
20
|
+
"__delattr__",
|
|
21
|
+
"__getattribute__",
|
|
22
|
+
"f_globals",
|
|
23
|
+
"f_locals",
|
|
24
|
+
"f_builtins",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
_MODULE_APIS = {
|
|
28
|
+
"sys": frozenset({"modules", "_getframe"}),
|
|
29
|
+
"importlib": frozenset({"import_module"}),
|
|
30
|
+
"builtins": _NAMESPACE_APIS,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dynamic_namespace_access(
|
|
35
|
+
nodes: Iterable[ast.AST], imports: ImportIndex, parents: Mapping[ast.AST, ast.AST]
|
|
36
|
+
) -> bool:
|
|
37
|
+
indexed = tuple(nodes)
|
|
38
|
+
aliases = _namespace_api_aliases(indexed)
|
|
39
|
+
modules = _namespace_module_bindings(indexed)
|
|
40
|
+
return any(_namespace_reference(node, aliases, modules, imports, parents) for node in indexed)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _namespace_api_aliases(nodes: Iterable[ast.AST]) -> set[str]:
|
|
44
|
+
aliases = set(_NAMESPACE_APIS) | {"__builtins__"}
|
|
45
|
+
for node in nodes:
|
|
46
|
+
if not isinstance(node, ast.ImportFrom) or node.module not in _MODULE_APIS:
|
|
47
|
+
continue
|
|
48
|
+
members = _MODULE_APIS[node.module]
|
|
49
|
+
aliases.update(alias.asname or alias.name for alias in node.names if alias.name in members)
|
|
50
|
+
return aliases
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _namespace_module_bindings(nodes: Iterable[ast.AST]) -> dict[str, str]:
|
|
54
|
+
modules: dict[str, str] = {}
|
|
55
|
+
for node in nodes:
|
|
56
|
+
if not isinstance(node, ast.Import):
|
|
57
|
+
continue
|
|
58
|
+
modules.update({alias.asname or alias.name: alias.name for alias in node.names if alias.name in _MODULE_APIS})
|
|
59
|
+
return modules
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _namespace_reference(
|
|
63
|
+
node: ast.AST,
|
|
64
|
+
aliases: set[str],
|
|
65
|
+
modules: dict[str, str],
|
|
66
|
+
imports: ImportIndex,
|
|
67
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
68
|
+
) -> bool:
|
|
69
|
+
if isinstance(node, ast.Attribute):
|
|
70
|
+
if node.attr in _DYNAMIC_MEMBERS:
|
|
71
|
+
return True
|
|
72
|
+
qualified = imports.resolved_qualified_name(node)
|
|
73
|
+
return qualified in {"sys.modules", "importlib.import_module"} or (
|
|
74
|
+
imports.resolved_symbol(node, sources=frozenset({"builtins"})) in _NAMESPACE_APIS
|
|
75
|
+
)
|
|
76
|
+
if not isinstance(node, ast.Name) or not isinstance(node.ctx, ast.Load):
|
|
77
|
+
return False
|
|
78
|
+
if node.id in aliases:
|
|
79
|
+
return True
|
|
80
|
+
module = modules.get(node.id)
|
|
81
|
+
if module is None:
|
|
82
|
+
return False
|
|
83
|
+
parent = parents.get(node)
|
|
84
|
+
return not isinstance(parent, ast.Attribute) or parent.value is not node or parent.attr in _MODULE_APIS[module]
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
import tomllib
|
|
7
|
+
from typing import TYPE_CHECKING, TypeIs
|
|
8
|
+
|
|
9
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
10
|
+
from packaging.version import InvalidVersion, Version
|
|
11
|
+
|
|
12
|
+
from sarj_python_lint.json_boundary import is_object_mapping
|
|
13
|
+
from sarj_python_lint.rules._first_party import distribution_root
|
|
14
|
+
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
|
|
22
|
+
FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
|
|
23
|
+
DECORATOR_SOURCES = frozenset({"pydantic", "pydantic.functional_validators", "pydantic.functional_serializers"})
|
|
24
|
+
TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
25
|
+
_ALIAS_VERSION = Version("2.11")
|
|
26
|
+
_BUILTIN_BINDINGS = frozenset({"int", "float", "str", "list", "classmethod", "staticmethod"})
|
|
27
|
+
_SENSITIVE_MODULES = frozenset({"pydantic", "typing", "typing_extensions", "builtins"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def direct_models(context: PythonFileContext, *, methods: bool = False) -> tuple[ast.ClassDef, ...]:
|
|
31
|
+
tree = context.tree
|
|
32
|
+
if tree is None or context.generated or context.has_wildcard_import:
|
|
33
|
+
return ()
|
|
34
|
+
if (
|
|
35
|
+
_mutated_imports(context)
|
|
36
|
+
or _ambiguous_imports(context)
|
|
37
|
+
or _escaped_imports(context)
|
|
38
|
+
or dynamic_namespace_access(context.nodes(ast.AST), context.imports, context.parents)
|
|
39
|
+
):
|
|
40
|
+
return ()
|
|
41
|
+
return tuple(
|
|
42
|
+
node
|
|
43
|
+
for node in tree.body
|
|
44
|
+
if isinstance(node, ast.ClassDef)
|
|
45
|
+
and len(node.bases) == 1
|
|
46
|
+
and context.imports.resolves(node.bases[0], sources=MODEL_SOURCES, symbol="BaseModel")
|
|
47
|
+
and not node.decorator_list
|
|
48
|
+
and not node.keywords
|
|
49
|
+
and not node.type_params
|
|
50
|
+
and all(_plain_member(member, context, methods=methods) for member in node.body)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _plain_member(node: ast.stmt, context: PythonFileContext, *, methods: bool) -> bool:
|
|
55
|
+
match node:
|
|
56
|
+
case ast.AnnAssign(target=ast.Name(id=name), annotation=annotation, value=value):
|
|
57
|
+
return (
|
|
58
|
+
not name.startswith("__")
|
|
59
|
+
and _inert_annotation(annotation, context)
|
|
60
|
+
and (value is None or _inert_expr(value, context))
|
|
61
|
+
)
|
|
62
|
+
case ast.Assign(targets=targets, value=value):
|
|
63
|
+
return all(
|
|
64
|
+
isinstance(target, ast.Name) and not target.id.startswith("__") for target in targets
|
|
65
|
+
) and _inert_expr(value, context)
|
|
66
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name):
|
|
67
|
+
return (
|
|
68
|
+
methods and not name.startswith("__") and name != "model_post_init" and _inert_signature(node, context)
|
|
69
|
+
)
|
|
70
|
+
case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
|
|
71
|
+
return True
|
|
72
|
+
case _:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _inert_annotation(node: ast.expr, context: PythonFileContext) -> bool:
|
|
77
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
78
|
+
try:
|
|
79
|
+
node = ast.parse(node.value, mode="eval").body
|
|
80
|
+
except SyntaxError:
|
|
81
|
+
return False
|
|
82
|
+
return _inert_expr(node, context)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _inert_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
86
|
+
if node.type_params:
|
|
87
|
+
return False
|
|
88
|
+
arguments = (
|
|
89
|
+
*node.args.posonlyargs,
|
|
90
|
+
*node.args.args,
|
|
91
|
+
*node.args.kwonlyargs,
|
|
92
|
+
*((node.args.vararg,) if node.args.vararg else ()),
|
|
93
|
+
*((node.args.kwarg,) if node.args.kwarg else ()),
|
|
94
|
+
)
|
|
95
|
+
annotations = [argument.annotation for argument in arguments if argument.annotation is not None]
|
|
96
|
+
if node.returns is not None:
|
|
97
|
+
annotations.append(node.returns)
|
|
98
|
+
defaults = (*node.args.defaults, *(value for value in node.args.kw_defaults if value is not None))
|
|
99
|
+
return all(_inert_annotation(annotation, context) for annotation in annotations) and all(
|
|
100
|
+
_inert_expr(value, context) for value in defaults
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
|
|
105
|
+
if isinstance(node, ast.Lambda):
|
|
106
|
+
return not node.args.defaults and not node.args.kw_defaults
|
|
107
|
+
if isinstance(node, ast.Call):
|
|
108
|
+
target = context.imports.resolved_qualified_name(node.func)
|
|
109
|
+
if target not in {
|
|
110
|
+
"pydantic.Field",
|
|
111
|
+
"pydantic.fields.Field",
|
|
112
|
+
"pydantic.ConfigDict",
|
|
113
|
+
"pydantic.config.ConfigDict",
|
|
114
|
+
}:
|
|
115
|
+
return False
|
|
116
|
+
if any(keyword.arg is None for keyword in node.keywords) or any(
|
|
117
|
+
isinstance(argument, ast.Starred) for argument in node.args
|
|
118
|
+
):
|
|
119
|
+
return False
|
|
120
|
+
if isinstance(
|
|
121
|
+
node,
|
|
122
|
+
(ast.NamedExpr, ast.Await, ast.Yield, ast.YieldFrom, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp),
|
|
123
|
+
):
|
|
124
|
+
return False
|
|
125
|
+
return all(_inert_expr(child, context) for child in ast.iter_child_nodes(node))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _mutated_imports(context: PythonFileContext) -> bool:
|
|
129
|
+
for node in context.nodes(ast.Attribute, ast.Subscript, ast.Call):
|
|
130
|
+
if isinstance(node, (ast.Attribute, ast.Subscript)) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
131
|
+
if _sensitive_import_root(node, context):
|
|
132
|
+
return True
|
|
133
|
+
elif (
|
|
134
|
+
isinstance(node, ast.Call)
|
|
135
|
+
and node.args
|
|
136
|
+
and _is_attribute_mutator(node.func, context)
|
|
137
|
+
and _sensitive_import_root(node.args[0], context)
|
|
138
|
+
):
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _sensitive_import_root(node: ast.expr, context: PythonFileContext) -> bool:
|
|
144
|
+
while isinstance(node, (ast.Attribute, ast.Subscript)):
|
|
145
|
+
node = node.value
|
|
146
|
+
binding = context.module_imports.bindings.get(node.id) if isinstance(node, ast.Name) else None
|
|
147
|
+
return binding is not None and binding.module.partition(".")[0] in _SENSITIVE_MODULES
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_attribute_mutator(node: ast.expr, context: PythonFileContext) -> bool:
|
|
151
|
+
names = {"setattr", "delattr"}
|
|
152
|
+
return (
|
|
153
|
+
isinstance(node, ast.Name) and node.id in names and context.imports.builtin_is_unshadowed(node.id)
|
|
154
|
+
) or context.imports.resolved_symbol(node, sources=frozenset({"builtins"})) in names
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _escaped_imports(context: PythonFileContext) -> bool:
|
|
158
|
+
for node in context.nodes(ast.Name, ast.Attribute):
|
|
159
|
+
if not isinstance(node.ctx, ast.Load) or not _sensitive_import_root(node, context):
|
|
160
|
+
continue
|
|
161
|
+
parent = context.parents.get(node)
|
|
162
|
+
if isinstance(parent, ast.Attribute) and parent.value is node:
|
|
163
|
+
continue
|
|
164
|
+
if (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or _type_reference(node, parent):
|
|
165
|
+
continue
|
|
166
|
+
return True
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _type_reference(node: ast.expr, parent: ast.AST | None) -> bool:
|
|
171
|
+
match parent:
|
|
172
|
+
case ast.Call(func=func):
|
|
173
|
+
return func is node
|
|
174
|
+
case ast.ClassDef(bases=bases):
|
|
175
|
+
return node in bases
|
|
176
|
+
case ast.Subscript(value=value):
|
|
177
|
+
return value is node
|
|
178
|
+
case ast.arg(annotation=annotation) | ast.AnnAssign(annotation=annotation):
|
|
179
|
+
return annotation is node
|
|
180
|
+
case ast.FunctionDef(returns=annotation) | ast.AsyncFunctionDef(returns=annotation):
|
|
181
|
+
return annotation is node
|
|
182
|
+
case _:
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _ambiguous_imports(context: PythonFileContext) -> bool:
|
|
187
|
+
return any(
|
|
188
|
+
_nested_namespace_import(node, context) or _ambiguous_import(node, context)
|
|
189
|
+
for node in context.nodes(ast.Import, ast.ImportFrom)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _nested_namespace_import(node: ast.Import | ast.ImportFrom, context: PythonFileContext) -> bool:
|
|
194
|
+
if context.parents.get(node) is context.tree:
|
|
195
|
+
return False
|
|
196
|
+
if isinstance(node, ast.ImportFrom):
|
|
197
|
+
return (node.module or "").partition(".")[0] in _SENSITIVE_MODULES
|
|
198
|
+
return any(alias.name.partition(".")[0] in _SENSITIVE_MODULES for alias in node.names)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _ambiguous_import(node: ast.Import | ast.ImportFrom, context: PythonFileContext) -> bool:
|
|
202
|
+
for alias in node.names:
|
|
203
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
204
|
+
if name in _BUILTIN_BINDINGS:
|
|
205
|
+
return True
|
|
206
|
+
binding = context.module_imports.bindings.get(name)
|
|
207
|
+
if binding is None or binding.module.partition(".")[0] not in {"pydantic", "typing", "typing_extensions"}:
|
|
208
|
+
continue
|
|
209
|
+
if context.parents.get(node) is not context.tree:
|
|
210
|
+
return True
|
|
211
|
+
if isinstance(node, ast.ImportFrom) and (
|
|
212
|
+
node.level != 0 or node.module != binding.module or alias.name != binding.symbol
|
|
213
|
+
):
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def unique_members(model: ast.ClassDef) -> frozenset[str]:
|
|
219
|
+
names: list[str] = []
|
|
220
|
+
for member in model.body:
|
|
221
|
+
match member:
|
|
222
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.AnnAssign(target=ast.Name(id=name)):
|
|
223
|
+
names.append(name)
|
|
224
|
+
case ast.Assign(targets=targets):
|
|
225
|
+
names.extend(target.id for target in targets if isinstance(target, ast.Name))
|
|
226
|
+
case _:
|
|
227
|
+
pass
|
|
228
|
+
return frozenset(name for name, count in Counter(names).items() if count == 1)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def has_alias_validation_options(context: PythonFileContext) -> bool:
|
|
232
|
+
root = distribution_root(context.path, facts=context.session.first_party)
|
|
233
|
+
if root is None:
|
|
234
|
+
return False
|
|
235
|
+
try:
|
|
236
|
+
source = (root / "pyproject.toml").read_text(encoding="utf-8")
|
|
237
|
+
except OSError:
|
|
238
|
+
return False
|
|
239
|
+
return _declares_alias_validation_options(source)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@lru_cache(maxsize=256)
|
|
243
|
+
def _declares_alias_validation_options(source: str) -> bool:
|
|
244
|
+
try:
|
|
245
|
+
document = _parse_toml(source)
|
|
246
|
+
except tomllib.TOMLDecodeError:
|
|
247
|
+
return False
|
|
248
|
+
if not is_object_mapping(document):
|
|
249
|
+
return False
|
|
250
|
+
project = document.get("project")
|
|
251
|
+
if not is_object_mapping(project):
|
|
252
|
+
return False
|
|
253
|
+
dependencies = project.get("dependencies")
|
|
254
|
+
if not _is_object_list(dependencies):
|
|
255
|
+
return False
|
|
256
|
+
return any(_alias_dependency(raw) for raw in dependencies)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _alias_dependency(raw: object) -> bool:
|
|
260
|
+
if not isinstance(raw, str):
|
|
261
|
+
return False
|
|
262
|
+
try:
|
|
263
|
+
requirement = Requirement(raw)
|
|
264
|
+
except InvalidRequirement:
|
|
265
|
+
return False
|
|
266
|
+
if requirement.name.casefold() != "pydantic" or requirement.marker is not None or requirement.url is not None:
|
|
267
|
+
return False
|
|
268
|
+
for specifier in requirement.specifier:
|
|
269
|
+
if specifier.operator not in {">", ">=", "==", "===", "~="}:
|
|
270
|
+
continue
|
|
271
|
+
try:
|
|
272
|
+
version = Version(specifier.version.removesuffix(".*"))
|
|
273
|
+
except InvalidVersion:
|
|
274
|
+
continue
|
|
275
|
+
if version >= _ALIAS_VERSION and version.major == _ALIAS_VERSION.major:
|
|
276
|
+
return True
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _parse_toml(source: str) -> object:
|
|
281
|
+
return tomllib.loads(source)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _is_object_list(value: object) -> TypeIs[list[object]]:
|
|
285
|
+
return isinstance(value, list)
|
|
@@ -209,12 +209,18 @@ if TYPE_CHECKING:
|
|
|
209
209
|
from sarj_python_lint.rule_base import Rule
|
|
210
210
|
from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
|
|
211
211
|
from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
|
|
212
|
+
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
213
|
+
from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
|
|
214
|
+
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
212
215
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
213
216
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
214
217
|
|
|
215
218
|
|
|
216
219
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
217
220
|
{
|
|
221
|
+
NoConflictingPydanticAliasValidation.id: NoConflictingPydanticAliasValidation,
|
|
222
|
+
NoUnknownPydanticValidatorField.id: NoUnknownPydanticValidatorField,
|
|
223
|
+
NoImpossiblePydanticFieldBounds.id: NoImpossiblePydanticFieldBounds,
|
|
218
224
|
RequireInjectableRetrySleep.id: RequireInjectableRetrySleep,
|
|
219
225
|
NoFixedSleepInLoop.id: NoFixedSleepInLoop,
|
|
220
226
|
NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
|
|
@@ -33,6 +33,7 @@ _PYDANTIC_SOURCES = frozenset({"pydantic"})
|
|
|
33
33
|
_MAPPING_SOURCES = frozenset({"collections.abc", "typing"})
|
|
34
34
|
_MAPPING_NAMES = frozenset({"dict", "Dict", "Mapping", "MutableMapping"})
|
|
35
35
|
_MAPPING_ARGUMENT_COUNT = 2
|
|
36
|
+
_BUILTIN_TYPE_CONTAINERS = frozenset({"dict", "list", "set", "frozenset", "tuple", "type"})
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@final
|
|
@@ -65,6 +66,7 @@ class NoAnyMappingTypes(Rule):
|
|
|
65
66
|
"used in a module-level annotation; imported, conditional, rebound, and ambiguous aliases are excluded."
|
|
66
67
|
),
|
|
67
68
|
"Generated and vendored sources are excluded; exact local suppressions remain auditable.",
|
|
69
|
+
"Annotated metadata, Literal values and unresolved generic parameter positions are excluded; only type arguments are inspected.",
|
|
68
70
|
),
|
|
69
71
|
examples=(
|
|
70
72
|
RuleExample(
|
|
@@ -117,7 +119,7 @@ class NoAnyMappingTypes(Rule):
|
|
|
117
119
|
seen: set[tuple[int, int]] = set()
|
|
118
120
|
expressions = [
|
|
119
121
|
*_type_expressions(tree, imports, parents, scope_cache, node_index=context.node_index),
|
|
120
|
-
*_implicit_mapping_aliases(tree),
|
|
122
|
+
*_implicit_mapping_aliases(tree, semantics),
|
|
121
123
|
]
|
|
122
124
|
for expression, owner in expressions:
|
|
123
125
|
for mapping, location in _unshadowed_any_mappings(expression, owner, semantics, parents, scope_cache):
|
|
@@ -140,7 +142,7 @@ class NoAnyMappingTypes(Rule):
|
|
|
140
142
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
141
143
|
|
|
142
144
|
|
|
143
|
-
def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assign]]:
|
|
145
|
+
def _implicit_mapping_aliases(tree: ast.Module, semantics: AnnotationSemantics) -> list[tuple[ast.expr, ast.Assign]]:
|
|
144
146
|
bindings = [scope_bound_names([statement]) for statement in tree.body]
|
|
145
147
|
aliases: list[tuple[ast.expr, ast.Assign]] = []
|
|
146
148
|
for statement in tree.body:
|
|
@@ -151,14 +153,14 @@ def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assi
|
|
|
151
153
|
continue
|
|
152
154
|
if sum(target.id in names for names in bindings) != 1:
|
|
153
155
|
continue
|
|
154
|
-
if _used_in_module_annotation(tree, target.id):
|
|
156
|
+
if _used_in_module_annotation(tree, target.id, semantics):
|
|
155
157
|
aliases.append((statement.value, statement))
|
|
156
158
|
return aliases
|
|
157
159
|
|
|
158
160
|
|
|
159
|
-
def _used_in_module_annotation(tree: ast.Module, name: str) -> bool:
|
|
161
|
+
def _used_in_module_annotation(tree: ast.Module, name: str, semantics: AnnotationSemantics) -> bool:
|
|
160
162
|
return any(
|
|
161
|
-
_annotation_contains_name(annotation, name)
|
|
163
|
+
_annotation_contains_name(annotation, name, semantics)
|
|
162
164
|
for statement in tree.body
|
|
163
165
|
for annotation in _module_statement_annotations(statement, name)
|
|
164
166
|
)
|
|
@@ -179,14 +181,17 @@ def _module_statement_annotations(statement: ast.stmt, alias_name: str) -> list[
|
|
|
179
181
|
return annotations
|
|
180
182
|
|
|
181
183
|
|
|
182
|
-
def _annotation_contains_name(annotation: ast.expr, name: str) -> bool:
|
|
183
|
-
parsed = annotation
|
|
184
|
-
if
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
def _annotation_contains_name(annotation: ast.expr, name: str, semantics: AnnotationSemantics) -> bool:
|
|
185
|
+
parsed = semantics.parse(annotation)
|
|
186
|
+
if parsed is None:
|
|
187
|
+
return False
|
|
188
|
+
pending = [parsed]
|
|
189
|
+
while pending:
|
|
190
|
+
node = pending.pop()
|
|
191
|
+
if isinstance(node, ast.Name) and node.id == name:
|
|
192
|
+
return True
|
|
193
|
+
pending.extend(_type_children(node, semantics))
|
|
194
|
+
return False
|
|
190
195
|
|
|
191
196
|
|
|
192
197
|
def _unshadowed_any_mappings(
|
|
@@ -320,7 +325,14 @@ def _any_mappings(annotation: ast.expr, semantics: AnnotationSemantics) -> list[
|
|
|
320
325
|
resolved = semantics.parse(annotation)
|
|
321
326
|
if resolved is None:
|
|
322
327
|
return []
|
|
323
|
-
|
|
328
|
+
pending = [resolved]
|
|
329
|
+
mappings: list[ast.Subscript] = []
|
|
330
|
+
while pending:
|
|
331
|
+
node = pending.pop()
|
|
332
|
+
if isinstance(node, ast.Subscript) and _is_any_mapping(node, semantics):
|
|
333
|
+
mappings.append(node)
|
|
334
|
+
pending.extend(_type_children(node, semantics))
|
|
335
|
+
return mappings
|
|
324
336
|
|
|
325
337
|
|
|
326
338
|
def _is_any_mapping(node: ast.Subscript, semantics: AnnotationSemantics) -> bool:
|
|
@@ -355,11 +367,29 @@ def _contains_any(
|
|
|
355
367
|
return True
|
|
356
368
|
if isinstance(parsed, ast.Name) and parsed.id in semantics.aliases and parsed.id not in seen:
|
|
357
369
|
return _contains_any(semantics.aliases[parsed.id], semantics, seen=seen | {parsed.id})
|
|
358
|
-
return any(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
)
|
|
370
|
+
return any(_contains_any(child, semantics, seen=seen) for child in _type_children(parsed, semantics))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _type_children(node: ast.expr, semantics: AnnotationSemantics) -> tuple[ast.expr, ...]:
|
|
374
|
+
if isinstance(node, ast.Subscript):
|
|
375
|
+
imports = semantics.imports
|
|
376
|
+
symbol = imports.resolved_symbol(node.value, sources=_TYPING_SOURCES | _MAPPING_SOURCES)
|
|
377
|
+
if symbol == "Literal":
|
|
378
|
+
return ()
|
|
379
|
+
members = tuple(node.slice.elts) if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
|
380
|
+
if symbol == "Annotated":
|
|
381
|
+
return members[:1]
|
|
382
|
+
builtin = (
|
|
383
|
+
isinstance(node.value, ast.Name)
|
|
384
|
+
and node.value.id in _BUILTIN_TYPE_CONTAINERS
|
|
385
|
+
and imports.builtin_is_unshadowed(node.value.id)
|
|
386
|
+
)
|
|
387
|
+
return members if builtin or symbol is not None else ()
|
|
388
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
389
|
+
return node.left, node.right
|
|
390
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
391
|
+
return tuple(node.elts)
|
|
392
|
+
return ()
|
|
363
393
|
|
|
364
394
|
|
|
365
395
|
def _is_vendor_path(path: Path) -> bool:
|