sarj-python-lint 1.61.1__tar.gz → 1.62.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.61.1 → sarj_python_lint-1.62.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/pyproject.toml +1 -1
- sarj_python_lint-1.62.0/src/sarj_python_lint/cast_access.py +86 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +2 -4
- sarj_python_lint-1.62.0/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +389 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/unused_mock_setup.py +3 -3
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/.gitignore +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/LICENSE +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/README.md +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.61.1 → sarj_python_lint-1.62.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.62.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,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
8
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_TYPING = frozenset({"typing", "typing_extensions"})
|
|
16
|
+
_GETATTR_ARITIES = frozenset({2, 3})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class CastAccess:
|
|
21
|
+
node: ast.Call | ast.ImportFrom
|
|
22
|
+
qualified_api: str
|
|
23
|
+
kind: Literal["wildcard", "reflection"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def indirect_typing_cast_accesses(path: Path, source: str) -> tuple[CastAccess, ...]:
|
|
27
|
+
context = PythonFileContext(path, source)
|
|
28
|
+
if "*" not in source and "getattr" not in context.symbol_source:
|
|
29
|
+
return ()
|
|
30
|
+
if context.tree is None:
|
|
31
|
+
return ()
|
|
32
|
+
accesses = [
|
|
33
|
+
CastAccess(node, f"{node.module}.cast", "wildcard")
|
|
34
|
+
for node in context.nodes(ast.ImportFrom)
|
|
35
|
+
if node.level == 0 and node.module in _TYPING and any(alias.name == "*" for alias in node.names)
|
|
36
|
+
]
|
|
37
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
38
|
+
if provenance.wildcard or provenance.dynamic_namespace or not provenance.builtin_namespace_untouched:
|
|
39
|
+
return tuple(accesses)
|
|
40
|
+
for call in context.nodes(ast.Call):
|
|
41
|
+
if not _getattr_call(call, provenance):
|
|
42
|
+
continue
|
|
43
|
+
module = _typing_receiver(call.args[0], provenance)
|
|
44
|
+
if module is not None:
|
|
45
|
+
accesses.append(CastAccess(call, f"{module}.cast", "reflection"))
|
|
46
|
+
return tuple(sorted(accesses, key=lambda access: (access.node.lineno, access.node.col_offset)))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _getattr_call(call: ast.Call, provenance: ResourceProvenance) -> bool:
|
|
50
|
+
if not (
|
|
51
|
+
isinstance(call.func, ast.Name)
|
|
52
|
+
and call.func.id == "getattr"
|
|
53
|
+
and len(call.args) in _GETATTR_ARITIES
|
|
54
|
+
and not call.keywords
|
|
55
|
+
and isinstance(call.args[1], ast.Constant)
|
|
56
|
+
and call.args[1].value == "cast"
|
|
57
|
+
):
|
|
58
|
+
return False
|
|
59
|
+
scope = provenance.scope(call.func)
|
|
60
|
+
while scope is not None:
|
|
61
|
+
if "getattr" in provenance.bindings.get(scope, {}):
|
|
62
|
+
return False
|
|
63
|
+
scope = provenance.scope(scope)
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _typing_receiver(node: ast.expr, provenance: ResourceProvenance) -> str | None:
|
|
68
|
+
if not isinstance(node, ast.Name) or node.id in provenance.mutated | provenance.external_bindings:
|
|
69
|
+
return None
|
|
70
|
+
binding = provenance.binding(node.id, node)
|
|
71
|
+
if not (
|
|
72
|
+
isinstance(binding, ast.alias)
|
|
73
|
+
and isinstance(provenance.context.parents.get(binding), ast.Import)
|
|
74
|
+
and binding.name in _TYPING
|
|
75
|
+
):
|
|
76
|
+
return None
|
|
77
|
+
for read in provenance.reads.get(node.id, ()):
|
|
78
|
+
if provenance.binding(read.id, read) is not binding:
|
|
79
|
+
continue
|
|
80
|
+
parent = provenance.context.parents.get(read)
|
|
81
|
+
if isinstance(parent, ast.Attribute) and parent.value is read:
|
|
82
|
+
continue
|
|
83
|
+
if isinstance(parent, ast.Call) and _getattr_call(parent, provenance) and parent.args[0] is read:
|
|
84
|
+
continue
|
|
85
|
+
return None
|
|
86
|
+
return binding.name
|
|
@@ -44,6 +44,7 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
|
44
44
|
from sarj_python_lint.rules.no_database_programmability import NoDatabaseProgrammability
|
|
45
45
|
from sarj_python_lint.rules.no_delete_statement import NoDeleteStatement
|
|
46
46
|
from sarj_python_lint.rules.no_discarded_validation_result import NoDiscardedValidationResult
|
|
47
|
+
from sarj_python_lint.rules.no_dual_shape_model_field_reader import NoDualShapeModelFieldReader
|
|
47
48
|
from sarj_python_lint.rules.no_dunder_all import NoDunderAll
|
|
48
49
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
49
50
|
from sarj_python_lint.rules.no_excessive_cognitive_complexity import NoExcessiveCognitiveComplexity
|
|
@@ -307,6 +308,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
307
308
|
RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
|
|
308
309
|
RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
|
|
309
310
|
RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
|
|
311
|
+
NoDualShapeModelFieldReader.id: NoDualShapeModelFieldReader,
|
|
310
312
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
311
313
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
312
314
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import builtins
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
|
+
from types import MappingProxyType
|
|
6
7
|
from typing import (
|
|
7
8
|
TYPE_CHECKING,
|
|
8
9
|
ClassVar,
|
|
9
|
-
cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits isinstance narrowing.
|
|
10
10
|
final,
|
|
11
11
|
override,
|
|
12
12
|
)
|
|
@@ -31,14 +31,12 @@ from sarj_python_lint.rules._suppression_comments import scan_comments
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
|
-
from collections.abc import Mapping
|
|
35
|
-
|
|
36
34
|
from sarj_python_lint._file_context import PythonFileContext
|
|
37
35
|
from sarj_python_lint.rules._project_index import SourceUnit, SymbolRef
|
|
38
36
|
from sarj_python_lint.rules._suppression_comments import Comment
|
|
39
37
|
|
|
40
38
|
|
|
41
|
-
_BUILTIN_NAMESPACE =
|
|
39
|
+
_BUILTIN_NAMESPACE = MappingProxyType[str, object](vars(builtins))
|
|
42
40
|
_BUILTIN_EXCEPTIONS = frozenset(
|
|
43
41
|
name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, Exception)
|
|
44
42
|
)
|
|
@@ -0,0 +1,389 @@
|
|
|
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._resource_provenance import ResourceProvenance, import_name_for_binding
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_TYPING = ("typing", "typing_extensions")
|
|
28
|
+
_MODEL_BASES = ("pydantic.BaseModel", "pydantic.main.BaseModel")
|
|
29
|
+
_MAPPINGS = (
|
|
30
|
+
"collections.abc.Mapping",
|
|
31
|
+
"collections.abc.MutableMapping",
|
|
32
|
+
"typing.Mapping",
|
|
33
|
+
"typing.MutableMapping",
|
|
34
|
+
)
|
|
35
|
+
_PAIR = 2
|
|
36
|
+
_MAX_SELECTOR_ALIAS_DEPTH = 16
|
|
37
|
+
_MODEL = "from pydantic import BaseModel\nclass Route(BaseModel):\n destination: str\n"
|
|
38
|
+
_BAD = (
|
|
39
|
+
_MODEL
|
|
40
|
+
+ """
|
|
41
|
+
def _field(route: object, name: str) -> object:
|
|
42
|
+
if isinstance(route, Route):
|
|
43
|
+
return getattr(route, name)
|
|
44
|
+
if isinstance(route, dict):
|
|
45
|
+
return route.get(name)
|
|
46
|
+
return None
|
|
47
|
+
def destination(raw: object) -> object:
|
|
48
|
+
return _field(raw, 'destination')
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
_GOOD = (
|
|
52
|
+
_MODEL
|
|
53
|
+
+ """
|
|
54
|
+
def destination(raw: object) -> str:
|
|
55
|
+
route = Route.model_validate(raw)
|
|
56
|
+
return route.destination
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@final
|
|
62
|
+
class NoDualShapeModelFieldReader(Rule):
|
|
63
|
+
id = "no-dual-shape-model-field-reader"
|
|
64
|
+
code = "SARJ522"
|
|
65
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
66
|
+
default_level=Severity.WARNING,
|
|
67
|
+
summary="Normalize a known model instead of reading its fields through a model-or-dict accessor.",
|
|
68
|
+
rationale=(
|
|
69
|
+
"A string-selected object return discards the model's field types and duplicates the raw and validated "
|
|
70
|
+
"representations at every read. Literal field names at all local callers prove this is a fixed record API."
|
|
71
|
+
),
|
|
72
|
+
remediation=(
|
|
73
|
+
"Normalize raw input once with the named model's model_validate, then use typed attributes. Preserve "
|
|
74
|
+
"missing-field, coercion, default and validation-error behavior; before validators still own raw input."
|
|
75
|
+
),
|
|
76
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
77
|
+
autofix=AutofixPolicy.NONE,
|
|
78
|
+
limitations=(
|
|
79
|
+
"Checks stable private module functions with two parameters, an object/Any input and return, and a string field selector in either parameter order. The body must consist of a known local Pydantic model getattr branch, a builtin dict or import-proven standard Mapping get/subscript branch and a None fallback. Import-proven typing casts are transparent.",
|
|
80
|
+
"Every visible use must be a direct call with a declared model field selected by a literal or a bounded chain of dominating single-assignment constants. Dynamic selectors, escaped/public helpers, imported/inherited models, custom attribute hooks, shadowed builtins and ambiguous or rebound bindings abstain. No application modules are imported.",
|
|
81
|
+
"Opaque values, arbitrary dictionaries and generic reflection alone are valid. Raw Pydantic validators are not prohibited, but extracting this closed dual-shape reader does not make its erased field contract typed.",
|
|
82
|
+
"Authored tests are checked; generated/vendor code is excluded. This is a warning without an autofix because normalization can change rejection, coercion, default generation and error timing.",
|
|
83
|
+
),
|
|
84
|
+
examples=(
|
|
85
|
+
RuleExample(
|
|
86
|
+
example_id="model-or-dict-reader",
|
|
87
|
+
title="A fixed model field is hidden behind a string-selected object return",
|
|
88
|
+
outcome=ExampleOutcome.MATCH,
|
|
89
|
+
files=(ExampleFile.python("app/routes.py", _BAD),),
|
|
90
|
+
focus_path=PurePosixPath("app/routes.py"),
|
|
91
|
+
expected_count=1,
|
|
92
|
+
public=True,
|
|
93
|
+
),
|
|
94
|
+
RuleExample(
|
|
95
|
+
example_id="normalized-model",
|
|
96
|
+
title="Normalize the input and read the typed field",
|
|
97
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
98
|
+
files=(ExampleFile.python("app/routes.py", _GOOD),),
|
|
99
|
+
focus_path=PurePosixPath("app/routes.py"),
|
|
100
|
+
expected_count=0,
|
|
101
|
+
public=True,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
description = documentation.summary
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
109
|
+
if (
|
|
110
|
+
context.tree is None
|
|
111
|
+
or context.generated
|
|
112
|
+
or not {"vendor", "vendored", "third_party"}.isdisjoint(context.path.parts)
|
|
113
|
+
or "getattr" not in context.symbol_source
|
|
114
|
+
):
|
|
115
|
+
return []
|
|
116
|
+
provenance = ResourceProvenance(context, require_static_namespace=True)
|
|
117
|
+
if provenance.wildcard or provenance.dynamic_namespace or not provenance.builtin_namespace_untouched:
|
|
118
|
+
return []
|
|
119
|
+
findings: list[Diagnostic] = []
|
|
120
|
+
for function in context.tree.body:
|
|
121
|
+
if not isinstance(function, ast.FunctionDef) or not _reader_signature(function, provenance):
|
|
122
|
+
continue
|
|
123
|
+
model = _reader_model(function, context, provenance)
|
|
124
|
+
if model is None or not _fixed_field_calls(function, model, context, provenance):
|
|
125
|
+
continue
|
|
126
|
+
if is_suppressed(context.source_lines, function.lineno, self.code):
|
|
127
|
+
continue
|
|
128
|
+
findings.append(
|
|
129
|
+
Diagnostic(
|
|
130
|
+
context.path,
|
|
131
|
+
function.lineno,
|
|
132
|
+
function.col_offset + 1,
|
|
133
|
+
self.code,
|
|
134
|
+
f"Normalize input once with {model.name}.model_validate(...) and read typed attributes instead "
|
|
135
|
+
"of projecting the same known fields through model getattr and mapping lookup branches.",
|
|
136
|
+
severity=Severity.WARNING,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return findings
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _reader_signature(function: ast.FunctionDef, provenance: ResourceProvenance) -> bool:
|
|
143
|
+
return (
|
|
144
|
+
function.name.startswith("_")
|
|
145
|
+
and not function.decorator_list
|
|
146
|
+
and not function.type_params
|
|
147
|
+
and _reader_parameters(function, provenance) is not None
|
|
148
|
+
and function.args.vararg is None
|
|
149
|
+
and function.args.kwarg is None
|
|
150
|
+
and not function.args.defaults
|
|
151
|
+
and not any(function.args.kw_defaults)
|
|
152
|
+
and provenance.binding(function.name, function) is function
|
|
153
|
+
and _opaque(function.returns, provenance)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _reader_parameters(function: ast.FunctionDef, provenance: ResourceProvenance) -> tuple[ast.arg, ast.arg] | None:
|
|
158
|
+
arguments = (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs)
|
|
159
|
+
if len(arguments) != _PAIR:
|
|
160
|
+
return None
|
|
161
|
+
values = [argument for argument in arguments if _opaque(argument.annotation, provenance)]
|
|
162
|
+
keys = [
|
|
163
|
+
argument
|
|
164
|
+
for argument in arguments
|
|
165
|
+
if _builtin(AnnotationSemantics.parse(argument.annotation), "str", provenance)
|
|
166
|
+
]
|
|
167
|
+
return (values[0], keys[0]) if len(values) == len(keys) == 1 else None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _opaque(annotation: ast.expr | None, provenance: ResourceProvenance) -> bool:
|
|
171
|
+
node = AnnotationSemantics.parse(annotation)
|
|
172
|
+
return _builtin(node, "object", provenance) or (
|
|
173
|
+
node is not None and any(_imported(node, f"{module}.Any", provenance) for module in _TYPING)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _builtin(node: ast.expr | None, name: str, provenance: ResourceProvenance) -> bool:
|
|
178
|
+
return (
|
|
179
|
+
isinstance(node, ast.Name)
|
|
180
|
+
and node.id == name
|
|
181
|
+
and provenance.context.module_imports.builtin_is_unshadowed(name)
|
|
182
|
+
and provenance.binding(name, node) is None
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _imported(node: ast.expr, qualified: str, provenance: ResourceProvenance, *, at: ast.AST | None = None) -> bool:
|
|
187
|
+
root = node
|
|
188
|
+
while isinstance(root, ast.Attribute):
|
|
189
|
+
root = root.value
|
|
190
|
+
if not isinstance(root, ast.Name) or root.id in provenance.mutated | provenance.external_bindings:
|
|
191
|
+
return False
|
|
192
|
+
binding = provenance.binding(root.id, node if at is None else at)
|
|
193
|
+
if not isinstance(binding, ast.alias):
|
|
194
|
+
return False
|
|
195
|
+
return import_name_for_binding(node, binding, provenance.context.parents.get(binding)) == qualified
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _reader_model(
|
|
199
|
+
function: ast.FunctionDef, context: PythonFileContext, provenance: ResourceProvenance
|
|
200
|
+
) -> ast.ClassDef | None:
|
|
201
|
+
parameters = _reader_parameters(function, provenance)
|
|
202
|
+
if parameters is None:
|
|
203
|
+
return None
|
|
204
|
+
value, key = parameters
|
|
205
|
+
arms = _reader_arms(function.body)
|
|
206
|
+
if len(arms) != _PAIR:
|
|
207
|
+
return None
|
|
208
|
+
model: ast.ClassDef | None = None
|
|
209
|
+
mapping = False
|
|
210
|
+
for branch in arms:
|
|
211
|
+
guard = branch.test
|
|
212
|
+
if not (
|
|
213
|
+
isinstance(guard, ast.Call)
|
|
214
|
+
and _builtin(guard.func, "isinstance", provenance)
|
|
215
|
+
and len(guard.args) == _PAIR
|
|
216
|
+
and not guard.keywords
|
|
217
|
+
and _parameter(guard.args[0], value, provenance)
|
|
218
|
+
and len(branch.body) == 1
|
|
219
|
+
and isinstance(branch.body[0], ast.Return)
|
|
220
|
+
and branch.body[0].value is not None
|
|
221
|
+
):
|
|
222
|
+
return None
|
|
223
|
+
result = _without_cast(branch.body[0].value, provenance)
|
|
224
|
+
target = guard.args[1]
|
|
225
|
+
if _builtin(target, "dict", provenance) or any(_imported(target, kind, provenance) for kind in _MAPPINGS):
|
|
226
|
+
mapping = _mapping_read(result, value, key, provenance)
|
|
227
|
+
elif _attribute_read(result, value, key, provenance):
|
|
228
|
+
model = _local_model(target, context, provenance)
|
|
229
|
+
else:
|
|
230
|
+
return None
|
|
231
|
+
return model if mapping else None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _reader_arms(body: list[ast.stmt]) -> list[ast.If]:
|
|
235
|
+
statements = list(body)
|
|
236
|
+
if statements and isinstance(statements[0], ast.Expr) and isinstance(statements[0].value, ast.Constant):
|
|
237
|
+
statements = statements[1:]
|
|
238
|
+
branches: list[ast.If] = []
|
|
239
|
+
while statements:
|
|
240
|
+
branch = statements[0]
|
|
241
|
+
if not isinstance(branch, ast.If):
|
|
242
|
+
break
|
|
243
|
+
branches.append(branch)
|
|
244
|
+
statements = [*branch.orelse, *statements[1:]]
|
|
245
|
+
if len(statements) == 1 and isinstance(statements[0], ast.Return) and _none(statements[0].value):
|
|
246
|
+
return branches
|
|
247
|
+
return []
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _none(node: ast.expr | None) -> bool:
|
|
251
|
+
return node is None or (isinstance(node, ast.Constant) and node.value is None)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parameter(node: ast.expr, parameter: ast.arg, provenance: ResourceProvenance) -> bool:
|
|
255
|
+
return isinstance(node, ast.Name) and node.id == parameter.arg and provenance.binding(node.id, node) is parameter
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _without_cast(node: ast.expr, provenance: ResourceProvenance) -> ast.expr:
|
|
259
|
+
if (
|
|
260
|
+
isinstance(node, ast.Call)
|
|
261
|
+
and len(node.args) == _PAIR
|
|
262
|
+
and not node.keywords
|
|
263
|
+
and any(_imported(node.func, f"{module}.cast", provenance) for module in _TYPING)
|
|
264
|
+
):
|
|
265
|
+
return node.args[1]
|
|
266
|
+
return node
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _attribute_read(node: ast.expr, value: ast.arg, key: ast.arg, provenance: ResourceProvenance) -> bool:
|
|
270
|
+
return (
|
|
271
|
+
isinstance(node, ast.Call)
|
|
272
|
+
and _builtin(node.func, "getattr", provenance)
|
|
273
|
+
and len(node.args) in {_PAIR, _PAIR + 1}
|
|
274
|
+
and not node.keywords
|
|
275
|
+
and _parameter(node.args[0], value, provenance)
|
|
276
|
+
and _parameter(node.args[1], key, provenance)
|
|
277
|
+
and (len(node.args) == _PAIR or _none(node.args[_PAIR]))
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _mapping_read(node: ast.expr, value: ast.arg, key: ast.arg, provenance: ResourceProvenance) -> bool:
|
|
282
|
+
if isinstance(node, ast.Subscript):
|
|
283
|
+
return _parameter(_without_cast(node.value, provenance), value, provenance) and _parameter(
|
|
284
|
+
node.slice, key, provenance
|
|
285
|
+
)
|
|
286
|
+
return (
|
|
287
|
+
isinstance(node, ast.Call)
|
|
288
|
+
and isinstance(node.func, ast.Attribute)
|
|
289
|
+
and node.func.attr == "get"
|
|
290
|
+
and _parameter(_without_cast(node.func.value, provenance), value, provenance)
|
|
291
|
+
and len(node.args) in {1, _PAIR}
|
|
292
|
+
and not node.keywords
|
|
293
|
+
and _parameter(node.args[0], key, provenance)
|
|
294
|
+
and (len(node.args) == 1 or _none(node.args[1]))
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _local_model(target: ast.expr, context: PythonFileContext, provenance: ResourceProvenance) -> ast.ClassDef | None:
|
|
299
|
+
if not isinstance(target, ast.Name):
|
|
300
|
+
return None
|
|
301
|
+
model = provenance.binding(target.id, target)
|
|
302
|
+
if not (
|
|
303
|
+
isinstance(model, ast.ClassDef)
|
|
304
|
+
and context.parents.get(model) is context.tree
|
|
305
|
+
and len(model.bases) == 1
|
|
306
|
+
and not model.decorator_list
|
|
307
|
+
and not model.keywords
|
|
308
|
+
and not model.type_params
|
|
309
|
+
and target.id not in provenance.mutated
|
|
310
|
+
and any(_imported(model.bases[0], base, provenance) for base in _MODEL_BASES)
|
|
311
|
+
):
|
|
312
|
+
return None
|
|
313
|
+
if any(
|
|
314
|
+
isinstance(member, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
315
|
+
and member.name in {"__getattr__", "__getattribute__"}
|
|
316
|
+
for member in model.body
|
|
317
|
+
):
|
|
318
|
+
return None
|
|
319
|
+
return model
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _fixed_field_calls(
|
|
323
|
+
function: ast.FunctionDef, model: ast.ClassDef, context: PythonFileContext, provenance: ResourceProvenance
|
|
324
|
+
) -> bool:
|
|
325
|
+
parameters = _reader_parameters(function, provenance)
|
|
326
|
+
if parameters is None:
|
|
327
|
+
return False
|
|
328
|
+
fields = {
|
|
329
|
+
member.target.id
|
|
330
|
+
for member in model.body
|
|
331
|
+
if isinstance(member, ast.AnnAssign)
|
|
332
|
+
and isinstance(member.target, ast.Name)
|
|
333
|
+
and not member.target.id.startswith("_")
|
|
334
|
+
and member.target.id != "model_config"
|
|
335
|
+
and not _class_variable(member.annotation, provenance)
|
|
336
|
+
}
|
|
337
|
+
calls = 0
|
|
338
|
+
for reference in provenance.reads.get(function.name, ()):
|
|
339
|
+
if provenance.binding(reference.id, reference) is not function:
|
|
340
|
+
continue
|
|
341
|
+
call = context.parents.get(reference)
|
|
342
|
+
if not isinstance(call, ast.Call) or call.func is not reference:
|
|
343
|
+
return False
|
|
344
|
+
key = _call_key(call, function, parameters[1].arg)
|
|
345
|
+
literal = _literal_key(key, provenance)
|
|
346
|
+
if literal is None or literal not in fields:
|
|
347
|
+
return False
|
|
348
|
+
calls += 1
|
|
349
|
+
return calls > 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _class_variable(annotation: ast.expr, provenance: ResourceProvenance) -> bool:
|
|
353
|
+
parsed = AnnotationSemantics.parse(annotation)
|
|
354
|
+
if parsed is None:
|
|
355
|
+
return True
|
|
356
|
+
return any(
|
|
357
|
+
isinstance(node, ast.Subscript)
|
|
358
|
+
and any(_imported(node.value, f"{module}.ClassVar", provenance, at=annotation) for module in _TYPING)
|
|
359
|
+
for node in ast.walk(parsed)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _literal_key(node: ast.expr | None, provenance: ResourceProvenance, *, depth: int = 0) -> str | None:
|
|
364
|
+
if depth >= _MAX_SELECTOR_ALIAS_DEPTH:
|
|
365
|
+
return None
|
|
366
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
367
|
+
return node.value
|
|
368
|
+
if not isinstance(node, ast.Name) or node.id in provenance.external_bindings:
|
|
369
|
+
return None
|
|
370
|
+
binding = provenance.binding(node.id, node)
|
|
371
|
+
if not isinstance(binding, ast.Name):
|
|
372
|
+
return None
|
|
373
|
+
value = provenance.assignment_value(binding, node)
|
|
374
|
+
return _literal_key(value, provenance, depth=depth + 1) if value is not None else None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _call_key(call: ast.Call, function: ast.FunctionDef, key_name: str) -> ast.expr | None:
|
|
378
|
+
positional = (*function.args.posonlyargs, *function.args.args)
|
|
379
|
+
parameters = (*positional, *function.args.kwonlyargs)
|
|
380
|
+
if len(call.args) > len(positional) or any(isinstance(argument, ast.Starred) for argument in call.args):
|
|
381
|
+
return None
|
|
382
|
+
bound = {parameter.arg: argument for parameter, argument in zip(positional, call.args, strict=False)}
|
|
383
|
+
for keyword in call.keywords:
|
|
384
|
+
if keyword.arg is None or keyword.arg in bound or keyword.arg in {arg.arg for arg in function.args.posonlyargs}:
|
|
385
|
+
return None
|
|
386
|
+
bound[keyword.arg] = keyword.value
|
|
387
|
+
if set(bound) != {parameter.arg for parameter in parameters}:
|
|
388
|
+
return None
|
|
389
|
+
return bound.get(key_name)
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import builtins
|
|
5
5
|
from pathlib import PurePosixPath
|
|
6
|
+
from types import MappingProxyType
|
|
6
7
|
from typing import (
|
|
7
8
|
TYPE_CHECKING,
|
|
8
9
|
ClassVar,
|
|
9
10
|
NamedTuple,
|
|
10
|
-
cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits native exception narrowing.
|
|
11
11
|
override,
|
|
12
12
|
)
|
|
13
13
|
|
|
@@ -29,14 +29,14 @@ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
|
-
from collections.abc import Iterator
|
|
32
|
+
from collections.abc import Iterator
|
|
33
33
|
|
|
34
34
|
from sarj_python_lint._file_context import PythonFileContext
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# The two attributes that *configure* what a mock does when called.
|
|
38
38
|
_CONFIG_ATTRS = frozenset({"return_value", "side_effect"})
|
|
39
|
-
_BUILTIN_NAMESPACE =
|
|
39
|
+
_BUILTIN_NAMESPACE = MappingProxyType[str, object](vars(builtins))
|
|
40
40
|
_EXCEPTION_TYPES = frozenset(
|
|
41
41
|
name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, BaseException)
|
|
42
42
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.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.61.1 → sarj_python_lint-1.62.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.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.61.1 → sarj_python_lint-1.62.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.61.1 → sarj_python_lint-1.62.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|