sarj-python-lint 1.33.0__tar.gz → 1.35.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.33.0 → sarj_python_lint-1.35.0}/PKG-INFO +5 -5
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/pyproject.toml +4 -4
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +51 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_resource_provenance.py +7 -2
- sarj_python_lint-1.35.0/src/sarj_python_lint/rules/_static_json_alias.py +142 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +45 -12
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +60 -7
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +3 -2
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +26 -2
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +2 -2
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +2 -2
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +71 -9
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_docstring.py +15 -3
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +2 -30
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/.gitignore +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/LICENSE +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/README.md +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.33.0 → sarj_python_lint-1.35.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.35.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/
|
|
@@ -18,10 +18,10 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
|
-
Requires-Dist: ruff>=0.
|
|
22
|
-
Requires-Dist: sarj-rule-contracts==0.2.
|
|
23
|
-
Requires-Dist: sqlglot==30.
|
|
24
|
-
Requires-Dist: typer==0.27.
|
|
21
|
+
Requires-Dist: ruff>=0.17.0
|
|
22
|
+
Requires-Dist: sarj-rule-contracts==0.2.4
|
|
23
|
+
Requires-Dist: sqlglot==30.22.0
|
|
24
|
+
Requires-Dist: typer==0.27.3
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
27
|
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.35.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["sarj-rule-contracts==0.2.
|
|
18
|
+
dependencies = ["sarj-rule-contracts==0.2.4", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -31,8 +31,8 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
33
|
"pytest>=9.1.1",
|
|
34
|
-
"ruff>=0.
|
|
35
|
-
"basedpyright>=1.40.
|
|
34
|
+
"ruff>=0.17.0",
|
|
35
|
+
"basedpyright>=1.40.2",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[build-system]
|
|
@@ -107,3 +107,54 @@ def _literal(node: ast.expr) -> bool:
|
|
|
107
107
|
and isinstance(node.operand.value, int | float)
|
|
108
108
|
and not isinstance(node.operand.value, bool)
|
|
109
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
|
|
@@ -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)
|
|
@@ -18,7 +18,9 @@ from sarj_python_lint.rule_base import (
|
|
|
18
18
|
)
|
|
19
19
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
20
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
21
|
+
from sarj_python_lint.rules._inert_model_annotation import inert_direct_model_body
|
|
21
22
|
from sarj_python_lint.rules._paths import is_test_path
|
|
23
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
if TYPE_CHECKING:
|
|
@@ -44,7 +46,7 @@ class NoRedundantLiteralDescription(Rule):
|
|
|
44
46
|
id = "no-restated-closed-domain-description"
|
|
45
47
|
code = "SARJ423"
|
|
46
48
|
documentation = RuleDocumentation(
|
|
47
|
-
default_level=Severity.
|
|
49
|
+
default_level=Severity.ERROR,
|
|
48
50
|
summary="Do not restate a string Literal or local string Enum domain in its Pydantic description.",
|
|
49
51
|
rationale=(
|
|
50
52
|
"Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can "
|
|
@@ -61,6 +63,7 @@ class NoRedundantLiteralDescription(Rule):
|
|
|
61
63
|
"Only direct fields on classes with a direct BaseModel base are inspected.",
|
|
62
64
|
"Domains are resolved only from direct string Literals and static Literal aliases or string-valued Enums in the same module.",
|
|
63
65
|
"Descriptions are reported only when a narrow clause repeats the complete domain without mapping or rationale.",
|
|
66
|
+
"ERROR requires an inert direct model with no json_schema_extra options; custom construction, schema metadata, local aliases and Enum annotations retain WARNING with schema-ownership review guidance.",
|
|
64
67
|
"Test and generated files are excluded.",
|
|
65
68
|
),
|
|
66
69
|
examples=(
|
|
@@ -116,7 +119,9 @@ class NoRedundantLiteralDescription(Rule):
|
|
|
116
119
|
local_domains = _local_closed_domains(tree, imports)
|
|
117
120
|
diagnostics: list[Diagnostic] = []
|
|
118
121
|
|
|
119
|
-
def collect_field_descriptions(
|
|
122
|
+
def collect_field_descriptions(
|
|
123
|
+
statement: ast.AnnAssign, field_name: str, domain: frozenset[str], *, severity: Severity
|
|
124
|
+
) -> None:
|
|
120
125
|
for field_call in _field_calls(statement, imports):
|
|
121
126
|
description = next(
|
|
122
127
|
(keyword.value for keyword in field_call.keywords if keyword.arg == "description"), None
|
|
@@ -134,26 +139,74 @@ class NoRedundantLiteralDescription(Rule):
|
|
|
134
139
|
col=description.col_offset + 1,
|
|
135
140
|
code=self.code,
|
|
136
141
|
message=(
|
|
137
|
-
f"`{field_name}` repeats
|
|
138
|
-
"remove the repeated
|
|
142
|
+
f"`{field_name}` repeats its declared Literal or Enum domain; "
|
|
143
|
+
"remove the repeated list only after preserving required schema metadata and behavioral guidance."
|
|
139
144
|
),
|
|
140
|
-
severity=
|
|
145
|
+
severity=severity,
|
|
141
146
|
)
|
|
142
147
|
)
|
|
143
148
|
|
|
144
|
-
for model in
|
|
149
|
+
for model in context.nodes(ast.ClassDef):
|
|
145
150
|
if not _is_direct_model(model, imports) or _has_custom_schema_hook(model):
|
|
146
151
|
continue
|
|
152
|
+
severity = _description_level(model, context)
|
|
147
153
|
for statement in model.body:
|
|
148
154
|
if not isinstance(statement, ast.AnnAssign) or not isinstance(statement.target, ast.Name):
|
|
149
155
|
continue
|
|
150
156
|
domain = _closed_domain(statement.annotation, imports, local_domains)
|
|
151
157
|
if domain is None or _has_schema_override(statement.annotation, imports):
|
|
152
158
|
continue
|
|
153
|
-
collect_field_descriptions(
|
|
159
|
+
collect_field_descriptions(
|
|
160
|
+
statement,
|
|
161
|
+
statement.target.id,
|
|
162
|
+
domain,
|
|
163
|
+
severity=severity,
|
|
164
|
+
)
|
|
154
165
|
return diagnostics
|
|
155
166
|
|
|
156
167
|
|
|
168
|
+
def _description_level(model: ast.ClassDef, context: PythonFileContext) -> Severity:
|
|
169
|
+
return Severity.ERROR if _inert_description_model(model, context) else Severity.WARNING
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _inert_description_model(model: ast.ClassDef, context: PythonFileContext) -> bool:
|
|
173
|
+
if context.dynamic_namespace or not inert_direct_model_body(model, context.imports):
|
|
174
|
+
return False
|
|
175
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
176
|
+
for node in walk_ast(model):
|
|
177
|
+
if isinstance(node, ast.keyword) and node.arg == "json_schema_extra":
|
|
178
|
+
return False
|
|
179
|
+
if not _annotation_provider_proven(node, context, provenance):
|
|
180
|
+
return False
|
|
181
|
+
if isinstance(node, ast.Call) and not provenance.imported(
|
|
182
|
+
node.func, context.imports.resolved_qualified_name(node.func) or "pydantic.Field"
|
|
183
|
+
):
|
|
184
|
+
return False
|
|
185
|
+
return any(
|
|
186
|
+
provenance.imported(base, context.imports.resolved_qualified_name(base) or "pydantic.BaseModel")
|
|
187
|
+
for base in model.bases
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _annotation_provider_proven(node: ast.AST, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
|
|
192
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Constant):
|
|
193
|
+
return False
|
|
194
|
+
if isinstance(node, ast.Subscript) and context.imports.resolved_qualified_name(node.value) in {
|
|
195
|
+
"typing.Annotated",
|
|
196
|
+
"typing_extensions.Annotated",
|
|
197
|
+
}:
|
|
198
|
+
argument = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) and node.slice.elts else node.slice
|
|
199
|
+
return not isinstance(argument, ast.Constant)
|
|
200
|
+
if isinstance(node, ast.Name | ast.Attribute) and (qualified := context.imports.resolved_qualified_name(node)) in {
|
|
201
|
+
"typing.Literal",
|
|
202
|
+
"typing.Annotated",
|
|
203
|
+
"typing_extensions.Literal",
|
|
204
|
+
"typing_extensions.Annotated",
|
|
205
|
+
}:
|
|
206
|
+
return provenance.imported(node, qualified)
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
|
|
157
210
|
def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
158
211
|
return any(imports.resolves(base, sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel") for base in node.bases)
|
|
159
212
|
|
|
@@ -30,7 +30,7 @@ class NoRedundantModuleAliasExports(Rule):
|
|
|
30
30
|
id: str = "no-redundant-module-alias-exports"
|
|
31
31
|
code: str = "SARJ440"
|
|
32
32
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
33
|
-
default_level=Severity.
|
|
33
|
+
default_level=Severity.ERROR,
|
|
34
34
|
summary="Do not manufacture public APIs from private names or replace the current module.",
|
|
35
35
|
rationale=(
|
|
36
36
|
"Binding a public name to a private implementation creates two names for one API and disguises the "
|
|
@@ -41,7 +41,8 @@ class NoRedundantModuleAliasExports(Rule):
|
|
|
41
41
|
remediation=(
|
|
42
42
|
"Define maintained APIs under their public names and update local callers directly. Import the canonical "
|
|
43
43
|
"path inside maintained code. If a compatibility module is still required, expose its supported names with "
|
|
44
|
-
"explicit same-name imports grouped by source module."
|
|
44
|
+
"explicit same-name imports grouped by source module. Preserve a required compatibility or module-identity "
|
|
45
|
+
"contract with a reasoned exact suppression rather than renaming automatically."
|
|
45
46
|
),
|
|
46
47
|
category=RuleCategory.MAINTAINABILITY,
|
|
47
48
|
autofix=AutofixPolicy.NONE,
|
|
@@ -19,6 +19,7 @@ from sarj_python_lint.rule_base import (
|
|
|
19
19
|
)
|
|
20
20
|
from sarj_python_lint.rules._ast_index import nodes
|
|
21
21
|
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
@@ -41,7 +42,7 @@ class NoSwallowedAsyncioCancellation(Rule):
|
|
|
41
42
|
id = "no-swallowed-asyncio-cancellation"
|
|
42
43
|
code = "SARJ458"
|
|
43
44
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
44
|
-
default_level=Severity.
|
|
45
|
+
default_level=Severity.ERROR,
|
|
45
46
|
summary="Preserve cancellation when an async operation is cancelled.",
|
|
46
47
|
rationale=(
|
|
47
48
|
"Returning or falling through after catching CancelledError can make cancelled work look successful "
|
|
@@ -60,6 +61,7 @@ class NoSwallowedAsyncioCancellation(Rule):
|
|
|
60
61
|
"Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.",
|
|
61
62
|
"A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.",
|
|
62
63
|
"Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.",
|
|
64
|
+
"Only a try body consisting of one awaited, unmodified canonical asyncio.sleep call with literal arguments is blocking. Unresolved operations and child cancellation remain advisory.",
|
|
63
65
|
),
|
|
64
66
|
examples=(
|
|
65
67
|
RuleExample(
|
|
@@ -129,12 +131,34 @@ def _function_diagnostics(function: ast.AsyncFunctionDef, context: PythonFileCon
|
|
|
129
131
|
col=handler.col_offset + 1,
|
|
130
132
|
code=code,
|
|
131
133
|
message=_MESSAGE,
|
|
132
|
-
severity=Severity.WARNING,
|
|
134
|
+
severity=Severity.ERROR if _caller_sleep(node, context) else Severity.WARNING,
|
|
133
135
|
)
|
|
134
136
|
)
|
|
135
137
|
return diagnostics
|
|
136
138
|
|
|
137
139
|
|
|
140
|
+
def _caller_sleep(node: ast.Try, context: PythonFileContext) -> bool:
|
|
141
|
+
if context.dynamic_namespace or len(node.body) != 1 or len(node.handlers) != 1:
|
|
142
|
+
return False
|
|
143
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
144
|
+
caught = node.handlers[0].type
|
|
145
|
+
if caught is None or not provenance.imported(
|
|
146
|
+
caught, context.imports.resolved_qualified_name(caught) or "asyncio.CancelledError"
|
|
147
|
+
):
|
|
148
|
+
return False
|
|
149
|
+
match node.body[0]:
|
|
150
|
+
case ast.Expr(value=ast.Await(value=ast.Call() as call)):
|
|
151
|
+
if not context.imports.resolves(call.func, sources=frozenset({"asyncio"}), symbol="sleep"):
|
|
152
|
+
return False
|
|
153
|
+
if not provenance.imported(call.func, "asyncio.sleep"):
|
|
154
|
+
return False
|
|
155
|
+
if not all(isinstance(value, ast.Constant) for value in call.args):
|
|
156
|
+
return False
|
|
157
|
+
return not call.keywords
|
|
158
|
+
case _:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
138
162
|
def _local_nodes(function: ast.AsyncFunctionDef) -> Iterator[ast.AST]:
|
|
139
163
|
stack: list[ast.AST] = list(reversed(function.body))
|
|
140
164
|
yield function
|
|
@@ -28,7 +28,7 @@ class NoTestMethodGrafting(ProjectRule):
|
|
|
28
28
|
id = "no-test-method-grafting"
|
|
29
29
|
code = "SARJ478"
|
|
30
30
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
31
|
-
default_level=Severity.
|
|
31
|
+
default_level=Severity.ERROR,
|
|
32
32
|
summary="Avoid assigning replacement implementations directly onto resolved methods in tests.",
|
|
33
33
|
rationale=(
|
|
34
34
|
"Grafting a function onto a collaborator or mock hides the boundary's callable contract and bypasses "
|
|
@@ -135,7 +135,7 @@ class NoTestMethodGrafting(ProjectRule):
|
|
|
135
135
|
line=position.line,
|
|
136
136
|
col=position.column + 1,
|
|
137
137
|
code=self.code,
|
|
138
|
-
severity=Severity.
|
|
138
|
+
severity=Severity.ERROR,
|
|
139
139
|
message=(
|
|
140
140
|
f"Test assigns an implementation directly to resolved method `{method}`; inject a fake, "
|
|
141
141
|
"or configure the existing autospecced mock method's side_effect"
|
|
@@ -26,7 +26,7 @@ class NoUnusedCopyResult(ProjectRule):
|
|
|
26
26
|
id = "no-unused-copy-result"
|
|
27
27
|
code = "SARJ483"
|
|
28
28
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
29
|
-
default_level=Severity.
|
|
29
|
+
default_level=Severity.ERROR,
|
|
30
30
|
summary="Use the replacement returned by a resolved copy API.",
|
|
31
31
|
rationale="Copy-on-write replacement APIs return another value rather than updating the original. Discarding their result can silently leave subsequent operations using unchanged data.",
|
|
32
32
|
remediation="Assign, return, or otherwise consume the replacement. A copy hook intentionally exercised for its effects or exceptions can use a narrow suppression; do not automatically delete the call.",
|
|
@@ -113,7 +113,7 @@ class NoUnusedCopyResult(ProjectRule):
|
|
|
113
113
|
col=node.col_offset + 1,
|
|
114
114
|
code=self.code,
|
|
115
115
|
message="This copy API returns a replacement that is discarded; assign or return it, or document intentional copy-hook effects.",
|
|
116
|
-
severity=Severity.
|
|
116
|
+
severity=Severity.ERROR,
|
|
117
117
|
)
|
|
118
118
|
for node in (context.copy_on_write.discarded)
|
|
119
119
|
if not is_suppressed(context.source_lines, node.lineno, self.code)
|