sarj-python-lint 1.76.0__tar.gz → 1.77.2__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.76.0 → sarj_python_lint-1.77.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/pyproject.toml +1 -1
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_fixes.py +3 -1
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_fastapi.py +156 -60
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +53 -4
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +87 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/.gitignore +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/LICENSE +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/README.md +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/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.2
|
|
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/
|
|
@@ -54,12 +54,14 @@ def apply_fixes(source: str, fixes: Sequence[Fix]) -> str:
|
|
|
54
54
|
edits.sort()
|
|
55
55
|
result: list[bytes] = []
|
|
56
56
|
previous = 0
|
|
57
|
+
previous_start: int | None = None
|
|
57
58
|
for start, end, replacement in edits:
|
|
58
|
-
if start < previous:
|
|
59
|
+
if start < previous or start == previous_start:
|
|
59
60
|
msg = "fix groups overlap"
|
|
60
61
|
raise ValueError(msg)
|
|
61
62
|
result.extend((raw[previous:start], replacement))
|
|
62
63
|
previous = end
|
|
64
|
+
previous_start = start
|
|
63
65
|
result.append(raw[previous:])
|
|
64
66
|
fixed = b"".join(result).decode("utf-8")
|
|
65
67
|
tree = ast.parse(fixed)
|
|
@@ -11,6 +11,7 @@ from sarj_python_lint.rules._imports import ImportIndex
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
|
|
16
17
|
|
|
@@ -32,6 +33,7 @@ RESPONSE_TYPES = frozenset(
|
|
|
32
33
|
}
|
|
33
34
|
)
|
|
34
35
|
_DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
|
|
36
|
+
_CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
|
|
35
37
|
_MAX_IMPORTED_MODULE_BYTES = 500_000
|
|
36
38
|
_MAX_IMPORT_HOPS = 8
|
|
37
39
|
_MAX_PATH_ANCESTORS = 32
|
|
@@ -80,6 +82,12 @@ class _ReceiverKey(NamedTuple):
|
|
|
80
82
|
name: str
|
|
81
83
|
|
|
82
84
|
|
|
85
|
+
class _ScopeFrame(NamedTuple):
|
|
86
|
+
node: ast.AST
|
|
87
|
+
scope: int
|
|
88
|
+
class_owner: int | None
|
|
89
|
+
|
|
90
|
+
|
|
83
91
|
class _DecoratorBinding(NamedTuple):
|
|
84
92
|
receiver: str
|
|
85
93
|
method: str
|
|
@@ -108,6 +116,24 @@ def _binding_name(node: ast.expr) -> str:
|
|
|
108
116
|
return ""
|
|
109
117
|
|
|
110
118
|
|
|
119
|
+
def _binding_targets(node: ast.AST) -> Sequence[ast.expr]:
|
|
120
|
+
match node:
|
|
121
|
+
case ast.Assign(targets=targets) | ast.Delete(targets=targets):
|
|
122
|
+
return targets
|
|
123
|
+
case (
|
|
124
|
+
ast.AnnAssign(target=target)
|
|
125
|
+
| ast.AugAssign(target=target)
|
|
126
|
+
| ast.For(target=target)
|
|
127
|
+
| ast.AsyncFor(target=target)
|
|
128
|
+
| ast.NamedExpr(target=target)
|
|
129
|
+
):
|
|
130
|
+
return (target,)
|
|
131
|
+
case ast.With(items=items) | ast.AsyncWith(items=items):
|
|
132
|
+
return tuple(item.optional_vars for item in items if item.optional_vars is not None)
|
|
133
|
+
case _:
|
|
134
|
+
return ()
|
|
135
|
+
|
|
136
|
+
|
|
111
137
|
class FastapiIndex:
|
|
112
138
|
def __init__(self, tree: ast.Module, *, path: Path | None = None) -> None:
|
|
113
139
|
self.tree: ast.Module = tree
|
|
@@ -134,25 +160,82 @@ class FastapiIndex:
|
|
|
134
160
|
self._node_classes: dict[int, int | None] = {}
|
|
135
161
|
self._route_scopes: dict[int, int] = {}
|
|
136
162
|
self._scope_parents: dict[int, int | None] = {0: None}
|
|
163
|
+
self._class_scopes: set[int] = set()
|
|
164
|
+
self._comprehension_scopes: set[int] = set()
|
|
137
165
|
self._index_scopes(tree)
|
|
138
166
|
self._read_imports(tree)
|
|
139
167
|
self._read_aliases(tree)
|
|
140
168
|
|
|
141
169
|
def _index_scopes(self, tree: ast.Module) -> None:
|
|
142
|
-
pending
|
|
170
|
+
pending = [_ScopeFrame(tree, 0, None)]
|
|
143
171
|
while pending:
|
|
144
172
|
node, scope, class_owner = pending.pop()
|
|
145
173
|
self._node_scopes[id(node)] = scope
|
|
146
|
-
if isinstance(node, ast.ClassDef):
|
|
147
|
-
class_owner = node.lineno
|
|
148
|
-
self._scope_parents[node.lineno] = scope
|
|
149
|
-
scope = node.lineno
|
|
150
174
|
self._node_classes[id(node)] = class_owner
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
match node:
|
|
176
|
+
case ast.ClassDef() | ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
177
|
+
self._index_definition(node, scope, class_owner, pending)
|
|
178
|
+
case ast.Lambda():
|
|
179
|
+
self._index_lambda(node, scope, class_owner, pending)
|
|
180
|
+
case ast.ListComp() | ast.SetComp() | ast.DictComp() | ast.GeneratorExp():
|
|
181
|
+
self._index_comprehension(node, scope, class_owner, pending)
|
|
182
|
+
case _:
|
|
183
|
+
pending.extend(
|
|
184
|
+
_ScopeFrame(child, scope, class_owner) for child in reversed(tuple(ast.iter_child_nodes(node)))
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _index_definition(
|
|
188
|
+
self,
|
|
189
|
+
node: ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef,
|
|
190
|
+
scope: int,
|
|
191
|
+
class_owner: int | None,
|
|
192
|
+
pending: list[_ScopeFrame],
|
|
193
|
+
) -> None:
|
|
194
|
+
body = set(node.body)
|
|
195
|
+
owner = class_owner
|
|
196
|
+
self._scope_parents[node.lineno] = scope
|
|
197
|
+
if isinstance(node, ast.ClassDef):
|
|
198
|
+
owner = node.lineno
|
|
199
|
+
self._class_scopes.add(node.lineno)
|
|
200
|
+
self._node_classes[id(node)] = owner
|
|
201
|
+
else:
|
|
202
|
+
self._route_scopes[id(node)] = scope
|
|
203
|
+
pending.extend(
|
|
204
|
+
_ScopeFrame(child, node.lineno, owner) if child in body else _ScopeFrame(child, scope, class_owner)
|
|
205
|
+
for child in reversed(tuple(ast.iter_child_nodes(node)))
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _index_lambda(self, node: ast.Lambda, scope: int, class_owner: int | None, pending: list[_ScopeFrame]) -> None:
|
|
209
|
+
implicit_scope = -id(node)
|
|
210
|
+
self._scope_parents[implicit_scope] = scope
|
|
211
|
+
self._bind_arguments(node.args, implicit_scope)
|
|
212
|
+
pending.extend(
|
|
213
|
+
(_ScopeFrame(node.args, scope, class_owner), _ScopeFrame(node.body, implicit_scope, class_owner))
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _index_comprehension(
|
|
217
|
+
self,
|
|
218
|
+
node: ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp,
|
|
219
|
+
outer_scope: int,
|
|
220
|
+
class_owner: int | None,
|
|
221
|
+
pending: list[_ScopeFrame],
|
|
222
|
+
) -> None:
|
|
223
|
+
scope = -id(node)
|
|
224
|
+
self._scope_parents[scope] = outer_scope
|
|
225
|
+
self._comprehension_scopes.add(scope)
|
|
226
|
+
for index, generator in enumerate(node.generators):
|
|
227
|
+
self._node_scopes[id(generator)] = scope
|
|
228
|
+
self._node_classes[id(generator)] = class_owner
|
|
229
|
+
self.bound_names.update(_ReceiverKey(scope, name) for name in self._target_names(generator.target))
|
|
230
|
+
pending.extend(
|
|
231
|
+
_ScopeFrame(child, outer_scope if index == 0 and child is generator.iter else scope, class_owner)
|
|
232
|
+
for child in ast.iter_child_nodes(generator)
|
|
233
|
+
)
|
|
234
|
+
pending.extend(
|
|
235
|
+
_ScopeFrame(child, scope, class_owner)
|
|
236
|
+
for child in ast.iter_child_nodes(node)
|
|
237
|
+
if not isinstance(child, ast.comprehension)
|
|
238
|
+
)
|
|
156
239
|
|
|
157
240
|
def _read_imports(self, tree: ast.Module) -> None:
|
|
158
241
|
for node in walk_ast(tree):
|
|
@@ -219,7 +302,17 @@ class FastapiIndex:
|
|
|
219
302
|
changed = True
|
|
220
303
|
|
|
221
304
|
def _read_bound_names(self, node: ast.AST) -> None:
|
|
222
|
-
if
|
|
305
|
+
if targets := _binding_targets(node):
|
|
306
|
+
self.bound_names.update(
|
|
307
|
+
_ReceiverKey(self._binding_scope(node), name)
|
|
308
|
+
for target in targets
|
|
309
|
+
for name in self._target_names(target)
|
|
310
|
+
)
|
|
311
|
+
elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name is not None:
|
|
312
|
+
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
313
|
+
elif isinstance(node, ast.MatchMapping) and node.rest is not None:
|
|
314
|
+
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.rest))
|
|
315
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
223
316
|
self._bind_function_names(node)
|
|
224
317
|
elif isinstance(node, ast.ClassDef):
|
|
225
318
|
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
@@ -228,34 +321,30 @@ class FastapiIndex:
|
|
|
228
321
|
_ReceiverKey(self._node_scopes[id(node)], alias.asname or alias.name.split(".")[0])
|
|
229
322
|
for alias in node.names
|
|
230
323
|
)
|
|
231
|
-
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
232
|
-
self.bound_names.update(
|
|
233
|
-
_ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(node.target)
|
|
234
|
-
)
|
|
235
|
-
elif isinstance(node, ast.With):
|
|
236
|
-
self._bind_with_targets(node)
|
|
237
324
|
elif isinstance(node, ast.ExceptHandler) and node.name is not None:
|
|
238
325
|
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
326
|
+
|
|
327
|
+
def _binding_scope(self, node: ast.AST) -> int:
|
|
328
|
+
scope = self._node_scopes[id(node)]
|
|
329
|
+
if isinstance(node, ast.NamedExpr):
|
|
330
|
+
while scope in self._comprehension_scopes:
|
|
331
|
+
parent = self._scope_parents[scope]
|
|
332
|
+
if parent is None:
|
|
333
|
+
break
|
|
334
|
+
scope = parent
|
|
335
|
+
return scope
|
|
243
336
|
|
|
244
337
|
def _bind_function_names(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
245
338
|
self.bound_names.add(_ReceiverKey(self._node_scopes[id(node)], node.name))
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for
|
|
255
|
-
if item.optional_vars is not None:
|
|
256
|
-
self.bound_names.update(
|
|
257
|
-
_ReceiverKey(self._node_scopes[id(node)], name) for name in self._target_names(item.optional_vars)
|
|
258
|
-
)
|
|
339
|
+
self._bind_arguments(node.args, node.lineno)
|
|
340
|
+
|
|
341
|
+
def _bind_arguments(self, arguments: ast.arguments, scope: int) -> None:
|
|
342
|
+
names = [*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs]
|
|
343
|
+
if arguments.vararg is not None:
|
|
344
|
+
names.append(arguments.vararg)
|
|
345
|
+
if arguments.kwarg is not None:
|
|
346
|
+
names.append(arguments.kwarg)
|
|
347
|
+
self.bound_names.update(_ReceiverKey(scope, argument.arg) for argument in names)
|
|
259
348
|
|
|
260
349
|
def _alias_assignment(self, node: ast.AST) -> tuple[int, str, ast.expr] | None:
|
|
261
350
|
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
@@ -272,7 +361,7 @@ class FastapiIndex:
|
|
|
272
361
|
|
|
273
362
|
def _resolve_alias(self, key: _ReceiverKey, value: ast.expr) -> bool:
|
|
274
363
|
source = self._receiver_key(key.scope, _binding_name(value))
|
|
275
|
-
constructor_kind = self.
|
|
364
|
+
constructor_kind = self.constructor_kind(value)
|
|
276
365
|
if constructor_kind is not None:
|
|
277
366
|
self._register_constructor(key, value, constructor_kind)
|
|
278
367
|
return True
|
|
@@ -306,11 +395,15 @@ class FastapiIndex:
|
|
|
306
395
|
self.hidden_receivers.add(key)
|
|
307
396
|
|
|
308
397
|
def _target_names(self, node: ast.expr) -> tuple[str, ...]:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
398
|
+
match node:
|
|
399
|
+
case ast.Name(id=name):
|
|
400
|
+
return (name,)
|
|
401
|
+
case ast.Starred(value=value):
|
|
402
|
+
return self._target_names(value)
|
|
403
|
+
case ast.Tuple(elts=items) | ast.List(elts=items):
|
|
404
|
+
return tuple(name for item in items for name in self._target_names(item))
|
|
405
|
+
case _:
|
|
406
|
+
return ()
|
|
314
407
|
|
|
315
408
|
def _assignment_scope(self, node: ast.Assign | ast.AnnAssign, name: str) -> int:
|
|
316
409
|
if name.startswith("self."):
|
|
@@ -337,25 +430,26 @@ class FastapiIndex:
|
|
|
337
430
|
return isinstance(value, ast.Constant) and value.value is False
|
|
338
431
|
|
|
339
432
|
def _is_constructor_call(self, node: ast.expr) -> bool:
|
|
340
|
-
return self.
|
|
433
|
+
return self.constructor_kind(node) is not None
|
|
341
434
|
|
|
342
|
-
def
|
|
435
|
+
def constructor_kind(self, node: ast.expr) -> Literal["FastAPI", "APIRouter"] | None:
|
|
343
436
|
if not isinstance(node, ast.Call):
|
|
344
437
|
return None
|
|
345
|
-
if isinstance(node.func, ast.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
438
|
+
root = self._root_name(node.func) if isinstance(node.func, ast.Attribute) else flat_name(node.func)
|
|
439
|
+
local_scope = self._node_scopes.get(id(node), 0)
|
|
440
|
+
scope: int | None = local_scope
|
|
441
|
+
while scope is not None and scope != 0:
|
|
442
|
+
# Method bodies do not close over their class namespace.
|
|
443
|
+
if (scope == local_scope or scope not in self._class_scopes) and _ReceiverKey(
|
|
444
|
+
scope, root
|
|
445
|
+
) in self.bound_names:
|
|
446
|
+
return None
|
|
447
|
+
scope = self._scope_parents[scope]
|
|
448
|
+
canonical = self.imports.resolved_symbol(node.func, sources=_CONSTRUCTOR_SOURCES)
|
|
449
|
+
if canonical == "FastAPI":
|
|
450
|
+
return "FastAPI"
|
|
451
|
+
if canonical == "APIRouter":
|
|
452
|
+
return "APIRouter"
|
|
359
453
|
return None
|
|
360
454
|
|
|
361
455
|
def routes(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Route, ...]:
|
|
@@ -522,11 +616,13 @@ class FastapiIndex:
|
|
|
522
616
|
if isinstance(node, ast.Attribute):
|
|
523
617
|
root = self._root_name(node)
|
|
524
618
|
node_scope = self._node_scopes.get(id(node), 0)
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
scope: int | None =
|
|
619
|
+
# The index records where the annotation is evaluated. A method body
|
|
620
|
+
# does not close over an intervening class namespace.
|
|
621
|
+
scope: int | None = node_scope
|
|
528
622
|
while scope is not None and scope != 0:
|
|
529
|
-
if
|
|
623
|
+
if (scope == node_scope or scope not in self._class_scopes) and _ReceiverKey(
|
|
624
|
+
scope, root
|
|
625
|
+
) in self.bound_names:
|
|
530
626
|
return True
|
|
531
627
|
scope = self._scope_parents.get(scope)
|
|
532
628
|
return False
|
|
@@ -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,51 @@ 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
|
+
schema_override = route.has_unpack or "openapi_extra" in route.keywords
|
|
931
|
+
if (
|
|
932
|
+
route.is_hidden
|
|
933
|
+
or route.path is None
|
|
934
|
+
or route.method == "*"
|
|
935
|
+
or schema_override
|
|
936
|
+
or _conditional_registration(route.decorator, parents)
|
|
937
|
+
):
|
|
938
|
+
continue
|
|
939
|
+
operation_id = route.keywords.get("operation_id")
|
|
940
|
+
if (
|
|
941
|
+
not isinstance(operation_id, ast.Constant)
|
|
942
|
+
or not isinstance(operation_id.value, str)
|
|
943
|
+
or not operation_id.value
|
|
944
|
+
):
|
|
945
|
+
continue
|
|
946
|
+
key = scope, route.receiver, operation_id.value
|
|
947
|
+
earlier = seen.get(key)
|
|
948
|
+
if earlier is None:
|
|
949
|
+
seen[key] = route
|
|
950
|
+
elif (earlier.path, earlier.method) != (route.path, route.method) and id(route.decorator) not in reported:
|
|
951
|
+
findings.append(
|
|
952
|
+
_Finding(
|
|
953
|
+
operation_id,
|
|
954
|
+
f"[operation-id] duplicate operation_id `{operation_id.value}`; use a unique ID for each operation.",
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
reported.add(id(route.decorator))
|
|
958
|
+
return findings
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _conditional_registration(decorator: ast.Call, parents: Mapping[ast.AST, ast.AST]) -> bool:
|
|
962
|
+
owner = parents.get(decorator)
|
|
963
|
+
while owner is not None:
|
|
964
|
+
if isinstance(owner, (ast.If, ast.Match, ast.Try, ast.TryStar, ast.For, ast.AsyncFor, ast.While)):
|
|
965
|
+
return True
|
|
966
|
+
owner = parents.get(owner)
|
|
967
|
+
return False
|
|
968
|
+
|
|
969
|
+
|
|
883
970
|
def _path_pattern(path: str) -> re.Pattern[str] | None:
|
|
884
971
|
parts: list[str] = []
|
|
885
972
|
position = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_aliases.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_annotations.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_declared_tests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_exhaustiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_parameters.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_repeated.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_security.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/_autofix_sql_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.76.0 → sarj_python_lint-1.77.2}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|