sarj-python-lint 1.88.20__tar.gz → 1.90.1__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.88.20 → sarj_python_lint-1.90.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_ast_index.py +2 -2
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.90.1/src/sarj_python_lint/rules/no_async_client_in_constructor.py +340 -0
- sarj_python_lint-1.90.1/src/sarj_python_lint/rules/prefer_grouped_settings.py +220 -0
- sarj_python_lint-1.88.20/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -137
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/.gitignore +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/LICENSE +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/README.md +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_prepared_sources.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/application_logging.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_postgres.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_store_sql.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_optional_composite.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/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.90.1
|
|
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.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
@@ -59,8 +59,8 @@ def comprehension_results(node: ast.ListComp | ast.SetComp | ast.DictComp | ast.
|
|
|
59
59
|
return [child for child in children(node) if isinstance(child, ast.expr)]
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def has_own_yield(function: ast.AsyncFunctionDef) -> bool:
|
|
63
|
-
pending: list[ast.AST] = list(function.body)
|
|
62
|
+
def has_own_yield(function: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -> bool:
|
|
63
|
+
pending: list[ast.AST] = [function.body] if isinstance(function, ast.Lambda) else list(function.body)
|
|
64
64
|
while pending:
|
|
65
65
|
match pending.pop():
|
|
66
66
|
case ast.Yield() | ast.YieldFrom():
|
{sarj_python_lint-1.88.20 → sarj_python_lint-1.90.1}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -225,6 +225,7 @@ from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedP
|
|
|
225
225
|
from sarj_python_lint.rules.duplicate_pytest_fixture_params import DuplicatePytestFixtureParams
|
|
226
226
|
from sarj_python_lint.rules.failed_probe_zero_threshold import FailedProbeZeroThreshold
|
|
227
227
|
from sarj_python_lint.rules.nan_hidden_by_builtin_min import NanHiddenByBuiltinMin
|
|
228
|
+
from sarj_python_lint.rules.no_async_client_in_constructor import NoAsyncClientInConstructor
|
|
228
229
|
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
229
230
|
from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
|
|
230
231
|
from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
|
|
@@ -274,6 +275,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
|
|
|
274
275
|
|
|
275
276
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
276
277
|
{
|
|
278
|
+
NoAsyncClientInConstructor.id: NoAsyncClientInConstructor,
|
|
277
279
|
NoDiscardedTaggedOutcome.id: NoDiscardedTaggedOutcome,
|
|
278
280
|
StoreInsertRequiresExplicitConflictPolicy.id: StoreInsertRequiresExplicitConflictPolicy,
|
|
279
281
|
StoreMutationCommandFanout.id: StoreMutationCommandFanout,
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, 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._ast_index import has_own_yield
|
|
20
|
+
from sarj_python_lint.rules._resource_provenance import import_name_for_binding
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_CLIENTS = frozenset({"httpx.AsyncClient", "httpx._client.AsyncClient"})
|
|
28
|
+
_MODULES = frozenset({"httpx", "httpx._client"})
|
|
29
|
+
_COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
30
|
+
_MAX_ALIAS_DEPTH = 32
|
|
31
|
+
_BAD = "import httpx\nclass Service:\n def __init__(self):\n self.client = httpx.AsyncClient()\n"
|
|
32
|
+
_GOOD = "from contextlib import asynccontextmanager\nimport httpx\nclass Service:\n def __init__(self, client: httpx.AsyncClient):\n self.client = client\n@asynccontextmanager\nasync def service_scope(endpoint):\n async with httpx.AsyncClient(base_url=endpoint) as client:\n yield Service(client)\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class NoAsyncClientInConstructor(Rule):
|
|
37
|
+
id = "no-async-client-in-constructor"
|
|
38
|
+
code = "SARJ549"
|
|
39
|
+
documentation = RuleDocumentation(
|
|
40
|
+
default_level=Severity.ERROR,
|
|
41
|
+
summary="Inject an HTTPX AsyncClient instead of constructing one in __init__.",
|
|
42
|
+
rationale="Constructor-owned clients repeat transport setup for each service instance and hide the boundary that owns pooling and asynchronous cleanup.",
|
|
43
|
+
remediation="Create and close an appropriately scoped AsyncClient at the async composition boundary, and inject it into consumers. Keep tenant credentials, cookies, event loops and independent ownership boundaries separate; do not replace them with a global client.",
|
|
44
|
+
category=RuleCategory.ARCHITECTURE,
|
|
45
|
+
autofix=AutofixPolicy.NONE,
|
|
46
|
+
limitations=(
|
|
47
|
+
"This is an explicit architecture policy for import-proven HTTPX AsyncClient calls executed by synchronous nongenerator class __init__ methods, including import aliases and stable constructor assignment aliases bounded to 32 links, eager comprehensions and nested definition defaults/decorators.",
|
|
48
|
+
"One-hop same-file undecorated synchronous factories with stable lexical bindings are followed only when their sole statement after an optional docstring returns a proven constructor call. Zero-argument lambda factories are supported when invoked; deferred function/lambda/generator bodies are not constructor execution.",
|
|
49
|
+
"Unknown or imported factory implementations, overridable methods, wrapper chains, dynamic namespace access, wildcard imports, ambiguous/rebound aliases and visible namespace mutation are not inferred. A generator's first iterable is eager; other generator expressions remain deferred even when a caller might consume them immediately.",
|
|
50
|
+
"Constructor parameter defaults execute at class definition, not on each constructor call, and are outside this rule. No automatic ownership transfer, sharing decision or lifecycle rewrite is safe.",
|
|
51
|
+
"Annotations and lazy type-alias/type-parameter expressions are excluded: their source location does not prove constructor-time execution. Framework-driven evaluation of annotations is not inferred.",
|
|
52
|
+
"Escaped HTTPX module objects cause conservative abstention, including module-object assignment aliases. Constructor aliases must remain confined to calls, annotations, attribute access or stable single-name aliases; unknown argument, return or storage escapes and visible mutation invalidate the import proof.",
|
|
53
|
+
"Nested class bodies are followed only without bases or metaclass keywords, because custom class namespace preparation can replace imported names. Method bodies still use their normal lexical bindings. Generator factories and lambdas create deferred generators rather than clients and are excluded.",
|
|
54
|
+
"HTTPX recommends scoped client reuse and explicit asynchronous closure; its API does not universally prohibit constructor ownership. This stricter constructor boundary is the requested policy. See https://www.python-httpx.org/async/ . Ruff ASYNC212 instead owns blocking HTTPX use in async functions.",
|
|
55
|
+
),
|
|
56
|
+
examples=(
|
|
57
|
+
RuleExample(
|
|
58
|
+
example_id="constructor-allocation",
|
|
59
|
+
title="Do not hide client allocation in a service constructor",
|
|
60
|
+
outcome=ExampleOutcome.MATCH,
|
|
61
|
+
files=(ExampleFile.python("app/service.py", _BAD),),
|
|
62
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
63
|
+
expected_count=1,
|
|
64
|
+
public=True,
|
|
65
|
+
),
|
|
66
|
+
RuleExample(
|
|
67
|
+
example_id="scoped-injection",
|
|
68
|
+
title="Inject a client owned by the async composition scope",
|
|
69
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
70
|
+
files=(ExampleFile.python("app/service.py", _GOOD),),
|
|
71
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
72
|
+
expected_count=0,
|
|
73
|
+
public=True,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
description = documentation.summary
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
81
|
+
if "__init__" not in context.symbol_source or "AsyncClient" not in context.symbol_source:
|
|
82
|
+
return []
|
|
83
|
+
if context.generated or context.tree is None or context.has_wildcard_import or context.dynamic_namespace:
|
|
84
|
+
return []
|
|
85
|
+
if not _imports_untouched(context):
|
|
86
|
+
return []
|
|
87
|
+
constructors = frozenset(
|
|
88
|
+
node
|
|
89
|
+
for node in context.nodes(ast.FunctionDef)
|
|
90
|
+
if node.name == "__init__"
|
|
91
|
+
and isinstance(context.parents.get(node), ast.ClassDef)
|
|
92
|
+
and not has_own_yield(node)
|
|
93
|
+
)
|
|
94
|
+
factories: dict[ast.FunctionDef | ast.Lambda, bool] = {}
|
|
95
|
+
findings = [
|
|
96
|
+
Diagnostic(
|
|
97
|
+
path=context.path,
|
|
98
|
+
line=call.lineno,
|
|
99
|
+
col=call.col_offset + 1,
|
|
100
|
+
code=self.code,
|
|
101
|
+
severity=Severity.ERROR,
|
|
102
|
+
message="httpx.AsyncClient construction in __init__; inject a caller-owned, appropriately scoped AsyncClient and close it at the async composition boundary",
|
|
103
|
+
)
|
|
104
|
+
for call in context.nodes(ast.Call)
|
|
105
|
+
if _in_constructor(context, call, constructors)
|
|
106
|
+
and _allocates(context, call, follow_factory=True, factories=factories)
|
|
107
|
+
and not is_suppressed(context.source_lines, call.lineno, self.code)
|
|
108
|
+
]
|
|
109
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _imports_untouched(context: PythonFileContext) -> bool:
|
|
113
|
+
safe: dict[ast.alias | ast.Name, bool] = {}
|
|
114
|
+
for binding in context.nodes(ast.alias):
|
|
115
|
+
expression = ast.Name(id=binding.asname or binding.name, ctx=ast.Load())
|
|
116
|
+
qualified = import_name_for_binding(expression, binding, context.parents.get(binding))
|
|
117
|
+
if (
|
|
118
|
+
qualified is not None
|
|
119
|
+
and qualified in _MODULES | _CLIENTS
|
|
120
|
+
and not _alias_untouched(context, binding, qualified, safe, remaining=_MAX_ALIAS_DEPTH)
|
|
121
|
+
):
|
|
122
|
+
return False
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _alias_untouched(
|
|
127
|
+
context: PythonFileContext,
|
|
128
|
+
binding: ast.alias | ast.Name,
|
|
129
|
+
qualified: str,
|
|
130
|
+
safe: dict[ast.alias | ast.Name, bool],
|
|
131
|
+
*,
|
|
132
|
+
remaining: int,
|
|
133
|
+
) -> bool:
|
|
134
|
+
if binding in safe:
|
|
135
|
+
return safe[binding]
|
|
136
|
+
safe[binding] = False
|
|
137
|
+
name = binding.id if isinstance(binding, ast.Name) else binding.asname or binding.name.partition(".")[0]
|
|
138
|
+
if remaining == 0 or name in context.binding_inventory.mutated:
|
|
139
|
+
return False
|
|
140
|
+
safe[binding] = all(
|
|
141
|
+
_reference_untouched(context, read, qualified, safe, remaining=remaining)
|
|
142
|
+
for read in context.nodes(ast.Name)
|
|
143
|
+
if isinstance(read.ctx, ast.Load) and read.id == name and _binding(context, name, read) is binding
|
|
144
|
+
)
|
|
145
|
+
return safe[binding]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _reference_untouched(
|
|
149
|
+
context: PythonFileContext,
|
|
150
|
+
expression: ast.expr,
|
|
151
|
+
qualified: str,
|
|
152
|
+
safe: dict[ast.alias | ast.Name, bool],
|
|
153
|
+
*,
|
|
154
|
+
remaining: int,
|
|
155
|
+
) -> bool:
|
|
156
|
+
if not _runtime_expression(context, expression):
|
|
157
|
+
return True
|
|
158
|
+
while qualified in _MODULES:
|
|
159
|
+
parent = context.parents.get(expression)
|
|
160
|
+
if not isinstance(parent, ast.Attribute) or parent.value is not expression:
|
|
161
|
+
return False
|
|
162
|
+
expression, qualified = parent, f"{qualified}.{parent.attr}"
|
|
163
|
+
if qualified not in _CLIENTS:
|
|
164
|
+
return True
|
|
165
|
+
parent = context.parents.get(expression)
|
|
166
|
+
if (isinstance(parent, ast.Call) and parent.func is expression) or (
|
|
167
|
+
isinstance(parent, ast.Attribute) and parent.value is expression
|
|
168
|
+
):
|
|
169
|
+
return True
|
|
170
|
+
target = _alias_target(context, parent, expression)
|
|
171
|
+
return target is not None and _alias_untouched(context, target, qualified, safe, remaining=remaining - 1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _alias_target(context: PythonFileContext, statement: ast.AST | None, value: ast.expr) -> ast.Name | None:
|
|
175
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and statement.value is value:
|
|
176
|
+
target = statement.targets[0]
|
|
177
|
+
elif isinstance(statement, ast.AnnAssign) and statement.value is value:
|
|
178
|
+
target = statement.target
|
|
179
|
+
else:
|
|
180
|
+
return None
|
|
181
|
+
if not isinstance(context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
182
|
+
return None
|
|
183
|
+
return target if isinstance(target, ast.Name) and _binding(context, target.id, target) is target else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _in_constructor(context: PythonFileContext, call: ast.Call, constructors: frozenset[ast.FunctionDef]) -> bool:
|
|
187
|
+
if not _runtime_expression(context, call):
|
|
188
|
+
return False
|
|
189
|
+
scope = context.binding_inventory.scope(call)
|
|
190
|
+
while isinstance(scope, (ast.ClassDef, *_COMPREHENSIONS)):
|
|
191
|
+
if isinstance(scope, ast.ClassDef) and (scope.bases or scope.keywords):
|
|
192
|
+
return False
|
|
193
|
+
if isinstance(scope, ast.GeneratorExp) and not context.resource_provenance.contains(
|
|
194
|
+
scope.generators[0].iter, call
|
|
195
|
+
):
|
|
196
|
+
return False
|
|
197
|
+
scope = context.binding_inventory.scope(scope)
|
|
198
|
+
return scope in constructors
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _runtime_expression(context: PythonFileContext, expression: ast.expr) -> bool:
|
|
202
|
+
child: ast.AST = expression
|
|
203
|
+
parent = context.parents.get(child)
|
|
204
|
+
while parent is not None:
|
|
205
|
+
if isinstance(parent, ast.TypeAlias | ast.type_param):
|
|
206
|
+
return False
|
|
207
|
+
if isinstance(parent, ast.AnnAssign | ast.arg) and parent.annotation is child:
|
|
208
|
+
return False
|
|
209
|
+
if isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef) and parent.returns is child:
|
|
210
|
+
return False
|
|
211
|
+
child, parent = parent, context.parents.get(parent)
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _allocates(
|
|
216
|
+
context: PythonFileContext,
|
|
217
|
+
call: ast.Call,
|
|
218
|
+
*,
|
|
219
|
+
follow_factory: bool,
|
|
220
|
+
factories: dict[ast.FunctionDef | ast.Lambda, bool],
|
|
221
|
+
) -> bool:
|
|
222
|
+
target = _target(context, call.func)
|
|
223
|
+
if isinstance(target, str):
|
|
224
|
+
return target in _CLIENTS
|
|
225
|
+
if not follow_factory or not isinstance(target, ast.FunctionDef | ast.Lambda):
|
|
226
|
+
return False
|
|
227
|
+
if isinstance(target, ast.Lambda):
|
|
228
|
+
arguments = target.args
|
|
229
|
+
if any(
|
|
230
|
+
(
|
|
231
|
+
call.args,
|
|
232
|
+
call.keywords,
|
|
233
|
+
arguments.posonlyargs,
|
|
234
|
+
arguments.args,
|
|
235
|
+
arguments.kwonlyargs,
|
|
236
|
+
arguments.vararg,
|
|
237
|
+
arguments.kwarg,
|
|
238
|
+
)
|
|
239
|
+
):
|
|
240
|
+
return False
|
|
241
|
+
if target not in factories:
|
|
242
|
+
factories[target] = _factory_allocates(context, target, factories)
|
|
243
|
+
return factories[target]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _factory_allocates(
|
|
247
|
+
context: PythonFileContext,
|
|
248
|
+
target: ast.FunctionDef | ast.Lambda,
|
|
249
|
+
factories: dict[ast.FunctionDef | ast.Lambda, bool],
|
|
250
|
+
) -> bool:
|
|
251
|
+
if has_own_yield(target):
|
|
252
|
+
return False
|
|
253
|
+
if isinstance(target, ast.Lambda):
|
|
254
|
+
return isinstance(target.body, ast.Call) and _allocates(
|
|
255
|
+
context, target.body, follow_factory=False, factories=factories
|
|
256
|
+
)
|
|
257
|
+
if target.decorator_list:
|
|
258
|
+
return False
|
|
259
|
+
body = target.body
|
|
260
|
+
if (
|
|
261
|
+
body
|
|
262
|
+
and isinstance(body[0], ast.Expr)
|
|
263
|
+
and isinstance(body[0].value, ast.Constant)
|
|
264
|
+
and isinstance(body[0].value.value, str)
|
|
265
|
+
):
|
|
266
|
+
body = body[1:]
|
|
267
|
+
return (
|
|
268
|
+
len(body) == 1
|
|
269
|
+
and isinstance(body[0], ast.Return)
|
|
270
|
+
and isinstance(body[0].value, ast.Call)
|
|
271
|
+
and _allocates(context, body[0].value, follow_factory=False, factories=factories)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _target(context: PythonFileContext, expression: ast.expr) -> str | ast.FunctionDef | ast.Lambda | None:
|
|
276
|
+
for _ in range(_MAX_ALIAS_DEPTH):
|
|
277
|
+
if isinstance(expression, ast.Lambda):
|
|
278
|
+
return expression
|
|
279
|
+
root = expression
|
|
280
|
+
while isinstance(root, ast.Attribute):
|
|
281
|
+
root = root.value
|
|
282
|
+
if not isinstance(root, ast.Name) or root.id in context.binding_inventory.mutated:
|
|
283
|
+
return None
|
|
284
|
+
binding = _binding(context, root.id, expression)
|
|
285
|
+
if isinstance(binding, ast.alias):
|
|
286
|
+
return import_name_for_binding(expression, binding, context.parents.get(binding))
|
|
287
|
+
if not isinstance(expression, ast.Name):
|
|
288
|
+
return None
|
|
289
|
+
if isinstance(binding, ast.FunctionDef):
|
|
290
|
+
return _factory_binding(context, binding, expression)
|
|
291
|
+
if not isinstance(binding, ast.Name):
|
|
292
|
+
return None
|
|
293
|
+
value = context.resource_provenance.assignment_value(binding, expression)
|
|
294
|
+
if value is None:
|
|
295
|
+
return None
|
|
296
|
+
expression = value
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _factory_binding(context: PythonFileContext, definition: ast.FunctionDef, at: ast.expr) -> ast.FunctionDef | None:
|
|
301
|
+
parent = context.parents.get(definition)
|
|
302
|
+
if parent is context.tree:
|
|
303
|
+
return definition
|
|
304
|
+
if isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef) and definition.lineno < at.lineno:
|
|
305
|
+
return definition
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _binding(context: PythonFileContext, name: str, at: ast.AST) -> ast.AST | None:
|
|
310
|
+
if (binding := context.resource_provenance.runtime_binding(name, at)) is not None:
|
|
311
|
+
return binding
|
|
312
|
+
scope = context.binding_inventory.scope(at)
|
|
313
|
+
lambda_body = False
|
|
314
|
+
while isinstance(scope, (*_COMPREHENSIONS, ast.Lambda)):
|
|
315
|
+
lambda_body |= isinstance(scope, ast.Lambda)
|
|
316
|
+
first_iterable = isinstance(scope, _COMPREHENSIONS) and context.resource_provenance.contains(
|
|
317
|
+
scope.generators[0].iter, at
|
|
318
|
+
)
|
|
319
|
+
if not first_iterable and name in context.binding_inventory.bindings.get(scope, {}):
|
|
320
|
+
return None
|
|
321
|
+
scope = context.binding_inventory.scope(scope)
|
|
322
|
+
bindings = context.runtime_bindings.lookup(scope, name)
|
|
323
|
+
if bindings is None and lambda_body:
|
|
324
|
+
return _lambda_module_binding(context, scope, name)
|
|
325
|
+
return bindings[0] if bindings is not None and len(bindings) == 1 else None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _lambda_module_binding(context: PythonFileContext, scope: ast.AST | None, name: str) -> ast.AST | None:
|
|
329
|
+
if name in context.binding_inventory.external_bindings:
|
|
330
|
+
return None
|
|
331
|
+
while scope is not context.tree:
|
|
332
|
+
if scope is None:
|
|
333
|
+
return None
|
|
334
|
+
if isinstance(scope, ast.FunctionDef | ast.AsyncFunctionDef) and name in context.binding_inventory.bindings.get(
|
|
335
|
+
scope, {}
|
|
336
|
+
):
|
|
337
|
+
return None
|
|
338
|
+
scope = context.binding_inventory.scope(scope)
|
|
339
|
+
bindings = context.runtime_bindings.lookup(scope, name)
|
|
340
|
+
return bindings[0] if bindings is not None and len(bindings) == 1 else None
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
ProjectRule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._declared_contracts import SETTINGS_BASES, DeclaredContracts
|
|
21
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
29
|
+
from sarj_python_lint.rules._declared_contracts import DeclaredClass
|
|
30
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_MIN_GROUP_FIELDS = 2
|
|
34
|
+
_MODIFIER_PREFIXES = frozenset(
|
|
35
|
+
{"is", "has", "min", "max", "num", "total", "default", "enable", "disable", "expected", "use"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@final
|
|
40
|
+
class PreferGroupedSettings(ProjectRule):
|
|
41
|
+
id = "prefer-grouped-settings"
|
|
42
|
+
code = "SARJ532"
|
|
43
|
+
documentation = RuleDocumentation(
|
|
44
|
+
default_level=Severity.ERROR,
|
|
45
|
+
summary="Group settings fields that share a Python namespace prefix into a nested settings object.",
|
|
46
|
+
rationale=(
|
|
47
|
+
"Repeated namespace prefixes flatten one component's configuration across the parent settings API. "
|
|
48
|
+
"A focused settings object gives that component an explicit boundary."
|
|
49
|
+
),
|
|
50
|
+
remediation=(
|
|
51
|
+
"Move the related fields into a focused nested settings object, normally constructed with "
|
|
52
|
+
"Field(default_factory=...). Preserve existing environment aliases, prefixes, validation, defaults "
|
|
53
|
+
"and source precedence, and verify real environment loading before changing the settings API."
|
|
54
|
+
),
|
|
55
|
+
category=RuleCategory.ARCHITECTURE,
|
|
56
|
+
autofix=AutofixPolicy.NONE,
|
|
57
|
+
limitations=(
|
|
58
|
+
"Only source-proven Pydantic BaseSettings classes and directly owned public fields are checked. Two distinct fields must share the first complete snake-case namespace token; character prefixes, environment aliases, inherited fields and unrelated data classes do not establish a group.",
|
|
59
|
+
"Boolean, expectation and quantitative modifiers such as is, has, use, expected, min, max, num, total and default do not establish component namespaces. A prefix already present as a field is left for explicit API review.",
|
|
60
|
+
"ClassVar, Final, private attributes, model_config, computed fields, tests, generated code, unknown decorators and unresolved bases are excluded. Each namespace produces one error, anchored at its first field; an exact class or first-field suppression is honored.",
|
|
61
|
+
"Source-proven nested BaseSettings fields do not count as flat candidates. A source-proven nested child already groups a namespace when its parent field has the same first complete Python namespace token, or when its public directly owned fields share that prefix apart from bare fields that are not other source-proven nested settings objects. A competing namespace or nested settings component under a different parent namespace retains subgroup findings. Class names alone, unknown annotations and unknown parent types do not establish ownership.",
|
|
62
|
+
"There is no automatic rewrite: nesting may change environment-variable decoding, aliases, source precedence and consumer access paths.",
|
|
63
|
+
),
|
|
64
|
+
examples=(
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="flat-component-settings",
|
|
67
|
+
title="Related settings expose a repeated component namespace",
|
|
68
|
+
outcome=ExampleOutcome.MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"app/settings.py",
|
|
72
|
+
"from pydantic_settings import BaseSettings\n"
|
|
73
|
+
"class Settings(BaseSettings):\n"
|
|
74
|
+
" embedder_url: str\n"
|
|
75
|
+
" embedder_token: str\n",
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
focus_path=PurePosixPath("app/settings.py"),
|
|
79
|
+
expected_count=1,
|
|
80
|
+
public=True,
|
|
81
|
+
),
|
|
82
|
+
RuleExample(
|
|
83
|
+
example_id="nested-component-settings",
|
|
84
|
+
title="The component owns its settings and environment prefix",
|
|
85
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
86
|
+
files=(
|
|
87
|
+
ExampleFile.python(
|
|
88
|
+
"app/settings.py",
|
|
89
|
+
"from pydantic import Field\n"
|
|
90
|
+
"from pydantic_settings import BaseSettings, SettingsConfigDict\n"
|
|
91
|
+
"class EmbedderSettings(BaseSettings):\n"
|
|
92
|
+
" model_config = SettingsConfigDict(env_prefix='EMBEDDER_')\n"
|
|
93
|
+
" url: str\n"
|
|
94
|
+
" token: str\n"
|
|
95
|
+
"class Settings(BaseSettings):\n"
|
|
96
|
+
" embedder: EmbedderSettings = Field(default_factory=EmbedderSettings)\n",
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
focus_path=PurePosixPath("app/settings.py"),
|
|
100
|
+
expected_count=0,
|
|
101
|
+
public=True,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
description = documentation.summary
|
|
106
|
+
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
self._ownership_project: ProjectIndexSet | None = None
|
|
109
|
+
self._ownership: dict[tuple[Path, str], frozenset[str]] = {}
|
|
110
|
+
|
|
111
|
+
@override
|
|
112
|
+
def prepare(self, indexes: ProjectIndexSet) -> None:
|
|
113
|
+
super().prepare(indexes)
|
|
114
|
+
self._ownership_project = None
|
|
115
|
+
self._ownership.clear()
|
|
116
|
+
|
|
117
|
+
def _namespaces(self, contracts: DeclaredContracts, model: DeclaredClass) -> frozenset[str]:
|
|
118
|
+
if self._ownership_project is not contracts.project:
|
|
119
|
+
self._ownership = _nested_ownership_index(contracts)
|
|
120
|
+
self._ownership_project = contracts.project
|
|
121
|
+
return self._ownership.get((model.unit.path, model.declaration.name), frozenset())
|
|
122
|
+
|
|
123
|
+
@override
|
|
124
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
125
|
+
if context.tree is None or context.generated or is_test_path(context.path):
|
|
126
|
+
return []
|
|
127
|
+
contracts = DeclaredContracts(context)
|
|
128
|
+
findings: list[Diagnostic] = []
|
|
129
|
+
for model in _settings_models(contracts):
|
|
130
|
+
if not is_suppressed(context.source_lines, model.declaration.lineno, self.code):
|
|
131
|
+
findings.extend(self._model_diagnostics(context, contracts, model))
|
|
132
|
+
return findings
|
|
133
|
+
|
|
134
|
+
def _model_diagnostics(
|
|
135
|
+
self, context: PythonFileContext, contracts: DeclaredContracts, model: DeclaredClass
|
|
136
|
+
) -> list[Diagnostic]:
|
|
137
|
+
fields = contracts.model_fields(model)
|
|
138
|
+
nested_fields = frozenset(
|
|
139
|
+
name
|
|
140
|
+
for name, field in fields.items()
|
|
141
|
+
if (child := contracts.annotation(field.annotation, model.unit)) is not None
|
|
142
|
+
and contracts.inherits(child, SETTINGS_BASES)
|
|
143
|
+
)
|
|
144
|
+
groups = _settings_groups(fields, nested_fields=nested_fields)
|
|
145
|
+
if not groups:
|
|
146
|
+
return []
|
|
147
|
+
namespaces = self._namespaces(contracts, model)
|
|
148
|
+
findings: list[Diagnostic] = []
|
|
149
|
+
for prefix, members in groups.items():
|
|
150
|
+
if _already_grouped(prefix, fields, namespaces, nested_fields=nested_fields):
|
|
151
|
+
continue
|
|
152
|
+
first = next(iter(members.values()))
|
|
153
|
+
if is_suppressed(context.source_lines, first.lineno, self.code):
|
|
154
|
+
continue
|
|
155
|
+
names = ", ".join(f"`{name}`" for name in members)
|
|
156
|
+
findings.append(
|
|
157
|
+
Diagnostic(
|
|
158
|
+
context.path,
|
|
159
|
+
first.lineno,
|
|
160
|
+
first.col_offset + 1,
|
|
161
|
+
self.code,
|
|
162
|
+
f"Settings fields {names} share the `{prefix}_` namespace; group them under a focused `{prefix}` settings object while preserving environment loading.",
|
|
163
|
+
Severity.ERROR,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return findings
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _settings_models(contracts: DeclaredContracts) -> Iterator[DeclaredClass]:
|
|
170
|
+
unit = contracts.unit
|
|
171
|
+
if unit is None or unit.tree is None:
|
|
172
|
+
return
|
|
173
|
+
for declaration in unit.tree.body:
|
|
174
|
+
if not isinstance(declaration, ast.ClassDef):
|
|
175
|
+
continue
|
|
176
|
+
model = contracts.class_type(ast.Name(id=declaration.name))
|
|
177
|
+
if model is not None and contracts.inherits(model, SETTINGS_BASES):
|
|
178
|
+
yield model
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _already_grouped(
|
|
182
|
+
prefix: str, fields: dict[str, ast.AnnAssign], namespaces: frozenset[str], *, nested_fields: frozenset[str]
|
|
183
|
+
) -> bool:
|
|
184
|
+
if any(name.partition("_")[0] == prefix for name in namespaces):
|
|
185
|
+
return True
|
|
186
|
+
return bool(namespaces) and all(
|
|
187
|
+
name.startswith(f"{prefix}_") or ("_" not in name and name not in nested_fields) for name in fields
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _settings_groups(
|
|
192
|
+
fields: dict[str, ast.AnnAssign], *, nested_fields: frozenset[str] = frozenset()
|
|
193
|
+
) -> dict[str, dict[str, ast.AnnAssign]]:
|
|
194
|
+
groups: dict[str, dict[str, ast.AnnAssign]] = defaultdict(dict)
|
|
195
|
+
for name, field in fields.items():
|
|
196
|
+
if name in nested_fields:
|
|
197
|
+
continue
|
|
198
|
+
prefix, separator, tail = name.partition("_")
|
|
199
|
+
if separator and tail and prefix not in _MODIFIER_PREFIXES and prefix not in fields:
|
|
200
|
+
groups[prefix][name] = field
|
|
201
|
+
return {prefix: members for prefix, members in groups.items() if len(members) >= _MIN_GROUP_FIELDS}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _nested_ownership_index(contracts: DeclaredContracts) -> dict[tuple[Path, str], frozenset[str]]:
|
|
205
|
+
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer the context import until the rule registry and analysis session finish initializing.
|
|
206
|
+
|
|
207
|
+
ownership: dict[tuple[Path, str], set[str]] = defaultdict(set)
|
|
208
|
+
for source in contracts.project.source_snapshot().units:
|
|
209
|
+
unit = contracts.project.unit(source.key)
|
|
210
|
+
if unit is None or unit.tree is None or is_test_path(unit.path) or is_generated(unit.path, unit.source):
|
|
211
|
+
continue
|
|
212
|
+
context = PythonFileContext(unit.path, unit.source)
|
|
213
|
+
context.session.project = contracts.project
|
|
214
|
+
local = DeclaredContracts(context)
|
|
215
|
+
for parent in _settings_models(local):
|
|
216
|
+
for name, field in local.model_fields(parent).items():
|
|
217
|
+
child = local.annotation(field.annotation)
|
|
218
|
+
if child is not None:
|
|
219
|
+
ownership[child.unit.path, child.declaration.name].add(name)
|
|
220
|
+
return {identity: frozenset(namespaces) for identity, namespaces in ownership.items()}
|