sarj-python-lint 1.75.0__tar.gz → 1.76.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.75.0 → sarj_python_lint-1.76.0}/PKG-INFO +2 -2
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/pyproject.toml +2 -2
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/__main__.py +11 -6
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/benchmark.py +13 -3
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +130 -36
- sarj_python_lint-1.76.0/src/sarj_python_lint/rules/prefer_grouped_settings.py +137 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +61 -6
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +213 -13
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/.gitignore +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/LICENSE +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/README.md +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.76.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.76.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/
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
21
|
Requires-Dist: ruff>=0.17.2
|
|
22
|
-
Requires-Dist: sarj-rule-contracts==0.4.
|
|
22
|
+
Requires-Dist: sarj-rule-contracts==0.4.1
|
|
23
23
|
Requires-Dist: sqlglot==30.24.0
|
|
24
24
|
Requires-Dist: typer==0.27.3
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.76.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["sarj-rule-contracts==0.4.
|
|
18
|
+
dependencies = ["sarj-rule-contracts==0.4.1", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -9,8 +9,9 @@ import json
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import sys
|
|
11
11
|
from types import MappingProxyType
|
|
12
|
-
from typing import Annotated, NamedTuple
|
|
12
|
+
from typing import TYPE_CHECKING, Annotated, NamedTuple
|
|
13
13
|
|
|
14
|
+
from sarj_rule_contracts import RuleId
|
|
14
15
|
import typer
|
|
15
16
|
|
|
16
17
|
from sarj_python_lint import __version__
|
|
@@ -26,6 +27,10 @@ from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
|
26
27
|
from sarj_python_lint.suppression_policy import PROTECTED_SUPPRESSION_CODES
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
|
|
33
|
+
|
|
29
34
|
SKIP_DIR_NAMES = frozenset(
|
|
30
35
|
{
|
|
31
36
|
"node_modules",
|
|
@@ -86,18 +91,18 @@ def _python_files(p: Path) -> list[Path]:
|
|
|
86
91
|
return sorted(out)
|
|
87
92
|
|
|
88
93
|
|
|
89
|
-
def _canonical_rule_ids(
|
|
94
|
+
def _canonical_rule_ids(rule_names: Sequence[str]) -> list[RuleId]:
|
|
90
95
|
aliases = {
|
|
91
96
|
alias: rule_id
|
|
92
97
|
for rule_id, cls in REGISTRY.items()
|
|
93
98
|
if (documentation := cls.documentation) is not None
|
|
94
99
|
for alias in documentation.aliases
|
|
95
100
|
}
|
|
96
|
-
return list(dict.fromkeys(
|
|
101
|
+
return list(dict.fromkeys(RuleId(name if name in REGISTRY else aliases.get(name, name)) for name in rule_names))
|
|
97
102
|
|
|
98
103
|
|
|
99
|
-
def _check(
|
|
100
|
-
rule_ids = _canonical_rule_ids(
|
|
104
|
+
def _check(rule_names: Sequence[str], paths: list[Path], *, jobs: int = 1) -> list[Diagnostic]:
|
|
105
|
+
rule_ids = _canonical_rule_ids(rule_names)
|
|
101
106
|
unknown = [rid for rid in rule_ids if rid not in REGISTRY]
|
|
102
107
|
if unknown:
|
|
103
108
|
sys.stderr.write(f"unknown rule(s): {', '.join(unknown)}\n")
|
|
@@ -145,7 +150,7 @@ def check_source(
|
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
def analyze(
|
|
148
|
-
rule_ids:
|
|
153
|
+
rule_ids: Sequence[str], # sarj-noqa: SARJ093 -- Public parsing boundary accepts raw names.
|
|
149
154
|
paths: list[Path],
|
|
150
155
|
*,
|
|
151
156
|
baseline: Path | None = None,
|
|
@@ -8,14 +8,19 @@ from pathlib import Path
|
|
|
8
8
|
from statistics import median
|
|
9
9
|
import sys
|
|
10
10
|
from time import perf_counter
|
|
11
|
-
from typing import Annotated
|
|
11
|
+
from typing import TYPE_CHECKING, Annotated
|
|
12
12
|
|
|
13
|
+
from sarj_rule_contracts import RuleId
|
|
13
14
|
import typer
|
|
14
15
|
|
|
15
16
|
from sarj_python_lint.__main__ import analyze, expand_paths
|
|
16
17
|
from sarj_python_lint.rules import REGISTRY
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
@dataclass(frozen=True, slots=True)
|
|
20
25
|
class BenchmarkSample:
|
|
21
26
|
seconds: float
|
|
@@ -43,11 +48,16 @@ def peak_memory_bytes() -> int | None:
|
|
|
43
48
|
return peak if sys.platform == "darwin" else peak * 1024
|
|
44
49
|
|
|
45
50
|
|
|
46
|
-
def measure(
|
|
51
|
+
def measure(
|
|
52
|
+
paths: list[Path],
|
|
53
|
+
rule_ids: Sequence[str] | None = None, # sarj-noqa: SARJ093 -- Public input accepts raw rule names.
|
|
54
|
+
*,
|
|
55
|
+
repeats: int = 5,
|
|
56
|
+
) -> BenchmarkReport:
|
|
47
57
|
if repeats < 1:
|
|
48
58
|
msg = "repeats must be positive"
|
|
49
59
|
raise ValueError(msg)
|
|
50
|
-
selected = sorted(REGISTRY
|
|
60
|
+
selected = [RuleId(name) for name in sorted(REGISTRY if rule_ids is None else rule_ids)]
|
|
51
61
|
unknown = sorted(set(selected) - REGISTRY.keys())
|
|
52
62
|
if unknown:
|
|
53
63
|
msg = f"unknown rule(s): {', '.join(unknown)}"
|
|
@@ -240,6 +240,7 @@ from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
|
|
|
240
240
|
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
241
241
|
from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
|
|
242
242
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
243
|
+
from sarj_python_lint.rules.prefer_grouped_settings import PreferGroupedSettings
|
|
243
244
|
from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
|
|
244
245
|
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
245
246
|
from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
|
|
@@ -255,6 +256,7 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
|
|
|
255
256
|
|
|
256
257
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
257
258
|
{
|
|
259
|
+
PreferGroupedSettings.id: PreferGroupedSettings,
|
|
258
260
|
NoRedundantLocalAlias.id: NoRedundantLocalAlias,
|
|
259
261
|
PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
|
|
260
262
|
DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
|
|
@@ -15,6 +15,7 @@ from sarj_python_lint.rule_base import (
|
|
|
15
15
|
RuleDocumentation,
|
|
16
16
|
RuleExample,
|
|
17
17
|
Severity,
|
|
18
|
+
is_suppressed,
|
|
18
19
|
)
|
|
19
20
|
from sarj_python_lint.rules._ast_index import walk, walk as walk_ast
|
|
20
21
|
from sarj_python_lint.rules._logging import LOG_METHODS, is_logger_expr
|
|
@@ -32,8 +33,22 @@ if TYPE_CHECKING:
|
|
|
32
33
|
_SQL_RE = re.compile(
|
|
33
34
|
r"^\s*(?:(?i:(?:select\b.+\bfrom|insert\s+into|update\s+\S+\s+set|delete\s+from|merge\s+into)\b|"
|
|
34
35
|
r"with\s+\w+\s+as\s*\(|(?:create|alter|drop)\s+(?:table|index|view|schema|type|database)\b|"
|
|
35
|
-
r"explain\s+(?:select|insert|update|delete|with)\b)|(?:FROM|JOIN|ORDER\s+BY|GROUP\s+BY|VALUES)\b)",
|
|
36
|
+
r"explain\s+(?:select|insert|update|delete|with)\b)|(?:SELECT|FROM|JOIN|ORDER\s+BY|GROUP\s+BY|VALUES)\b|CAST\s*\()",
|
|
36
37
|
)
|
|
38
|
+
_SOURCE_FRAGMENT_RE = re.compile(
|
|
39
|
+
r"(?:^|\n)[ \t]*(?:(?:async[ \t]+)?def[ \t]+\w+|class[ \t]+\w+[ \t]*[(:]|"
|
|
40
|
+
r"from[ \t]+[\w.]+[ \t]+import\b|import[ \t]+[\w.]+(?:[ \t]+as[ \t]+\w+)?[ \t]*(?:\n|$)|"
|
|
41
|
+
r"(?:return|yield|raise|assert)[ \t]+\w+[ \t]*\(|"
|
|
42
|
+
r"\#[^\r\n]*(?:\r\n?|\n)|"
|
|
43
|
+
r"(?:\#[ \t]*@generated|--[ \t]*(?:@generated|generated))\b)"
|
|
44
|
+
)
|
|
45
|
+
_CONFIG_FRAGMENT_RE = re.compile(
|
|
46
|
+
r"(?m)^[ \t]*(?:[^\W\d][\w.-]*[ \t]*=|"
|
|
47
|
+
r"\[{1,2}[A-Za-z_][\w.\"'-]*\]{1,2}[ \t]*(?:\n|$)|"
|
|
48
|
+
r"[A-Za-z_][\w.-]*:[ \t]*[\[{])"
|
|
49
|
+
)
|
|
50
|
+
_MAPPING_FIELD_RE = re.compile(r"(?m)^[ \t]*[A-Za-z_][\w.-]*:[ \t]*(?:\S|$)")
|
|
51
|
+
_MIN_CONFIG_FIELDS = 2
|
|
37
52
|
|
|
38
53
|
# Calls returning a lazy translation proxy or an escape-aware SafeString.
|
|
39
54
|
_LAZY_CALLS = frozenset(
|
|
@@ -118,11 +133,16 @@ class _RegexBindings(NamedTuple):
|
|
|
118
133
|
scoped_names: frozenset[str]
|
|
119
134
|
|
|
120
135
|
|
|
136
|
+
class _InterpolationParts(NamedTuple):
|
|
137
|
+
literals: list[str]
|
|
138
|
+
dynamic: list[ast.expr]
|
|
139
|
+
|
|
140
|
+
|
|
121
141
|
class PreferFstringOverConcat(Rule):
|
|
122
142
|
id: str = "prefer-fstring-over-concat"
|
|
123
143
|
code: str = "SARJ068"
|
|
124
144
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
125
|
-
default_level=Severity.
|
|
145
|
+
default_level=Severity.ERROR,
|
|
126
146
|
summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
|
|
127
147
|
rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
|
|
128
148
|
remediation=(
|
|
@@ -136,12 +156,13 @@ class PreferFstringOverConcat(Rule):
|
|
|
136
156
|
limitations=(
|
|
137
157
|
"Safe fixes require every runtime operand to be an explicit unshadowed builtin str, repr or ascii conversion of a proven exact native primitive. Bare strings, subclasses, custom operators and formatting contracts remain review-only.",
|
|
138
158
|
"Runtime operands must have concrete string evidence.",
|
|
159
|
+
"Unique literal-string bindings and aliases supply surrounding text, including module prefixes. Rebinding, global/nonlocal writers, conditional assignments, imported values, and unresolved scopes are not inferred.",
|
|
139
160
|
"String evidence alone does not prove custom concatenation and formatting equivalence; only exact-native-primitive builtin-conversion subsets are fixed.",
|
|
140
161
|
(
|
|
141
|
-
"Logging, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
|
|
162
|
+
"Logging, source and configuration fragments, generated headers, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long "
|
|
142
163
|
"literals, and punctuation-oriented fragment composition are excluded."
|
|
143
164
|
),
|
|
144
|
-
"The
|
|
165
|
+
"The policy is stylistic and intentionally leaves URL, path, protocol and declarative fragments alone. Stable aliases of join-built strings retain the fragment-composition exclusion; unresolved or rebound values are not inferred.",
|
|
145
166
|
),
|
|
146
167
|
examples=(
|
|
147
168
|
RuleExample(
|
|
@@ -170,6 +191,36 @@ class PreferFstringOverConcat(Rule):
|
|
|
170
191
|
expected_count=0,
|
|
171
192
|
public=True,
|
|
172
193
|
),
|
|
194
|
+
RuleExample(
|
|
195
|
+
example_id="named-literal-prefix",
|
|
196
|
+
scenario="literal-prefix",
|
|
197
|
+
title="A literal prefix still participates in interpolation",
|
|
198
|
+
outcome=ExampleOutcome.MATCH,
|
|
199
|
+
files=(
|
|
200
|
+
ExampleFile.python(
|
|
201
|
+
"src/render.py",
|
|
202
|
+
'PREFIX = "Question: "\ndef render(value: str) -> str:\n return PREFIX + value\n',
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
focus_path=PurePosixPath("src/render.py"),
|
|
206
|
+
expected_count=1,
|
|
207
|
+
public=True,
|
|
208
|
+
),
|
|
209
|
+
RuleExample(
|
|
210
|
+
example_id="formatted-literal-prefix",
|
|
211
|
+
scenario="literal-prefix",
|
|
212
|
+
title="The prefix and value share one f-string",
|
|
213
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
214
|
+
files=(
|
|
215
|
+
ExampleFile.python(
|
|
216
|
+
"src/render.py",
|
|
217
|
+
'PREFIX = "Question: "\ndef render(value: str) -> str:\n return f"{PREFIX}{value}"\n',
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
focus_path=PurePosixPath("src/render.py"),
|
|
221
|
+
expected_count=0,
|
|
222
|
+
public=True,
|
|
223
|
+
),
|
|
173
224
|
RuleExample(
|
|
174
225
|
example_id="safe-autofix-subset",
|
|
175
226
|
title="A proven subset has a convergent automatic fix",
|
|
@@ -218,12 +269,14 @@ class PreferFstringOverConcat(Rule):
|
|
|
218
269
|
for node in adds:
|
|
219
270
|
if id(node) in inner or id(node) in excluded:
|
|
220
271
|
continue
|
|
272
|
+
if is_suppressed(context.source_lines, node.lineno, self.code):
|
|
273
|
+
continue
|
|
221
274
|
if not _in_callable_scope(node, parents):
|
|
222
275
|
continue
|
|
223
276
|
if module_evidence is None:
|
|
224
277
|
shadowed = _shadowed_producers(tree.body, None)
|
|
225
278
|
module_evidence = _StringEvidence(frozenset(), shadowed, _imported_modules(tree.body, None) - shadowed)
|
|
226
|
-
message = _verdict(node, _string_evidence(node, parents, module_evidence, scope_shadowing))
|
|
279
|
+
message = _verdict(node, _string_evidence(node, parents, module_evidence, scope_shadowing), context)
|
|
227
280
|
if message is None:
|
|
228
281
|
continue
|
|
229
282
|
diags.append(
|
|
@@ -232,7 +285,7 @@ class PreferFstringOverConcat(Rule):
|
|
|
232
285
|
line=node.lineno,
|
|
233
286
|
col=node.col_offset + 1,
|
|
234
287
|
code=self.code,
|
|
235
|
-
severity=Severity.
|
|
288
|
+
severity=Severity.ERROR,
|
|
236
289
|
message=message,
|
|
237
290
|
)
|
|
238
291
|
)
|
|
@@ -353,20 +406,13 @@ def _in_callable_scope(node: ast.AST, parents: Mapping[ast.AST, ast.AST]) -> boo
|
|
|
353
406
|
return False
|
|
354
407
|
|
|
355
408
|
|
|
356
|
-
def _verdict(node: ast.BinOp, evidence: _StringEvidence) -> str | None:
|
|
357
|
-
literals: list[str] = []
|
|
358
|
-
dynamic: list[ast.expr] = []
|
|
409
|
+
def _verdict(node: ast.BinOp, evidence: _StringEvidence, context: PythonFileContext) -> str | None:
|
|
359
410
|
operands = _flatten(node)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return None
|
|
366
|
-
literals.append(operand.value)
|
|
367
|
-
else:
|
|
368
|
-
dynamic.append(operand)
|
|
369
|
-
if _unsupported_interpolation_operands(literals, dynamic, evidence):
|
|
411
|
+
parts = _interpolation_parts(operands, evidence, context)
|
|
412
|
+
if parts is None:
|
|
413
|
+
return None
|
|
414
|
+
literals, dynamic = parts
|
|
415
|
+
if _unsupported_interpolation_operands(literals, dynamic, evidence) or _has_bound_join(dynamic, context):
|
|
370
416
|
return None
|
|
371
417
|
if len(operands) == _TERMINATOR_OPERANDS and all(not text.strip() for text in literals):
|
|
372
418
|
return None
|
|
@@ -389,12 +435,70 @@ def _verdict(node: ast.BinOp, evidence: _StringEvidence) -> str | None:
|
|
|
389
435
|
return message
|
|
390
436
|
|
|
391
437
|
|
|
438
|
+
def _interpolation_parts(
|
|
439
|
+
operands: list[ast.expr], evidence: _StringEvidence, context: PythonFileContext
|
|
440
|
+
) -> _InterpolationParts | None:
|
|
441
|
+
literals: list[str] = []
|
|
442
|
+
dynamic: list[ast.expr] = []
|
|
443
|
+
inline_literal = any(isinstance(operand, ast.Constant) and isinstance(operand.value, str) for operand in operands)
|
|
444
|
+
for operand in operands:
|
|
445
|
+
literal = (
|
|
446
|
+
None
|
|
447
|
+
if inline_literal and not isinstance(operand, ast.Constant) and _is_string_expr(operand, evidence)
|
|
448
|
+
else _literal_string(operand, context)
|
|
449
|
+
)
|
|
450
|
+
if literal is not None:
|
|
451
|
+
literals.append(literal)
|
|
452
|
+
elif isinstance(operand, ast.Constant):
|
|
453
|
+
# A bytes/int/float literal is positive evidence this `+` is not
|
|
454
|
+
# string concatenation at all.
|
|
455
|
+
return None
|
|
456
|
+
else:
|
|
457
|
+
dynamic.append(operand)
|
|
458
|
+
return _InterpolationParts(literals, dynamic)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _literal_string(expression: ast.expr, context: PythonFileContext) -> str | None:
|
|
462
|
+
value = _bound_value(expression, context)
|
|
463
|
+
return value.value if isinstance(value, ast.Constant) and isinstance(value.value, str) else None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _bound_value(expression: ast.expr, context: PythonFileContext) -> ast.expr | None:
|
|
467
|
+
seen: set[ast.AST] = set()
|
|
468
|
+
while isinstance(expression, ast.Name):
|
|
469
|
+
provenance = context.static_resource_provenance
|
|
470
|
+
if provenance.dynamic_namespace:
|
|
471
|
+
return None
|
|
472
|
+
binding = provenance.runtime_binding(expression.id, expression)
|
|
473
|
+
if not isinstance(binding, ast.Name) or binding in seen:
|
|
474
|
+
return None
|
|
475
|
+
seen.add(binding)
|
|
476
|
+
value = provenance.assignment_value(binding, expression)
|
|
477
|
+
if value is None:
|
|
478
|
+
return None
|
|
479
|
+
expression = value
|
|
480
|
+
return expression
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _has_bound_join(dynamic: list[ast.expr], context: PythonFileContext) -> bool:
|
|
484
|
+
if not any(_is_join_call(call) for call in context.nodes(ast.Call)):
|
|
485
|
+
return False
|
|
486
|
+
return any(_is_join_call(_bound_value(expression, context)) for expression in dynamic)
|
|
487
|
+
|
|
488
|
+
|
|
392
489
|
def _is_structural_fragment(literals: list[str]) -> bool:
|
|
490
|
+
if any(
|
|
491
|
+
_SOURCE_FRAGMENT_RE.search(text)
|
|
492
|
+
or _CONFIG_FRAGMENT_RE.search(text)
|
|
493
|
+
or len(_MAPPING_FIELD_RE.findall(text)) >= _MIN_CONFIG_FIELDS
|
|
494
|
+
for text in literals
|
|
495
|
+
):
|
|
496
|
+
return True
|
|
393
497
|
substantive = "".join(literals)
|
|
394
498
|
return bool(substantive) and (
|
|
395
499
|
"\\" in substantive
|
|
396
500
|
or any(marker in substantive for marker in ("(?:", "(?=", "(?!", ".*", ".+", "^", "$"))
|
|
397
|
-
or not any(character.isalpha()
|
|
501
|
+
or not any(character.isalpha() for character in substantive)
|
|
398
502
|
)
|
|
399
503
|
|
|
400
504
|
|
|
@@ -463,7 +567,7 @@ def _unsupported_interpolation_operands(
|
|
|
463
567
|
return bool(any(isinstance(expr, ast.IfExp | ast.BoolOp) for expr in dynamic))
|
|
464
568
|
|
|
465
569
|
|
|
466
|
-
def _is_join_call(expr: ast.expr) -> bool:
|
|
570
|
+
def _is_join_call(expr: ast.expr | None) -> bool:
|
|
467
571
|
return isinstance(expr, ast.Call) and isinstance(expr.func, ast.Attribute) and expr.func.attr == "join"
|
|
468
572
|
|
|
469
573
|
|
|
@@ -573,9 +677,11 @@ def _string_evidence(
|
|
|
573
677
|
|
|
574
678
|
|
|
575
679
|
def _annotation_is_string(annotation: ast.expr, shadowed: frozenset[str]) -> bool:
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
680
|
+
match annotation:
|
|
681
|
+
case ast.Name(id="str") | ast.Constant(value="str"):
|
|
682
|
+
return "str" not in shadowed
|
|
683
|
+
case _:
|
|
684
|
+
return False
|
|
579
685
|
|
|
580
686
|
|
|
581
687
|
def _is_string_expr(expr: ast.expr, evidence: _StringEvidence) -> bool:
|
|
@@ -681,18 +787,6 @@ def _collect_string_parameters(
|
|
|
681
787
|
for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs):
|
|
682
788
|
if arg.annotation is not None and _annotation_is_string(arg.annotation, shadowed):
|
|
683
789
|
known.add(arg.arg)
|
|
684
|
-
if (
|
|
685
|
-
args.vararg is not None
|
|
686
|
-
and args.vararg.annotation is not None
|
|
687
|
-
and _annotation_is_string(args.vararg.annotation, shadowed)
|
|
688
|
-
):
|
|
689
|
-
known.add(args.vararg.arg)
|
|
690
|
-
if (
|
|
691
|
-
args.kwarg is not None
|
|
692
|
-
and args.kwarg.annotation is not None
|
|
693
|
-
and _annotation_is_string(args.kwarg.annotation, shadowed)
|
|
694
|
-
):
|
|
695
|
-
known.add(args.kwarg.arg)
|
|
696
790
|
|
|
697
791
|
|
|
698
792
|
def _update_string_bindings(
|
|
@@ -0,0 +1,137 @@
|
|
|
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_test_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_MIN_GROUP_FIELDS = 2
|
|
29
|
+
_MODIFIER_PREFIXES = frozenset({"is", "has", "min", "max", "num", "total", "default", "enable", "disable"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@final
|
|
33
|
+
class PreferGroupedSettings(ProjectRule):
|
|
34
|
+
id = "prefer-grouped-settings"
|
|
35
|
+
code = "SARJ532"
|
|
36
|
+
documentation = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
38
|
+
summary="Group settings fields that share a Python namespace prefix into a nested settings object.",
|
|
39
|
+
rationale=(
|
|
40
|
+
"Repeated namespace prefixes flatten one component's configuration across the parent settings API. "
|
|
41
|
+
"A focused settings object gives that component an explicit boundary."
|
|
42
|
+
),
|
|
43
|
+
remediation=(
|
|
44
|
+
"Move the related fields into a focused nested settings object, normally constructed with "
|
|
45
|
+
"Field(default_factory=...). Preserve existing environment aliases, prefixes, validation, defaults "
|
|
46
|
+
"and source precedence, and verify real environment loading before changing the settings API."
|
|
47
|
+
),
|
|
48
|
+
category=RuleCategory.ARCHITECTURE,
|
|
49
|
+
autofix=AutofixPolicy.NONE,
|
|
50
|
+
limitations=(
|
|
51
|
+
"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.",
|
|
52
|
+
"Boolean and quantitative modifiers such as is, has, min, max, num, total and default do not establish component namespaces. A prefix already present as a field is left for explicit API review.",
|
|
53
|
+
"ClassVar, Final, private attributes, model_config, computed fields, tests, generated code, unknown decorators and unresolved bases are excluded. Each namespace produces one warning, anchored at its first field; an exact class or first-field suppression is honored.",
|
|
54
|
+
"There is no automatic rewrite: nesting may change environment-variable decoding, aliases, source precedence and consumer access paths.",
|
|
55
|
+
),
|
|
56
|
+
examples=(
|
|
57
|
+
RuleExample(
|
|
58
|
+
example_id="flat-component-settings",
|
|
59
|
+
title="Related settings expose a repeated component namespace",
|
|
60
|
+
outcome=ExampleOutcome.MATCH,
|
|
61
|
+
files=(
|
|
62
|
+
ExampleFile.python(
|
|
63
|
+
"app/settings.py",
|
|
64
|
+
"from pydantic_settings import BaseSettings\n"
|
|
65
|
+
"class Settings(BaseSettings):\n"
|
|
66
|
+
" embedder_url: str\n"
|
|
67
|
+
" embedder_token: str\n",
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
focus_path=PurePosixPath("app/settings.py"),
|
|
71
|
+
expected_count=1,
|
|
72
|
+
public=True,
|
|
73
|
+
),
|
|
74
|
+
RuleExample(
|
|
75
|
+
example_id="nested-component-settings",
|
|
76
|
+
title="The component owns its settings and environment prefix",
|
|
77
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
78
|
+
files=(
|
|
79
|
+
ExampleFile.python(
|
|
80
|
+
"app/settings.py",
|
|
81
|
+
"from pydantic import Field\n"
|
|
82
|
+
"from pydantic_settings import BaseSettings, SettingsConfigDict\n"
|
|
83
|
+
"class EmbedderSettings(BaseSettings):\n"
|
|
84
|
+
" model_config = SettingsConfigDict(env_prefix='EMBEDDER_')\n"
|
|
85
|
+
" url: str\n"
|
|
86
|
+
" token: str\n"
|
|
87
|
+
"class Settings(BaseSettings):\n"
|
|
88
|
+
" embedder: EmbedderSettings = Field(default_factory=EmbedderSettings)\n",
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
focus_path=PurePosixPath("app/settings.py"),
|
|
92
|
+
expected_count=0,
|
|
93
|
+
public=True,
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
description = documentation.summary
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
101
|
+
if context.tree is None or context.generated or is_test_path(context.path):
|
|
102
|
+
return []
|
|
103
|
+
contracts = DeclaredContracts(context)
|
|
104
|
+
findings: list[Diagnostic] = []
|
|
105
|
+
for declaration in context.tree.body:
|
|
106
|
+
if not isinstance(declaration, ast.ClassDef):
|
|
107
|
+
continue
|
|
108
|
+
model = contracts.class_type(ast.Name(id=declaration.name))
|
|
109
|
+
if model is None or not contracts.inherits(model, SETTINGS_BASES):
|
|
110
|
+
continue
|
|
111
|
+
if is_suppressed(context.source_lines, declaration.lineno, self.code):
|
|
112
|
+
continue
|
|
113
|
+
for prefix, members in _settings_groups(contracts.model_fields(model)).items():
|
|
114
|
+
first = next(iter(members.values()))
|
|
115
|
+
if is_suppressed(context.source_lines, first.lineno, self.code):
|
|
116
|
+
continue
|
|
117
|
+
names = ", ".join(f"`{name}`" for name in members)
|
|
118
|
+
findings.append(
|
|
119
|
+
Diagnostic(
|
|
120
|
+
context.path,
|
|
121
|
+
first.lineno,
|
|
122
|
+
first.col_offset + 1,
|
|
123
|
+
self.code,
|
|
124
|
+
f"Settings fields {names} share the `{prefix}_` namespace; group them under a focused `{prefix}` settings object while preserving environment loading.",
|
|
125
|
+
Severity.WARNING,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return findings
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _settings_groups(fields: dict[str, ast.AnnAssign]) -> dict[str, dict[str, ast.AnnAssign]]:
|
|
132
|
+
groups: dict[str, dict[str, ast.AnnAssign]] = defaultdict(dict)
|
|
133
|
+
for name, field in fields.items():
|
|
134
|
+
prefix, separator, tail = name.partition("_")
|
|
135
|
+
if separator and tail and prefix not in _MODIFIER_PREFIXES and prefix not in fields:
|
|
136
|
+
groups[prefix][name] = field
|
|
137
|
+
return {prefix: members for prefix, members in groups.items() if len(members) >= _MIN_GROUP_FIELDS}
|