sarj-python-lint 1.53.0__tar.gz → 1.54.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.53.0 → sarj_python_lint-1.54.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__main__.py +5 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.54.0/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +467 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/.gitignore +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/LICENSE +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/README.md +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.53.0 → sarj_python_lint-1.54.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.54.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/
|
|
@@ -171,10 +171,15 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
171
171
|
"SARJ094": frozenset({"SARJ008", "SARJ447"}),
|
|
172
172
|
"SARJ095": frozenset({"SARJ468"}),
|
|
173
173
|
"SARJ099": frozenset({"SARJ420"}),
|
|
174
|
+
# A proven external or nested request boundary already requires schema
|
|
175
|
+
# validation at this field read. Retain standalone shape advice when
|
|
176
|
+
# that more specific rule is unselected or locally suppressed.
|
|
177
|
+
"SARJ411": frozenset({"SARJ521"}),
|
|
174
178
|
# Any-valued mappings are the stronger correctness failure. SARJ008
|
|
175
179
|
# still owns every fixed dictionary return when selected alone, while
|
|
176
180
|
# an all-rules run emits one actionable diagnostic for this overlap.
|
|
177
181
|
"SARJ447": frozenset({"SARJ008"}),
|
|
182
|
+
"SARJ450": frozenset({"SARJ521"}),
|
|
178
183
|
"SARJ457": frozenset({"SARJ045"}),
|
|
179
184
|
"SARJ465": frozenset({"SARJ071"}),
|
|
180
185
|
"SARJ491": frozenset({"SARJ400"}),
|
|
@@ -189,6 +189,7 @@ from sarj_python_lint.rules.require_public_dependency_contract import RequirePub
|
|
|
189
189
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
190
190
|
RequirePydanticForExternalJson,
|
|
191
191
|
)
|
|
192
|
+
from sarj_python_lint.rules.require_pydantic_for_mapping_shape import RequirePydanticForMappingShape
|
|
192
193
|
from sarj_python_lint.rules.require_pydantic_for_structured_payload import RequirePydanticForStructuredPayload
|
|
193
194
|
from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
|
|
194
195
|
RequirePydanticOrdinalLowerBound,
|
|
@@ -305,6 +306,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
305
306
|
PreferMatchAssertNever.id: PreferMatchAssertNever,
|
|
306
307
|
RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
|
|
307
308
|
RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
|
|
309
|
+
RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
|
|
308
310
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
309
311
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
310
312
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
20
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
21
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
22
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Iterator
|
|
27
|
+
|
|
28
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_TYPING = frozenset({"typing", "typing_extensions"})
|
|
32
|
+
_MAPPINGS = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"typing.Dict",
|
|
35
|
+
"typing.Mapping",
|
|
36
|
+
"typing.MutableMapping",
|
|
37
|
+
"collections.abc.Mapping",
|
|
38
|
+
"collections.abc.MutableMapping",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
_WRAPPERS = frozenset({"Annotated", "Final", "ClassVar", "Required", "NotRequired", "ReadOnly", "Optional"})
|
|
42
|
+
_VALIDATORS = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"pydantic.model_validator",
|
|
45
|
+
"pydantic.field_validator",
|
|
46
|
+
"pydantic.functional_validators.model_validator",
|
|
47
|
+
"pydantic.functional_validators.field_validator",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
_DISCRIMINATORS = frozenset({"pydantic.Discriminator", "pydantic.types.Discriminator"})
|
|
51
|
+
_RAW_METADATA = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"pydantic.BeforeValidator",
|
|
54
|
+
"pydantic.WrapValidator",
|
|
55
|
+
"pydantic.functional_validators.BeforeValidator",
|
|
56
|
+
"pydantic.functional_validators.WrapValidator",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
_MAX_DEPTH = 32
|
|
60
|
+
_MAPPING_ARITY = 2
|
|
61
|
+
_BAD = (
|
|
62
|
+
"def verified(raw: dict[str, object]) -> bool:\n"
|
|
63
|
+
" match raw.get('factor_age'):\n"
|
|
64
|
+
" case [int(), int(age)] if not isinstance(age, bool):\n"
|
|
65
|
+
" return age >= 0\n"
|
|
66
|
+
" case _:\n"
|
|
67
|
+
" return False\n"
|
|
68
|
+
)
|
|
69
|
+
_GOOD = (
|
|
70
|
+
"from typing import Annotated\n"
|
|
71
|
+
"from pydantic import BaseModel, Field, StrictInt, ValidationError\n"
|
|
72
|
+
"class FactorClaim(BaseModel):\n"
|
|
73
|
+
" factor_age: tuple[StrictInt, Annotated[StrictInt, Field(ge=0)]]\n"
|
|
74
|
+
"def verified(raw: object) -> bool:\n"
|
|
75
|
+
" try:\n"
|
|
76
|
+
" FactorClaim.model_validate(raw)\n"
|
|
77
|
+
" except ValidationError:\n"
|
|
78
|
+
" return False\n"
|
|
79
|
+
" return True\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@final
|
|
84
|
+
class RequirePydanticForMappingShape(Rule):
|
|
85
|
+
id = "require-pydantic-for-mapping-shape"
|
|
86
|
+
code = "SARJ521"
|
|
87
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
88
|
+
default_level=Severity.WARNING,
|
|
89
|
+
summary="Declare opaque mapping field shapes in Pydantic instead of probing them by hand.",
|
|
90
|
+
rationale=(
|
|
91
|
+
"Fixed-key reads followed by runtime type, shape, or discriminator checks spread a record contract "
|
|
92
|
+
"across control flow. An object/Any/JsonValue mapping does not declare those field contracts."
|
|
93
|
+
),
|
|
94
|
+
remediation=(
|
|
95
|
+
"Parse the raw object with a named Pydantic BaseModel or RootModel before using typed attributes. "
|
|
96
|
+
"Prefer strict fields, Field bounds, aliases/AliasPath and discriminated unions to handwritten probes. "
|
|
97
|
+
"For an already trusted internal record, declare a precise named record such as TypedDict instead of "
|
|
98
|
+
"parsing it again. Preserve rejection, defaults and fail-closed behavior explicitly; bool is an int subclass."
|
|
99
|
+
),
|
|
100
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
101
|
+
autofix=AutofixPolicy.NONE,
|
|
102
|
+
limitations=(
|
|
103
|
+
"Checks literal-string get/subscript reads on parameters or local names annotated as standard string mappings with object, Any or pydantic.JsonValue values. Stable module type aliases, quoted annotations, wrappers and local value aliases are recognized with bounded resolution.",
|
|
104
|
+
"A field must feed builtin isinstance/type checks, literal comparisons, truth tests or a constrained match pattern. Ordinary reads/forwarding, dynamic keys, homogeneous typed maps, imported application aliases, attributes and ambiguous/rebound assignments are excluded.",
|
|
105
|
+
"Import-resolved Pydantic before/wrap decorator or metadata validators and explicitly registered Discriminator callbacks own raw input normalization and are excluded. Stable callback function aliases and simple local callable instances/bound __call__ methods are recognized. Prefer declarative fields before introducing such callbacks.",
|
|
106
|
+
"Callable classes must be unique undecorated local declarations with no bases, class options, custom constructor or descriptor hooks; only an undecorated __call__ method and inert class data are admitted. Imported, inherited, decorated, rebound and directly mutated callback classes/instances abstain.",
|
|
107
|
+
"Parsing helpers in authored tests are checked, but assert-contained test oracles are excluded. Generated/vendor sources and dynamic namespaces are excluded. This is a warning with no automatic rewrite because coercion, missing-field handling and error policy can differ.",
|
|
108
|
+
),
|
|
109
|
+
examples=(
|
|
110
|
+
RuleExample(
|
|
111
|
+
example_id="manual-mapping-shape",
|
|
112
|
+
title="A match statement parses an opaque field by hand",
|
|
113
|
+
outcome=ExampleOutcome.MATCH,
|
|
114
|
+
files=(ExampleFile.python("app/claims.py", _BAD),),
|
|
115
|
+
focus_path=PurePosixPath("app/claims.py"),
|
|
116
|
+
expected_count=1,
|
|
117
|
+
public=True,
|
|
118
|
+
),
|
|
119
|
+
RuleExample(
|
|
120
|
+
example_id="declared-mapping-shape",
|
|
121
|
+
title="Strict fields and bounds declare the accepted shape",
|
|
122
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
123
|
+
files=(ExampleFile.python("app/claims.py", _GOOD),),
|
|
124
|
+
focus_path=PurePosixPath("app/claims.py"),
|
|
125
|
+
expected_count=0,
|
|
126
|
+
public=True,
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
description = documentation.summary
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
134
|
+
if (
|
|
135
|
+
context.tree is None
|
|
136
|
+
or context.generated
|
|
137
|
+
or not {"vendor", "vendored", "third_party"}.isdisjoint(context.path.parts)
|
|
138
|
+
):
|
|
139
|
+
return []
|
|
140
|
+
facts = _Facts(context, context.tree)
|
|
141
|
+
if facts.provenance.wildcard or facts.provenance.dynamic_namespace:
|
|
142
|
+
return []
|
|
143
|
+
sources: set[ast.expr] = set()
|
|
144
|
+
for node in context.nodes(ast.Call, ast.Compare, ast.Match, ast.If, ast.While, ast.IfExp, ast.Assert):
|
|
145
|
+
if facts.raw_boundary(node) or _test_assertion(node, context):
|
|
146
|
+
continue
|
|
147
|
+
sources.update(_probe_sources(node, facts))
|
|
148
|
+
return [
|
|
149
|
+
Diagnostic(
|
|
150
|
+
path=context.path,
|
|
151
|
+
line=node.lineno,
|
|
152
|
+
col=node.col_offset + 1,
|
|
153
|
+
code=self.code,
|
|
154
|
+
severity=Severity.WARNING,
|
|
155
|
+
message="Declare this mapping field's type, shape and discriminator in a named Pydantic model; use a precise named record for already trusted internals instead of probing an opaque get/subscript result.",
|
|
156
|
+
)
|
|
157
|
+
for node in sorted(sources, key=lambda source: (source.lineno, source.col_offset))
|
|
158
|
+
if not is_suppressed(context.source_lines, node.lineno, self.code)
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _Facts:
|
|
163
|
+
def __init__(self, context: PythonFileContext, tree: ast.Module) -> None:
|
|
164
|
+
self.context = context
|
|
165
|
+
self.provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
166
|
+
self.semantics = AnnotationSemantics.from_tree(tree)
|
|
167
|
+
self.imports = {
|
|
168
|
+
node: ImportIndex.from_tree(ast.Module(body=[node], type_ignores=[]))
|
|
169
|
+
for node in context.nodes(ast.Import, ast.ImportFrom)
|
|
170
|
+
}
|
|
171
|
+
self.callbacks = {
|
|
172
|
+
binding
|
|
173
|
+
for call in context.nodes(ast.Call)
|
|
174
|
+
if (qualified := self.qualified(call.func, call)) in _DISCRIMINATORS | _RAW_METADATA
|
|
175
|
+
if (argument := _callback_argument(call, qualified)) is not None
|
|
176
|
+
if (binding := self.callback(argument)) is not None
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def callback(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> ast.AST | None:
|
|
180
|
+
if len(seen) >= _MAX_DEPTH:
|
|
181
|
+
return None
|
|
182
|
+
if isinstance(node, ast.Call):
|
|
183
|
+
return self.callable_method(node, seen=seen)
|
|
184
|
+
if isinstance(node, ast.Attribute) and node.attr == "__call__":
|
|
185
|
+
return self.callable_method(node.value, seen=seen)
|
|
186
|
+
if not isinstance(node, ast.Name) or node.id in self.provenance.mutated | self.provenance.external_bindings:
|
|
187
|
+
return None
|
|
188
|
+
binding = self.provenance.binding(node.id, node)
|
|
189
|
+
if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
190
|
+
return binding if not binding.decorator_list else None
|
|
191
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
192
|
+
return None
|
|
193
|
+
value = self.provenance.assignment_value(binding, node)
|
|
194
|
+
return self.callback(value, seen=seen | {binding}) if value is not None else None
|
|
195
|
+
|
|
196
|
+
def callable_method(self, node: ast.expr, *, seen: frozenset[ast.AST]) -> ast.FunctionDef | None:
|
|
197
|
+
if len(seen) >= _MAX_DEPTH:
|
|
198
|
+
return None
|
|
199
|
+
if isinstance(node, ast.Name):
|
|
200
|
+
if node.id in self.provenance.mutated | self.provenance.external_bindings:
|
|
201
|
+
return None
|
|
202
|
+
binding = self.provenance.binding(node.id, node)
|
|
203
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
204
|
+
return None
|
|
205
|
+
value = self.provenance.assignment_value(binding, node)
|
|
206
|
+
return self.callable_method(value, seen=seen | {binding}) if value is not None else None
|
|
207
|
+
if not isinstance(node, ast.Call) or node.args or node.keywords or not isinstance(node.func, ast.Name):
|
|
208
|
+
return None
|
|
209
|
+
if node.func.id in self.provenance.mutated | self.provenance.external_bindings:
|
|
210
|
+
return None
|
|
211
|
+
binding = self.provenance.binding(node.func.id, node)
|
|
212
|
+
if not isinstance(binding, ast.ClassDef) or not isinstance(
|
|
213
|
+
self.context.parents.get(binding), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef
|
|
214
|
+
):
|
|
215
|
+
return None
|
|
216
|
+
return _plain_callable_method(binding)
|
|
217
|
+
|
|
218
|
+
def qualified(self, node: ast.expr, at: ast.AST) -> str | None:
|
|
219
|
+
root = node
|
|
220
|
+
while isinstance(root, ast.Attribute):
|
|
221
|
+
root = root.value
|
|
222
|
+
if not isinstance(root, ast.Name) or root.id in self.provenance.mutated | self.provenance.external_bindings:
|
|
223
|
+
return None
|
|
224
|
+
binding = self.provenance.binding(root.id, at)
|
|
225
|
+
parent = self.context.parents.get(binding) if binding is not None else None
|
|
226
|
+
index = self.imports.get(parent) if isinstance(parent, ast.Import | ast.ImportFrom) else None
|
|
227
|
+
return index.resolved_qualified_name(node) if index is not None else None
|
|
228
|
+
|
|
229
|
+
def builtin(self, node: ast.expr, name: str, at: ast.AST) -> bool:
|
|
230
|
+
return (
|
|
231
|
+
self.provenance.builtin_namespace_untouched
|
|
232
|
+
and isinstance(node, ast.Name)
|
|
233
|
+
and node.id == name
|
|
234
|
+
and self.provenance.binding(name, at) is None
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def annotation(self, node: ast.expr, at: ast.AST, *, mapping: bool, depth: int = 0) -> bool:
|
|
238
|
+
if depth >= _MAX_DEPTH:
|
|
239
|
+
return False
|
|
240
|
+
parsed = self.semantics.parse(node)
|
|
241
|
+
if parsed is None:
|
|
242
|
+
return False
|
|
243
|
+
if isinstance(parsed, ast.Name) and parsed.id in self.semantics.aliases:
|
|
244
|
+
return self.alias_annotation(parsed, at, mapping=mapping, depth=depth)
|
|
245
|
+
if isinstance(parsed, ast.BinOp) and isinstance(parsed.op, ast.BitOr):
|
|
246
|
+
return any(
|
|
247
|
+
self.annotation(member, at, mapping=mapping, depth=depth + 1) for member in (parsed.left, parsed.right)
|
|
248
|
+
)
|
|
249
|
+
qualified = self.qualified(parsed, at)
|
|
250
|
+
if not mapping and (
|
|
251
|
+
self.builtin(parsed, "object", at)
|
|
252
|
+
or qualified in {"typing.Any", "typing_extensions.Any", "pydantic.JsonValue"}
|
|
253
|
+
):
|
|
254
|
+
return True
|
|
255
|
+
if not isinstance(parsed, ast.Subscript):
|
|
256
|
+
return False
|
|
257
|
+
return self.subscription_annotation(parsed, at, mapping=mapping, depth=depth)
|
|
258
|
+
|
|
259
|
+
def alias_annotation(self, node: ast.Name, at: ast.AST, *, mapping: bool, depth: int) -> bool:
|
|
260
|
+
binding = self.provenance.binding(node.id, at)
|
|
261
|
+
if not isinstance(binding, ast.Name) or not isinstance(
|
|
262
|
+
self.context.parents.get(binding), ast.TypeAlias | ast.AnnAssign
|
|
263
|
+
):
|
|
264
|
+
return False
|
|
265
|
+
value = self.semantics.aliases[node.id]
|
|
266
|
+
return self.annotation(value, value, mapping=mapping, depth=depth + 1)
|
|
267
|
+
|
|
268
|
+
def subscription_annotation(self, node: ast.Subscript, at: ast.AST, *, mapping: bool, depth: int) -> bool:
|
|
269
|
+
qualified = self.qualified(node.value, at)
|
|
270
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
271
|
+
wrapper = qualified.rpartition(".")[-1] if qualified is not None else None
|
|
272
|
+
if qualified is not None and qualified.partition(".")[0] in _TYPING:
|
|
273
|
+
if wrapper in _WRAPPERS and members:
|
|
274
|
+
return self.annotation(members[0], at, mapping=mapping, depth=depth + 1)
|
|
275
|
+
if wrapper == "Union":
|
|
276
|
+
return any(self.annotation(member, at, mapping=mapping, depth=depth + 1) for member in members)
|
|
277
|
+
if not mapping or len(members) != _MAPPING_ARITY:
|
|
278
|
+
return False
|
|
279
|
+
key, value = members
|
|
280
|
+
return (
|
|
281
|
+
(self.builtin(node.value, "dict", at) or qualified in _MAPPINGS)
|
|
282
|
+
and self.builtin(key, "str", at)
|
|
283
|
+
and self.annotation(value, at, mapping=False, depth=depth + 1)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def mapping(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> bool:
|
|
287
|
+
if not isinstance(node, ast.Name) or len(seen) >= _MAX_DEPTH or node.id in self.provenance.external_bindings:
|
|
288
|
+
return False
|
|
289
|
+
binding = self.provenance.binding(node.id, node)
|
|
290
|
+
if binding is None or binding in seen:
|
|
291
|
+
return False
|
|
292
|
+
if isinstance(binding, ast.arg):
|
|
293
|
+
return binding.annotation is not None and self.annotation(binding.annotation, binding, mapping=True)
|
|
294
|
+
parent = self.context.parents.get(binding)
|
|
295
|
+
if isinstance(parent, ast.AnnAssign) and parent.annotation is not None:
|
|
296
|
+
return self.annotation(parent.annotation, binding, mapping=True)
|
|
297
|
+
value = self.provenance.assignment_value(binding, node) if isinstance(binding, ast.Name) else None
|
|
298
|
+
return value is not None and self.mapping(value, seen=seen | {binding})
|
|
299
|
+
|
|
300
|
+
def field(self, node: ast.expr, *, seen: frozenset[ast.AST] = frozenset()) -> ast.expr | None:
|
|
301
|
+
if len(seen) >= _MAX_DEPTH:
|
|
302
|
+
return None
|
|
303
|
+
if isinstance(node, ast.NamedExpr):
|
|
304
|
+
return self.field(node.value, seen=seen)
|
|
305
|
+
if isinstance(node, ast.Name):
|
|
306
|
+
binding = self.provenance.binding(node.id, node)
|
|
307
|
+
if not isinstance(binding, ast.Name) or binding in seen or node.id in self.provenance.external_bindings:
|
|
308
|
+
return None
|
|
309
|
+
value = self.provenance.assignment_value(binding, node)
|
|
310
|
+
return self.field(value, seen=seen | {binding}) if value is not None else None
|
|
311
|
+
if (
|
|
312
|
+
isinstance(node, ast.Subscript)
|
|
313
|
+
and isinstance(node.ctx, ast.Load)
|
|
314
|
+
and _string(node.slice)
|
|
315
|
+
and self.mapping(node.value)
|
|
316
|
+
):
|
|
317
|
+
return node
|
|
318
|
+
if (receiver := _get_receiver(node)) is not None and self.mapping(receiver):
|
|
319
|
+
return node
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def raw_boundary(self, node: ast.AST) -> bool:
|
|
323
|
+
current: ast.AST | None = node
|
|
324
|
+
while current is not None:
|
|
325
|
+
if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef) and self.raw_validator(current):
|
|
326
|
+
return True
|
|
327
|
+
current = self.context.parents.get(current)
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
def raw_validator(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
331
|
+
if function in self.callbacks:
|
|
332
|
+
return True
|
|
333
|
+
for decorator in function.decorator_list:
|
|
334
|
+
if not isinstance(decorator, ast.Call) or self.qualified(decorator.func, decorator) not in _VALIDATORS:
|
|
335
|
+
continue
|
|
336
|
+
if any(
|
|
337
|
+
keyword.arg == "mode"
|
|
338
|
+
and isinstance(keyword.value, ast.Constant)
|
|
339
|
+
and keyword.value.value in {"before", "wrap"}
|
|
340
|
+
for keyword in decorator.keywords
|
|
341
|
+
):
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _string(node: ast.expr) -> bool:
|
|
347
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, str)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _test_assertion(node: ast.AST, context: PythonFileContext) -> bool:
|
|
351
|
+
if not is_test_path(context.path):
|
|
352
|
+
return False
|
|
353
|
+
current: ast.AST | None = node
|
|
354
|
+
while current is not None:
|
|
355
|
+
if isinstance(current, ast.Assert):
|
|
356
|
+
return True
|
|
357
|
+
if isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
|
|
358
|
+
return False
|
|
359
|
+
current = context.parents.get(current)
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _get_receiver(node: ast.expr) -> ast.expr | None:
|
|
364
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute) or node.func.attr != "get":
|
|
365
|
+
return None
|
|
366
|
+
if len(node.args) not in {1, 2} or node.keywords or not _string(node.args[0]):
|
|
367
|
+
return None
|
|
368
|
+
return node.func.value
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _callback_argument(call: ast.Call, qualified: str | None) -> ast.expr | None:
|
|
372
|
+
if call.args:
|
|
373
|
+
return call.args[0]
|
|
374
|
+
parameter = "discriminator" if qualified in _DISCRIMINATORS else "func"
|
|
375
|
+
return next((keyword.value for keyword in call.keywords if keyword.arg == parameter), None)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _plain_callable_method(node: ast.ClassDef) -> ast.FunctionDef | None:
|
|
379
|
+
if node.bases or node.keywords or node.decorator_list or node.type_params:
|
|
380
|
+
return None
|
|
381
|
+
method: ast.FunctionDef | None = None
|
|
382
|
+
for statement in node.body:
|
|
383
|
+
match statement:
|
|
384
|
+
case ast.FunctionDef(name="__call__", decorator_list=[]) if method is None:
|
|
385
|
+
method = statement
|
|
386
|
+
case ast.Assign(value=ast.Constant(), targets=targets) if all(
|
|
387
|
+
isinstance(target, ast.Name) and _inert_callable_attribute(target.id) for target in targets
|
|
388
|
+
):
|
|
389
|
+
continue
|
|
390
|
+
case ast.AnnAssign(target=ast.Name(id=name), value=None | ast.Constant()) if _inert_callable_attribute(
|
|
391
|
+
name
|
|
392
|
+
):
|
|
393
|
+
continue
|
|
394
|
+
case ast.Pass() | ast.Expr(value=ast.Constant(value=str())):
|
|
395
|
+
continue
|
|
396
|
+
case _:
|
|
397
|
+
return None
|
|
398
|
+
return method
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _inert_callable_attribute(name: str) -> bool:
|
|
402
|
+
return name == "__name__" or not (name.startswith("__") and name.endswith("__"))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _probe_sources(node: ast.AST, facts: _Facts) -> Iterator[ast.expr]:
|
|
406
|
+
match node:
|
|
407
|
+
case ast.Call(func=func, args=[value, _], keywords=[]) if facts.builtin(func, "isinstance", node):
|
|
408
|
+
if (field := facts.field(value)) is not None:
|
|
409
|
+
yield field
|
|
410
|
+
case ast.Compare():
|
|
411
|
+
yield from _comparison_fields(node, facts)
|
|
412
|
+
case ast.Match(subject=subject, cases=cases):
|
|
413
|
+
for case in cases:
|
|
414
|
+
yield from _match_fields(subject, case.pattern, facts)
|
|
415
|
+
case ast.If(test=test) | ast.While(test=test) | ast.IfExp(test=test) | ast.Assert(test=test):
|
|
416
|
+
yield from _truth_fields(test, facts)
|
|
417
|
+
case _:
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _comparison_fields(node: ast.Compare, facts: _Facts) -> Iterator[ast.expr]:
|
|
422
|
+
operands = [node.left, *node.comparators]
|
|
423
|
+
for value in operands:
|
|
424
|
+
if (
|
|
425
|
+
any(_literal(other) for other in operands if other is not value)
|
|
426
|
+
and (field := facts.field(value)) is not None
|
|
427
|
+
):
|
|
428
|
+
yield field
|
|
429
|
+
if (field := _type_field(value, facts)) is not None:
|
|
430
|
+
yield field
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _literal(node: ast.expr) -> bool:
|
|
434
|
+
return isinstance(node, ast.Constant) or (
|
|
435
|
+
isinstance(node, ast.Set | ast.List | ast.Tuple) and all(isinstance(item, ast.Constant) for item in node.elts)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _type_field(node: ast.expr, facts: _Facts) -> ast.expr | None:
|
|
440
|
+
if not isinstance(node, ast.Call) or len(node.args) != 1 or node.keywords:
|
|
441
|
+
return None
|
|
442
|
+
return facts.field(node.args[0]) if facts.builtin(node.func, "type", node) else None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _truth_fields(node: ast.expr, facts: _Facts) -> Iterator[ast.expr]:
|
|
446
|
+
if (field := facts.field(node)) is not None:
|
|
447
|
+
yield field
|
|
448
|
+
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
|
|
449
|
+
yield from _truth_fields(node.operand, facts)
|
|
450
|
+
elif isinstance(node, ast.BoolOp):
|
|
451
|
+
for value in node.values:
|
|
452
|
+
yield from _truth_fields(value, facts)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _match_fields(subject: ast.expr, pattern: ast.pattern, facts: _Facts) -> Iterator[ast.expr]:
|
|
456
|
+
if isinstance(pattern, ast.MatchAs):
|
|
457
|
+
if pattern.pattern is not None:
|
|
458
|
+
yield from _match_fields(subject, pattern.pattern, facts)
|
|
459
|
+
elif isinstance(pattern, ast.MatchOr):
|
|
460
|
+
for member in pattern.patterns:
|
|
461
|
+
yield from _match_fields(subject, member, facts)
|
|
462
|
+
elif isinstance(subject, ast.Tuple | ast.List) and isinstance(pattern, ast.MatchSequence):
|
|
463
|
+
if len(subject.elts) == len(pattern.patterns):
|
|
464
|
+
for value, member in zip(subject.elts, pattern.patterns, strict=True):
|
|
465
|
+
yield from _match_fields(value, member, facts)
|
|
466
|
+
elif not isinstance(pattern, ast.MatchStar) and (field := facts.field(subject)) is not None:
|
|
467
|
+
yield field
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_runtime_bindings.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_static_json_alias.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.53.0 → sarj_python_lint-1.54.0}/src/sarj_python_lint/rules/_store_writes.py
RENAMED
|
File without changes
|