sarj-python-lint 1.32.0__tar.gz → 1.34.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.32.0 → sarj_python_lint-1.34.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_python_target.py +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_ratchet_cli.py +2 -3
- sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_inert_model_annotation.py +160 -0
- sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_resolved_operation_signature.py +102 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_resource_provenance.py +7 -2
- sarj_python_lint-1.34.0/src/sarj_python_lint/rules/_static_json_alias.py +142 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +45 -12
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +23 -5
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +60 -7
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +3 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +5 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +26 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +9 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +6 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +71 -9
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +4 -4
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_docstring.py +15 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +9 -97
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +3 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +74 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +16 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +67 -7
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/.gitignore +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/LICENSE +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/README.md +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.34.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.34.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/
|
|
@@ -99,7 +99,7 @@ def _project_declaration(path: Path) -> str | None:
|
|
|
99
99
|
return declaration if isinstance(declaration, str) else None
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def _is_toml_table(value: object) -> TypeGuard[dict[
|
|
102
|
+
def _is_toml_table(value: object) -> TypeGuard[dict[object, object]]:
|
|
103
103
|
return isinstance(value, dict)
|
|
104
104
|
|
|
105
105
|
|
|
@@ -148,7 +148,7 @@ def _ruff_selector_aliases() -> dict[str, str]:
|
|
|
148
148
|
raise TypeError(msg)
|
|
149
149
|
aliases: dict[str, str] = {}
|
|
150
150
|
for raw_item in raw:
|
|
151
|
-
if not
|
|
151
|
+
if not _is_object_mapping(raw_item):
|
|
152
152
|
msg = "Ruff rule catalog contains a non-object entry"
|
|
153
153
|
raise TypeError(msg)
|
|
154
154
|
code = raw_item.get("code")
|
|
@@ -173,8 +173,7 @@ def _is_object_list(value: object) -> TypeGuard[list[object]]:
|
|
|
173
173
|
return isinstance(value, list)
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def
|
|
177
|
-
# JSON object keys are strings by definition; json.loads supplied `value`.
|
|
176
|
+
def _is_object_mapping(value: object) -> TypeGuard[dict[object, object]]:
|
|
178
177
|
return isinstance(value, dict)
|
|
179
178
|
|
|
180
179
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_BUILTINS = frozenset({"bool", "bytes", "dict", "float", "frozenset", "int", "list", "object", "set", "str", "tuple"})
|
|
12
|
+
_QUALIFIED_BUILTINS = frozenset(f"builtins.{name}" for name in _BUILTINS)
|
|
13
|
+
_MAX_ANNOTATION_LENGTH = 4096
|
|
14
|
+
_CANONICAL_TYPES = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"typing.Annotated",
|
|
17
|
+
"typing.Any",
|
|
18
|
+
"typing.ClassVar",
|
|
19
|
+
"typing.Literal",
|
|
20
|
+
"typing.Optional",
|
|
21
|
+
"typing.Union",
|
|
22
|
+
"typing_extensions.Annotated",
|
|
23
|
+
"typing_extensions.ClassVar",
|
|
24
|
+
"typing_extensions.Literal",
|
|
25
|
+
"pydantic.PositiveInt",
|
|
26
|
+
"pydantic.NonNegativeInt",
|
|
27
|
+
"pydantic.types.PositiveInt",
|
|
28
|
+
"pydantic.types.NonNegativeInt",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
_LITERAL_CONSTRAINTS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"pydantic.Field",
|
|
34
|
+
"pydantic.fields.Field",
|
|
35
|
+
"annotated_types.Ge",
|
|
36
|
+
"annotated_types.Gt",
|
|
37
|
+
"annotated_types.Le",
|
|
38
|
+
"annotated_types.Lt",
|
|
39
|
+
"annotated_types.MultipleOf",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def inert_model_annotation(node: ast.expr, imports: ImportIndex, *, remaining: int = 64) -> bool:
|
|
45
|
+
if remaining <= 0:
|
|
46
|
+
return False
|
|
47
|
+
match node:
|
|
48
|
+
case ast.Constant(value=str() as source):
|
|
49
|
+
if len(source) > _MAX_ANNOTATION_LENGTH:
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
parsed = ast.parse(source, mode="eval").body
|
|
53
|
+
except SyntaxError, ValueError, RecursionError:
|
|
54
|
+
return False
|
|
55
|
+
return inert_model_annotation(parsed, imports, remaining=remaining - 1)
|
|
56
|
+
case ast.Constant(value=None):
|
|
57
|
+
return True
|
|
58
|
+
case ast.Name(id=name):
|
|
59
|
+
return (name in _BUILTINS and imports.builtin_is_unshadowed(name)) or imports.resolved_qualified_name(
|
|
60
|
+
node
|
|
61
|
+
) in _CANONICAL_TYPES
|
|
62
|
+
case ast.Attribute():
|
|
63
|
+
qualified = imports.resolved_qualified_name(node)
|
|
64
|
+
return qualified in _CANONICAL_TYPES or qualified in _QUALIFIED_BUILTINS
|
|
65
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
66
|
+
return inert_model_annotation(left, imports, remaining=remaining - 1) and inert_model_annotation(
|
|
67
|
+
right, imports, remaining=remaining - 1
|
|
68
|
+
)
|
|
69
|
+
case ast.Subscript():
|
|
70
|
+
return _subscript_annotation(node, imports, remaining=remaining)
|
|
71
|
+
case _:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _subscript_annotation(node: ast.Subscript, imports: ImportIndex, *, remaining: int) -> bool:
|
|
76
|
+
if not inert_model_annotation(node.value, imports, remaining=remaining - 1):
|
|
77
|
+
return False
|
|
78
|
+
arguments = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
79
|
+
qualified = imports.resolved_qualified_name(node.value)
|
|
80
|
+
if qualified in {"typing.Literal", "typing_extensions.Literal"}:
|
|
81
|
+
return all(_literal(item) for item in arguments)
|
|
82
|
+
if qualified in {"typing.Annotated", "typing_extensions.Annotated"}:
|
|
83
|
+
return (
|
|
84
|
+
bool(arguments)
|
|
85
|
+
and inert_model_annotation(arguments[0], imports, remaining=remaining - 1)
|
|
86
|
+
and all(_constraint(item, imports) for item in arguments[1:])
|
|
87
|
+
)
|
|
88
|
+
return all(inert_model_annotation(item, imports, remaining=remaining - 1) for item in arguments)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _constraint(node: ast.expr, imports: ImportIndex) -> bool:
|
|
92
|
+
if _literal(node):
|
|
93
|
+
return True
|
|
94
|
+
return (
|
|
95
|
+
isinstance(node, ast.Call)
|
|
96
|
+
and imports.resolved_qualified_name(node.func) in _LITERAL_CONSTRAINTS
|
|
97
|
+
and all(_literal(argument) for argument in node.args)
|
|
98
|
+
and all(keyword.arg is not None and _literal(keyword.value) for keyword in node.keywords)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _literal(node: ast.expr) -> bool:
|
|
103
|
+
return isinstance(node, ast.Constant) or (
|
|
104
|
+
isinstance(node, ast.UnaryOp)
|
|
105
|
+
and isinstance(node.op, ast.UAdd | ast.USub)
|
|
106
|
+
and isinstance(node.operand, ast.Constant)
|
|
107
|
+
and isinstance(node.operand.value, int | float)
|
|
108
|
+
and not isinstance(node.operand.value, bool)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_MODEL_FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields", "pydantic.v1", "pydantic.v1.fields"})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def inert_direct_model_body(cls: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
116
|
+
if cls.decorator_list or cls.keywords or cls.type_params or len(cls.bases) != 1:
|
|
117
|
+
return False
|
|
118
|
+
for statement in cls.body:
|
|
119
|
+
match statement:
|
|
120
|
+
case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
|
|
121
|
+
continue
|
|
122
|
+
case ast.AnnAssign(target=ast.Name(), annotation=annotation, value=value):
|
|
123
|
+
if not inert_model_annotation(annotation, imports):
|
|
124
|
+
return False
|
|
125
|
+
if value is None or isinstance(value, ast.Constant):
|
|
126
|
+
continue
|
|
127
|
+
if (
|
|
128
|
+
isinstance(value, ast.Call)
|
|
129
|
+
and imports.resolves(value.func, sources=_MODEL_FIELD_SOURCES, symbol="Field")
|
|
130
|
+
and not value.args
|
|
131
|
+
and all(
|
|
132
|
+
keyword.arg is not None and _model_literal_value(keyword.value) is not None
|
|
133
|
+
for keyword in value.keywords
|
|
134
|
+
)
|
|
135
|
+
):
|
|
136
|
+
continue
|
|
137
|
+
return False
|
|
138
|
+
case _:
|
|
139
|
+
return False
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _model_literal_value(node: ast.expr | None) -> object:
|
|
144
|
+
if isinstance(node, ast.Constant):
|
|
145
|
+
return node.value
|
|
146
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Constant):
|
|
147
|
+
numeric = _model_numeric_value(node.operand.value)
|
|
148
|
+
if numeric is not None:
|
|
149
|
+
return -numeric
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _model_numeric_value(value: object) -> float | None:
|
|
154
|
+
match value:
|
|
155
|
+
case bool():
|
|
156
|
+
return None
|
|
157
|
+
case int() | float():
|
|
158
|
+
return float(value)
|
|
159
|
+
case _:
|
|
160
|
+
return None
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
from sarj_python_lint.rules._project_index import SourceUnit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_BUILTINS = frozenset({"str", "bytes", "int", "float", "bool", "object", "list", "dict", "tuple", "set", "frozenset"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stable_local_class(unit: SourceUnit, name: str) -> bool:
|
|
16
|
+
if unit.tree is None:
|
|
17
|
+
return False
|
|
18
|
+
return sum(
|
|
19
|
+
isinstance(node, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef) and node.name == name
|
|
20
|
+
for node in ast.walk(unit.tree)
|
|
21
|
+
) == 1 and not any(
|
|
22
|
+
(isinstance(node, ast.Name) and node.id == name and not isinstance(node.ctx, ast.Load))
|
|
23
|
+
or (isinstance(node, ast.arg) and node.arg == name)
|
|
24
|
+
for node in ast.walk(unit.tree)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolved_operation_signature(
|
|
29
|
+
method: ast.FunctionDef | ast.AsyncFunctionDef, unit: SourceUnit, imports: ImportIndex
|
|
30
|
+
) -> str | None:
|
|
31
|
+
arguments = method.args
|
|
32
|
+
if method.type_params or arguments.vararg is not None or arguments.kwarg is not None or method.returns is None:
|
|
33
|
+
return None
|
|
34
|
+
positional = [*arguments.posonlyargs, *arguments.args]
|
|
35
|
+
if not positional or positional[0].arg != "self" or positional[0].annotation is not None:
|
|
36
|
+
return None
|
|
37
|
+
keys = [_type_key(argument.annotation, unit, imports) for argument in (*positional[1:], *arguments.kwonlyargs)]
|
|
38
|
+
result = _type_key(method.returns, unit, imports)
|
|
39
|
+
defaults = [*arguments.defaults, *(default for default in arguments.kw_defaults if default is not None)]
|
|
40
|
+
if (
|
|
41
|
+
result is None
|
|
42
|
+
or any(key is None for key in keys)
|
|
43
|
+
or any(not isinstance(default, ast.Constant) for default in defaults)
|
|
44
|
+
):
|
|
45
|
+
return None
|
|
46
|
+
return repr(
|
|
47
|
+
(
|
|
48
|
+
type(method).__name__,
|
|
49
|
+
len(arguments.posonlyargs),
|
|
50
|
+
tuple(argument.arg for argument in (*positional, *arguments.kwonlyargs)),
|
|
51
|
+
keys,
|
|
52
|
+
result,
|
|
53
|
+
tuple(ast.dump(default) for default in arguments.defaults),
|
|
54
|
+
tuple(None if default is None else ast.dump(default) for default in arguments.kw_defaults),
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _type_key(node: ast.expr | None, unit: SourceUnit, imports: ImportIndex) -> str | None:
|
|
60
|
+
if node is None or unit.tree is None:
|
|
61
|
+
return None
|
|
62
|
+
match node:
|
|
63
|
+
case ast.Name():
|
|
64
|
+
return _name_type_key(node, unit, imports)
|
|
65
|
+
case ast.Attribute():
|
|
66
|
+
return imports.resolved_qualified_name(node)
|
|
67
|
+
case ast.Constant(value=str()):
|
|
68
|
+
return None
|
|
69
|
+
case ast.Constant():
|
|
70
|
+
return ast.dump(node)
|
|
71
|
+
case _:
|
|
72
|
+
return _compound_type_key(node, unit, imports)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _name_type_key(node: ast.Name, unit: SourceUnit, imports: ImportIndex) -> str | None:
|
|
76
|
+
if unit.tree is None:
|
|
77
|
+
return None
|
|
78
|
+
name = node.id
|
|
79
|
+
if name in _BUILTINS and imports.builtin_is_unshadowed(name):
|
|
80
|
+
return f"builtins.{name}"
|
|
81
|
+
imported = imports.resolved_qualified_name(node)
|
|
82
|
+
if imported is not None:
|
|
83
|
+
return imported
|
|
84
|
+
if any(isinstance(child, ast.ClassDef) and child.name == name for child in unit.tree.body) and stable_local_class(
|
|
85
|
+
unit, name
|
|
86
|
+
):
|
|
87
|
+
return f"{unit.module}.{name}"
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _compound_type_key(node: ast.expr, unit: SourceUnit, imports: ImportIndex) -> str | None:
|
|
92
|
+
match node:
|
|
93
|
+
case ast.Subscript(value=value, slice=argument):
|
|
94
|
+
kind, parts = "subscript", (value, argument)
|
|
95
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
96
|
+
kind, parts = "union", (left, right)
|
|
97
|
+
case ast.Tuple(elts=elements):
|
|
98
|
+
kind, parts = "tuple", tuple(elements)
|
|
99
|
+
case _:
|
|
100
|
+
return None
|
|
101
|
+
keys = tuple(_type_key(part, unit, imports) for part in parts)
|
|
102
|
+
return repr((kind, keys)) if all(key is not None for key in keys) else None
|
|
@@ -26,18 +26,23 @@ _SCOPES = (
|
|
|
26
26
|
)
|
|
27
27
|
_RESOURCE_OWNERS = MappingProxyType(
|
|
28
28
|
{
|
|
29
|
+
"asyncio": frozenset({"sleep", "CancelledError", "exceptions.CancelledError"}),
|
|
29
30
|
"contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
|
|
30
31
|
"subprocess": frozenset({"Popen", "TimeoutExpired"}),
|
|
31
32
|
"re": frozenset({"compile", "Match", "Pattern"}),
|
|
32
33
|
"unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
|
|
33
|
-
"typing": frozenset({"final"}),
|
|
34
|
-
"typing_extensions": frozenset({"final"}),
|
|
34
|
+
"typing": frozenset({"final", "Literal", "Annotated"}),
|
|
35
|
+
"typing_extensions": frozenset({"final", "Literal", "Annotated"}),
|
|
35
36
|
"pydantic": frozenset(
|
|
36
37
|
{
|
|
37
38
|
"BaseModel",
|
|
38
39
|
"main.BaseModel",
|
|
40
|
+
"v1.BaseModel",
|
|
41
|
+
"v1.main.BaseModel",
|
|
39
42
|
"Field",
|
|
40
43
|
"fields.Field",
|
|
44
|
+
"v1.Field",
|
|
45
|
+
"v1.fields.Field",
|
|
41
46
|
"ConfigDict",
|
|
42
47
|
"config.ConfigDict",
|
|
43
48
|
"model_validator",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_CONTAINERS = frozenset({"list", "dict", "tuple"})
|
|
12
|
+
_TYPING_CONTAINERS = frozenset({"List", "Dict", "Tuple", "Optional", "Union"})
|
|
13
|
+
_SCALARS = frozenset({"str", "int", "float", "bool", "None"})
|
|
14
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
15
|
+
_RUNTIME_ATTRIBUTES = frozenset({"__annotations__", "__value__", "get_type_hints", "TypeAdapter", "validate_call"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def private_static_json_alias(tree: ast.Module, name: str, semantics: AnnotationSemantics) -> bool:
|
|
19
|
+
if not name.startswith("_") or name.startswith("__"):
|
|
20
|
+
return False
|
|
21
|
+
nodes = tuple(ast.walk(tree))
|
|
22
|
+
parents = {id(child): parent for parent in nodes for child in ast.iter_child_nodes(parent)}
|
|
23
|
+
if any(_runtime_annotation_consumer(node, semantics) for node in nodes):
|
|
24
|
+
return False
|
|
25
|
+
safe: set[int] = set()
|
|
26
|
+
for statement in tree.body:
|
|
27
|
+
if (value := _alias_value(statement, name)) is not None:
|
|
28
|
+
safe.update(id(node) for node in ast.walk(value))
|
|
29
|
+
annotations = _private_annotations(tree, name, semantics, nodes, parents)
|
|
30
|
+
if not annotations:
|
|
31
|
+
return False
|
|
32
|
+
safe.update(id(node) for annotation in annotations for node in ast.walk(annotation))
|
|
33
|
+
return all(id(node) in safe for node in nodes if _references_name(node, name))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _private_annotations(
|
|
37
|
+
tree: ast.Module, name: str, semantics: AnnotationSemantics, nodes: tuple[ast.AST, ...], parents: dict[int, ast.AST]
|
|
38
|
+
) -> list[ast.expr]:
|
|
39
|
+
annotations: list[ast.expr] = []
|
|
40
|
+
for function in tree.body:
|
|
41
|
+
if not isinstance(function, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
42
|
+
continue
|
|
43
|
+
if not _function_stays_local(function, nodes, parents):
|
|
44
|
+
continue
|
|
45
|
+
annotations.extend(
|
|
46
|
+
annotation
|
|
47
|
+
for annotation in _function_annotations(function)
|
|
48
|
+
if any(_references_name(node, name) for node in ast.walk(annotation))
|
|
49
|
+
and _static_annotation(annotation, name, semantics)
|
|
50
|
+
)
|
|
51
|
+
return annotations
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _alias_value(statement: ast.stmt, name: str) -> ast.expr | None:
|
|
55
|
+
match statement:
|
|
56
|
+
case ast.TypeAlias(name=ast.Name(id=identifier), value=value) if identifier == name:
|
|
57
|
+
return value
|
|
58
|
+
case ast.AnnAssign(target=ast.Name(id=identifier), value=value) if identifier == name:
|
|
59
|
+
return value
|
|
60
|
+
case _:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _references_name(node: ast.AST, name: str) -> bool:
|
|
65
|
+
if isinstance(node, ast.Name):
|
|
66
|
+
return isinstance(node.ctx, ast.Load) and node.id == name
|
|
67
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, str) and name in node.value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _function_stays_local(
|
|
71
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef, nodes: tuple[ast.AST, ...], parents: dict[int, ast.AST]
|
|
72
|
+
) -> bool:
|
|
73
|
+
if not function.name.startswith("_") or function.name.startswith("__") or function.decorator_list:
|
|
74
|
+
return False
|
|
75
|
+
for node in nodes:
|
|
76
|
+
if not _references_name(node, function.name):
|
|
77
|
+
continue
|
|
78
|
+
parent = parents.get(id(node))
|
|
79
|
+
if not isinstance(node, ast.Name) or not isinstance(parent, ast.Call) or parent.func is not node:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _function_annotations(function: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.expr]:
|
|
85
|
+
arguments = function.args
|
|
86
|
+
annotations = [
|
|
87
|
+
argument.annotation
|
|
88
|
+
for argument in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs)
|
|
89
|
+
if argument.annotation is not None
|
|
90
|
+
]
|
|
91
|
+
if function.returns is not None:
|
|
92
|
+
annotations.append(function.returns)
|
|
93
|
+
return annotations
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _static_annotation(node: ast.expr, name: str, semantics: AnnotationSemantics, *, remaining: int = 64) -> bool:
|
|
97
|
+
if remaining <= 0 or (parsed := semantics.parse(node)) is None:
|
|
98
|
+
return False
|
|
99
|
+
match parsed:
|
|
100
|
+
case ast.Name(id=identifier):
|
|
101
|
+
return identifier == name or (
|
|
102
|
+
identifier in _SCALARS | _CONTAINERS and semantics.imports.builtin_is_unshadowed(identifier)
|
|
103
|
+
)
|
|
104
|
+
case ast.Constant(value=None):
|
|
105
|
+
return True
|
|
106
|
+
case ast.BinOp(left=left, op=ast.BitOr(), right=right):
|
|
107
|
+
return _static_annotation(left, name, semantics, remaining=remaining - 1) and _static_annotation(
|
|
108
|
+
right, name, semantics, remaining=remaining - 1
|
|
109
|
+
)
|
|
110
|
+
case ast.Subscript(value=value, slice=arguments):
|
|
111
|
+
builtin = (
|
|
112
|
+
isinstance(value, ast.Name)
|
|
113
|
+
and value.id in _CONTAINERS
|
|
114
|
+
and semantics.imports.builtin_is_unshadowed(value.id)
|
|
115
|
+
)
|
|
116
|
+
canonical = any(
|
|
117
|
+
semantics.imports.resolves(value, sources=_TYPING_SOURCES, symbol=symbol)
|
|
118
|
+
for symbol in _TYPING_CONTAINERS
|
|
119
|
+
)
|
|
120
|
+
return (builtin or canonical) and all(
|
|
121
|
+
_static_annotation(argument, name, semantics, remaining=remaining - 1)
|
|
122
|
+
for argument in (arguments.elts if isinstance(arguments, ast.Tuple) else [arguments])
|
|
123
|
+
)
|
|
124
|
+
case _:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _runtime_annotation_consumer(node: ast.AST, semantics: AnnotationSemantics) -> bool:
|
|
129
|
+
if isinstance(node, ast.Attribute) and node.attr in _RUNTIME_ATTRIBUTES:
|
|
130
|
+
return True
|
|
131
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) and node.value in _RUNTIME_ATTRIBUTES:
|
|
132
|
+
return True
|
|
133
|
+
if not isinstance(node, ast.Call):
|
|
134
|
+
return False
|
|
135
|
+
qualified = semantics.imports.resolved_qualified_name(node.func)
|
|
136
|
+
return qualified in {
|
|
137
|
+
"pydantic.TypeAdapter",
|
|
138
|
+
"pydantic.type_adapter.TypeAdapter",
|
|
139
|
+
"pydantic.validate_call",
|
|
140
|
+
"typing.get_type_hints",
|
|
141
|
+
"typing_extensions.get_type_hints",
|
|
142
|
+
}
|
|
@@ -21,6 +21,7 @@ from sarj_python_lint.rule_base import (
|
|
|
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
23
|
from sarj_python_lint.rules._pytest import collected_tests
|
|
24
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
@@ -78,7 +79,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
78
79
|
id = "async-mock-call-without-await-assertion"
|
|
79
80
|
code = "SARJ456"
|
|
80
81
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
81
|
-
default_level=Severity.
|
|
82
|
+
default_level=Severity.ERROR,
|
|
82
83
|
summary="Pair positive AsyncMock call assertions with evidence that the mock was awaited.",
|
|
83
84
|
rationale=(
|
|
84
85
|
"Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can "
|
|
@@ -94,6 +95,7 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
94
95
|
limitations=(
|
|
95
96
|
"Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.",
|
|
96
97
|
"Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.",
|
|
98
|
+
"Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.",
|
|
97
99
|
"Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.",
|
|
98
100
|
"Positive call evidence includes direct assertion methods and literal integer or boolean call-state checks; unresolved call expectations are excluded.",
|
|
99
101
|
"Await evidence excludes provably vacuous checks, including zero or nonnegative counts and empty expected await sequences; unresolved expectations are accepted without inferring their values.",
|
|
@@ -215,23 +217,54 @@ class AsyncMockCallWithoutAwaitAssertion(Rule):
|
|
|
215
217
|
for node in context.nodes(ast.AST)
|
|
216
218
|
if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
217
219
|
}
|
|
218
|
-
lines = context.source_lines
|
|
219
220
|
return [
|
|
220
|
-
|
|
221
|
-
path=path,
|
|
222
|
-
line=call.lineno,
|
|
223
|
-
col=call.col_offset + 1,
|
|
224
|
-
code=self.code,
|
|
225
|
-
severity=Severity.WARNING,
|
|
226
|
-
message="AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion",
|
|
227
|
-
)
|
|
221
|
+
finding
|
|
228
222
|
for test in collected_tests(tree.body)
|
|
229
223
|
if not _is_pytest_fixture(test, imports)
|
|
230
|
-
for
|
|
231
|
-
if not is_suppressed(lines, call.lineno, self.code)
|
|
224
|
+
for finding in _test_diagnostics(test, context, imports, writes, self.code)
|
|
232
225
|
]
|
|
233
226
|
|
|
234
227
|
|
|
228
|
+
def _test_diagnostics(
|
|
229
|
+
test: _TestFunction, context: PythonFileContext, imports: ImportIndex, writes: set[_Reference], code: str
|
|
230
|
+
) -> list[Diagnostic]:
|
|
231
|
+
calls = tuple(_uncovered_calls(_test_imports(imports, test), test, writes))
|
|
232
|
+
if not calls:
|
|
233
|
+
return []
|
|
234
|
+
severity = Severity.ERROR if _mock_provider_proven(test, context) else Severity.WARNING
|
|
235
|
+
message = "AsyncMock call assertions do not prove awaiting; keep this assertion and add an await assertion"
|
|
236
|
+
if severity is Severity.WARNING:
|
|
237
|
+
message = (
|
|
238
|
+
"Verify the mock provider is asynchronous before adding an await assertion; "
|
|
239
|
+
"keep the call assertion and preserve intentional scheduling or synchronous replacement contracts."
|
|
240
|
+
)
|
|
241
|
+
return [
|
|
242
|
+
Diagnostic(
|
|
243
|
+
path=context.path,
|
|
244
|
+
line=call.lineno,
|
|
245
|
+
col=call.col_offset + 1,
|
|
246
|
+
code=code,
|
|
247
|
+
severity=severity,
|
|
248
|
+
message=message,
|
|
249
|
+
)
|
|
250
|
+
for call in calls
|
|
251
|
+
if not is_suppressed(context.source_lines, call.lineno, code)
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _mock_provider_proven(test: _TestFunction, context: PythonFileContext) -> bool:
|
|
256
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
257
|
+
constructors = [
|
|
258
|
+
node
|
|
259
|
+
for node in ast.walk(test)
|
|
260
|
+
if isinstance(node, ast.Call)
|
|
261
|
+
and context.imports.resolved_qualified_name(node.func) == "unittest.mock.AsyncMock"
|
|
262
|
+
]
|
|
263
|
+
return bool(constructors) and all(
|
|
264
|
+
provenance.imported(node.func, "unittest.mock.AsyncMock") for node in constructors
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
235
268
|
def _is_pytest_fixture(test: _TestFunction, imports: ImportIndex) -> bool:
|
|
236
269
|
return any(
|
|
237
270
|
imports.resolved_qualified_name(decorator.func if isinstance(decorator, ast.Call) else decorator)
|
|
@@ -45,13 +45,13 @@ class NoBroadKeywordCapture(Rule):
|
|
|
45
45
|
id = "no-broad-keyword-capture"
|
|
46
46
|
code = "SARJ477"
|
|
47
47
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
48
|
-
default_level=Severity.
|
|
48
|
+
default_level=Severity.ERROR,
|
|
49
49
|
summary="Prefer precise keyword interfaces over missing, Any- or object-valued captures.",
|
|
50
50
|
rationale=(
|
|
51
51
|
"Replacing a fixed typed interface with broad **kwargs can erase required keyword names and per-key "
|
|
52
52
|
"value types, making test doubles accept calls the real implementation rejects. object remains a safe "
|
|
53
|
-
"typed top type, and deliberately open data interfaces can be valid; this
|
|
54
|
-
"
|
|
53
|
+
"typed top type, and deliberately open data interfaces can be valid; this strict interface policy "
|
|
54
|
+
"requires a precise contract or a documented open-boundary exception."
|
|
55
55
|
),
|
|
56
56
|
remediation=(
|
|
57
57
|
"Keep explicit typed parameters at fixed interfaces. Preserve required keyword names with named unused "
|
|
@@ -61,13 +61,14 @@ class NoBroadKeywordCapture(Rule):
|
|
|
61
61
|
category=RuleCategory.MAINTAINABILITY,
|
|
62
62
|
autofix=AutofixPolicy.NONE,
|
|
63
63
|
limitations=(
|
|
64
|
-
"This
|
|
64
|
+
"This strict interface policy is not proof of incorrect behavior or a universal ban on the object type; genuinely open boundaries use a reasoned exact-code exception.",
|
|
65
65
|
"Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
|
|
66
66
|
"Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.",
|
|
67
67
|
"Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.",
|
|
68
68
|
"Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.",
|
|
69
69
|
"Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.",
|
|
70
70
|
"No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
|
|
71
|
+
"Runtime special methods and canonical override declarations retain WARNING because framework and inherited signatures can require an open keyword contract; ordinary declared interfaces use ERROR.",
|
|
71
72
|
),
|
|
72
73
|
examples=(
|
|
73
74
|
RuleExample(
|
|
@@ -196,7 +197,7 @@ class NoBroadKeywordCapture(Rule):
|
|
|
196
197
|
line=argument.lineno,
|
|
197
198
|
col=argument.col_offset + 1,
|
|
198
199
|
code=self.code,
|
|
199
|
-
severity=
|
|
200
|
+
severity=_interface_severity(function, context, semantics, mutated_members),
|
|
200
201
|
message=(
|
|
201
202
|
f"keyword capture `{argument.arg}` has no declared names or narrow value type; "
|
|
202
203
|
"prefer an explicit typed interface or document a deliberately open contract."
|
|
@@ -206,6 +207,23 @@ class NoBroadKeywordCapture(Rule):
|
|
|
206
207
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
207
208
|
|
|
208
209
|
|
|
210
|
+
def _interface_severity(
|
|
211
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
212
|
+
context: PythonFileContext,
|
|
213
|
+
semantics: AnnotationSemantics,
|
|
214
|
+
mutated_members: frozenset[str],
|
|
215
|
+
) -> Severity:
|
|
216
|
+
if function.name.startswith("__") and function.name.endswith("__"):
|
|
217
|
+
return Severity.WARNING
|
|
218
|
+
if any(
|
|
219
|
+
_known_decorator(decorator, function, context, semantics, mutated_members)
|
|
220
|
+
in {"typing.override", "typing_extensions.override"}
|
|
221
|
+
for decorator in function.decorator_list
|
|
222
|
+
):
|
|
223
|
+
return Severity.WARNING
|
|
224
|
+
return Severity.ERROR
|
|
225
|
+
|
|
226
|
+
|
|
209
227
|
def _stable_annotation_semantics(context: PythonFileContext, tree: ast.Module) -> AnnotationSemantics:
|
|
210
228
|
semantics = AnnotationSemantics.from_tree(tree)
|
|
211
229
|
unstable = _conflicting_annotation_imports(context, semantics) | _annotation_binding_writes(context)
|
|
@@ -45,7 +45,7 @@ class NoFixedSleepInLoop(Rule):
|
|
|
45
45
|
id = "no-fixed-sleep-in-loop"
|
|
46
46
|
code = "SARJ485"
|
|
47
47
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
48
|
-
default_level=Severity.
|
|
48
|
+
default_level=Severity.ERROR,
|
|
49
49
|
summary="Do not sleep for a fixed literal interval inside a production loop.",
|
|
50
50
|
rationale=(
|
|
51
51
|
"A polling or cleanup loop that sleeps for a literal interval gives tests no seam: every test that "
|
|
@@ -161,7 +161,7 @@ class NoFixedSleepInLoop(Rule):
|
|
|
161
161
|
col=reported.col_offset + 1,
|
|
162
162
|
code=self.code,
|
|
163
163
|
message=_MESSAGE,
|
|
164
|
-
severity=Severity.
|
|
164
|
+
severity=Severity.ERROR,
|
|
165
165
|
)
|
|
166
166
|
for reported, duration in sleeps
|
|
167
167
|
if (seconds := _folded_seconds(duration, constants)) is not None
|