sarj-python-lint 1.45.3__tar.gz → 1.46.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.45.3 → sarj_python_lint-1.46.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/pyproject.toml +1 -1
- sarj_python_lint-1.46.0/src/sarj_python_lint/rules/_native_namespace_capability.py +116 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +4 -2
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +2 -107
- sarj_python_lint-1.46.0/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +325 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_tautological_expect.py +12 -5
- sarj_python_lint-1.46.0/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +599 -0
- sarj_python_lint-1.45.3/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -347
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/.gitignore +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/LICENSE +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/README.md +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.45.3 → sarj_python_lint-1.46.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.46.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/
|
|
@@ -0,0 +1,116 @@
|
|
|
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._file_context import PythonFileContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
|
|
12
|
+
_BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"__build_class__",
|
|
15
|
+
"__import__",
|
|
16
|
+
"__lazy_import__",
|
|
17
|
+
"abs",
|
|
18
|
+
"aiter",
|
|
19
|
+
"all",
|
|
20
|
+
"anext",
|
|
21
|
+
"any",
|
|
22
|
+
"ascii",
|
|
23
|
+
"bin",
|
|
24
|
+
"breakpoint",
|
|
25
|
+
"callable",
|
|
26
|
+
"chr",
|
|
27
|
+
"compile",
|
|
28
|
+
"delattr",
|
|
29
|
+
"dir",
|
|
30
|
+
"divmod",
|
|
31
|
+
"eval",
|
|
32
|
+
"exec",
|
|
33
|
+
"format",
|
|
34
|
+
"getattr",
|
|
35
|
+
"globals",
|
|
36
|
+
"hasattr",
|
|
37
|
+
"hash",
|
|
38
|
+
"hex",
|
|
39
|
+
"id",
|
|
40
|
+
"input",
|
|
41
|
+
"isinstance",
|
|
42
|
+
"issubclass",
|
|
43
|
+
"iter",
|
|
44
|
+
"len",
|
|
45
|
+
"locals",
|
|
46
|
+
"max",
|
|
47
|
+
"min",
|
|
48
|
+
"next",
|
|
49
|
+
"oct",
|
|
50
|
+
"ord",
|
|
51
|
+
"pow",
|
|
52
|
+
"print",
|
|
53
|
+
"repr",
|
|
54
|
+
"round",
|
|
55
|
+
"setattr",
|
|
56
|
+
"sorted",
|
|
57
|
+
"sum",
|
|
58
|
+
"vars",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def native_namespace_capability_escapes(context: PythonFileContext) -> bool:
|
|
64
|
+
function_names = _possible_native_function_names(context)
|
|
65
|
+
namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
|
|
66
|
+
module_names = frozenset(
|
|
67
|
+
alias.asname or alias.name
|
|
68
|
+
for node in context.nodes(ast.Import)
|
|
69
|
+
for alias in node.names
|
|
70
|
+
if alias.name == "builtins"
|
|
71
|
+
)
|
|
72
|
+
for node in context.nodes(ast.Name, ast.Attribute):
|
|
73
|
+
if isinstance(node, ast.Attribute) and (
|
|
74
|
+
not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
75
|
+
):
|
|
76
|
+
continue
|
|
77
|
+
if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
|
|
78
|
+
continue
|
|
79
|
+
parent = context.parents.get(node)
|
|
80
|
+
if (
|
|
81
|
+
not isinstance(parent, ast.Call)
|
|
82
|
+
or parent.func is not node
|
|
83
|
+
or _namespace_returning_function(node, namespace_function_names)
|
|
84
|
+
):
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _possible_native_function_names(
|
|
90
|
+
context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
|
|
91
|
+
) -> frozenset[str]:
|
|
92
|
+
return symbols | frozenset(
|
|
93
|
+
alias.asname or alias.name
|
|
94
|
+
for node in context.nodes(ast.ImportFrom)
|
|
95
|
+
if node.level == 0 and node.module == "builtins"
|
|
96
|
+
for alias in node.names
|
|
97
|
+
if alias.name in symbols
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
|
|
102
|
+
if isinstance(node, ast.Name):
|
|
103
|
+
return node.id in function_names
|
|
104
|
+
return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _possible_native_function(
|
|
108
|
+
node: ast.Name | ast.Attribute,
|
|
109
|
+
function_names: frozenset[str],
|
|
110
|
+
module_names: frozenset[str],
|
|
111
|
+
) -> bool:
|
|
112
|
+
if isinstance(node, ast.Name):
|
|
113
|
+
return node.id in function_names
|
|
114
|
+
return (
|
|
115
|
+
isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
116
|
+
)
|
|
@@ -12,6 +12,7 @@ from packaging.version import InvalidVersion, Version
|
|
|
12
12
|
from sarj_python_lint.json_boundary import is_object_mapping
|
|
13
13
|
from sarj_python_lint.rules._first_party import distribution_root
|
|
14
14
|
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
15
|
+
from sarj_python_lint.rules._native_namespace_capability import native_namespace_capability_escapes
|
|
15
16
|
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
16
17
|
|
|
17
18
|
|
|
@@ -35,7 +36,8 @@ def direct_models(context: PythonFileContext, *, methods: bool = False) -> tuple
|
|
|
35
36
|
if tree is None or context.generated or context.has_wildcard_import:
|
|
36
37
|
return ()
|
|
37
38
|
if (
|
|
38
|
-
|
|
39
|
+
native_namespace_capability_escapes(context)
|
|
40
|
+
or _mutated_imports(context)
|
|
39
41
|
or _ambiguous_imports(context)
|
|
40
42
|
or _escaped_imports(context)
|
|
41
43
|
or dynamic_namespace_access(context.nodes(ast.AST), context.imports, context.parents)
|
|
@@ -83,7 +85,7 @@ def inert_annotation(node: ast.expr, context: PythonFileContext) -> bool:
|
|
|
83
85
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
84
86
|
try:
|
|
85
87
|
node = ast.parse(node.value, mode="eval").body
|
|
86
|
-
except SyntaxError:
|
|
88
|
+
except SyntaxError, UnicodeError:
|
|
87
89
|
return False
|
|
88
90
|
return _inert_expr(node, context)
|
|
89
91
|
|
|
@@ -217,6 +217,7 @@ from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoCo
|
|
|
217
217
|
from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
|
|
218
218
|
from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
|
|
219
219
|
from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
|
|
220
|
+
from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
|
|
220
221
|
from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
|
|
221
222
|
from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
|
|
222
223
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
@@ -231,6 +232,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
231
232
|
NoExplicitTestOracle.id: NoExplicitTestOracle,
|
|
232
233
|
PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
|
|
233
234
|
NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
|
|
235
|
+
NoInvalidPydanticValidatorReturn.id: NoInvalidPydanticValidatorReturn,
|
|
234
236
|
NoPartialTypeIs.id: NoPartialTypeIs,
|
|
235
237
|
NoContradictoryTypeGuard.id: NoContradictoryTypeGuard,
|
|
236
238
|
NoShallowRecordTypeGuard.id: NoShallowRecordTypeGuard,
|
|
@@ -5,6 +5,7 @@ from collections import Counter
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from sarj_python_lint.rules._imports import TYPING_SOURCES
|
|
8
|
+
from sarj_python_lint.rules._native_namespace_capability import native_namespace_capability_escapes
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
@@ -15,56 +16,6 @@ if TYPE_CHECKING:
|
|
|
15
16
|
_NAMESPACE_IMPORT_CAPABILITIES = frozenset(
|
|
16
17
|
{"__dict__", "__builtins__", "__getattribute__", "__setattr__", "__delattr__"}
|
|
17
18
|
)
|
|
18
|
-
_NAMESPACE_RETURNING_FUNCTIONS = frozenset({"__lazy_import__"})
|
|
19
|
-
_BUILTIN_NAMESPACE_FUNCTIONS = frozenset(
|
|
20
|
-
{
|
|
21
|
-
"__build_class__",
|
|
22
|
-
"__import__",
|
|
23
|
-
"__lazy_import__",
|
|
24
|
-
"abs",
|
|
25
|
-
"aiter",
|
|
26
|
-
"all",
|
|
27
|
-
"anext",
|
|
28
|
-
"any",
|
|
29
|
-
"ascii",
|
|
30
|
-
"bin",
|
|
31
|
-
"breakpoint",
|
|
32
|
-
"callable",
|
|
33
|
-
"chr",
|
|
34
|
-
"compile",
|
|
35
|
-
"delattr",
|
|
36
|
-
"dir",
|
|
37
|
-
"divmod",
|
|
38
|
-
"eval",
|
|
39
|
-
"exec",
|
|
40
|
-
"format",
|
|
41
|
-
"getattr",
|
|
42
|
-
"globals",
|
|
43
|
-
"hasattr",
|
|
44
|
-
"hash",
|
|
45
|
-
"hex",
|
|
46
|
-
"id",
|
|
47
|
-
"input",
|
|
48
|
-
"isinstance",
|
|
49
|
-
"issubclass",
|
|
50
|
-
"iter",
|
|
51
|
-
"len",
|
|
52
|
-
"locals",
|
|
53
|
-
"max",
|
|
54
|
-
"min",
|
|
55
|
-
"next",
|
|
56
|
-
"oct",
|
|
57
|
-
"ord",
|
|
58
|
-
"pow",
|
|
59
|
-
"print",
|
|
60
|
-
"repr",
|
|
61
|
-
"round",
|
|
62
|
-
"setattr",
|
|
63
|
-
"sorted",
|
|
64
|
-
"sum",
|
|
65
|
-
"vars",
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
19
|
|
|
69
20
|
|
|
70
21
|
def builtin_guard_symbol(node: ast.expr | None, imports: ImportIndex, builtin_symbols: frozenset[str]) -> str | None:
|
|
@@ -150,7 +101,7 @@ def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[st
|
|
|
150
101
|
for node in context.nodes(ast.ImportFrom)
|
|
151
102
|
):
|
|
152
103
|
return True
|
|
153
|
-
if
|
|
104
|
+
if native_namespace_capability_escapes(context):
|
|
154
105
|
return True
|
|
155
106
|
for name in context.nodes(ast.Name):
|
|
156
107
|
if name.id not in namespace_names:
|
|
@@ -161,62 +112,6 @@ def _namespace_escapes(context: PythonFileContext, namespace_names: frozenset[st
|
|
|
161
112
|
return False
|
|
162
113
|
|
|
163
114
|
|
|
164
|
-
def _native_function_escapes(context: PythonFileContext) -> bool:
|
|
165
|
-
function_names = _possible_native_function_names(context)
|
|
166
|
-
namespace_function_names = _possible_native_function_names(context, symbols=_NAMESPACE_RETURNING_FUNCTIONS)
|
|
167
|
-
module_names = frozenset(
|
|
168
|
-
alias.asname or alias.name
|
|
169
|
-
for node in context.nodes(ast.Import)
|
|
170
|
-
for alias in node.names
|
|
171
|
-
if alias.name == "builtins"
|
|
172
|
-
)
|
|
173
|
-
for node in context.nodes(ast.Name, ast.Attribute):
|
|
174
|
-
if isinstance(node, ast.Attribute) and (
|
|
175
|
-
not isinstance(node.value, ast.Name) or node.attr not in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
176
|
-
):
|
|
177
|
-
continue
|
|
178
|
-
if not isinstance(node.ctx, ast.Load) or not _possible_native_function(node, function_names, module_names):
|
|
179
|
-
continue
|
|
180
|
-
parent = context.parents.get(node)
|
|
181
|
-
if (
|
|
182
|
-
not isinstance(parent, ast.Call)
|
|
183
|
-
or parent.func is not node
|
|
184
|
-
or _namespace_returning_function(node, namespace_function_names)
|
|
185
|
-
):
|
|
186
|
-
return True
|
|
187
|
-
return False
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _possible_native_function_names(
|
|
191
|
-
context: PythonFileContext, *, symbols: frozenset[str] = _BUILTIN_NAMESPACE_FUNCTIONS
|
|
192
|
-
) -> frozenset[str]:
|
|
193
|
-
return symbols | frozenset(
|
|
194
|
-
alias.asname or alias.name
|
|
195
|
-
for node in context.nodes(ast.ImportFrom)
|
|
196
|
-
if node.level == 0 and node.module == "builtins"
|
|
197
|
-
for alias in node.names
|
|
198
|
-
if alias.name in symbols
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _namespace_returning_function(node: ast.Name | ast.Attribute, function_names: frozenset[str]) -> bool:
|
|
203
|
-
if isinstance(node, ast.Name):
|
|
204
|
-
return node.id in function_names
|
|
205
|
-
return node.attr in _NAMESPACE_RETURNING_FUNCTIONS
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _possible_native_function(
|
|
209
|
-
node: ast.Name | ast.Attribute,
|
|
210
|
-
function_names: frozenset[str],
|
|
211
|
-
module_names: frozenset[str],
|
|
212
|
-
) -> bool:
|
|
213
|
-
if isinstance(node, ast.Name):
|
|
214
|
-
return node.id in function_names
|
|
215
|
-
return (
|
|
216
|
-
isinstance(node.value, ast.Name) and node.value.id in module_names and node.attr in _BUILTIN_NAMESPACE_FUNCTIONS
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
|
|
220
115
|
def _ambiguous_node(
|
|
221
116
|
node: ast.AST, context: PythonFileContext, mutation_targets: set[str], sensitive_names: frozenset[str]
|
|
222
117
|
) -> bool:
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._pydantic_field_contract import (
|
|
21
|
+
DECORATOR_SOURCES,
|
|
22
|
+
TYPING_SOURCES,
|
|
23
|
+
direct_models,
|
|
24
|
+
unique_members,
|
|
25
|
+
)
|
|
26
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
type _Primitive = str | bytes | int | float | complex | bool | None
|
|
34
|
+
_SCALARS = frozenset({"str", "bytes", "int", "float", "complex", "bool"})
|
|
35
|
+
_MODEL_CALLS = frozenset({"model_validate", "model_validate_json", "model_validate_strings", "model_construct"})
|
|
36
|
+
_MAX_ANNOTATION_NODES = 256
|
|
37
|
+
_MAX_ANNOTATION_DEPTH = 32
|
|
38
|
+
_VALIDATOR_ARITY = 2
|
|
39
|
+
_DECORATOR_STACK_SIZE = 2
|
|
40
|
+
_BAD = "from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n text: str\n @field_validator('text', mode='after')\n @classmethod\n def replace(cls, value: str) -> int:\n return 3\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class _Returned:
|
|
45
|
+
node: ast.expr
|
|
46
|
+
value: _Primitive
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@final
|
|
50
|
+
class NoInvalidPydanticValidatorReturn(Rule):
|
|
51
|
+
id = "no-invalid-pydantic-validator-return"
|
|
52
|
+
code = "SARJ515"
|
|
53
|
+
documentation = RuleDocumentation(
|
|
54
|
+
default_level=Severity.WARNING,
|
|
55
|
+
summary="A terminal field validator must not return a literal contradicting its stored field type.",
|
|
56
|
+
rationale="Pydantic trusts the result of after and plain field validators without validating it against the field again. A correctly annotated validator can therefore store an integer in a string field while the model still promises a string to callers.",
|
|
57
|
+
remediation="Return a value satisfying every targeted field's stored contract. Split validators for different field domains, or perform input conversion in a before validator so the normal field validation follows it. Preserve intentional validation chains and wire serializers.",
|
|
58
|
+
category=RuleCategory.CORRECTNESS,
|
|
59
|
+
autofix=AutofixPolicy.NONE,
|
|
60
|
+
limitations=(
|
|
61
|
+
"Checks stable direct module-level BaseModel classes containing one synchronous field-validator method, plain annotated fields, and optional literal defaults. Inheritance, configuration, custom methods, field metadata, class decorators and escaped or mutated model classes are excluded.",
|
|
62
|
+
"The validator must have the canonical field_validator/classmethod stack, explicit literal field targets, mode after or plain (including omitted default after), two required positional parameters, and a sole primitive literal return, optionally after a docstring. Before, wrap, serializer, unknown and composed validator contracts abstain.",
|
|
63
|
+
"Only contradictions with builtin scalar types, None, canonical Optional/Union and closed string Literal domains are proved. Numeric assignability preserves bool as int and int as float/complex. Opaque aliases, numeric Literal domains, containers and annotation graphs exceeding fixed budgets abstain.",
|
|
64
|
+
"Every targeted field must exist; unknown-field diagnostics belong to their existing owner. Multiple validators are excluded because a later validator can repair an intermediate value. One warning identifies all contradictory fields for the returned literal.",
|
|
65
|
+
"Generated and vendor files are excluded; authored tests remain checked. No autofix changes validation phase, chooses domain values or narrows a public contract.",
|
|
66
|
+
),
|
|
67
|
+
examples=(
|
|
68
|
+
RuleExample(
|
|
69
|
+
example_id="invalid-after-output",
|
|
70
|
+
title="An after validator stores an incompatible primitive unchanged",
|
|
71
|
+
outcome=ExampleOutcome.MATCH,
|
|
72
|
+
files=(ExampleFile.python("app/models.py", _BAD),),
|
|
73
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
74
|
+
expected_count=1,
|
|
75
|
+
public=True,
|
|
76
|
+
),
|
|
77
|
+
RuleExample(
|
|
78
|
+
example_id="valid-after-output",
|
|
79
|
+
title="The validator output satisfies the stored field contract",
|
|
80
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
81
|
+
files=(
|
|
82
|
+
ExampleFile.python(
|
|
83
|
+
"app/models.py", _BAD.replace("-> int", "-> str").replace("return 3", "return 'ready'")
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
87
|
+
expected_count=0,
|
|
88
|
+
public=True,
|
|
89
|
+
),
|
|
90
|
+
RuleExample(
|
|
91
|
+
example_id="plain-output-is-not-revalidated",
|
|
92
|
+
scenario="validation-phase",
|
|
93
|
+
title="A plain validator also bypasses subsequent field validation",
|
|
94
|
+
outcome=ExampleOutcome.MATCH,
|
|
95
|
+
files=(ExampleFile.python("app/models.py", _BAD.replace("mode='after'", "mode='plain'")),),
|
|
96
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
97
|
+
expected_count=1,
|
|
98
|
+
public=True,
|
|
99
|
+
),
|
|
100
|
+
RuleExample(
|
|
101
|
+
example_id="before-output-is-revalidated",
|
|
102
|
+
scenario="validation-phase",
|
|
103
|
+
title="Input conversion is followed by ordinary field validation",
|
|
104
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
105
|
+
files=(
|
|
106
|
+
ExampleFile.python(
|
|
107
|
+
"app/models.py",
|
|
108
|
+
_BAD.replace("text: str", "text: int")
|
|
109
|
+
.replace("mode='after'", "mode='before'")
|
|
110
|
+
.replace("-> int", "-> str")
|
|
111
|
+
.replace("return 3", "return '3'"),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
115
|
+
expected_count=0,
|
|
116
|
+
public=True,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
description = documentation.summary
|
|
121
|
+
|
|
122
|
+
@override
|
|
123
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
124
|
+
if context.tree is None or context.generated or "field_validator" not in context.symbol_source:
|
|
125
|
+
return []
|
|
126
|
+
annotations = [node.annotation for node in context.nodes(ast.AnnAssign)]
|
|
127
|
+
if any(sum(1 for _ in ast.walk(annotation)) > _MAX_ANNOTATION_NODES for annotation in annotations):
|
|
128
|
+
return []
|
|
129
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
130
|
+
findings: list[Diagnostic] = []
|
|
131
|
+
for model in direct_models(context, methods=True):
|
|
132
|
+
proof = _model_return(model, context, provenance)
|
|
133
|
+
if proof is None:
|
|
134
|
+
continue
|
|
135
|
+
returned, fields = proof
|
|
136
|
+
if not is_suppressed(context.source_lines, returned.node.lineno, self.code):
|
|
137
|
+
findings.append(
|
|
138
|
+
Diagnostic(
|
|
139
|
+
context.path,
|
|
140
|
+
returned.node.lineno,
|
|
141
|
+
returned.node.col_offset + 1,
|
|
142
|
+
self.code,
|
|
143
|
+
f"This terminal validator returns a literal outside the stored contract of {', '.join(sorted(fields))}. Return valid field values or validate converted input before storing it.",
|
|
144
|
+
severity=Severity.WARNING,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return findings
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _model_return(
|
|
151
|
+
model: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance
|
|
152
|
+
) -> tuple[_Returned, frozenset[str]] | None:
|
|
153
|
+
if not _stable_model(model, context, provenance):
|
|
154
|
+
return None
|
|
155
|
+
methods = [member for member in model.body if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
156
|
+
if len(methods) != 1 or not isinstance(methods[0], ast.FunctionDef):
|
|
157
|
+
return None
|
|
158
|
+
fields = _plain_fields(model)
|
|
159
|
+
if fields is None:
|
|
160
|
+
return None
|
|
161
|
+
validator = methods[0]
|
|
162
|
+
targets = _targets(validator, context)
|
|
163
|
+
returned = _returned_literal(validator)
|
|
164
|
+
if targets is None or returned is None or not targets <= fields.keys():
|
|
165
|
+
return None
|
|
166
|
+
rejected = frozenset(name for name in targets if _rejects(fields[name], returned.value, context, depth=0))
|
|
167
|
+
return (returned, rejected) if rejected else None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _plain_fields(model: ast.ClassDef) -> dict[str, ast.expr] | None:
|
|
171
|
+
fields: dict[str, ast.expr] = {}
|
|
172
|
+
unique = unique_members(model)
|
|
173
|
+
if unique & (_SCALARS | {"Literal", "Optional", "Union", "classmethod", "field_validator"}):
|
|
174
|
+
return None
|
|
175
|
+
for member in model.body:
|
|
176
|
+
if isinstance(member, ast.AnnAssign) and isinstance(member.target, ast.Name):
|
|
177
|
+
if member.target.id not in unique or member.target.id.startswith("_"):
|
|
178
|
+
return None
|
|
179
|
+
if member.value is not None and not isinstance(member.value, ast.Constant):
|
|
180
|
+
return None
|
|
181
|
+
fields[member.target.id] = member.annotation
|
|
182
|
+
elif not isinstance(member, (ast.FunctionDef, ast.Pass)) and not (
|
|
183
|
+
isinstance(member, ast.Expr)
|
|
184
|
+
and isinstance(member.value, ast.Constant)
|
|
185
|
+
and isinstance(member.value.value, str)
|
|
186
|
+
):
|
|
187
|
+
return None
|
|
188
|
+
return fields
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _stable_model(model: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance) -> bool:
|
|
192
|
+
if model.name in provenance.mutated or model.name in provenance.external_bindings:
|
|
193
|
+
return False
|
|
194
|
+
return all(
|
|
195
|
+
_stable_model_read(read, context)
|
|
196
|
+
for read in provenance.reads.get(model.name, ())
|
|
197
|
+
if provenance.binding(read.id, read) is model
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _stable_model_read(read: ast.Name, context: PythonFileContext) -> bool:
|
|
202
|
+
parent = context.parents.get(read)
|
|
203
|
+
if isinstance(parent, ast.Call) and parent.func is read:
|
|
204
|
+
return True
|
|
205
|
+
if isinstance(parent, (ast.arg, ast.AnnAssign)) and parent.annotation is read:
|
|
206
|
+
return True
|
|
207
|
+
if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)) and parent.returns is read:
|
|
208
|
+
return True
|
|
209
|
+
if isinstance(parent, ast.Attribute) and parent.value is read and parent.attr in _MODEL_CALLS:
|
|
210
|
+
call = context.parents.get(parent)
|
|
211
|
+
return isinstance(call, ast.Call) and call.func is parent
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _targets(validator: ast.FunctionDef, context: PythonFileContext) -> frozenset[str] | None:
|
|
216
|
+
arguments = validator.args
|
|
217
|
+
if validator.type_params or len(validator.decorator_list) != _DECORATOR_STACK_SIZE:
|
|
218
|
+
return None
|
|
219
|
+
if (
|
|
220
|
+
len((*arguments.posonlyargs, *arguments.args)) != _VALIDATOR_ARITY
|
|
221
|
+
or arguments.vararg
|
|
222
|
+
or arguments.kwarg
|
|
223
|
+
or arguments.kwonlyargs
|
|
224
|
+
or arguments.defaults
|
|
225
|
+
):
|
|
226
|
+
return None
|
|
227
|
+
outer, inner = validator.decorator_list
|
|
228
|
+
if not (
|
|
229
|
+
isinstance(inner, ast.Name)
|
|
230
|
+
and inner.id == "classmethod"
|
|
231
|
+
and context.imports.builtin_is_unshadowed("classmethod")
|
|
232
|
+
and isinstance(outer, ast.Call)
|
|
233
|
+
and context.imports.resolves(outer.func, sources=DECORATOR_SOURCES, symbol="field_validator")
|
|
234
|
+
):
|
|
235
|
+
return None
|
|
236
|
+
if len(outer.keywords) > 1 or any(
|
|
237
|
+
keyword.arg != "mode"
|
|
238
|
+
or not isinstance(keyword.value, ast.Constant)
|
|
239
|
+
or keyword.value.value not in {"after", "plain"}
|
|
240
|
+
for keyword in outer.keywords
|
|
241
|
+
):
|
|
242
|
+
return None
|
|
243
|
+
targets: set[str] = set()
|
|
244
|
+
for argument in outer.args:
|
|
245
|
+
if not isinstance(argument, ast.Constant) or not isinstance(argument.value, str) or argument.value == "*":
|
|
246
|
+
return None
|
|
247
|
+
targets.add(argument.value)
|
|
248
|
+
return frozenset(targets) if targets else None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _returned_literal(validator: ast.FunctionDef) -> _Returned | None:
|
|
252
|
+
body = validator.body
|
|
253
|
+
if (
|
|
254
|
+
body
|
|
255
|
+
and isinstance(body[0], ast.Expr)
|
|
256
|
+
and isinstance(body[0].value, ast.Constant)
|
|
257
|
+
and isinstance(body[0].value.value, str)
|
|
258
|
+
):
|
|
259
|
+
body = body[1:]
|
|
260
|
+
if len(body) != 1 or not isinstance(body[0], ast.Return):
|
|
261
|
+
return None
|
|
262
|
+
value = body[0].value
|
|
263
|
+
if isinstance(value, ast.Constant) and (
|
|
264
|
+
value.value is None or isinstance(value.value, (str, bytes, int, float, complex, bool))
|
|
265
|
+
):
|
|
266
|
+
return _Returned(value, value.value)
|
|
267
|
+
if (
|
|
268
|
+
isinstance(value, ast.UnaryOp)
|
|
269
|
+
and isinstance(value.op, (ast.USub, ast.UAdd))
|
|
270
|
+
and isinstance(value.operand, ast.Constant)
|
|
271
|
+
and isinstance(value.operand.value, (int, float, complex))
|
|
272
|
+
):
|
|
273
|
+
number = value.operand.value
|
|
274
|
+
return _Returned(value, -number if isinstance(value.op, ast.USub) else +number)
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _rejects(annotation: ast.expr, value: _Primitive, context: PythonFileContext, *, depth: int) -> bool:
|
|
279
|
+
if depth > _MAX_ANNOTATION_DEPTH:
|
|
280
|
+
return False
|
|
281
|
+
if isinstance(annotation, ast.Constant):
|
|
282
|
+
return annotation.value is None and value is not None
|
|
283
|
+
if isinstance(annotation, ast.Name) and context.imports.builtin_is_unshadowed(annotation.id):
|
|
284
|
+
return _scalar_rejects(annotation.id, value)
|
|
285
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
286
|
+
return _rejects(annotation.left, value, context, depth=depth + 1) and _rejects(
|
|
287
|
+
annotation.right, value, context, depth=depth + 1
|
|
288
|
+
)
|
|
289
|
+
if not isinstance(annotation, ast.Subscript):
|
|
290
|
+
return False
|
|
291
|
+
return _subscript_rejects(annotation, value, context, depth=depth)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _scalar_rejects(name: str, value: _Primitive) -> bool:
|
|
295
|
+
match name:
|
|
296
|
+
case "str":
|
|
297
|
+
return not isinstance(value, str)
|
|
298
|
+
case "bytes":
|
|
299
|
+
return not isinstance(value, bytes)
|
|
300
|
+
case "bool":
|
|
301
|
+
return not isinstance(value, bool)
|
|
302
|
+
case "int":
|
|
303
|
+
return not isinstance(value, int)
|
|
304
|
+
case "float":
|
|
305
|
+
return not isinstance(value, (int, float))
|
|
306
|
+
case "complex":
|
|
307
|
+
return not isinstance(value, (int, float, complex))
|
|
308
|
+
case _:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _subscript_rejects(annotation: ast.Subscript, value: _Primitive, context: PythonFileContext, *, depth: int) -> bool:
|
|
313
|
+
members = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else [annotation.slice]
|
|
314
|
+
wrapper = context.imports.resolved_symbol(annotation.value, sources=TYPING_SOURCES)
|
|
315
|
+
if wrapper == "Optional" and len(members) == 1:
|
|
316
|
+
return value is not None and _rejects(members[0], value, context, depth=depth + 1)
|
|
317
|
+
if wrapper == "Union" and members:
|
|
318
|
+
return all(_rejects(member, value, context, depth=depth + 1) for member in members)
|
|
319
|
+
if (
|
|
320
|
+
wrapper == "Literal"
|
|
321
|
+
and members
|
|
322
|
+
and all(isinstance(member, ast.Constant) and isinstance(member.value, str) for member in members)
|
|
323
|
+
):
|
|
324
|
+
return not any(isinstance(member, ast.Constant) and member.value == value for member in members)
|
|
325
|
+
return False
|