sarj-python-lint 1.28.6__tar.gz → 1.29.1__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.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/__main__.py +55 -21
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_exports.py +27 -0
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_namespace_access.py +84 -0
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/_pydantic_field_contract.py +285 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_pytest.py +16 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_registry.py +6 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +2 -12
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +49 -19
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +148 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +2 -24
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +2 -12
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +2 -23
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +244 -0
- sarj_python_lint-1.29.1/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +163 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/.gitignore +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/LICENSE +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/README.md +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/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.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.28.6 → sarj_python_lint-1.29.1}/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.1
|
|
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,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mentions_dunder_all(node: ast.AST) -> bool:
|
|
9
|
+
for child in walk(node):
|
|
10
|
+
match child:
|
|
11
|
+
case ast.Name(id="__all__"):
|
|
12
|
+
return True
|
|
13
|
+
case ast.alias() if (child.asname or child.name).split(".")[0] == "__all__":
|
|
14
|
+
return True
|
|
15
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef() if child.name == "__all__":
|
|
16
|
+
return True
|
|
17
|
+
case ast.arg(arg="__all__") | ast.ExceptHandler(name="__all__"):
|
|
18
|
+
return True
|
|
19
|
+
case ast.MatchAs() | ast.MatchStar() if child.name == "__all__":
|
|
20
|
+
return True
|
|
21
|
+
case ast.MatchMapping(rest="__all__"):
|
|
22
|
+
return True
|
|
23
|
+
case ast.Global() | ast.Nonlocal() if "__all__" in child.names:
|
|
24
|
+
return True
|
|
25
|
+
case _:
|
|
26
|
+
pass
|
|
27
|
+
return False
|
|
@@ -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)
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from sarj_python_lint.rules._ast_index import walk
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def collected_tests(statements: list[ast.stmt]) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
14
|
+
for statement in statements:
|
|
15
|
+
match statement:
|
|
16
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
|
|
17
|
+
yield statement
|
|
18
|
+
case ast.ClassDef() if statement.name.startswith("Test"):
|
|
19
|
+
yield from collected_tests(statement.body)
|
|
20
|
+
case _:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
8
24
|
# The pytest-benchmark fixture: the test measures time, it does not verify.
|
|
9
25
|
_BENCHMARK = "benchmark"
|
|
10
26
|
|
|
@@ -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,
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
)
|
|
21
21
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
22
22
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
23
|
+
from sarj_python_lint.rules._pytest import collected_tests
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -224,24 +225,13 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
224
225
|
severity=Severity.WARNING,
|
|
225
226
|
message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
|
|
226
227
|
)
|
|
227
|
-
for test in
|
|
228
|
+
for test in collected_tests(tree.body)
|
|
228
229
|
if not _is_pytest_fixture(test, imports)
|
|
229
230
|
for call in _uncovered_calls(_test_imports(imports, test), test, writes)
|
|
230
231
|
if not is_suppressed(lines, call.lineno, self.code)
|
|
231
232
|
]
|
|
232
233
|
|
|
233
234
|
|
|
234
|
-
def _collected_tests(statements: list[ast.stmt]) -> Iterator[_TestFunction]:
|
|
235
|
-
for statement in statements:
|
|
236
|
-
match statement:
|
|
237
|
-
case ast.FunctionDef() | ast.AsyncFunctionDef() if statement.name.startswith("test"):
|
|
238
|
-
yield statement
|
|
239
|
-
case ast.ClassDef() if statement.name.startswith("Test"):
|
|
240
|
-
yield from _collected_tests(statement.body)
|
|
241
|
-
case _:
|
|
242
|
-
pass
|
|
243
|
-
|
|
244
|
-
|
|
245
235
|
def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
|
|
246
236
|
return any(
|
|
247
237
|
imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)
|