sarj-python-lint 1.88.0__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.0 → sarj_python_lint-1.88.20}/PKG-INFO +1 -1
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/pyproject.toml +1 -1
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fastapi.py +185 -10
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_opaque_contracts.py +96 -4
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +5 -5
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +59 -21
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +2 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_cli.py +15 -1
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/.gitignore +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/LICENSE +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/README.md +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_prepared_sources.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/application_logging.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_postgres.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_sql.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_optional_composite.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.88.0 → 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.0 → 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.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.88.0 → 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
|
{sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_opaque_contracts.py
RENAMED
|
@@ -192,7 +192,7 @@ class OpaqueContractFacts:
|
|
|
192
192
|
|
|
193
193
|
def _find_checked(self, function: _Function) -> frozenset[ast.arg]:
|
|
194
194
|
found = set(self.native.namespace(function).parameters())
|
|
195
|
-
terminal_rejection = _rejects(function.body)
|
|
195
|
+
terminal_rejection = self._rejects(function.body)
|
|
196
196
|
for node in direct_nodes(function):
|
|
197
197
|
if not self.reachable(node, function):
|
|
198
198
|
continue
|
|
@@ -206,8 +206,85 @@ class OpaqueContractFacts:
|
|
|
206
206
|
found.update(self._statement_parameters(node, terminal_rejection=terminal_rejection))
|
|
207
207
|
found.update(self._identity_parameters(function))
|
|
208
208
|
found.update(self._key_parameters(function))
|
|
209
|
+
found.update(self._model_iteration_parameters(function))
|
|
209
210
|
return frozenset(found)
|
|
210
211
|
|
|
212
|
+
def _model_iteration_parameters(self, function: _Function) -> set[ast.arg]:
|
|
213
|
+
found: set[ast.arg] = set()
|
|
214
|
+
for parameter in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
|
|
215
|
+
if not self._opaque_iterable(parameter):
|
|
216
|
+
continue
|
|
217
|
+
loops = self._parameter_iterations(parameter, function)
|
|
218
|
+
if loops and all(self._validated_iteration(loop, function) for loop in loops):
|
|
219
|
+
found.add(parameter)
|
|
220
|
+
return found
|
|
221
|
+
|
|
222
|
+
def _opaque_iterable(self, parameter: ast.arg) -> bool:
|
|
223
|
+
if parameter.annotation is None:
|
|
224
|
+
return False
|
|
225
|
+
resolved = self.facts.resolve_annotation(parameter.annotation)
|
|
226
|
+
if resolved is None or not isinstance(resolved.expression, ast.Subscript):
|
|
227
|
+
return False
|
|
228
|
+
node = resolved.expression
|
|
229
|
+
return self.facts.qualified(node.value, resolved.at) in {
|
|
230
|
+
"collections.abc.Iterable",
|
|
231
|
+
"typing.Iterable",
|
|
232
|
+
} and _unknown_slot(node.slice, self.facts, resolved.at)
|
|
233
|
+
|
|
234
|
+
def _parameter_iterations(self, parameter: ast.arg, function: _Function) -> list[ast.For] | None:
|
|
235
|
+
loops: list[ast.For] = []
|
|
236
|
+
members = set(direct_nodes(function))
|
|
237
|
+
for read in self.facts.provenance.reads.get(parameter.arg, ()):
|
|
238
|
+
runtime = self.facts.provenance.runtime_binding(read.id, read)
|
|
239
|
+
if runtime is not parameter and self.facts.provenance.binding(read.id, read) is not parameter:
|
|
240
|
+
continue
|
|
241
|
+
if not self.reachable(read, function):
|
|
242
|
+
continue
|
|
243
|
+
owner = self.context.parents.get(read)
|
|
244
|
+
if (
|
|
245
|
+
runtime is not parameter
|
|
246
|
+
or not isinstance(owner, ast.For)
|
|
247
|
+
or owner not in members
|
|
248
|
+
or owner.iter is not read
|
|
249
|
+
or not isinstance(owner.target, ast.Name)
|
|
250
|
+
):
|
|
251
|
+
return None
|
|
252
|
+
loops.append(owner)
|
|
253
|
+
return loops
|
|
254
|
+
|
|
255
|
+
def _validated_iteration(self, loop: ast.For, function: _Function) -> bool:
|
|
256
|
+
if not isinstance(loop.target, ast.Name):
|
|
257
|
+
return False
|
|
258
|
+
reads = [
|
|
259
|
+
read
|
|
260
|
+
for read in self.facts.provenance.reads.get(loop.target.id, ())
|
|
261
|
+
if (
|
|
262
|
+
self.facts.provenance.runtime_binding(read.id, read) is loop.target
|
|
263
|
+
or self.facts.provenance.binding(read.id, read) is loop.target
|
|
264
|
+
)
|
|
265
|
+
and self.reachable(read, function)
|
|
266
|
+
]
|
|
267
|
+
members = set(direct_nodes(loop))
|
|
268
|
+
return bool(reads) and all(
|
|
269
|
+
self.facts.provenance.runtime_binding(read.id, read) is loop.target
|
|
270
|
+
and read in members
|
|
271
|
+
and self._model_element_read(read)
|
|
272
|
+
for read in reads
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _model_element_read(self, read: ast.Name) -> bool:
|
|
276
|
+
call = self.context.parents.get(read)
|
|
277
|
+
if isinstance(call, ast.keyword):
|
|
278
|
+
call = self.context.parents.get(call)
|
|
279
|
+
if not isinstance(call, ast.Call) or not isinstance(call.func, ast.Attribute):
|
|
280
|
+
return False
|
|
281
|
+
arguments = call_arguments(call, ("obj",))
|
|
282
|
+
if call.func.attr != "model_validate" or arguments is None or arguments.get("obj") is not read:
|
|
283
|
+
return False
|
|
284
|
+
contracts = DeclaredContracts(self.context)
|
|
285
|
+
model = contracts.class_type(call.func.value)
|
|
286
|
+
return model is not None and contracts.inherits(model, _VALIDATION_BASES)
|
|
287
|
+
|
|
211
288
|
def _key_parameters(self, function: _Function) -> set[ast.arg]:
|
|
212
289
|
found: set[ast.arg] = set()
|
|
213
290
|
for parameter in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
|
|
@@ -337,7 +414,7 @@ class OpaqueContractFacts:
|
|
|
337
414
|
return set()
|
|
338
415
|
|
|
339
416
|
def _if_parameters(self, node: ast.If) -> set[ast.arg]:
|
|
340
|
-
if _rejects(node.body):
|
|
417
|
+
if self._rejects(node.body):
|
|
341
418
|
candidate = node.test
|
|
342
419
|
if isinstance(candidate, ast.BoolOp) and isinstance(candidate.op, ast.Or):
|
|
343
420
|
candidate = candidate.values[0]
|
|
@@ -345,7 +422,7 @@ class OpaqueContractFacts:
|
|
|
345
422
|
return {parameter}
|
|
346
423
|
if isinstance(candidate, ast.UnaryOp) and isinstance(candidate.op, ast.Not):
|
|
347
424
|
return self._predicate(candidate.operand)
|
|
348
|
-
return self._predicate(node.test) if _rejects(node.orelse) else set()
|
|
425
|
+
return self._predicate(node.test) if self._rejects(node.orelse) else set()
|
|
349
426
|
|
|
350
427
|
def _match_parameters(self, node: ast.Match) -> set[ast.arg]:
|
|
351
428
|
if not node.cases:
|
|
@@ -355,7 +432,7 @@ class OpaqueContractFacts:
|
|
|
355
432
|
isinstance(last.pattern, ast.MatchAs)
|
|
356
433
|
and last.pattern.name is None
|
|
357
434
|
and last.guard is None
|
|
358
|
-
and _rejects(last.body)
|
|
435
|
+
and self._rejects(last.body)
|
|
359
436
|
):
|
|
360
437
|
return set()
|
|
361
438
|
parameter = self.parameter(node.subject)
|
|
@@ -363,6 +440,21 @@ class OpaqueContractFacts:
|
|
|
363
440
|
return {parameter}
|
|
364
441
|
return set()
|
|
365
442
|
|
|
443
|
+
def _rejects(self, body: list[ast.stmt]) -> bool:
|
|
444
|
+
if _rejects(body):
|
|
445
|
+
return True
|
|
446
|
+
if not body or not isinstance(body[-1], ast.Expr) or not isinstance(body[-1].value, ast.Call):
|
|
447
|
+
return False
|
|
448
|
+
source = self._source(body[-1].value.func)
|
|
449
|
+
if source is None:
|
|
450
|
+
return False
|
|
451
|
+
_owner, function = source
|
|
452
|
+
return (
|
|
453
|
+
bool(function.body)
|
|
454
|
+
and isinstance(function.body[-1], ast.Raise)
|
|
455
|
+
and not any(isinstance(node, ast.Return | ast.Yield | ast.YieldFrom) for node in direct_nodes(function))
|
|
456
|
+
)
|
|
457
|
+
|
|
366
458
|
def _predicate(self, expression: ast.expr) -> set[ast.arg]:
|
|
367
459
|
if isinstance(expression, ast.BoolOp):
|
|
368
460
|
return self._predicate(expression.values[0])
|
|
@@ -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:
|
|
@@ -54,6 +54,8 @@ class PreferPreciseObjectContract(Rule):
|
|
|
54
54
|
"Ordinary raw validators retain object inputs tied to builtin isinstance, exact builtin type comparisons or an actual source-resolved TypeGuard/TypeIs through rejecting branches, actual type/refinement assertions, accepting terminal branches with a rejecting fallback, or conditional expressions. Stable projections from containers with unchecked value slots retain their raw root contract when those values are validated. Source-visible local and imported validators may delegate those inputs and raw results; precise result narrowing does not justify a bare object output. Related unchecked mapping projections and transforms retain unknown values; import-proven native parsers and JSONDecoder preserve raw outputs. Universal builtin and stdlib interoperation may retain raw inputs. Independent object parameters, domain constructors and unproven parser names remain checked. Source resolution and dataflow are bounded with cycle guards; rebinding, decorators and namespace mutation invalidate delegated proof.",
|
|
55
55
|
"Native opaque contracts include canonical ast.Constant.value reads and captures, FunctionType invocations, identity-only inputs and sys.getrefcount. Actual exec/eval globals and locals retain unknown values only through bounded native dict construction and alias flow; unrelated dictionaries and source domain factories do not borrow that ownership. Stub property returns on an import-proven runtime_checkable Protocol retain unchecked incoming attributes. Generic callable results, ordinary Protocol methods, application properties and unused parameters remain checked.",
|
|
56
56
|
"Raw input unions may include scalar or None branches; only genuine unchecked container branches seed raw projections. Mapping consumers that inspect only keys retain uninspected value slots. Projected aliases require simple statement dominance, and precise active guards permit typed consumption after validation. Native namespace output reads require a preceding direct function-scope execution and abstain after authored mutations. Benign builtins imports do not hide builtin object contracts; explicit object shadowing and source-visible object mutations remain outside scope.",
|
|
57
|
+
"An import-proven Iterable of unchecked values retains its raw element contract when every input use is a direct synchronous iteration and every reachable element read directly enters source-proven Pydantic model_validate. Deferred closures, rebinding, other iterator/element uses, unknown model providers and JSON-text parsers do not establish that boundary.",
|
|
58
|
+
"Rejecting branches may end in a stable source-visible synchronous helper whose body ends with an unconditional raise and contains no return or yield path. Names and NoReturn annotations alone do not establish termination; decorated, mutated, asynchronous, generator and returning helpers remain unknown.",
|
|
57
59
|
),
|
|
58
60
|
examples=(
|
|
59
61
|
RuleExample(
|
{sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_cli.py
RENAMED
|
@@ -42,7 +42,7 @@ class PreferTypedCli(Rule):
|
|
|
42
42
|
category=RuleCategory.MAINTAINABILITY,
|
|
43
43
|
autofix=AutofixPolicy.NONE,
|
|
44
44
|
limitations=(
|
|
45
|
-
"Checks import-proven sys.argv reads, argparse/optparse parser construction, getopt parsing and Click command construction. sys.argv[0], assignment targets and forwarding through args= to a proven local Typer application are retained.",
|
|
45
|
+
"Checks import-proven sys.argv reads, argparse/optparse parser construction, getopt parsing and Click command construction. sys.argv[0] and its direct nonempty guard with a literal string fallback, assignment targets and forwarding through args= to a proven local Typer application are retained.",
|
|
46
46
|
"Checks parameters of source-visible functions passed directly to typer.run or registered with a stable local Typer application's command/callback decorator. Missing annotations and bare Any/object, including Annotated wrappers, are reported. Ordinary functions and unrelated command decorators are excluded.",
|
|
47
47
|
"Stable local import and application aliases are followed. Shadowed or mutated imports, wildcard imports, dynamic namespaces and generated/vendor files abstain. Imported command functions, dynamic registrations and composite erased types require separate type checking.",
|
|
48
48
|
"No automatic rewrite: positional/option compatibility and conversion behavior require review. This policy does not claim every argparse or Click program is incorrectly validated.",
|
|
@@ -191,6 +191,20 @@ def _retained_argv(node: ast.expr, facts: AnnotationOrigins) -> bool:
|
|
|
191
191
|
if isinstance(parent.slice, ast.Constant) and type(parent.slice.value) is int and parent.slice.value == 0:
|
|
192
192
|
return True
|
|
193
193
|
parent = facts.context.parents.get(parent)
|
|
194
|
+
match parent:
|
|
195
|
+
case ast.IfExp(
|
|
196
|
+
test=test,
|
|
197
|
+
body=ast.Subscript(value=arguments, slice=ast.Constant(value=index)),
|
|
198
|
+
orelse=ast.Constant(value=str()),
|
|
199
|
+
):
|
|
200
|
+
return (
|
|
201
|
+
test is node
|
|
202
|
+
and type(index) is int
|
|
203
|
+
and index == 0
|
|
204
|
+
and facts.qualified(arguments, arguments) == "sys.argv"
|
|
205
|
+
)
|
|
206
|
+
case _:
|
|
207
|
+
pass
|
|
194
208
|
if not isinstance(parent, ast.keyword) or parent.arg != "args":
|
|
195
209
|
return False
|
|
196
210
|
call = facts.context.parents.get(parent)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|