sarj-python-lint 1.66.2__tar.gz → 1.67.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/PKG-INFO +4 -4
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/pyproject.toml +3 -3
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_registry.py +4 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_resource_provenance.py +4 -0
- sarj_python_lint-1.67.0/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +174 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +18 -2
- sarj_python_lint-1.67.0/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +190 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/.gitignore +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/LICENSE +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/README.md +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/suppression_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.67.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
|
-
Requires-Dist: ruff>=0.17.
|
|
22
|
-
Requires-Dist: sarj-rule-contracts==0.2.
|
|
23
|
-
Requires-Dist: sqlglot==30.
|
|
21
|
+
Requires-Dist: ruff>=0.17.2
|
|
22
|
+
Requires-Dist: sarj-rule-contracts==0.2.6
|
|
23
|
+
Requires-Dist: sqlglot==30.24.0
|
|
24
24
|
Requires-Dist: typer==0.27.3
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.67.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["sarj-rule-contracts==0.2.
|
|
18
|
+
dependencies = ["sarj-rule-contracts==0.2.6", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -32,7 +32,7 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
|
|
|
32
32
|
dev = [
|
|
33
33
|
"pydantic==2.14.1",
|
|
34
34
|
"pytest>=9.1.1",
|
|
35
|
-
"ruff>=0.17.
|
|
35
|
+
"ruff>=0.17.2",
|
|
36
36
|
"basedpyright>=1.40.2",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -219,6 +219,7 @@ if TYPE_CHECKING:
|
|
|
219
219
|
from sarj_python_lint.rules.delete_route_returns_no_content import DeleteRouteReturnsNoContent
|
|
220
220
|
from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
|
|
221
221
|
from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
|
|
222
|
+
from sarj_python_lint.rules.duplicate_pytest_fixture_params import DuplicatePytestFixtureParams
|
|
222
223
|
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
223
224
|
from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
|
|
224
225
|
from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
|
|
@@ -238,6 +239,7 @@ from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
|
|
|
238
239
|
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
239
240
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
240
241
|
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
242
|
+
from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
|
|
241
243
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
242
244
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
243
245
|
from sarj_python_lint.rules.require_exp_on_jwt_decode import RequireExpOnJwtDecode
|
|
@@ -247,6 +249,8 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
|
|
|
247
249
|
|
|
248
250
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
249
251
|
{
|
|
252
|
+
PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
|
|
253
|
+
DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
|
|
250
254
|
PreferBulkUpsert.id: PreferBulkUpsert,
|
|
251
255
|
PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
|
|
252
256
|
StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
|
|
@@ -31,6 +31,10 @@ _SCOPES = (
|
|
|
31
31
|
)
|
|
32
32
|
_RESOURCE_OWNERS = MappingProxyType(
|
|
33
33
|
{
|
|
34
|
+
"builtins": frozenset({"open"}),
|
|
35
|
+
"io": frozenset({"open", "StringIO", "BytesIO"}),
|
|
36
|
+
"tempfile": frozenset({"TemporaryFile", "NamedTemporaryFile"}),
|
|
37
|
+
"httpx": frozenset({"Client", "AsyncClient"}),
|
|
34
38
|
"asyncio": frozenset({"sleep", "CancelledError", "exceptions.CancelledError"}),
|
|
35
39
|
"contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
|
|
36
40
|
"subprocess": frozenset({"Popen", "TimeoutExpired"}),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
20
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
21
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import fixture_declarations
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import FixtureDeclaration
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_BAD = 'import pytest\n\n@pytest.fixture(params=("empty", "empty"))\ndef payload(request):\n return request.param\n'
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@final
|
|
33
|
+
class DuplicatePytestFixtureParams(Rule):
|
|
34
|
+
id: str = "duplicate-pytest-fixture-params"
|
|
35
|
+
code: str = "SARJ524"
|
|
36
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
38
|
+
summary="Review repeated immutable values in a pytest fixture parameter list.",
|
|
39
|
+
rationale=(
|
|
40
|
+
"Each fixture parameter reruns its dependent tests. Repeating the same immutable literal can add "
|
|
41
|
+
"execution time without exercising a distinct input; Ruff PT014 covers test parametrization, not fixture params."
|
|
42
|
+
),
|
|
43
|
+
remediation=(
|
|
44
|
+
"Review the dependent test contracts, then remove an accidental duplicate. Preserve intentional repetition "
|
|
45
|
+
"with explicit case IDs or an exact SARJ524 suppression explaining the repeated-run contract."
|
|
46
|
+
),
|
|
47
|
+
category=RuleCategory.TESTING,
|
|
48
|
+
autofix=AutofixPolicy.NONE,
|
|
49
|
+
limitations=(
|
|
50
|
+
"Only directly imported pytest/pytest-asyncio fixtures in test paths with one decorator and literal list/tuple params are analyzed.",
|
|
51
|
+
"Compares immutable scalar/tuple literals with type and floating-point sign preserved; mutable values, calls, aliases and pytest.param metadata are not compared.",
|
|
52
|
+
"Explicit IDs, decorator unpacking and fixtures using request beyond direct request.param loads are excluded.",
|
|
53
|
+
"Dependent fixtures, hooks and tests may observe case indices, generated IDs or repeated execution; findings require contract review and are never auto-deleted.",
|
|
54
|
+
),
|
|
55
|
+
examples=(
|
|
56
|
+
RuleExample(
|
|
57
|
+
example_id="repeated-fixture-input",
|
|
58
|
+
title="The same input schedules the dependent tests twice",
|
|
59
|
+
outcome=ExampleOutcome.MATCH,
|
|
60
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD),),
|
|
61
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
62
|
+
expected_count=1,
|
|
63
|
+
public=True,
|
|
64
|
+
),
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="distinct-fixture-inputs",
|
|
67
|
+
title="Each fixture parameter represents a distinct input",
|
|
68
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
69
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD.replace('"empty", "empty"', '"empty", "filled"')),),
|
|
70
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
71
|
+
expected_count=0,
|
|
72
|
+
public=True,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
description: str = documentation.summary
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
80
|
+
if (
|
|
81
|
+
not is_test_path(context.path)
|
|
82
|
+
or context.generated
|
|
83
|
+
or "params" not in context.symbol_source
|
|
84
|
+
or context.tree is None
|
|
85
|
+
):
|
|
86
|
+
return []
|
|
87
|
+
findings: list[Diagnostic] = []
|
|
88
|
+
for fixture in fixture_declarations(context.tree):
|
|
89
|
+
params = _parameter_values(fixture, context)
|
|
90
|
+
if params is None:
|
|
91
|
+
continue
|
|
92
|
+
first_values: dict[str, ast.expr] = {}
|
|
93
|
+
for value in params.elts:
|
|
94
|
+
key = _literal_key(value)
|
|
95
|
+
if key is None:
|
|
96
|
+
continue
|
|
97
|
+
first = first_values.setdefault(key, value)
|
|
98
|
+
if first is value or is_suppressed(context.source_lines, value.lineno, self.code):
|
|
99
|
+
continue
|
|
100
|
+
findings.append(
|
|
101
|
+
Diagnostic(
|
|
102
|
+
path=context.path,
|
|
103
|
+
line=value.lineno,
|
|
104
|
+
col=value.col_offset + 1,
|
|
105
|
+
code=self.code,
|
|
106
|
+
severity=Severity.WARNING,
|
|
107
|
+
message=(
|
|
108
|
+
f"fixture `{fixture.name}` repeats the immutable parameter from line {first.lineno}; "
|
|
109
|
+
"review dependent contracts before removing it, or give intentional repetition explicit IDs"
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return findings
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parameter_values(fixture: FixtureDeclaration, context: PythonFileContext) -> ast.List | ast.Tuple | None:
|
|
117
|
+
decorator = fixture.decorator
|
|
118
|
+
if (
|
|
119
|
+
len(fixture.node.decorator_list) != 1
|
|
120
|
+
or not isinstance(decorator, ast.Call)
|
|
121
|
+
or decorator.args
|
|
122
|
+
or any(keyword.arg is None for keyword in decorator.keywords)
|
|
123
|
+
or _observes_request_identity(fixture.node, context)
|
|
124
|
+
):
|
|
125
|
+
return None
|
|
126
|
+
keywords = {keyword.arg: keyword.value for keyword in decorator.keywords}
|
|
127
|
+
if len(keywords) != len(decorator.keywords):
|
|
128
|
+
return None
|
|
129
|
+
ids = keywords.get("ids")
|
|
130
|
+
if ids is not None and not (isinstance(ids, ast.Constant) and ids.value is None):
|
|
131
|
+
return None
|
|
132
|
+
params = keywords.get("params")
|
|
133
|
+
return params if isinstance(params, ast.List | ast.Tuple) else None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _literal_key(value: ast.expr) -> str | None:
|
|
137
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str | bytes | bool | int | float | type(None)):
|
|
138
|
+
return _scalar_key(value=value.value)
|
|
139
|
+
if (
|
|
140
|
+
isinstance(value, ast.UnaryOp)
|
|
141
|
+
and isinstance(value.op, ast.UAdd | ast.USub)
|
|
142
|
+
and isinstance(value.operand, ast.Constant)
|
|
143
|
+
and isinstance(value.operand.value, int | float)
|
|
144
|
+
and not isinstance(value.operand.value, bool)
|
|
145
|
+
):
|
|
146
|
+
number = value.operand.value
|
|
147
|
+
number = -number if isinstance(value.op, ast.USub) else +number
|
|
148
|
+
return _scalar_key(value=number)
|
|
149
|
+
if isinstance(value, ast.Tuple):
|
|
150
|
+
keys = tuple(_literal_key(item) for item in value.elts)
|
|
151
|
+
if all(key is not None for key in keys):
|
|
152
|
+
return "tuple:" + "".join(f"{len(key)}:{key}" for key in keys if key is not None)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _scalar_key(*, value: str | bytes | float | None) -> str:
|
|
157
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
158
|
+
return f"int:{value:x}"
|
|
159
|
+
return f"{type(value).__name__}:{value!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _observes_request_identity(node: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
163
|
+
for child in walk_ast(node):
|
|
164
|
+
if isinstance(child, ast.Name) and child.id == "request":
|
|
165
|
+
parent = context.parents.get(child)
|
|
166
|
+
if not (
|
|
167
|
+
isinstance(child.ctx, ast.Load)
|
|
168
|
+
and isinstance(parent, ast.Attribute)
|
|
169
|
+
and parent.value is child
|
|
170
|
+
and parent.attr == "param"
|
|
171
|
+
and isinstance(parent.ctx, ast.Load)
|
|
172
|
+
):
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
@@ -21,6 +21,8 @@ from sarj_python_lint.rules._paths import is_test_path
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterator
|
|
25
|
+
|
|
24
26
|
from sarj_python_lint._file_context import PythonFileContext
|
|
25
27
|
|
|
26
28
|
|
|
@@ -42,6 +44,12 @@ class _FixtureFunction(NamedTuple):
|
|
|
42
44
|
owner: ast.ClassDef | None
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
class FixtureDeclaration(NamedTuple):
|
|
48
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef
|
|
49
|
+
name: str
|
|
50
|
+
decorator: ast.expr
|
|
51
|
+
|
|
52
|
+
|
|
45
53
|
_NO_FIXTURE_DECORATORS = _FixtureDecorators(frozenset(), {}, frozenset())
|
|
46
54
|
|
|
47
55
|
|
|
@@ -153,8 +161,7 @@ class PytestFixtureReturnsBareTuple(Rule):
|
|
|
153
161
|
return diags
|
|
154
162
|
|
|
155
163
|
|
|
156
|
-
def
|
|
157
|
-
hits: list[_TupleResult] = []
|
|
164
|
+
def fixture_declarations(tree: ast.Module) -> Iterator[FixtureDeclaration]:
|
|
158
165
|
for node, owner in _discoverable_functions(tree):
|
|
159
166
|
if not node.decorator_list:
|
|
160
167
|
continue
|
|
@@ -162,6 +169,15 @@ def _bare_tuple_results(tree: ast.Module) -> list[_TupleResult]:
|
|
|
162
169
|
fixture_name = _fixture_name(node, fixture_names)
|
|
163
170
|
if fixture_name is None:
|
|
164
171
|
continue
|
|
172
|
+
decorator = next(
|
|
173
|
+
dec for dec in node.decorator_list if _names_fixture(dec, fixture_names.names, fixture_names.roots)
|
|
174
|
+
)
|
|
175
|
+
yield FixtureDeclaration(node, fixture_name, decorator)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _bare_tuple_results(tree: ast.Module) -> list[_TupleResult]:
|
|
179
|
+
hits: list[_TupleResult] = []
|
|
180
|
+
for node, fixture_name, _decorator in fixture_declarations(tree):
|
|
165
181
|
aliases = _type_aliases(tree, before_line=node.lineno)
|
|
166
182
|
has_yield = _has_own_yield(node)
|
|
167
183
|
if node.returns is not None and _homogeneous_tuple_annotation(
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
20
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import fixture_declarations
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
25
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_SYNC_RESOURCES = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"builtins.open",
|
|
31
|
+
"io.open",
|
|
32
|
+
"io.StringIO",
|
|
33
|
+
"io.BytesIO",
|
|
34
|
+
"tempfile.TemporaryFile",
|
|
35
|
+
"tempfile.NamedTemporaryFile",
|
|
36
|
+
"httpx.Client",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
_BAD = 'from io import StringIO\nimport pytest\n\n@pytest.fixture\ndef stream():\n with StringIO("payload") as handle:\n return handle\n'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
class PytestFixtureReturnsClosedResource(Rule):
|
|
44
|
+
id: str = "pytest-fixture-returns-closed-resource"
|
|
45
|
+
code: str = "SARJ525"
|
|
46
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
47
|
+
default_level=Severity.WARNING,
|
|
48
|
+
summary="Yield a managed fixture resource until its requesting test finishes.",
|
|
49
|
+
rationale=(
|
|
50
|
+
"Returning a context-managed stream or HTTPX client exits its context before pytest receives the value. "
|
|
51
|
+
"The requesting test receives an already closed resource instead of a usable dependency."
|
|
52
|
+
),
|
|
53
|
+
remediation=(
|
|
54
|
+
"Yield the resource inside its context so teardown follows the requesting test, updating the generator "
|
|
55
|
+
"annotation as needed. If the contract deliberately supplies a closed resource, use an exact SARJ525 "
|
|
56
|
+
"suppression with that reason. Review exception and cleanup behavior; no automatic rewrite is provided."
|
|
57
|
+
),
|
|
58
|
+
category=RuleCategory.TESTING,
|
|
59
|
+
autofix=AutofixPolicy.NONE,
|
|
60
|
+
limitations=(
|
|
61
|
+
"Only proven pytest/pytest-asyncio fixtures in test paths with one decorator and a direct return inside the owning with/async with are analyzed.",
|
|
62
|
+
"Recognizes built-in/io streams, tempfile TemporaryFile/NamedTemporaryFile, HTTPX Client and async-with AsyncClient; arbitrary context managers and TestClient are excluded.",
|
|
63
|
+
"Aliases, escaped/rebound resources, provider mutation, nested helpers, generator fixtures, returned snapshots and returns after the context are not inferred.",
|
|
64
|
+
"Unreachable branches and intentional closed-resource contracts are not inferred; findings are warnings requiring review.",
|
|
65
|
+
),
|
|
66
|
+
examples=(
|
|
67
|
+
RuleExample(
|
|
68
|
+
example_id="fixture-returns-closed-stream",
|
|
69
|
+
title="Returning exits the stream context before the test runs",
|
|
70
|
+
outcome=ExampleOutcome.MATCH,
|
|
71
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD),),
|
|
72
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
73
|
+
expected_count=1,
|
|
74
|
+
public=True,
|
|
75
|
+
),
|
|
76
|
+
RuleExample(
|
|
77
|
+
example_id="fixture-yields-live-stream",
|
|
78
|
+
title="Yield keeps the stream open for the requesting test",
|
|
79
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
80
|
+
files=(ExampleFile.python("tests/conftest.py", _BAD.replace("return handle", "yield handle")),),
|
|
81
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
82
|
+
expected_count=0,
|
|
83
|
+
public=True,
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
description: str = documentation.summary
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
91
|
+
if (
|
|
92
|
+
not is_test_path(context.path)
|
|
93
|
+
or context.generated
|
|
94
|
+
or "with" not in context.symbol_source
|
|
95
|
+
or context.tree is None
|
|
96
|
+
):
|
|
97
|
+
return []
|
|
98
|
+
returns = [node for node in context.nodes(ast.Return) if isinstance(node.value, ast.Name)]
|
|
99
|
+
if not returns:
|
|
100
|
+
return []
|
|
101
|
+
fixtures = {
|
|
102
|
+
fixture.node
|
|
103
|
+
for fixture in fixture_declarations(context.tree)
|
|
104
|
+
if len(fixture.node.decorator_list) == 1
|
|
105
|
+
and not (
|
|
106
|
+
isinstance(fixture.decorator, ast.Call)
|
|
107
|
+
and (fixture.decorator.args or any(keyword.arg is None for keyword in fixture.decorator.keywords))
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
if not fixtures:
|
|
111
|
+
return []
|
|
112
|
+
provenance = context.static_resource_provenance
|
|
113
|
+
generators = {provenance.scope(node) for node in context.nodes(ast.Yield, ast.YieldFrom)}
|
|
114
|
+
fixtures.difference_update(generators)
|
|
115
|
+
findings: list[Diagnostic] = []
|
|
116
|
+
for returned in returns:
|
|
117
|
+
if (
|
|
118
|
+
provenance.scope(returned) not in fixtures
|
|
119
|
+
or not isinstance(returned.value, ast.Name)
|
|
120
|
+
or not _returns_managed_resource(returned.value, returned, provenance)
|
|
121
|
+
or is_suppressed(context.source_lines, returned.lineno, self.code)
|
|
122
|
+
):
|
|
123
|
+
continue
|
|
124
|
+
findings.append(
|
|
125
|
+
Diagnostic(
|
|
126
|
+
path=context.path,
|
|
127
|
+
line=returned.lineno,
|
|
128
|
+
col=returned.col_offset + 1,
|
|
129
|
+
code=self.code,
|
|
130
|
+
severity=Severity.WARNING,
|
|
131
|
+
message=(
|
|
132
|
+
f"fixture returns `{returned.value.id}` after its context closes it; yield inside the context "
|
|
133
|
+
"until the requesting test finishes, or document an intentional closed-resource contract"
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return findings
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _returns_managed_resource(value: ast.Name, returned: ast.Return, provenance: ResourceProvenance) -> bool:
|
|
141
|
+
binding = provenance.binding(value.id, returned)
|
|
142
|
+
item = provenance.parents.get(binding) if binding is not None else None
|
|
143
|
+
if (
|
|
144
|
+
not isinstance(binding, ast.Name)
|
|
145
|
+
or not isinstance(item, ast.withitem)
|
|
146
|
+
or item.optional_vars is not binding
|
|
147
|
+
or not isinstance(item.context_expr, ast.Call)
|
|
148
|
+
):
|
|
149
|
+
return False
|
|
150
|
+
if (
|
|
151
|
+
not provenance.contains(provenance.parents.get(item), returned)
|
|
152
|
+
or value.id in provenance.mutated
|
|
153
|
+
or not _resource_reads_are_local(value, binding, provenance)
|
|
154
|
+
):
|
|
155
|
+
return False
|
|
156
|
+
constructor = item.context_expr.func
|
|
157
|
+
qualified = provenance.runtime_import_name(constructor)
|
|
158
|
+
if qualified is None and _builtin_open(constructor, provenance):
|
|
159
|
+
qualified = "builtins.open"
|
|
160
|
+
owner = provenance.parents.get(item)
|
|
161
|
+
return (
|
|
162
|
+
qualified == "httpx.AsyncClient"
|
|
163
|
+
if isinstance(owner, ast.AsyncWith)
|
|
164
|
+
else isinstance(owner, ast.With) and qualified in _SYNC_RESOURCES
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _builtin_open(constructor: ast.expr, provenance: ResourceProvenance) -> bool:
|
|
169
|
+
if not isinstance(constructor, ast.Name) or constructor.id != "open":
|
|
170
|
+
return False
|
|
171
|
+
return (
|
|
172
|
+
"open" not in provenance.mutated
|
|
173
|
+
and not any("open" in names for names in provenance.bindings.values())
|
|
174
|
+
and provenance.builtin_namespace_untouched
|
|
175
|
+
and not provenance.wildcard
|
|
176
|
+
and not provenance.dynamic_namespace
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _resource_reads_are_local(value: ast.Name, binding: ast.AST, provenance: ResourceProvenance) -> bool:
|
|
181
|
+
for read in provenance.reads.get(value.id, ()):
|
|
182
|
+
if provenance.binding(value.id, read) is not binding:
|
|
183
|
+
continue
|
|
184
|
+
parent = provenance.parents.get(read)
|
|
185
|
+
if read is value or (
|
|
186
|
+
isinstance(parent, ast.Attribute) and parent.value is read and isinstance(parent.ctx, ast.Load)
|
|
187
|
+
):
|
|
188
|
+
continue
|
|
189
|
+
return False
|
|
190
|
+
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/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
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_names.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/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.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_namespace_access.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_route_shapes.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_runtime_bindings.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_static_json_alias.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_store_writes.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.66.2 → sarj_python_lint-1.67.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|