sarj-python-lint 1.75.0__tar.gz → 1.77.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.77.0}/PKG-INFO +2 -2
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/pyproject.toml +2 -2
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/__main__.py +11 -6
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/benchmark.py +13 -3
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_fastapi.py +21 -17
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +53 -4
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +86 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +130 -36
- sarj_python_lint-1.77.0/src/sarj_python_lint/rules/prefer_grouped_settings.py +137 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +61 -6
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +213 -13
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/.gitignore +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/LICENSE +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/README.md +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.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.77.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.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.77.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.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.77.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.75.0 → sarj_python_lint-1.77.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.77.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.77.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)}"
|
|
@@ -32,6 +32,7 @@ RESPONSE_TYPES = frozenset(
|
|
|
32
32
|
}
|
|
33
33
|
)
|
|
34
34
|
_DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
|
|
35
|
+
_CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
|
|
35
36
|
_MAX_IMPORTED_MODULE_BYTES = 500_000
|
|
36
37
|
_MAX_IMPORT_HOPS = 8
|
|
37
38
|
_MAX_PATH_ANCESTORS = 32
|
|
@@ -134,6 +135,7 @@ class FastapiIndex:
|
|
|
134
135
|
self._node_classes: dict[int, int | None] = {}
|
|
135
136
|
self._route_scopes: dict[int, int] = {}
|
|
136
137
|
self._scope_parents: dict[int, int | None] = {0: None}
|
|
138
|
+
self._class_scopes: set[int] = set()
|
|
137
139
|
self._index_scopes(tree)
|
|
138
140
|
self._read_imports(tree)
|
|
139
141
|
self._read_aliases(tree)
|
|
@@ -145,6 +147,7 @@ class FastapiIndex:
|
|
|
145
147
|
self._node_scopes[id(node)] = scope
|
|
146
148
|
if isinstance(node, ast.ClassDef):
|
|
147
149
|
class_owner = node.lineno
|
|
150
|
+
self._class_scopes.add(node.lineno)
|
|
148
151
|
self._scope_parents[node.lineno] = scope
|
|
149
152
|
scope = node.lineno
|
|
150
153
|
self._node_classes[id(node)] = class_owner
|
|
@@ -272,7 +275,7 @@ class FastapiIndex:
|
|
|
272
275
|
|
|
273
276
|
def _resolve_alias(self, key: _ReceiverKey, value: ast.expr) -> bool:
|
|
274
277
|
source = self._receiver_key(key.scope, _binding_name(value))
|
|
275
|
-
constructor_kind = self.
|
|
278
|
+
constructor_kind = self.constructor_kind(value)
|
|
276
279
|
if constructor_kind is not None:
|
|
277
280
|
self._register_constructor(key, value, constructor_kind)
|
|
278
281
|
return True
|
|
@@ -337,25 +340,26 @@ class FastapiIndex:
|
|
|
337
340
|
return isinstance(value, ast.Constant) and value.value is False
|
|
338
341
|
|
|
339
342
|
def _is_constructor_call(self, node: ast.expr) -> bool:
|
|
340
|
-
return self.
|
|
343
|
+
return self.constructor_kind(node) is not None
|
|
341
344
|
|
|
342
|
-
def
|
|
345
|
+
def constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
|
|
343
346
|
if not isinstance(node, ast.Call):
|
|
344
347
|
return None
|
|
345
|
-
if isinstance(node.func, ast.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
348
|
+
root = self._root_name(node.func) if isinstance(node.func, ast.Attribute) else flat_name(node.func)
|
|
349
|
+
local_scope = self._node_scopes.get(id(node), 0)
|
|
350
|
+
scope: int | None = local_scope
|
|
351
|
+
while scope is not None and scope != 0:
|
|
352
|
+
# Method bodies do not close over their class namespace.
|
|
353
|
+
if (scope == local_scope or scope not in self._class_scopes) and _ReceiverKey(
|
|
354
|
+
scope, root
|
|
355
|
+
) in self.bound_names:
|
|
356
|
+
return None
|
|
357
|
+
scope = self._scope_parents[scope]
|
|
358
|
+
canonical = self.imports.resolved_symbol(node.func, sources=_CONSTRUCTOR_SOURCES)
|
|
359
|
+
if canonical == "FastAPI":
|
|
360
|
+
return "FastAPI"
|
|
361
|
+
if canonical == "APIRouter":
|
|
362
|
+
return "APIRouter"
|
|
359
363
|
return None
|
|
360
364
|
|
|
361
365
|
def routes(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Route, ...]:
|
|
@@ -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,
|
|
@@ -42,19 +42,21 @@ class FastapiClassRouterContract(Rule):
|
|
|
42
42
|
id = "fastapi-class-router-contract"
|
|
43
43
|
code = "SARJ451"
|
|
44
44
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
45
|
-
summary="FastAPI
|
|
45
|
+
summary="FastAPI HTTP routes use injected `*Router.build()` owners and explicit named object response models.",
|
|
46
46
|
rationale=(
|
|
47
47
|
"Class-owned construction gives dependencies one reviewable composition boundary, while explicit object models "
|
|
48
48
|
"keep success envelopes stable and generated clients precise."
|
|
49
49
|
),
|
|
50
50
|
remediation=(
|
|
51
51
|
"Inject dependencies through `*Router.__init__`, create and return a local `APIRouter` from `build()`, and declare "
|
|
52
|
-
"`response_model=NamedResponse` with a matching named return annotation."
|
|
52
|
+
"`response_model=NamedResponse` with a matching named return annotation. Compose the application with "
|
|
53
|
+
"`FastAPI.include_router(...)` instead of declaring HTTP handlers directly on it."
|
|
53
54
|
),
|
|
54
55
|
category=RuleCategory.ARCHITECTURE,
|
|
55
56
|
autofix=AutofixPolicy.NONE,
|
|
56
57
|
limitations=(
|
|
57
|
-
"Tests, generated code, hidden routes
|
|
58
|
+
"Tests, generated code, and hidden routes are excluded. FastAPI application construction and include_router composition are allowed.",
|
|
59
|
+
"Response-model checks exclude 204/304 responses and explicit stream/file/HTML/text/redirect responses.",
|
|
58
60
|
),
|
|
59
61
|
examples=(
|
|
60
62
|
RuleExample(
|
|
@@ -66,6 +68,38 @@ class FastapiClassRouterContract(Rule):
|
|
|
66
68
|
expected_count=1,
|
|
67
69
|
public=True,
|
|
68
70
|
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="direct-app-route",
|
|
73
|
+
title="An application declares an HTTP handler directly",
|
|
74
|
+
outcome=ExampleOutcome.MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python(
|
|
77
|
+
"app/routes.py",
|
|
78
|
+
"from fastapi import FastAPI\napp = FastAPI()\n"
|
|
79
|
+
"@app.get('/items', response_model=ItemsResponse)\n"
|
|
80
|
+
"def items() -> ItemsResponse:\n return ItemsResponse(items=[])\n",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
focus_path=PurePosixPath("app/routes.py"),
|
|
84
|
+
expected_count=1,
|
|
85
|
+
public=True,
|
|
86
|
+
scenario="application-composition",
|
|
87
|
+
),
|
|
88
|
+
RuleExample(
|
|
89
|
+
example_id="app-router-composition",
|
|
90
|
+
title="An application includes a class-owned router",
|
|
91
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
92
|
+
files=(
|
|
93
|
+
ExampleFile.python(
|
|
94
|
+
"app/routes.py",
|
|
95
|
+
"from fastapi import FastAPI\napp = FastAPI()\napp.include_router(ItemRouter(service).build())\n",
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
focus_path=PurePosixPath("app/routes.py"),
|
|
99
|
+
expected_count=0,
|
|
100
|
+
public=True,
|
|
101
|
+
scenario="application-composition",
|
|
102
|
+
),
|
|
69
103
|
RuleExample(
|
|
70
104
|
example_id="class-router",
|
|
71
105
|
title="A class builds and returns its router",
|
|
@@ -99,6 +133,7 @@ class FastapiClassRouterContract(Rule):
|
|
|
99
133
|
node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
100
134
|
):
|
|
101
135
|
routes = tuple(route for route in index.routes(function) if not route.is_hidden)
|
|
136
|
+
findings.extend(_direct_app_route_findings(path, routes))
|
|
102
137
|
for route in routes:
|
|
103
138
|
problem = _response_problem(function, route, index)
|
|
104
139
|
if problem is not None:
|
|
@@ -106,6 +141,20 @@ class FastapiClassRouterContract(Rule):
|
|
|
106
141
|
return sorted(findings, key=lambda finding: (finding.line, finding.col, finding.message))
|
|
107
142
|
|
|
108
143
|
|
|
144
|
+
def _direct_app_route_findings(path: Path, routes: tuple[Route, ...]) -> list[Diagnostic]:
|
|
145
|
+
decorators = {route.decorator for route in routes if route.receiver_kind == "FastAPI"}
|
|
146
|
+
return [
|
|
147
|
+
Diagnostic(
|
|
148
|
+
path,
|
|
149
|
+
decorator.lineno,
|
|
150
|
+
decorator.col_offset + 1,
|
|
151
|
+
FastapiClassRouterContract.code,
|
|
152
|
+
"declare HTTP routes on a local APIRouter returned by `*Router.build()`, then compose with include_router",
|
|
153
|
+
)
|
|
154
|
+
for decorator in decorators
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
109
158
|
def _router_construction_findings(
|
|
110
159
|
path: Path,
|
|
111
160
|
tree: ast.Module,
|
|
@@ -116,7 +165,7 @@ def _router_construction_findings(
|
|
|
116
165
|
) -> list[Diagnostic]:
|
|
117
166
|
findings: list[Diagnostic] = []
|
|
118
167
|
for call in (node for node in walk_ast(tree, index=node_index) if isinstance(node, ast.Call)):
|
|
119
|
-
if index.
|
|
168
|
+
if index.constructor_kind(call) != "APIRouter":
|
|
120
169
|
continue
|
|
121
170
|
function = _ancestor(call, parents, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
122
171
|
cls = _ancestor(call, parents, ast.ClassDef)
|
|
@@ -30,6 +30,8 @@ from sarj_python_lint.rules._paths import is_test_path
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Mapping
|
|
34
|
+
|
|
33
35
|
from sarj_python_lint._file_context import PythonFileContext
|
|
34
36
|
|
|
35
37
|
|
|
@@ -87,6 +89,7 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
87
89
|
limitations=(
|
|
88
90
|
"Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
|
|
89
91
|
"Dynamic response mappings are accepted when their contents cannot be resolved statically.",
|
|
92
|
+
"Operation ID conflicts require unconditional registrations with literal IDs and paths on the same local router; conditional registrations, dynamic IDs, OpenAPI overrides, and cross-router composition require generated-schema validation.",
|
|
90
93
|
"Explicit status and parameter-location findings are organizational policy even when FastAPI inference would generate valid OpenAPI.",
|
|
91
94
|
(
|
|
92
95
|
"Fixed-record return inference is local and conservative: dynamic keys, opaque escapes, and ambiguous "
|
|
@@ -193,6 +196,44 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
193
196
|
scenario="unannotated-record",
|
|
194
197
|
public=True,
|
|
195
198
|
),
|
|
199
|
+
RuleExample(
|
|
200
|
+
example_id="duplicate-operation-id",
|
|
201
|
+
title="Two operations reuse one client method identifier",
|
|
202
|
+
outcome=ExampleOutcome.MATCH,
|
|
203
|
+
files=(
|
|
204
|
+
ExampleFile.python(
|
|
205
|
+
"api.py",
|
|
206
|
+
"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
|
|
207
|
+
"@router.get('/items', status_code=200, operation_id='items')\n"
|
|
208
|
+
"async def read_items() -> ItemsResponse: ...\n\n"
|
|
209
|
+
"@router.post('/items', status_code=201, operation_id='items')\n"
|
|
210
|
+
"async def create_item() -> ItemResponse: ...\n",
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
focus_path=PurePosixPath("api.py"),
|
|
214
|
+
expected_count=1,
|
|
215
|
+
scenario="operation-id",
|
|
216
|
+
public=True,
|
|
217
|
+
),
|
|
218
|
+
RuleExample(
|
|
219
|
+
example_id="unique-operation-ids",
|
|
220
|
+
title="Each operation has a distinct client method identifier",
|
|
221
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
222
|
+
files=(
|
|
223
|
+
ExampleFile.python(
|
|
224
|
+
"api.py",
|
|
225
|
+
"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
|
|
226
|
+
"@router.get('/items', status_code=200, operation_id='read_items')\n"
|
|
227
|
+
"async def read_items() -> ItemsResponse: ...\n\n"
|
|
228
|
+
"@router.post('/items', status_code=201, operation_id='create_item')\n"
|
|
229
|
+
"async def create_item() -> ItemResponse: ...\n",
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
focus_path=PurePosixPath("api.py"),
|
|
233
|
+
expected_count=0,
|
|
234
|
+
scenario="operation-id",
|
|
235
|
+
public=True,
|
|
236
|
+
),
|
|
196
237
|
),
|
|
197
238
|
)
|
|
198
239
|
description: str = documentation.summary
|
|
@@ -228,6 +269,7 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
228
269
|
findings.extend(_check_projection(route))
|
|
229
270
|
findings.extend(_check_response_statuses(route, index))
|
|
230
271
|
findings.extend(_check_route_conflicts(declared))
|
|
272
|
+
findings.extend(_check_operation_ids(declared, context.parents))
|
|
231
273
|
return [
|
|
232
274
|
Diagnostic(
|
|
233
275
|
path=path,
|
|
@@ -880,6 +922,50 @@ def _check_route_conflicts(routes: list[tuple[int, Route]]) -> list[_Finding]:
|
|
|
880
922
|
return findings
|
|
881
923
|
|
|
882
924
|
|
|
925
|
+
def _check_operation_ids(routes: list[tuple[int, Route]], parents: Mapping[ast.AST, ast.AST]) -> list[_Finding]:
|
|
926
|
+
findings: list[_Finding] = []
|
|
927
|
+
seen: dict[tuple[int, str, str], Route] = {}
|
|
928
|
+
reported: set[int] = set()
|
|
929
|
+
for scope, route in sorted(routes, key=lambda item: item[1].decorator.lineno):
|
|
930
|
+
if (
|
|
931
|
+
route.is_hidden
|
|
932
|
+
or route.path is None
|
|
933
|
+
or route.method == "*"
|
|
934
|
+
or "openapi_extra" in route.keywords
|
|
935
|
+
or _conditional_registration(route.decorator, parents)
|
|
936
|
+
):
|
|
937
|
+
continue
|
|
938
|
+
operation_id = route.keywords.get("operation_id")
|
|
939
|
+
if (
|
|
940
|
+
not isinstance(operation_id, ast.Constant)
|
|
941
|
+
or not isinstance(operation_id.value, str)
|
|
942
|
+
or not operation_id.value
|
|
943
|
+
):
|
|
944
|
+
continue
|
|
945
|
+
key = scope, route.receiver, operation_id.value
|
|
946
|
+
earlier = seen.get(key)
|
|
947
|
+
if earlier is None:
|
|
948
|
+
seen[key] = route
|
|
949
|
+
elif (earlier.path, earlier.method) != (route.path, route.method) and id(route.decorator) not in reported:
|
|
950
|
+
findings.append(
|
|
951
|
+
_Finding(
|
|
952
|
+
operation_id,
|
|
953
|
+
f"[operation-id] duplicate operation_id `{operation_id.value}`; use a unique ID for each operation.",
|
|
954
|
+
)
|
|
955
|
+
)
|
|
956
|
+
reported.add(id(route.decorator))
|
|
957
|
+
return findings
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _conditional_registration(decorator: ast.Call, parents: Mapping[ast.AST, ast.AST]) -> bool:
|
|
961
|
+
owner = parents.get(decorator)
|
|
962
|
+
while owner is not None:
|
|
963
|
+
if isinstance(owner, (ast.If, ast.Match, ast.Try, ast.TryStar, ast.For, ast.AsyncFor, ast.While)):
|
|
964
|
+
return True
|
|
965
|
+
owner = parents.get(owner)
|
|
966
|
+
return False
|
|
967
|
+
|
|
968
|
+
|
|
883
969
|
def _path_pattern(path: str) -> re.Pattern[str] | None:
|
|
884
970
|
parts: list[str] = []
|
|
885
971
|
position = 0
|