sarj-python-lint 1.88.1__tar.gz → 1.88.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/PKG-INFO +1 -1
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/pyproject.toml +1 -1
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fastapi.py +185 -10
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +5 -5
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +59 -21
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/.gitignore +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/LICENSE +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/README.md +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_prepared_sources.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/application_logging.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_postgres.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_sql.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_optional_composite.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/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.88.
|
|
3
|
+
Version: 1.88.20
|
|
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/
|
|
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import Counter
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from http import HTTPStatus
|
|
8
|
+
import re
|
|
9
|
+
from types import MappingProxyType
|
|
6
10
|
from typing import TYPE_CHECKING, Literal, NamedTuple
|
|
7
11
|
|
|
8
12
|
from sarj_python_lint._source import read_python_source
|
|
@@ -19,19 +23,24 @@ HTTP_METHODS = frozenset({"get", "post", "put", "patch", "delete", "options", "h
|
|
|
19
23
|
PARAM_MARKERS = frozenset({"Path", "Query", "Header", "Cookie", "Body", "Form", "File", "Depends", "Security"})
|
|
20
24
|
SCHEMA_MARKERS = PARAM_MARKERS - {"Depends", "Security"}
|
|
21
25
|
INJECTION_TYPES = frozenset({"Request", "Response", "WebSocket", "HTTPConnection", "BackgroundTasks", "SecurityScopes"})
|
|
22
|
-
|
|
26
|
+
_RESPONSE_DEFAULT_STATUSES = MappingProxyType(
|
|
23
27
|
{
|
|
24
|
-
"Response",
|
|
25
|
-
"JSONResponse",
|
|
26
|
-
"HTMLResponse",
|
|
27
|
-
"PlainTextResponse",
|
|
28
|
-
"RedirectResponse",
|
|
29
|
-
"StreamingResponse",
|
|
30
|
-
"FileResponse",
|
|
31
|
-
"ORJSONResponse",
|
|
32
|
-
"UJSONResponse",
|
|
28
|
+
"Response": HTTPStatus.OK,
|
|
29
|
+
"JSONResponse": HTTPStatus.OK,
|
|
30
|
+
"HTMLResponse": HTTPStatus.OK,
|
|
31
|
+
"PlainTextResponse": HTTPStatus.OK,
|
|
32
|
+
"RedirectResponse": HTTPStatus.TEMPORARY_REDIRECT,
|
|
33
|
+
"StreamingResponse": HTTPStatus.OK,
|
|
34
|
+
"FileResponse": HTTPStatus.OK,
|
|
35
|
+
"ORJSONResponse": HTTPStatus.OK,
|
|
36
|
+
"UJSONResponse": HTTPStatus.OK,
|
|
33
37
|
}
|
|
34
38
|
)
|
|
39
|
+
RESPONSE_TYPES = frozenset(_RESPONSE_DEFAULT_STATUSES)
|
|
40
|
+
_STARLETTE_RESPONSE_TYPES = RESPONSE_TYPES - {"ORJSONResponse", "UJSONResponse"}
|
|
41
|
+
_JSON_RESPONSE_TYPES = frozenset({"JSONResponse", "ORJSONResponse", "UJSONResponse"})
|
|
42
|
+
NO_BODY_STATUSES = frozenset({204, 205, 304})
|
|
43
|
+
_STATUS_MEMBER = re.compile(r"HTTP_(\d{3})_[A-Z0-9_]+")
|
|
35
44
|
_DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
|
|
36
45
|
_CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
|
|
37
46
|
_MAX_IMPORTED_MODULE_BYTES = 500_000
|
|
@@ -50,6 +59,7 @@ class Route:
|
|
|
50
59
|
inherited_responses: ast.expr | None = None
|
|
51
60
|
inherited_response_class: ast.expr | None = None
|
|
52
61
|
inherited_prefix: ast.expr | None = None
|
|
62
|
+
inherited_has_unpack: bool = False
|
|
53
63
|
|
|
54
64
|
@property
|
|
55
65
|
def keywords(self) -> dict[str, ast.expr]:
|
|
@@ -154,6 +164,7 @@ class FastapiIndex:
|
|
|
154
164
|
self.receiver_kinds: dict[_ReceiverKey, Literal["FastAPI", "APIRouter"]] = {}
|
|
155
165
|
self.hidden_receivers: set[_ReceiverKey] = set()
|
|
156
166
|
self.receiver_defaults: dict[_ReceiverKey, dict[str, ast.expr]] = {}
|
|
167
|
+
self.unpacked_receivers: set[_ReceiverKey] = set()
|
|
157
168
|
self.decorators: dict[_ReceiverKey, _DecoratorBinding] = {}
|
|
158
169
|
self.bound_names: set[_ReceiverKey] = set()
|
|
159
170
|
self._node_scopes: dict[int, int] = {}
|
|
@@ -394,6 +405,8 @@ class FastapiIndex:
|
|
|
394
405
|
for keyword in value.keywords
|
|
395
406
|
if keyword.arg in {"responses", "default_response_class", "prefix"}
|
|
396
407
|
}
|
|
408
|
+
if any(keyword.arg is None for keyword in value.keywords):
|
|
409
|
+
self.unpacked_receivers.add(key)
|
|
397
410
|
if self._constructor_is_hidden(value):
|
|
398
411
|
self.hidden_receivers.add(key)
|
|
399
412
|
|
|
@@ -497,6 +510,7 @@ class FastapiIndex:
|
|
|
497
510
|
inherited_responses=defaults.get("responses"),
|
|
498
511
|
inherited_response_class=defaults.get("default_response_class"),
|
|
499
512
|
inherited_prefix=defaults.get("prefix"),
|
|
513
|
+
inherited_has_unpack=origin in self.unpacked_receivers,
|
|
500
514
|
)
|
|
501
515
|
for route_method in route_methods
|
|
502
516
|
)
|
|
@@ -541,6 +555,167 @@ class FastapiIndex:
|
|
|
541
555
|
case _:
|
|
542
556
|
return ""
|
|
543
557
|
|
|
558
|
+
def status_code(self, node: ast.expr | None) -> int | None:
|
|
559
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
|
560
|
+
return node.value
|
|
561
|
+
if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
|
|
562
|
+
return None
|
|
563
|
+
qualified = self.imports.resolved_qualified_name(node) or ""
|
|
564
|
+
module, _, member = qualified.rpartition(".")
|
|
565
|
+
if (
|
|
566
|
+
module == "starlette.status"
|
|
567
|
+
and module not in self._unstable_native_providers
|
|
568
|
+
and (match := _STATUS_MEMBER.fullmatch(member))
|
|
569
|
+
):
|
|
570
|
+
return int(match.group(1))
|
|
571
|
+
if not isinstance(node, ast.Attribute):
|
|
572
|
+
return None
|
|
573
|
+
owner = self._status_provider(node.value)
|
|
574
|
+
if owner in self._unstable_native_providers:
|
|
575
|
+
return None
|
|
576
|
+
if owner == "starlette.status" and (match := _STATUS_MEMBER.fullmatch(node.attr)):
|
|
577
|
+
return int(match.group(1))
|
|
578
|
+
if owner == "http.HTTPStatus":
|
|
579
|
+
status = HTTPStatus.__members__.get(node.attr)
|
|
580
|
+
return status.value if status is not None else None
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
def operation_status_code(self, route: Route) -> int | None:
|
|
584
|
+
node = route.keywords.get("status_code")
|
|
585
|
+
if node is None and route.has_unpack:
|
|
586
|
+
return None
|
|
587
|
+
if node is None or (isinstance(node, ast.Constant) and node.value is None):
|
|
588
|
+
if not self.response_classes_are_stable:
|
|
589
|
+
return None
|
|
590
|
+
if route.has_unpack and "response_class" not in route.keywords:
|
|
591
|
+
return None
|
|
592
|
+
response_class = route.keywords.get("response_class") or route.inherited_response_class
|
|
593
|
+
if response_class is None:
|
|
594
|
+
return None if route.inherited_has_unpack else HTTPStatus.OK
|
|
595
|
+
response_name = self.response_class_name(response_class)
|
|
596
|
+
return _RESPONSE_DEFAULT_STATUSES.get(response_name) if response_name is not None else None
|
|
597
|
+
return self.status_code(node)
|
|
598
|
+
|
|
599
|
+
def response_class_name(self, node: ast.expr) -> str | None:
|
|
600
|
+
if not self.response_classes_are_stable:
|
|
601
|
+
return None
|
|
602
|
+
return self._native_response_name(self._response_import(node))
|
|
603
|
+
|
|
604
|
+
def can_infer_json_response(self, route: Route) -> bool:
|
|
605
|
+
if route.has_unpack:
|
|
606
|
+
return False
|
|
607
|
+
status = self.operation_status_code(route)
|
|
608
|
+
if status is None or status in NO_BODY_STATUSES:
|
|
609
|
+
return False
|
|
610
|
+
response_class = route.keywords.get("response_class") or route.inherited_response_class
|
|
611
|
+
if response_class is None:
|
|
612
|
+
return not route.inherited_has_unpack and self.response_classes_are_stable
|
|
613
|
+
return self.response_class_name(response_class) in _JSON_RESPONSE_TYPES
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def response_classes_are_stable(self) -> bool:
|
|
617
|
+
return "responses" not in self._unstable_native_providers
|
|
618
|
+
|
|
619
|
+
@staticmethod
|
|
620
|
+
def _native_response_name(qualified: str | None) -> str | None:
|
|
621
|
+
if qualified is None:
|
|
622
|
+
return None
|
|
623
|
+
module, _, name = qualified.rpartition(".")
|
|
624
|
+
if module == "fastapi" and name == "Response":
|
|
625
|
+
return name
|
|
626
|
+
if module == "fastapi.responses" and name in RESPONSE_TYPES:
|
|
627
|
+
return name
|
|
628
|
+
if module == "starlette.responses" and name in _STARLETTE_RESPONSE_TYPES:
|
|
629
|
+
return name
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
def _response_import(self, node: ast.expr) -> str | None:
|
|
633
|
+
if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
|
|
634
|
+
return None
|
|
635
|
+
qualified = self.imports.resolved_qualified_name(node)
|
|
636
|
+
if isinstance(node, ast.Name) and qualified is None:
|
|
637
|
+
binding = self.imports.bindings.get(node.id)
|
|
638
|
+
return binding.module if binding is not None and binding.symbol is None else None
|
|
639
|
+
if isinstance(node, ast.Attribute) and qualified is None:
|
|
640
|
+
module = self.imports.resolved_qualified_name(node.value)
|
|
641
|
+
return f"{module}.{node.attr}" if module is not None else None
|
|
642
|
+
return qualified
|
|
643
|
+
|
|
644
|
+
def _status_provider(self, node: ast.expr) -> str | None:
|
|
645
|
+
if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
|
|
646
|
+
return None
|
|
647
|
+
qualified = self.imports.resolved_qualified_name(node)
|
|
648
|
+
if isinstance(node, ast.Name) and qualified is None:
|
|
649
|
+
binding = self.imports.bindings.get(node.id)
|
|
650
|
+
qualified = (
|
|
651
|
+
binding.module
|
|
652
|
+
if binding is not None and binding.symbol is None and binding.module != "fastapi.status"
|
|
653
|
+
else None
|
|
654
|
+
)
|
|
655
|
+
# FastAPI exports Starlette's status module rather than a status submodule.
|
|
656
|
+
if qualified == "fastapi.status":
|
|
657
|
+
return "starlette.status"
|
|
658
|
+
return qualified if qualified in {"starlette.status", "http.HTTPStatus"} else None
|
|
659
|
+
|
|
660
|
+
@cached_property
|
|
661
|
+
def _unstable_native_providers(self) -> frozenset[str]:
|
|
662
|
+
providers: set[str] = set()
|
|
663
|
+
route_calls: set[int] = set()
|
|
664
|
+
for node in walk_ast(self.tree):
|
|
665
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
666
|
+
route_calls.update(id(route.decorator) for route in self.routes(node))
|
|
667
|
+
continue
|
|
668
|
+
values = self._provider_values(node)
|
|
669
|
+
if not values:
|
|
670
|
+
continue
|
|
671
|
+
for value in values:
|
|
672
|
+
if (provider := self._status_provider(value)) is not None:
|
|
673
|
+
providers.add(provider)
|
|
674
|
+
for value in self._response_provider_values(node, values, route_calls):
|
|
675
|
+
qualified = self._response_import(value)
|
|
676
|
+
if qualified in {"fastapi.responses", "starlette.responses"} or self._native_response_name(qualified):
|
|
677
|
+
providers.add("responses")
|
|
678
|
+
return frozenset(providers)
|
|
679
|
+
|
|
680
|
+
@staticmethod
|
|
681
|
+
def _provider_values(node: ast.AST) -> tuple[ast.expr, ...]:
|
|
682
|
+
match node:
|
|
683
|
+
case ast.Attribute(value=value, ctx=ast.Store() | ast.Del()):
|
|
684
|
+
return node, value
|
|
685
|
+
case ast.Assign(value=value) | ast.AnnAssign(value=ast.expr() as value) | ast.NamedExpr(value=value):
|
|
686
|
+
return (value,)
|
|
687
|
+
case ast.Return(value=ast.expr() as value):
|
|
688
|
+
return (value,)
|
|
689
|
+
case ast.Call(args=args, keywords=keywords):
|
|
690
|
+
return (*args, *(keyword.value for keyword in keywords))
|
|
691
|
+
case _:
|
|
692
|
+
return ()
|
|
693
|
+
|
|
694
|
+
def _response_provider_values(
|
|
695
|
+
self, node: ast.AST, values: tuple[ast.expr, ...], route_calls: set[int]
|
|
696
|
+
) -> tuple[ast.expr, ...]:
|
|
697
|
+
if not isinstance(node, ast.Call):
|
|
698
|
+
return values
|
|
699
|
+
trusted_keyword = self._trusted_response_keyword(node, route_calls)
|
|
700
|
+
return (*node.args, *(keyword.value for keyword in node.keywords if keyword.arg != trusted_keyword))
|
|
701
|
+
|
|
702
|
+
def _trusted_response_keyword(self, node: ast.Call, route_calls: set[int]) -> str | None:
|
|
703
|
+
trusted_keyword = "response_class" if id(node) in route_calls else None
|
|
704
|
+
if (
|
|
705
|
+
any(keyword.arg == "default_response_class" for keyword in node.keywords)
|
|
706
|
+
and self.constructor_kind(node) is not None
|
|
707
|
+
):
|
|
708
|
+
return "default_response_class"
|
|
709
|
+
if not isinstance(node.func, ast.Attribute) or node.func.attr not in {"include_router", "add_api_route"}:
|
|
710
|
+
return trusted_keyword
|
|
711
|
+
scope = self._node_scopes.get(id(node), 0)
|
|
712
|
+
receiver = _binding_name(node.func.value)
|
|
713
|
+
if receiver.startswith("self."):
|
|
714
|
+
scope = self._node_classes.get(id(node)) or scope
|
|
715
|
+
if self._receiver_key(scope, receiver) is None:
|
|
716
|
+
return trusted_keyword
|
|
717
|
+
return "default_response_class" if node.func.attr == "include_router" else "response_class"
|
|
718
|
+
|
|
544
719
|
@staticmethod
|
|
545
720
|
def _root_name(node: ast.Attribute) -> str:
|
|
546
721
|
value: ast.expr = node
|
|
@@ -27,7 +27,6 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
_NO_BODY = frozenset({204, 304})
|
|
31
30
|
_NON_JSON_RESPONSES = frozenset(
|
|
32
31
|
{"FileResponse", "HTMLResponse", "PlainTextResponse", "RedirectResponse", "StreamingResponse"}
|
|
33
32
|
)
|
|
@@ -56,7 +55,9 @@ class FastapiClassRouterContract(Rule):
|
|
|
56
55
|
autofix=AutofixPolicy.NONE,
|
|
57
56
|
limitations=(
|
|
58
57
|
"Tests, generated code, and hidden routes are excluded. FastAPI application construction and include_router composition are allowed.",
|
|
59
|
-
"Response-model checks exclude 204/304
|
|
58
|
+
"Response-model checks exclude literal or import-proven 204/205/304 statuses and proven non-JSON response classes, including raw Response.",
|
|
59
|
+
"Unpacked metadata or an explicit unresolved status cannot prove a JSON response; receiver ownership remains checked while response-model inference abstains.",
|
|
60
|
+
"Response classes require unshadowed native import provenance; direct provider writes/escapes, custom classes, and unresolved defaults disable JSON-model inference.",
|
|
60
61
|
),
|
|
61
62
|
examples=(
|
|
62
63
|
RuleExample(
|
|
@@ -211,12 +212,11 @@ def _response_problem(
|
|
|
211
212
|
function: ast.FunctionDef | ast.AsyncFunctionDef, route: Route, index: FastapiIndex
|
|
212
213
|
) -> str | None:
|
|
213
214
|
keywords = route.keywords
|
|
214
|
-
status = keywords.get("status_code")
|
|
215
|
-
if isinstance(status, ast.Constant) and status.value in _NO_BODY:
|
|
216
|
-
return None
|
|
217
215
|
response_class = keywords.get("response_class") or route.inherited_response_class
|
|
218
216
|
if response_class is not None and flat_name(response_class) in _NON_JSON_RESPONSES:
|
|
219
217
|
return None
|
|
218
|
+
if not index.can_infer_json_response(route):
|
|
219
|
+
return None
|
|
220
220
|
response_model = keywords.get("response_model")
|
|
221
221
|
if response_model is None or (isinstance(response_model, ast.Constant) and response_model.value is None):
|
|
222
222
|
return "JSON operation requires explicit `response_model=NamedResponse`"
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
)
|
|
21
21
|
from sarj_python_lint.rules._ast_index import children
|
|
22
22
|
from sarj_python_lint.rules._fastapi import (
|
|
23
|
+
NO_BODY_STATUSES,
|
|
23
24
|
SCHEMA_MARKERS,
|
|
24
25
|
FastapiIndex,
|
|
25
26
|
Route,
|
|
@@ -89,6 +90,9 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
89
90
|
limitations=(
|
|
90
91
|
"Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
|
|
91
92
|
"Dynamic response mappings are accepted when their contents cannot be resolved statically.",
|
|
93
|
+
"Response-body/model inference abstains for unresolved status/class metadata, direct native-provider writes/escapes, and opaque unpacked defaults; no-body JSON metadata includes 205.",
|
|
94
|
+
"Direct-status alias/callee/catch recognition retains its existing limits; broader coverage requires separate reachability and runtime-dispatch evidence.",
|
|
95
|
+
"Route conflicts require unconditional registrations on the same local router; conditional composition requires trusted route/schema validation.",
|
|
92
96
|
"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.",
|
|
93
97
|
"Explicit status and parameter-location findings are organizational policy even when FastAPI inference would generate valid OpenAPI.",
|
|
94
98
|
(
|
|
@@ -268,7 +272,7 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
268
272
|
findings.extend(_check_metadata(route, index))
|
|
269
273
|
findings.extend(_check_projection(route))
|
|
270
274
|
findings.extend(_check_response_statuses(route, index))
|
|
271
|
-
findings.extend(_check_route_conflicts(declared))
|
|
275
|
+
findings.extend(_check_route_conflicts(declared, context.parents))
|
|
272
276
|
findings.extend(_check_operation_ids(declared, context.parents))
|
|
273
277
|
return [
|
|
274
278
|
Diagnostic(
|
|
@@ -548,7 +552,9 @@ def _check_inferred_return(
|
|
|
548
552
|
if not builds_fixed_record(function, imports=index.imports):
|
|
549
553
|
return []
|
|
550
554
|
for route in _operations(routes):
|
|
551
|
-
if route.
|
|
555
|
+
if route.inherited_has_unpack and route.inherited_responses is None:
|
|
556
|
+
continue
|
|
557
|
+
if not index.can_infer_json_response(route):
|
|
552
558
|
continue
|
|
553
559
|
response_model = route.keywords.get("response_model")
|
|
554
560
|
status = _status_code(route.keywords.get("status_code"), index) or HTTPStatus.OK
|
|
@@ -572,13 +578,22 @@ def _check_return_route(route: Route, contract: _ReturnContract, index: FastapiI
|
|
|
572
578
|
if contract.erases_schema and not _concrete_response_model(response_model, index):
|
|
573
579
|
problems.add("response annotation erases its OpenAPI schema; define an output model")
|
|
574
580
|
status = _status_code(keywords.get("status_code"), index)
|
|
581
|
+
proven_status = index.operation_status_code(route)
|
|
582
|
+
body_status_is_known = proven_status is not None and proven_status not in NO_BODY_STATUSES
|
|
575
583
|
response_class = keywords.get("response_class") or route.inherited_response_class
|
|
576
|
-
|
|
584
|
+
default_class_is_known = not route.inherited_has_unpack and index.response_classes_are_stable
|
|
585
|
+
if (
|
|
586
|
+
contract.is_response
|
|
587
|
+
and status not in _NO_CONTENT_STATUSES
|
|
588
|
+
and body_status_is_known
|
|
589
|
+
and response_class is None
|
|
590
|
+
and default_class_is_known
|
|
591
|
+
):
|
|
577
592
|
problems.add("direct Response return requires response_class= for accurate OpenAPI")
|
|
578
593
|
_check_response_body(route, contract, index, problems)
|
|
579
594
|
if response_model is not None and not response_model_none and _schema_erasing(response_model, index):
|
|
580
595
|
problems.add("response_model erases its OpenAPI schema; define an output model")
|
|
581
|
-
if status in _NO_CONTENT_STATUSES:
|
|
596
|
+
if status in _NO_CONTENT_STATUSES and status == proven_status:
|
|
582
597
|
if response_model is not None and not response_model_none:
|
|
583
598
|
problems.add(f"status {status} must not declare a response model")
|
|
584
599
|
if not (contract.is_none or contract.is_response):
|
|
@@ -589,19 +604,30 @@ def _check_response_body(route: Route, contract: _ReturnContract, index: Fastapi
|
|
|
589
604
|
response_model = route.keywords.get("response_model")
|
|
590
605
|
response_model_none = response_model is not None and _literal_none(response_model)
|
|
591
606
|
status = _status_code(route.keywords.get("status_code"), index)
|
|
607
|
+
proven_status = index.operation_status_code(route)
|
|
592
608
|
response_class = route.keywords.get("response_class") or route.inherited_response_class
|
|
593
609
|
response_name = index.response_name(contract.annotation)
|
|
594
610
|
class_name = index.response_name(response_class)
|
|
595
|
-
|
|
611
|
+
proven_class_name = index.response_class_name(response_class) if response_class is not None else None
|
|
612
|
+
response_class_is_unresolved = (response_class is not None and (not class_name or proven_class_name is None)) or (
|
|
613
|
+
response_class is None and (route.inherited_has_unpack or not index.response_classes_are_stable)
|
|
614
|
+
)
|
|
596
615
|
has_documented_body = (
|
|
597
616
|
_concrete_response_model(response_model, index)
|
|
598
617
|
or any(_documents_response_content(node, status, index) for node in _route_response_nodes(route))
|
|
599
618
|
or class_name in _SELF_DOCUMENTING_RESPONSE_CLASSES
|
|
619
|
+
or proven_class_name in _SELF_DOCUMENTING_RESPONSE_CLASSES
|
|
600
620
|
or response_class_is_unresolved
|
|
601
621
|
)
|
|
602
|
-
if
|
|
622
|
+
if (
|
|
623
|
+
response_name not in {"", "Response"}
|
|
624
|
+
and class_name
|
|
625
|
+
and proven_class_name is not None
|
|
626
|
+
and response_name != class_name
|
|
627
|
+
):
|
|
603
628
|
problems.add(f"return type {response_name} conflicts with response_class={class_name}")
|
|
604
|
-
|
|
629
|
+
body_status_is_known = proven_status is not None and proven_status not in NO_BODY_STATUSES
|
|
630
|
+
if contract.is_response and status not in _NO_CONTENT_STATUSES and body_status_is_known and not has_documented_body:
|
|
605
631
|
problems.add("direct Response return requires a concrete response_model or responses content schema")
|
|
606
632
|
elif not contract.is_response and not contract.is_none and response_model_none:
|
|
607
633
|
problems.add("response_model=None suppresses the declared response schema")
|
|
@@ -725,14 +751,7 @@ def _check_direct_responses(
|
|
|
725
751
|
if not direct.statuses:
|
|
726
752
|
return findings
|
|
727
753
|
for route in _operations(routes):
|
|
728
|
-
|
|
729
|
-
if any(not isinstance(node, ast.Dict) for node in responses):
|
|
730
|
-
continue
|
|
731
|
-
documented = {
|
|
732
|
-
status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)
|
|
733
|
-
}
|
|
734
|
-
primary = _status_code(route.keywords.get("status_code"), index)
|
|
735
|
-
missing = sorted(direct.statuses - documented - ({primary} if primary is not None else set()))
|
|
754
|
+
missing = _undocumented_direct_statuses(route, direct.statuses, index)
|
|
736
755
|
if missing:
|
|
737
756
|
findings.append(
|
|
738
757
|
_Finding(
|
|
@@ -743,6 +762,23 @@ def _check_direct_responses(
|
|
|
743
762
|
return findings
|
|
744
763
|
|
|
745
764
|
|
|
765
|
+
def _undocumented_direct_statuses(route: Route, statuses: set[int], index: FastapiIndex) -> list[int]:
|
|
766
|
+
if route.has_unpack or (route.inherited_has_unpack and route.inherited_responses is None):
|
|
767
|
+
return []
|
|
768
|
+
proven_primary = index.operation_status_code(route)
|
|
769
|
+
if proven_primary is None:
|
|
770
|
+
return []
|
|
771
|
+
responses = _route_response_nodes(route)
|
|
772
|
+
if any(not isinstance(node, ast.Dict) for node in responses):
|
|
773
|
+
return []
|
|
774
|
+
documented = {status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)}
|
|
775
|
+
primary = _status_code(route.keywords.get("status_code"), index)
|
|
776
|
+
primary_statuses = {proven_primary}
|
|
777
|
+
if primary is not None:
|
|
778
|
+
primary_statuses.add(primary)
|
|
779
|
+
return sorted(statuses - documented - primary_statuses)
|
|
780
|
+
|
|
781
|
+
|
|
746
782
|
@dataclass(frozen=True, slots=True)
|
|
747
783
|
class _DirectResponseStatuses:
|
|
748
784
|
statuses: set[int]
|
|
@@ -794,13 +830,15 @@ def _direct_statuses(
|
|
|
794
830
|
statuses: list[tuple[ast.Call, int]] = []
|
|
795
831
|
call = current.exc if isinstance(current, ast.Raise) else None
|
|
796
832
|
if isinstance(call, ast.Call) and index.is_http_exception(call.func) and not catches_http_exception:
|
|
797
|
-
|
|
798
|
-
|
|
833
|
+
status_node = _keyword(call, "status_code") or (call.args[0] if call.args else None)
|
|
834
|
+
status = _status_code(status_node, index)
|
|
835
|
+
if status is not None and index.status_code(status_node) == status:
|
|
799
836
|
statuses.append((call, status))
|
|
800
837
|
returned = current.value if isinstance(current, ast.Return) else None
|
|
801
838
|
if isinstance(returned, ast.Call) and index.is_response(returned.func):
|
|
802
|
-
|
|
803
|
-
|
|
839
|
+
status_node = _keyword(returned, "status_code")
|
|
840
|
+
status = _status_code(status_node, index)
|
|
841
|
+
if status is not None and index.status_code(status_node) == status:
|
|
804
842
|
statuses.append((returned, status))
|
|
805
843
|
return statuses
|
|
806
844
|
|
|
@@ -890,12 +928,12 @@ def _function_scopes(tree: ast.Module) -> dict[int, int]:
|
|
|
890
928
|
return scopes
|
|
891
929
|
|
|
892
930
|
|
|
893
|
-
def _check_route_conflicts(routes: list[tuple[int, Route]]) -> list[_Finding]:
|
|
931
|
+
def _check_route_conflicts(routes: list[tuple[int, Route]], parents: Mapping[ast.AST, ast.AST]) -> list[_Finding]:
|
|
894
932
|
findings: list[_Finding] = []
|
|
895
933
|
seen: dict[tuple[int, str, str, str], Route] = {}
|
|
896
934
|
dynamic: dict[tuple[int, str, str], list[tuple[Route, re.Pattern[str]]]] = {}
|
|
897
935
|
for scope, route in sorted(routes, key=lambda item: item[1].decorator.lineno):
|
|
898
|
-
if route.path is None or route.method == "*":
|
|
936
|
+
if route.path is None or route.method == "*" or _conditional_registration(route.decorator, parents):
|
|
899
937
|
continue
|
|
900
938
|
key = scope, route.receiver, route.method, route.path
|
|
901
939
|
if key in seen:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_aliases.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_annotations.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_tests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_exhaustiveness.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_parameters.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_repeated.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_security.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/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
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_prepared_sources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/application_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/interpreter_argv.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
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_names.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_async_lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_alias_fix.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.88.1 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|