sarj-python-lint 1.32.0__tar.gz → 1.33.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.33.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_python_target.py +1 -1
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_ratchet_cli.py +2 -3
- sarj_python_lint-1.33.0/src/sarj_python_lint/rules/_inert_model_annotation.py +109 -0
- sarj_python_lint-1.33.0/src/sarj_python_lint/rules/_resolved_operation_signature.py +102 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +23 -5
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +5 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +9 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +6 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +4 -4
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +9 -97
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +3 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +74 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +2 -2
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +44 -3
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +67 -7
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/.gitignore +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/LICENSE +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/README.md +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.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.33.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.32.0 → sarj_python_lint-1.33.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.33.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,109 @@
|
|
|
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
|
+
)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -26,7 +26,7 @@ class NoInputModelMutation(ProjectRule):
|
|
|
26
26
|
id = "no-input-model-mutation"
|
|
27
27
|
code = "SARJ481"
|
|
28
28
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
29
|
-
default_level=Severity.
|
|
29
|
+
default_level=Severity.ERROR,
|
|
30
30
|
summary="Return replacement values instead of mutating caller-owned Pydantic models.",
|
|
31
31
|
rationale="Assignments through shared model inputs also change every caller alias. Copying only the outer list or model retains nested aliases and can make repeated transformations observe each other's changes.",
|
|
32
32
|
remediation="Return replacement models and explicitly capture the result. Copy each changed ancestor; permit fresh local builders and use exact suppressions for documented in-place APIs. model_copy updates must already satisfy the complete model contract.",
|
|
@@ -127,7 +127,7 @@ class NoInputModelMutation(ProjectRule):
|
|
|
127
127
|
col=node.col_offset + 1,
|
|
128
128
|
code=self.code,
|
|
129
129
|
message="This write changes a caller-owned model or shared descendant; return replacements and copy each changed ancestor, or document an intentional in-place contract.",
|
|
130
|
-
severity=Severity.
|
|
130
|
+
severity=Severity.ERROR,
|
|
131
131
|
)
|
|
132
132
|
for node in (mutation.node for mutation in context.copy_on_write.mutations if not mutation.before_validator)
|
|
133
133
|
if not is_suppressed(context.source_lines, node.lineno, self.code)
|
|
@@ -27,7 +27,7 @@ class NoMockedPydanticValueObject(ProjectRule):
|
|
|
27
27
|
id = "no-mocked-pydantic-value-object"
|
|
28
28
|
code = "SARJ479"
|
|
29
29
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
30
|
-
default_level=Severity.
|
|
30
|
+
default_level=Severity.ERROR,
|
|
31
31
|
summary="Build real Pydantic models when test doubles supply model field data.",
|
|
32
32
|
rationale=(
|
|
33
33
|
"Mocking a Pydantic value object bypasses validation, defaults, serialization, and field relationships. "
|
|
@@ -126,7 +126,7 @@ class NoMockedPydanticValueObject(ProjectRule):
|
|
|
126
126
|
line=call.lineno,
|
|
127
127
|
col=call.col_offset + 1,
|
|
128
128
|
code=self.code,
|
|
129
|
-
severity=Severity.
|
|
129
|
+
severity=Severity.ERROR,
|
|
130
130
|
message=(
|
|
131
131
|
f"Mock supplies field data for Pydantic model `{model}`; construct the real model "
|
|
132
132
|
"or use a typed factory with valid defaults"
|
|
@@ -43,7 +43,7 @@ class NoShallowContainerTypeGuard(Rule):
|
|
|
43
43
|
id = "no-shallow-container-type-guard"
|
|
44
44
|
code = "SARJ472"
|
|
45
45
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
46
|
-
default_level=Severity.
|
|
46
|
+
default_level=Severity.ERROR,
|
|
47
47
|
summary="Do not claim typed container contents from a container-only type guard.",
|
|
48
48
|
rationale="TypeGuard and TypeIs let callers trust their declared narrowing. Checking only dict, list, or set does not validate keys, values, or elements, so callers can perform operations that fail at runtime.",
|
|
49
49
|
remediation="Use object for unchecked container slots, or validate their contents when the narrower contract is required. Keep constrained input types that already prove the contents.",
|
|
@@ -56,6 +56,7 @@ class NoShallowContainerTypeGuard(Rule):
|
|
|
56
56
|
"Annotation imports include TYPE_CHECKING imports. Runtime imports use conservative file-wide shadow facts, so unrelated local rebinding can cause false negatives. Wildcard imports, explicit writes to relevant builtin or typing attributes, and exception, pattern or walrus bindings that shadow relevant names or imported aliases exclude the file; dynamic namespace mutation is not inferred.",
|
|
57
57
|
"Relative or conditional imports that replace relevant builtin or typing names exclude the file unless the import proves the same canonical symbol. Unrelated relative imports remain in scope.",
|
|
58
58
|
"No autofix: widening the return contract can require caller changes, while adding content validation changes behavior and cost.",
|
|
59
|
+
"Visible __builtins__ namespace access abstains because mapping-backed replacement can change the meaning of builtin checks.",
|
|
59
60
|
),
|
|
60
61
|
examples=(
|
|
61
62
|
RuleExample(
|
|
@@ -98,7 +99,7 @@ class NoShallowContainerTypeGuard(Rule):
|
|
|
98
99
|
col=statement.col_offset + 1,
|
|
99
100
|
code=self.code,
|
|
100
101
|
message="Container-only isinstance check does not validate the declared container contents; use object for unchecked slots or validate the contents required by the return type.",
|
|
101
|
-
severity=Severity.
|
|
102
|
+
severity=Severity.ERROR,
|
|
102
103
|
)
|
|
103
104
|
)
|
|
104
105
|
return findings
|
|
@@ -176,6 +177,8 @@ def _builtin_symbol(node: ast.expr | None, imports: ImportIndex) -> str | None:
|
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
def _ambiguous_symbols(context: PythonFileContext) -> bool:
|
|
180
|
+
if context.dynamic_namespace or any(node.id == "__builtins__" for node in context.nodes(ast.Name)):
|
|
181
|
+
return True
|
|
179
182
|
sensitive_names = _BUILTINS | {
|
|
180
183
|
name
|
|
181
184
|
for name, target in context.module_imports.bindings.items()
|
|
@@ -14,6 +14,7 @@ from sarj_python_lint.rule_base import (
|
|
|
14
14
|
RuleDocumentation,
|
|
15
15
|
RuleExample,
|
|
16
16
|
Severity,
|
|
17
|
+
is_suppressed,
|
|
17
18
|
)
|
|
18
19
|
from sarj_python_lint.rules._prose_budget import groups
|
|
19
20
|
|
|
@@ -29,7 +30,7 @@ class NoTypedDocSections(Rule):
|
|
|
29
30
|
id = "no-docstring-type-restatement"
|
|
30
31
|
code = "SARJ092"
|
|
31
32
|
documentation = RuleDocumentation(
|
|
32
|
-
default_level=Severity.
|
|
33
|
+
default_level=Severity.ERROR,
|
|
33
34
|
summary="A docstring type label repeats an annotation from the fully typed signature.",
|
|
34
35
|
rationale=(
|
|
35
36
|
"A repeated type spelling can drift from the annotation and obscures the behavioral contract that only prose "
|
|
@@ -47,6 +48,7 @@ class NoTypedDocSections(Rule):
|
|
|
47
48
|
"A documented label must parse as a Python type expression and match the corresponding annotation.",
|
|
48
49
|
"Generated files, runtime-consumed prompt, CLI, and route docstrings, properties, abstract methods, overloads, protocol methods, and partially typed signatures are excluded.",
|
|
49
50
|
"NumPy underlined sections and Sphinx type fields intentionally remain with their selected documentation convention.",
|
|
51
|
+
"A reasoned exact-code suppression on the function declaration or closing docstring line preserves required documentation tooling conventions; markers inside string content are not suppression comments.",
|
|
50
52
|
),
|
|
51
53
|
examples=(
|
|
52
54
|
RuleExample(
|
|
@@ -96,7 +98,7 @@ class NoTypedDocSections(Rule):
|
|
|
96
98
|
f"This docstring repeats {len(group.typed_restatements)} type label(s) already present in the "
|
|
97
99
|
"signature; remove only the type labels and keep the behavioral descriptions."
|
|
98
100
|
),
|
|
99
|
-
severity=Severity.
|
|
101
|
+
severity=Severity.ERROR,
|
|
100
102
|
column_encoding=group.column_encoding,
|
|
101
103
|
)
|
|
102
104
|
for group in groups(path, source)
|
|
@@ -120,7 +122,11 @@ def _public_contract_docstring_lines(context: PythonFileContext) -> frozenset[in
|
|
|
120
122
|
first = node.body[0]
|
|
121
123
|
if not (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)):
|
|
122
124
|
continue
|
|
123
|
-
if
|
|
125
|
+
if (
|
|
126
|
+
_is_public_contract(node, parents)
|
|
127
|
+
or is_suppressed(context.source_lines, node.lineno, NoTypedDocSections.code)
|
|
128
|
+
or is_suppressed(context.source_lines, first.end_lineno or first.lineno, NoTypedDocSections.code)
|
|
129
|
+
):
|
|
124
130
|
excluded.add(first.lineno)
|
|
125
131
|
return frozenset(excluded)
|
|
126
132
|
|
|
@@ -98,7 +98,7 @@ class PreferModuleLevelConstant(Rule):
|
|
|
98
98
|
id: str = "prefer-module-level-constant"
|
|
99
99
|
code: str = "SARJ039"
|
|
100
100
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
101
|
-
default_level=Severity.
|
|
101
|
+
default_level=Severity.ERROR,
|
|
102
102
|
summary="Hoist repeatedly read static values when a module constant preserves behavior.",
|
|
103
103
|
rationale=(
|
|
104
104
|
"Rebuilding a substantial static collection repeats allocation, while calling `re.compile` with a "
|
|
@@ -116,7 +116,8 @@ class PreferModuleLevelConstant(Rule):
|
|
|
116
116
|
"Test, test-support, and generated files are excluded.",
|
|
117
117
|
"Collections require at least eight deeply immutable elements, or three when allocated inside a loop; only representation-insensitive reads are accepted.",
|
|
118
118
|
"Final dictionaries retain mutable contents at runtime; unknown calls, mutation, and escaping references are excluded from hoisting candidates.",
|
|
119
|
-
"
|
|
119
|
+
"Visible frame or dynamic-namespace observation retains WARNING because moving a local binding can change observed caller state.",
|
|
120
|
+
"Regex findings require a proven stdlib `re.compile` binding and exclude `re.DEBUG`; regex cache lookup findings remain warnings because invocation frequency and initialization timing are unproved. Immutable literal collection tables enforce the explicit static-lifetime policy at error severity; preserve any intentional per-call ownership with an exact-code exception.",
|
|
120
121
|
),
|
|
121
122
|
examples=(
|
|
122
123
|
RuleExample(
|
|
@@ -209,7 +210,9 @@ class PreferModuleLevelConstant(Rule):
|
|
|
209
210
|
col=stmt.col_offset + 1,
|
|
210
211
|
code=self.code,
|
|
211
212
|
message=_message(name, candidate),
|
|
212
|
-
severity=Severity.WARNING
|
|
213
|
+
severity=Severity.WARNING
|
|
214
|
+
if candidate.kind == "regex" or context.dynamic_namespace
|
|
215
|
+
else Severity.ERROR,
|
|
213
216
|
)
|
|
214
217
|
)
|
|
215
218
|
diags.sort(key=lambda d: (d.line, d.col))
|
|
@@ -47,7 +47,7 @@ class PreferPytestFixtureInjection(Rule):
|
|
|
47
47
|
id: str = "prefer-pytest-fixture-injection"
|
|
48
48
|
code: str = "SARJ476"
|
|
49
49
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
50
|
-
default_level=Severity.
|
|
50
|
+
default_level=Severity.ERROR,
|
|
51
51
|
summary="Request shared database-pool setup through a pytest fixture.",
|
|
52
52
|
rationale=(
|
|
53
53
|
"Calling a shared conftest pool helper during initial test setup bypasses an explicit pytest "
|
|
@@ -199,7 +199,7 @@ class PreferPytestFixtureInjection(Rule):
|
|
|
199
199
|
"open/close with application lifespan when it owns them; keep factories for variants "
|
|
200
200
|
"or fresh instances."
|
|
201
201
|
),
|
|
202
|
-
severity=Severity.
|
|
202
|
+
severity=Severity.ERROR,
|
|
203
203
|
)
|
|
204
204
|
)
|
|
205
205
|
return diagnostics
|
|
@@ -35,10 +35,10 @@ class PreferRegexFullmatch(Rule):
|
|
|
35
35
|
id: str = "prefer-regex-fullmatch"
|
|
36
36
|
code: str = "SARJ460"
|
|
37
37
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
38
|
-
default_level=Severity.
|
|
38
|
+
default_level=Severity.ERROR,
|
|
39
39
|
summary="An anchored regex validation predicate does not require a full-string match.",
|
|
40
40
|
rationale="The regex `$` anchor can match before a final newline, so an anchored match can accept a value that was not fully validated.",
|
|
41
|
-
remediation="Use `re.fullmatch` or the compiled pattern's `fullmatch` method for whole-value validation
|
|
41
|
+
remediation="Use `re.fullmatch` or the compiled pattern's `fullmatch` method for strict whole-value validation. Preserve deliberate terminal-newline acceptance or an existing match contract with a reasoned exact SARJ460 exception; do not rewrite that behavior automatically.",
|
|
42
42
|
category=RuleCategory.CORRECTNESS,
|
|
43
43
|
autofix=AutofixPolicy.NONE,
|
|
44
44
|
limitations=(
|
|
@@ -133,8 +133,8 @@ class PreferRegexFullmatch(Rule):
|
|
|
133
133
|
line=call.lineno,
|
|
134
134
|
col=call.col_offset + 1,
|
|
135
135
|
code=self.code,
|
|
136
|
-
message="anchored match is used as a validation predicate, but `$` can accept a final newline; use fullmatch for whole-value
|
|
137
|
-
severity=Severity.
|
|
136
|
+
message="anchored match is used as a validation predicate, but `$` can accept a final newline; use fullmatch for the strict whole-value policy, or preserve an intentional newline contract with a reasoned SARJ460 exception.",
|
|
137
|
+
severity=Severity.ERROR,
|
|
138
138
|
)
|
|
139
139
|
)
|
|
140
140
|
return sorted(findings, key=lambda item: (item.line, item.col))
|