sarj-python-lint 1.77.3__tar.gz → 1.79.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.77.3 → sarj_python_lint-1.79.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__main__.py +1 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_nominal_project.py +31 -15
- sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_service_http_callbacks.py +403 -0
- sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_aggregate_identity.py +356 -0
- sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_explain.py +140 -0
- sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_sql_policy_source.py +32 -0
- sarj_python_lint-1.79.0/src/sarj_python_lint/rules/_transactional_sql.py +389 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +32 -67
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +11 -6
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/.gitignore +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/LICENSE +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/README.md +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/application_logging.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/suppression_policy.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.79.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/
|
{sarj_python_lint-1.77.3 → sarj_python_lint-1.79.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
@@ -100,21 +100,34 @@ class NominalProjectFacts:
|
|
|
100
100
|
# One request owns these caches; subsequent requests see source changes.
|
|
101
101
|
def __init__(self) -> None:
|
|
102
102
|
self._distributions: dict[Path, tuple[_Distribution, ...]] = {}
|
|
103
|
-
self._sources: dict[Path, dict[str, NominalSource]] = {}
|
|
104
|
-
self._scopes: dict[Path, dict[str, NominalSource]] = {}
|
|
103
|
+
self._sources: dict[tuple[Path, bool], dict[str, NominalSource]] = {}
|
|
104
|
+
self._scopes: dict[tuple[Path, bool], dict[str, NominalSource]] = {}
|
|
105
105
|
|
|
106
|
-
def sources(
|
|
106
|
+
def sources(
|
|
107
|
+
self, path: Path, first_party: FirstPartyFacts, *, include_generated: bool = False
|
|
108
|
+
) -> dict[str, NominalSource]:
|
|
107
109
|
owner = distribution_root(path, facts=first_party)
|
|
108
110
|
root = first_party.project_root(path)
|
|
109
111
|
if owner is None or root is None:
|
|
110
112
|
return {}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
key = (owner, include_generated)
|
|
114
|
+
if key in self._scopes:
|
|
115
|
+
return self._scopes[key]
|
|
113
116
|
distributions = self._accessible_distributions(owner, root)
|
|
114
|
-
sources = self._merge_sources(distributions)
|
|
115
|
-
self._scopes[
|
|
117
|
+
sources = self._merge_sources(distributions, include_generated=include_generated)
|
|
118
|
+
self._scopes[key] = sources
|
|
116
119
|
return sources
|
|
117
120
|
|
|
121
|
+
def owns_source(self, path: Path, source: Path, first_party: FirstPartyFacts) -> bool:
|
|
122
|
+
owner = distribution_root(path, facts=first_party)
|
|
123
|
+
root = first_party.project_root(path)
|
|
124
|
+
target = distribution_root(source, facts=first_party)
|
|
125
|
+
if owner is None or root is None or target is None:
|
|
126
|
+
return False
|
|
127
|
+
# Source ownership also applies to generated wrappers; nominal inventories
|
|
128
|
+
# deliberately keep their separate generated-source exclusion.
|
|
129
|
+
return any(distribution.root == target for distribution in self._accessible_distributions(owner, root))
|
|
130
|
+
|
|
118
131
|
def _accessible_distributions(self, owner: Path, root: Path) -> tuple[_Distribution, ...]:
|
|
119
132
|
if root not in self._distributions:
|
|
120
133
|
self._distributions[root] = tuple(
|
|
@@ -144,13 +157,16 @@ class NominalProjectFacts:
|
|
|
144
157
|
)
|
|
145
158
|
)
|
|
146
159
|
|
|
147
|
-
def _merge_sources(
|
|
160
|
+
def _merge_sources(
|
|
161
|
+
self, distributions: tuple[_Distribution, ...], *, include_generated: bool = False
|
|
162
|
+
) -> dict[str, NominalSource]:
|
|
148
163
|
sources: dict[str, NominalSource] = {}
|
|
149
164
|
ambiguous: set[str] = set()
|
|
150
165
|
for distribution in distributions:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
key = (distribution.root, include_generated)
|
|
167
|
+
if key not in self._sources:
|
|
168
|
+
self._sources[key] = _sources(distribution.root, include_generated=include_generated)
|
|
169
|
+
for module, source in self._sources[key].items():
|
|
154
170
|
if module in sources:
|
|
155
171
|
ambiguous.add(module)
|
|
156
172
|
sources[module] = source
|
|
@@ -234,7 +250,7 @@ def _files(root: Path, pattern: str) -> list[Path]:
|
|
|
234
250
|
return found
|
|
235
251
|
|
|
236
252
|
|
|
237
|
-
def _sources(root: Path) -> dict[str, NominalSource]:
|
|
253
|
+
def _sources(root: Path, *, include_generated: bool = False) -> dict[str, NominalSource]:
|
|
238
254
|
sources: dict[str, NominalSource] = {}
|
|
239
255
|
ambiguous: set[str] = set()
|
|
240
256
|
total_bytes = 0
|
|
@@ -248,7 +264,7 @@ def _sources(root: Path) -> dict[str, NominalSource]:
|
|
|
248
264
|
module = _module_name(root, path)
|
|
249
265
|
if not module:
|
|
250
266
|
continue
|
|
251
|
-
source = _source(path)
|
|
267
|
+
source = _source(path, include_generated=include_generated)
|
|
252
268
|
if source is None:
|
|
253
269
|
continue
|
|
254
270
|
if module in sources:
|
|
@@ -268,12 +284,12 @@ def _module_name(root: Path, path: Path) -> str | None:
|
|
|
268
284
|
return ".".join(parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], path.stem)) or None
|
|
269
285
|
|
|
270
286
|
|
|
271
|
-
def _source(path: Path) -> NominalSource | None:
|
|
287
|
+
def _source(path: Path, *, include_generated: bool = False) -> NominalSource | None:
|
|
272
288
|
try:
|
|
273
289
|
if path.is_symlink() or path.stat().st_size > _MAX_BYTES:
|
|
274
290
|
return None
|
|
275
291
|
text = read_python_source(path)
|
|
276
|
-
if is_generated(path, text):
|
|
292
|
+
if not include_generated and is_generated(path, text):
|
|
277
293
|
return None
|
|
278
294
|
except OSError, UnicodeError, SyntaxError:
|
|
279
295
|
return None
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rules._ast_index import children, walk
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
|
|
14
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
15
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_CALLABLE_SOURCES = frozenset({"typing", "typing_extensions", "collections.abc"})
|
|
19
|
+
_NATIVE_CLIENTS = frozenset({"httpx.Client", "httpx.AsyncClient"})
|
|
20
|
+
_MAX_REEXPORTS = 8
|
|
21
|
+
_CALLABLE_ARITY = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ResolvedWrapper:
|
|
26
|
+
symbol: "SymbolRef"
|
|
27
|
+
unit: "SourceUnit"
|
|
28
|
+
declaration: ast.ClassDef
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def http_callback_dispatch(call: ast.Call, context: PythonFileContext | None) -> bool:
|
|
32
|
+
if context is None or not isinstance(call.func, ast.Name) or len(call.args) != 1 or call.keywords:
|
|
33
|
+
return False
|
|
34
|
+
if not isinstance(context.parents.get(call), ast.Await) or not isinstance(call.args[0], ast.Name):
|
|
35
|
+
return False
|
|
36
|
+
provenance = context.static_resource_provenance
|
|
37
|
+
callback = provenance.binding(call.func.id, call)
|
|
38
|
+
if not isinstance(callback, ast.arg) or (target := _callback_target(callback.annotation, context)) is None:
|
|
39
|
+
return False
|
|
40
|
+
receiver = call.args[0]
|
|
41
|
+
constructor = provenance.constructor(receiver, call)
|
|
42
|
+
return (
|
|
43
|
+
constructor is not None
|
|
44
|
+
and _stable_constructor(constructor, context)
|
|
45
|
+
and _bounded_receiver(receiver, call, context)
|
|
46
|
+
and _same_http_wrapper(target, constructor.func, receiver, call, context)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _callback_target(annotation: ast.expr | None, context: PythonFileContext) -> ast.expr | None:
|
|
51
|
+
if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
|
|
52
|
+
try:
|
|
53
|
+
annotation = ast.parse(annotation.value, mode="eval").body
|
|
54
|
+
except SyntaxError:
|
|
55
|
+
return None
|
|
56
|
+
if context.tree is None:
|
|
57
|
+
return None
|
|
58
|
+
imports = ImportIndex.from_tree(context.tree, module_scope_only=True)
|
|
59
|
+
if not isinstance(annotation, ast.Subscript) or not imports.resolves(
|
|
60
|
+
annotation.value, sources=_CALLABLE_SOURCES, symbol="Callable"
|
|
61
|
+
):
|
|
62
|
+
return None
|
|
63
|
+
if not isinstance(annotation.slice, ast.Tuple) or len(annotation.slice.elts) != _CALLABLE_ARITY:
|
|
64
|
+
return None
|
|
65
|
+
arguments = annotation.slice.elts[0]
|
|
66
|
+
return arguments.elts[0] if isinstance(arguments, ast.List) and len(arguments.elts) == 1 else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _stable_constructor(constructor: ast.Call, context: PythonFileContext) -> bool:
|
|
70
|
+
return _stable_reference(constructor.func, context)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _stable_reference(expression: ast.expr, context: PythonFileContext) -> bool:
|
|
74
|
+
root = expression
|
|
75
|
+
while isinstance(root, ast.Attribute):
|
|
76
|
+
root = root.value
|
|
77
|
+
provenance = context.static_resource_provenance
|
|
78
|
+
binding = provenance.runtime_binding(root.id, expression) if isinstance(root, ast.Name) else None
|
|
79
|
+
return (
|
|
80
|
+
isinstance(root, ast.Name)
|
|
81
|
+
and not provenance.dynamic_namespace
|
|
82
|
+
and not provenance.wildcard
|
|
83
|
+
and root.id not in provenance.mutated
|
|
84
|
+
and isinstance(binding, ast.alias | ast.ClassDef)
|
|
85
|
+
and all(_safe_import_read(read, context) for read in provenance.binding_reads(root.id, binding))
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _safe_import_read(read: ast.Name, context: PythonFileContext) -> bool:
|
|
90
|
+
expression: ast.AST = read
|
|
91
|
+
parent = context.parents.get(expression)
|
|
92
|
+
while isinstance(parent, ast.Attribute) and parent.value is expression:
|
|
93
|
+
expression, parent = parent, context.parents.get(parent)
|
|
94
|
+
return (
|
|
95
|
+
_annotation_reference(expression, context)
|
|
96
|
+
or (isinstance(parent, ast.Call) and parent.func is expression)
|
|
97
|
+
or (isinstance(parent, ast.ClassDef) and expression in parent.decorator_list)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _annotation_reference(node: ast.AST, context: PythonFileContext) -> bool:
|
|
102
|
+
owner = context.parents.get(node)
|
|
103
|
+
while isinstance(owner, ast.expr):
|
|
104
|
+
node, owner = owner, context.parents.get(owner)
|
|
105
|
+
return (isinstance(owner, ast.arg | ast.AnnAssign) and owner.annotation is node) or (
|
|
106
|
+
isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef) and owner.returns is node
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _bounded_receiver(receiver: ast.Name, call: ast.Call, context: PythonFileContext) -> bool:
|
|
111
|
+
provenance = context.static_resource_provenance
|
|
112
|
+
binding = provenance.binding(receiver.id, call)
|
|
113
|
+
if binding is None or provenance.dynamic_namespace or receiver.id in provenance.mutated:
|
|
114
|
+
return False
|
|
115
|
+
return all(_receiver_read(read, receiver, context) for read in provenance.binding_reads(receiver.id, binding))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _receiver_read(read: ast.Name, receiver: ast.Name, context: PythonFileContext) -> bool:
|
|
119
|
+
if read is receiver:
|
|
120
|
+
return True
|
|
121
|
+
parent = context.parents.get(read)
|
|
122
|
+
if isinstance(parent, ast.withitem) and parent.context_expr is read:
|
|
123
|
+
return True
|
|
124
|
+
return (
|
|
125
|
+
isinstance(parent, ast.Attribute)
|
|
126
|
+
and parent.value is read
|
|
127
|
+
and parent.attr not in {"__setattr__", "__delattr__", "__getattribute__"}
|
|
128
|
+
and isinstance(owner := context.parents.get(parent), ast.Call)
|
|
129
|
+
and owner.func is parent
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _same_http_wrapper(
|
|
134
|
+
annotation: ast.expr, constructor: ast.expr, receiver: ast.Name, call: ast.Call, context: PythonFileContext
|
|
135
|
+
) -> bool:
|
|
136
|
+
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the rule registry/context initialization cycle.
|
|
137
|
+
|
|
138
|
+
project = context.session.project
|
|
139
|
+
unit = project.unit(context.path) if project is not None else None
|
|
140
|
+
if project is None or unit is None:
|
|
141
|
+
return False
|
|
142
|
+
declared = _resolve_wrapper(project, unit, annotation)
|
|
143
|
+
constructed = _resolve_wrapper(project, unit, constructor)
|
|
144
|
+
if declared is None or constructed is None or declared.symbol != constructed.symbol:
|
|
145
|
+
return False
|
|
146
|
+
source_unit = constructed.unit
|
|
147
|
+
declaration = constructed.declaration
|
|
148
|
+
if not context.session.nominal_project.owns_source(context.path, source_unit.path, context.session.first_party):
|
|
149
|
+
return False
|
|
150
|
+
source_context = PythonFileContext(source_unit.path, source_unit.source, context.session)
|
|
151
|
+
if not _wrapper_decorators(declaration, source_context):
|
|
152
|
+
return False
|
|
153
|
+
return _active_native_factory(declaration, _resource_entries(receiver, call, context), source_context)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resource_entries(receiver: ast.Name, call: ast.Call, context: PythonFileContext) -> set[str]:
|
|
157
|
+
binding = context.static_resource_provenance.binding(receiver.id, call)
|
|
158
|
+
if binding is None:
|
|
159
|
+
return set()
|
|
160
|
+
entries = {"__init__"}
|
|
161
|
+
bound_item = context.parents.get(binding)
|
|
162
|
+
bound_owner = context.parents.get(bound_item) if isinstance(bound_item, ast.withitem) else None
|
|
163
|
+
if context.static_resource_provenance.contains(bound_owner, call):
|
|
164
|
+
entries.add("__aenter__" if isinstance(bound_owner, ast.AsyncWith) else "__enter__")
|
|
165
|
+
for read in context.static_resource_provenance.binding_reads(receiver.id, binding):
|
|
166
|
+
item = context.parents.get(read)
|
|
167
|
+
owner = context.parents.get(item) if isinstance(item, ast.withitem) else None
|
|
168
|
+
if context.static_resource_provenance.contains(owner, call):
|
|
169
|
+
entries.add("__aenter__" if isinstance(owner, ast.AsyncWith) else "__enter__")
|
|
170
|
+
return entries
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _wrapper_decorators(declaration: ast.ClassDef, context: PythonFileContext) -> bool:
|
|
174
|
+
return (
|
|
175
|
+
not declaration.bases
|
|
176
|
+
and not declaration.keywords
|
|
177
|
+
and declaration.name not in context.static_resource_provenance.mutated
|
|
178
|
+
and all(
|
|
179
|
+
_stable_reference(expression := decorator.func if isinstance(decorator, ast.Call) else decorator, context)
|
|
180
|
+
and context.imports.resolved_qualified_name(expression)
|
|
181
|
+
in {
|
|
182
|
+
"attrs.define",
|
|
183
|
+
"attrs.frozen",
|
|
184
|
+
"attr.s",
|
|
185
|
+
"dataclasses.dataclass",
|
|
186
|
+
"typing.final",
|
|
187
|
+
"typing_extensions.final",
|
|
188
|
+
}
|
|
189
|
+
for decorator in declaration.decorator_list
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _active_native_factory(declaration: ast.ClassDef, entries: set[str], context: PythonFileContext) -> bool:
|
|
195
|
+
methods = {
|
|
196
|
+
method.name: method for method in declaration.body if isinstance(method, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
197
|
+
}
|
|
198
|
+
pending = list(entries)
|
|
199
|
+
seen: set[str] = set()
|
|
200
|
+
while pending:
|
|
201
|
+
name = pending.pop()
|
|
202
|
+
if name in seen or (method := methods.get(name)) is None:
|
|
203
|
+
continue
|
|
204
|
+
seen.add(name)
|
|
205
|
+
if method.decorator_list:
|
|
206
|
+
continue
|
|
207
|
+
if _returns_native_client(method, context):
|
|
208
|
+
return True
|
|
209
|
+
pending.extend(_unconditional_self_calls(method, context, methods))
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _unconditional_self_calls(
|
|
214
|
+
method: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
215
|
+
context: PythonFileContext,
|
|
216
|
+
methods: Mapping[str, ast.FunctionDef | ast.AsyncFunctionDef],
|
|
217
|
+
) -> set[str]:
|
|
218
|
+
called: set[str] = set()
|
|
219
|
+
for statement in method.body:
|
|
220
|
+
if isinstance(statement, ast.Raise):
|
|
221
|
+
break
|
|
222
|
+
if not isinstance(statement, ast.Expr | ast.Assign | ast.AnnAssign | ast.Return):
|
|
223
|
+
continue
|
|
224
|
+
called.update(
|
|
225
|
+
node.func.attr
|
|
226
|
+
for node in _eager_nodes(statement)
|
|
227
|
+
if isinstance(node, ast.Call)
|
|
228
|
+
and isinstance(node.func, ast.Attribute)
|
|
229
|
+
and isinstance(node.func.value, ast.Name)
|
|
230
|
+
and node.func.value.id == "self"
|
|
231
|
+
and _function_owner(node, context) is method
|
|
232
|
+
and (
|
|
233
|
+
not isinstance(methods.get(node.func.attr), ast.AsyncFunctionDef)
|
|
234
|
+
or isinstance(context.parents.get(node), ast.Await)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
if isinstance(statement, ast.Return):
|
|
238
|
+
break
|
|
239
|
+
return called
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _eager_nodes(node: ast.AST) -> list[ast.AST]:
|
|
243
|
+
pending = [node]
|
|
244
|
+
result: list[ast.AST] = []
|
|
245
|
+
while pending:
|
|
246
|
+
current = pending.pop()
|
|
247
|
+
result.append(current)
|
|
248
|
+
pending.extend(_eager_children(current))
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _eager_children(node: ast.AST) -> list[ast.AST]:
|
|
253
|
+
match node:
|
|
254
|
+
case ast.Lambda() | ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
255
|
+
return []
|
|
256
|
+
case ast.BoolOp():
|
|
257
|
+
return _boolean_operands(node)
|
|
258
|
+
case ast.IfExp():
|
|
259
|
+
return (
|
|
260
|
+
[node.test, node.body if node.test.value else node.orelse]
|
|
261
|
+
if isinstance(node.test, ast.Constant)
|
|
262
|
+
else [node.test]
|
|
263
|
+
)
|
|
264
|
+
case ast.GeneratorExp() | ast.ListComp() | ast.SetComp() | ast.DictComp():
|
|
265
|
+
return [node.generators[0].iter]
|
|
266
|
+
case _:
|
|
267
|
+
return children(node)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _boolean_operands(node: ast.BoolOp) -> list[ast.AST]:
|
|
271
|
+
operands: list[ast.AST] = []
|
|
272
|
+
for value in node.values:
|
|
273
|
+
operands.append(value)
|
|
274
|
+
if not isinstance(value, ast.Constant):
|
|
275
|
+
break
|
|
276
|
+
if (isinstance(node.op, ast.And) and not value.value) or (isinstance(node.op, ast.Or) and value.value):
|
|
277
|
+
break
|
|
278
|
+
return operands
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _resolve_wrapper(project: ProjectIndexSet, unit: SourceUnit, expression: ast.expr) -> ResolvedWrapper | None:
|
|
282
|
+
for _ in range(_MAX_REEXPORTS):
|
|
283
|
+
if not _stable_export(unit, expression):
|
|
284
|
+
return None
|
|
285
|
+
symbol = project.resolve(unit, expression)
|
|
286
|
+
if symbol is None:
|
|
287
|
+
return None
|
|
288
|
+
summary = project.class_for(unit, expression)
|
|
289
|
+
if summary is not None:
|
|
290
|
+
return _wrapper_declaration(project, summary.symbol)
|
|
291
|
+
target = project.source_unit(symbol.module)
|
|
292
|
+
if target is None or symbol.name not in target.imports:
|
|
293
|
+
return None
|
|
294
|
+
unit, expression = target, ast.Name(id=symbol.name)
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _wrapper_declaration(project: ProjectIndexSet, symbol: SymbolRef) -> ResolvedWrapper | None:
|
|
299
|
+
unit = project.source_unit(symbol.module)
|
|
300
|
+
if unit is None or unit.tree is None:
|
|
301
|
+
return None
|
|
302
|
+
declaration = next(
|
|
303
|
+
(node for node in unit.tree.body if isinstance(node, ast.ClassDef) and node.name == symbol.name), None
|
|
304
|
+
)
|
|
305
|
+
return ResolvedWrapper(symbol, unit, declaration) if declaration is not None else None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _stable_export(unit: SourceUnit, expression: ast.expr) -> bool:
|
|
309
|
+
if unit.tree is None:
|
|
310
|
+
return False
|
|
311
|
+
root = expression
|
|
312
|
+
while isinstance(root, ast.Attribute):
|
|
313
|
+
root = root.value
|
|
314
|
+
if not isinstance(root, ast.Name):
|
|
315
|
+
return False
|
|
316
|
+
imports = ImportIndex.from_tree(unit.tree, module_scope_only=True)
|
|
317
|
+
if root.id in unit.imports:
|
|
318
|
+
return root.id not in imports.shadowed_names and len(_import_bindings(unit.tree, root.id)) == 1
|
|
319
|
+
return (
|
|
320
|
+
sum(
|
|
321
|
+
(isinstance(node, ast.ClassDef) and node.name == root.id)
|
|
322
|
+
or (isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id == root.id)
|
|
323
|
+
for node in walk(unit.tree)
|
|
324
|
+
)
|
|
325
|
+
== 1
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _import_bindings(tree: ast.Module, local: str) -> set[tuple[int, str | None, str | None]]:
|
|
330
|
+
bindings: set[tuple[int, str | None, str | None]] = set()
|
|
331
|
+
for node in tree.body:
|
|
332
|
+
if not isinstance(node, ast.Import | ast.ImportFrom):
|
|
333
|
+
continue
|
|
334
|
+
for alias in node.names:
|
|
335
|
+
bound = alias.asname or (alias.name if isinstance(node, ast.ImportFrom) else alias.name.partition(".")[0])
|
|
336
|
+
if bound != local:
|
|
337
|
+
continue
|
|
338
|
+
bindings.add(
|
|
339
|
+
(node.level, node.module, alias.name) if isinstance(node, ast.ImportFrom) else (0, alias.name, None)
|
|
340
|
+
)
|
|
341
|
+
return bindings
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _returns_native_client(method: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
345
|
+
if (
|
|
346
|
+
method.decorator_list
|
|
347
|
+
or method.returns is None
|
|
348
|
+
or context.imports.resolved_qualified_name(method.returns) not in _NATIVE_CLIENTS
|
|
349
|
+
):
|
|
350
|
+
return False
|
|
351
|
+
members = [node for node in walk(method) if _function_owner(node, context) is method]
|
|
352
|
+
native_fields = _native_fields(members, context)
|
|
353
|
+
returns = [node for node in members if isinstance(node, ast.Return)]
|
|
354
|
+
return bool(returns) and all(
|
|
355
|
+
_native_constructor(node.value, context) or _returned_field(node.value) in native_fields for node in returns
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _native_fields(members: list[ast.AST], context: PythonFileContext) -> set[str]:
|
|
360
|
+
native: set[str] = set()
|
|
361
|
+
unknown: set[str] = set()
|
|
362
|
+
for node in members:
|
|
363
|
+
if not isinstance(node, ast.Assign | ast.AnnAssign):
|
|
364
|
+
continue
|
|
365
|
+
fields = _assigned_fields(node)
|
|
366
|
+
if _native_constructor(node.value, context):
|
|
367
|
+
native.update(fields)
|
|
368
|
+
elif node.value is not None and not (isinstance(node.value, ast.Constant) and node.value.value is None):
|
|
369
|
+
unknown.update(fields)
|
|
370
|
+
return native - unknown
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _assigned_fields(node: ast.Assign | ast.AnnAssign) -> set[str]:
|
|
374
|
+
return {
|
|
375
|
+
target.attr
|
|
376
|
+
for target in (node.targets if isinstance(node, ast.Assign) else [node.target])
|
|
377
|
+
if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == "self"
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _native_constructor(expression: ast.expr | None, context: PythonFileContext) -> bool:
|
|
382
|
+
return (
|
|
383
|
+
isinstance(expression, ast.Call)
|
|
384
|
+
and _stable_constructor(expression, context)
|
|
385
|
+
and context.imports.resolved_qualified_name(expression.func) in _NATIVE_CLIENTS
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _returned_field(expression: ast.expr | None) -> str | None:
|
|
390
|
+
if (
|
|
391
|
+
isinstance(expression, ast.Attribute)
|
|
392
|
+
and isinstance(expression.value, ast.Name)
|
|
393
|
+
and expression.value.id == "self"
|
|
394
|
+
):
|
|
395
|
+
return expression.attr
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _function_owner(node: ast.AST, context: PythonFileContext) -> ast.AST | None:
|
|
400
|
+
owner = context.parents.get(node)
|
|
401
|
+
while owner is not None and not isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
|
|
402
|
+
owner = context.parents.get(owner)
|
|
403
|
+
return owner
|