sarj-python-lint 1.14.4__tar.gz → 1.15.1__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.14.4 → sarj_python_lint-1.15.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_analysis_session.py +2 -0
- sarj_python_lint-1.15.1/src/sarj_python_lint/rules/_local_source.py +123 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.15.1/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +452 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/.gitignore +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/LICENSE +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/README.md +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/unused_test_factory_option.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.15.1
|
|
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/
|
{sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, final
|
|
|
4
4
|
|
|
5
5
|
from sarj_python_lint._python_target import PythonTargetFacts
|
|
6
6
|
from sarj_python_lint.rules._first_party import FirstPartyFacts
|
|
7
|
+
from sarj_python_lint.rules._local_source import LocalSourceFacts
|
|
7
8
|
from sarj_python_lint.rules._nominal_project import NominalProjectFacts
|
|
8
9
|
|
|
9
10
|
|
|
@@ -16,5 +17,6 @@ class AnalysisSession:
|
|
|
16
17
|
def __init__(self) -> None:
|
|
17
18
|
self.project: ProjectIndexSet | None = None
|
|
18
19
|
self.first_party = FirstPartyFacts()
|
|
20
|
+
self.local_source = LocalSourceFacts()
|
|
19
21
|
self.python_target = PythonTargetFacts()
|
|
20
22
|
self.nominal_project = NominalProjectFacts()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, final
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from sarj_python_lint.rules._first_party import FirstPartyFacts
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_MAX_ANCESTORS = 24
|
|
16
|
+
_MAX_MODULE_BYTES = 256_000
|
|
17
|
+
_MAX_MODULES = 512
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class LocalModule:
|
|
22
|
+
path: Path
|
|
23
|
+
source: str
|
|
24
|
+
tree: ast.Module
|
|
25
|
+
runtime_imports: ImportIndex
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@final
|
|
29
|
+
class LocalSourceFacts:
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._resolutions: dict[tuple[Path, str | None, int], Path | None] = {}
|
|
32
|
+
self._modules: dict[Path, LocalModule | None] = {}
|
|
33
|
+
|
|
34
|
+
def resolve_module(self, current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
|
|
35
|
+
try:
|
|
36
|
+
current = current.absolute()
|
|
37
|
+
except OSError:
|
|
38
|
+
return None
|
|
39
|
+
key = (current, module, level)
|
|
40
|
+
if key in self._resolutions:
|
|
41
|
+
return self._resolutions[key]
|
|
42
|
+
if len(self._resolutions) >= _MAX_MODULES:
|
|
43
|
+
return None
|
|
44
|
+
result = _resolve_module(current, module, level, facts=facts)
|
|
45
|
+
self._resolutions[key] = result
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
def read_module(self, path: Path) -> LocalModule | None:
|
|
49
|
+
if path in self._modules:
|
|
50
|
+
return self._modules[path]
|
|
51
|
+
if len(self._modules) >= _MAX_MODULES:
|
|
52
|
+
return None
|
|
53
|
+
module = _read_module(path)
|
|
54
|
+
self._modules[path] = module
|
|
55
|
+
return module
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_module(current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
|
|
59
|
+
if ".." in current.parts or level > _MAX_ANCESTORS:
|
|
60
|
+
return None
|
|
61
|
+
root = facts.project_root(current)
|
|
62
|
+
if root is None or not _safe_path(current, root):
|
|
63
|
+
return None
|
|
64
|
+
parts = module.split(".") if module else []
|
|
65
|
+
if any(not part.isidentifier() for part in parts):
|
|
66
|
+
return None
|
|
67
|
+
candidates: set[Path] = set()
|
|
68
|
+
for base in _import_bases(current, root, level):
|
|
69
|
+
target = base.joinpath(*parts)
|
|
70
|
+
for candidate in (target.with_suffix(".py"), target / "__init__.py"):
|
|
71
|
+
if _safe_path(candidate, root) and candidate.is_file():
|
|
72
|
+
candidates.add(candidate)
|
|
73
|
+
return next(iter(candidates)) if len(candidates) == 1 else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _import_bases(current: Path, root: Path, level: int) -> list[Path]:
|
|
77
|
+
if level:
|
|
78
|
+
base = current.parent
|
|
79
|
+
for _ in range(level - 1):
|
|
80
|
+
if base == root:
|
|
81
|
+
return []
|
|
82
|
+
base = base.parent
|
|
83
|
+
return [base]
|
|
84
|
+
bases: list[Path] = []
|
|
85
|
+
for ancestor in list(current.parents)[:_MAX_ANCESTORS]:
|
|
86
|
+
if not ancestor.is_relative_to(root):
|
|
87
|
+
break
|
|
88
|
+
bases.append(ancestor)
|
|
89
|
+
if (ancestor / "pyproject.toml").is_file():
|
|
90
|
+
bases.append(ancestor / "src")
|
|
91
|
+
if ancestor == root:
|
|
92
|
+
break
|
|
93
|
+
return bases
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _safe_path(path: Path, root: Path) -> bool:
|
|
97
|
+
if not path.is_relative_to(root):
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
for entry in (path, *list(path.parents)[:_MAX_ANCESTORS]):
|
|
101
|
+
if entry == root:
|
|
102
|
+
return True
|
|
103
|
+
if entry.is_symlink() or (entry != path and (entry / ".git").exists()):
|
|
104
|
+
return False
|
|
105
|
+
except OSError:
|
|
106
|
+
return False
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_module(path: Path) -> LocalModule | None:
|
|
111
|
+
try:
|
|
112
|
+
with path.open("rb") as stream:
|
|
113
|
+
data = stream.read(_MAX_MODULE_BYTES + 1)
|
|
114
|
+
if len(data) > _MAX_MODULE_BYTES:
|
|
115
|
+
return None
|
|
116
|
+
source = data.decode("utf-8")
|
|
117
|
+
tree = ast.parse(source, filename=str(path))
|
|
118
|
+
except OSError, UnicodeError, SyntaxError:
|
|
119
|
+
return None
|
|
120
|
+
runtime_imports = ast.Module(
|
|
121
|
+
body=[node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))], type_ignores=[]
|
|
122
|
+
)
|
|
123
|
+
return LocalModule(path, source, tree, ImportIndex.from_tree(runtime_imports))
|
|
@@ -133,6 +133,7 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
|
133
133
|
)
|
|
134
134
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
135
135
|
from sarj_python_lint.rules.prefer_pydantic_json_value import PreferPydanticJsonValue
|
|
136
|
+
from sarj_python_lint.rules.prefer_pytest_fixture_injection import PreferPytestFixtureInjection
|
|
136
137
|
from sarj_python_lint.rules.prefer_regex_fullmatch import PreferRegexFullmatch
|
|
137
138
|
from sarj_python_lint.rules.prefer_self_documenting_constant import (
|
|
138
139
|
PreferSelfDocumentingConstant,
|
|
@@ -278,6 +279,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
278
279
|
NoRepeatedTestBody.id: NoRepeatedTestBody,
|
|
279
280
|
UnusedMockSetup.id: UnusedMockSetup,
|
|
280
281
|
UnusedTestFactoryOption.id: UnusedTestFactoryOption,
|
|
282
|
+
PreferPytestFixtureInjection.id: PreferPytestFixtureInjection,
|
|
281
283
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
282
284
|
PreferOrPattern.id: PreferOrPattern,
|
|
283
285
|
PreferPydanticJsonValue.id: PreferPydanticJsonValue,
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, NamedTuple, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_support_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
|
|
25
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
+
from sarj_python_lint.rules._local_source import LocalModule
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
type Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
30
|
+
_POOL_CONSTRUCTORS = frozenset({"psycopg_pool.ConnectionPool", "psycopg_pool.AsyncConnectionPool"})
|
|
31
|
+
_SETUP_AND_CONSUMER = 2
|
|
32
|
+
_SUPPORT_SOURCE = (
|
|
33
|
+
"from psycopg_pool import ConnectionPool\n\n"
|
|
34
|
+
"def pool() -> ConnectionPool[object]:\n"
|
|
35
|
+
" return ConnectionPool[object](open=False)\n"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _HelperImport(NamedTuple):
|
|
40
|
+
local: str
|
|
41
|
+
symbol: str
|
|
42
|
+
module: str
|
|
43
|
+
level: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PreferPytestFixtureInjection(Rule):
|
|
47
|
+
id: str = "prefer-pytest-fixture-injection"
|
|
48
|
+
code: str = "SARJ476"
|
|
49
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
50
|
+
default_level=Severity.WARNING,
|
|
51
|
+
summary="Request shared database-pool setup through a pytest fixture.",
|
|
52
|
+
rationale=(
|
|
53
|
+
"Calling a shared conftest pool helper during initial test setup bypasses an explicit pytest "
|
|
54
|
+
"fixture dependency for that setup."
|
|
55
|
+
),
|
|
56
|
+
remediation=(
|
|
57
|
+
"Request one function-scoped fixture visible to the test, preserving initial pool state, creation "
|
|
58
|
+
"timing, and cleanup ownership. Use yield teardown when the fixture owns cleanup; keep factories "
|
|
59
|
+
"for variants or fresh instances."
|
|
60
|
+
),
|
|
61
|
+
category=RuleCategory.TESTING,
|
|
62
|
+
autofix=AutofixPolicy.NONE,
|
|
63
|
+
limitations=(
|
|
64
|
+
"Only directly collected module-level test functions in conventional pytest module names are checked.",
|
|
65
|
+
"Only unshadowed runtime imports from a uniquely resolved local conftest module are followed.",
|
|
66
|
+
"Helpers must be undecorated synchronous functions with no formal inputs and one direct return of an imported psycopg_pool pool constructor.",
|
|
67
|
+
"Setup must be the first executable statement and immediately feed a plain call, or appear inline as a direct argument of that first call.",
|
|
68
|
+
"Class tests, decorated helpers, unknown test decorators, dynamic imports, re-exports, and ambiguous or out-of-bound source paths are excluded.",
|
|
69
|
+
),
|
|
70
|
+
examples=(
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="manual-shared-generic-pool",
|
|
73
|
+
title="A test calls shared generic pool setup",
|
|
74
|
+
outcome=ExampleOutcome.MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python("tests/conftest.py", _SUPPORT_SOURCE),
|
|
77
|
+
ExampleFile.python(
|
|
78
|
+
"tests/test_app.py",
|
|
79
|
+
"from tests.conftest import pool\n\n"
|
|
80
|
+
"def test_app():\n resource = pool()\n app = Application(resource)\n assert app\n",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
focus_path=PurePosixPath("tests/test_app.py"),
|
|
84
|
+
expected_count=1,
|
|
85
|
+
public=True,
|
|
86
|
+
),
|
|
87
|
+
RuleExample(
|
|
88
|
+
example_id="injected-generic-pool",
|
|
89
|
+
title="A test requests its shared resource",
|
|
90
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
91
|
+
files=(
|
|
92
|
+
ExampleFile.python(
|
|
93
|
+
"tests/conftest.py",
|
|
94
|
+
"import pytest\nfrom psycopg_pool import ConnectionPool\n\n"
|
|
95
|
+
"@pytest.fixture\ndef pool() -> ConnectionPool[object]:\n"
|
|
96
|
+
" return ConnectionPool[object](open=False)\n",
|
|
97
|
+
),
|
|
98
|
+
ExampleFile.python(
|
|
99
|
+
"tests/test_app.py", "def test_app(pool):\n app = Application(pool)\n assert app\n"
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
focus_path=PurePosixPath("tests/test_app.py"),
|
|
103
|
+
expected_count=0,
|
|
104
|
+
public=True,
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
description: str = documentation.summary
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
112
|
+
if (
|
|
113
|
+
context.path.suffix != ".py"
|
|
114
|
+
or not (context.path.name.startswith("test_") or context.path.name.endswith("_test.py"))
|
|
115
|
+
or is_test_support_path(context.path)
|
|
116
|
+
or context.generated
|
|
117
|
+
or context.tree is None
|
|
118
|
+
):
|
|
119
|
+
return []
|
|
120
|
+
tree = context.tree
|
|
121
|
+
helpers = _resource_helpers(context)
|
|
122
|
+
if not helpers:
|
|
123
|
+
return []
|
|
124
|
+
module_bindings = _scope_bindings(tree)
|
|
125
|
+
if "__test__" in module_bindings or any(
|
|
126
|
+
isinstance(node, ast.Attribute) and node.attr == "__test__" and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
127
|
+
for node in ast.walk(tree)
|
|
128
|
+
):
|
|
129
|
+
return []
|
|
130
|
+
diagnostics: list[Diagnostic] = []
|
|
131
|
+
for statement in tree.body:
|
|
132
|
+
if (
|
|
133
|
+
not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
134
|
+
or module_bindings[statement.name] != 1
|
|
135
|
+
or _module_reference(tree, statement.name)
|
|
136
|
+
):
|
|
137
|
+
continue
|
|
138
|
+
call = _setup_call(context, statement, helpers)
|
|
139
|
+
if call is not None:
|
|
140
|
+
diagnostics.append(
|
|
141
|
+
Diagnostic(
|
|
142
|
+
path=context.path,
|
|
143
|
+
line=call.lineno,
|
|
144
|
+
col=call.col_offset + 1,
|
|
145
|
+
code=self.code,
|
|
146
|
+
message=(
|
|
147
|
+
"test calls a shared conftest pool helper during initial setup; request one visible "
|
|
148
|
+
"function-scoped fixture by parameter, preserving initial pool state, creation timing, "
|
|
149
|
+
"and cleanup ownership; keep factories for variants or fresh instances."
|
|
150
|
+
),
|
|
151
|
+
severity=Severity.WARNING,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return diagnostics
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resource_helpers(context: PythonFileContext) -> frozenset[str]:
|
|
158
|
+
tree = context.tree
|
|
159
|
+
if tree is None:
|
|
160
|
+
return frozenset()
|
|
161
|
+
references = tuple(_helper_imports(tree))
|
|
162
|
+
if not references:
|
|
163
|
+
return frozenset()
|
|
164
|
+
bindings = _scope_bindings(tree)
|
|
165
|
+
helpers: set[str] = set()
|
|
166
|
+
for reference in references:
|
|
167
|
+
if bindings[reference.local] != 1 or _root_mutated(tree, reference.local):
|
|
168
|
+
continue
|
|
169
|
+
target = context.session.local_source.resolve_module(
|
|
170
|
+
context.path, reference.module, reference.level, facts=context.session.first_party
|
|
171
|
+
)
|
|
172
|
+
module = (
|
|
173
|
+
context.session.local_source.read_module(target)
|
|
174
|
+
if target is not None and target.name == "conftest.py"
|
|
175
|
+
else None
|
|
176
|
+
)
|
|
177
|
+
if module is not None and _is_resource_helper(module, reference.symbol):
|
|
178
|
+
helpers.add(reference.local)
|
|
179
|
+
return frozenset(helpers)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _helper_imports(tree: ast.Module) -> Iterator[_HelperImport]:
|
|
183
|
+
candidates: list[_HelperImport] = []
|
|
184
|
+
for statement in tree.body:
|
|
185
|
+
if not isinstance(statement, ast.ImportFrom):
|
|
186
|
+
continue
|
|
187
|
+
module = statement.module
|
|
188
|
+
if module is None or module.rsplit(".", 1)[-1] != "conftest":
|
|
189
|
+
continue
|
|
190
|
+
candidates.extend(
|
|
191
|
+
_HelperImport(alias.asname or alias.name, alias.name, module, statement.level) for alias in statement.names
|
|
192
|
+
)
|
|
193
|
+
if not candidates or _has_wildcard(tree):
|
|
194
|
+
return
|
|
195
|
+
yield from candidates
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_resource_helper(module: LocalModule, symbol: str) -> bool:
|
|
199
|
+
if (
|
|
200
|
+
is_generated(module.path, module.source)
|
|
201
|
+
or _scope_bindings(module.tree)[symbol] != 1
|
|
202
|
+
or _has_wildcard(module.tree)
|
|
203
|
+
or _root_mutated(module.tree, symbol)
|
|
204
|
+
):
|
|
205
|
+
return False
|
|
206
|
+
helper = next(
|
|
207
|
+
(node for node in module.tree.body if isinstance(node, ast.FunctionDef) and node.name == symbol), None
|
|
208
|
+
)
|
|
209
|
+
if helper is None or helper.decorator_list:
|
|
210
|
+
return False
|
|
211
|
+
arguments = helper.args
|
|
212
|
+
if arguments.posonlyargs or arguments.args or arguments.kwonlyargs or arguments.vararg or arguments.kwarg:
|
|
213
|
+
return False
|
|
214
|
+
body = _executable_body(helper)
|
|
215
|
+
if len(body) != 1 or not isinstance(body[0], ast.Return) or not isinstance(body[0].value, ast.Call):
|
|
216
|
+
return False
|
|
217
|
+
constructor = body[0].value.func
|
|
218
|
+
if isinstance(constructor, ast.Subscript):
|
|
219
|
+
constructor = constructor.value
|
|
220
|
+
root = _root_name(constructor)
|
|
221
|
+
return (
|
|
222
|
+
root is not None
|
|
223
|
+
and _scope_bindings(module.tree)[root] == 1
|
|
224
|
+
and not _root_mutated(module.tree, root)
|
|
225
|
+
and module.runtime_imports.resolved_qualified_name(constructor) in _POOL_CONSTRUCTORS
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _setup_call(context: PythonFileContext, function: Function, helpers: frozenset[str]) -> ast.Call | None:
|
|
230
|
+
if not function.name.startswith("test_") or any(
|
|
231
|
+
not _pytest_mark(context, decorator) for decorator in function.decorator_list
|
|
232
|
+
):
|
|
233
|
+
return None
|
|
234
|
+
bindings = _scope_bindings(function)
|
|
235
|
+
available = helpers - bindings.keys()
|
|
236
|
+
# Looking through nested scopes here conservatively excludes captures and callable escapes.
|
|
237
|
+
uses = [node for node in ast.walk(function) if isinstance(node, ast.Name) and node.id in available]
|
|
238
|
+
if len(uses) != 1:
|
|
239
|
+
return None
|
|
240
|
+
body = _executable_body(function)
|
|
241
|
+
if not body:
|
|
242
|
+
return None
|
|
243
|
+
first = body[0]
|
|
244
|
+
setup = _plain_call(first)
|
|
245
|
+
if setup is None:
|
|
246
|
+
return None
|
|
247
|
+
if _helper_call(setup, available):
|
|
248
|
+
return _assigned_dependency_call(context, body, bindings, setup)
|
|
249
|
+
if not _unshadowed_consumer(context, setup, bindings):
|
|
250
|
+
return None
|
|
251
|
+
inline = [arg for arg in _arguments(setup) if isinstance(arg, ast.Call) and _helper_call(arg, available)]
|
|
252
|
+
return inline[0] if len(inline) == 1 else None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _assigned_dependency_call(
|
|
256
|
+
context: PythonFileContext, body: list[ast.stmt], bindings: Counter[str], setup: ast.Call
|
|
257
|
+
) -> ast.Call | None:
|
|
258
|
+
resource = _assignment_name(body[0])
|
|
259
|
+
if resource is None or bindings[resource] != 1 or len(body) < _SETUP_AND_CONSUMER:
|
|
260
|
+
return None
|
|
261
|
+
consumer = _plain_call(body[1])
|
|
262
|
+
if consumer is None or not _unshadowed_consumer(context, consumer, bindings):
|
|
263
|
+
return None
|
|
264
|
+
for argument in _arguments(consumer):
|
|
265
|
+
if isinstance(argument, ast.Name) and argument.id == resource:
|
|
266
|
+
return setup
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _pytest_mark(context: PythonFileContext, decorator: ast.expr) -> bool:
|
|
271
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
272
|
+
qualified = context.module_imports.resolved_qualified_name(target)
|
|
273
|
+
tree = context.tree
|
|
274
|
+
root = _root_name(target)
|
|
275
|
+
if (
|
|
276
|
+
qualified is None
|
|
277
|
+
or not qualified.startswith("pytest.mark.")
|
|
278
|
+
or tree is None
|
|
279
|
+
or root is None
|
|
280
|
+
or _scope_bindings(tree)[root] != 1
|
|
281
|
+
):
|
|
282
|
+
return False
|
|
283
|
+
for node in ast.walk(tree):
|
|
284
|
+
if not isinstance(node, (ast.Attribute, ast.Subscript)) or not isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
285
|
+
continue
|
|
286
|
+
if isinstance(node, ast.Subscript) and _root_name(node.value) == root:
|
|
287
|
+
return False
|
|
288
|
+
changed = context.module_imports.resolved_qualified_name(node)
|
|
289
|
+
if changed is not None and (qualified == changed or qualified.startswith(f"{changed}.")):
|
|
290
|
+
return False
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _unshadowed_consumer(context: PythonFileContext, call: ast.Call, bindings: Counter[str]) -> bool:
|
|
295
|
+
root = _root_name(call.func)
|
|
296
|
+
tree = context.tree
|
|
297
|
+
return (
|
|
298
|
+
root is not None
|
|
299
|
+
and root not in bindings
|
|
300
|
+
and tree is not None
|
|
301
|
+
and _scope_bindings(tree)[root] <= 1
|
|
302
|
+
and not _root_mutated(tree, root)
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _module_reference(tree: ast.Module, name: str) -> bool:
|
|
307
|
+
return any(
|
|
308
|
+
isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)
|
|
309
|
+
for statement in tree.body
|
|
310
|
+
if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
311
|
+
for node in ast.walk(statement)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _helper_call(call: ast.Call, helpers: frozenset[str] | set[str]) -> bool:
|
|
316
|
+
return isinstance(call.func, ast.Name) and call.func.id in helpers and not call.args and not call.keywords
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _arguments(call: ast.Call) -> Iterator[ast.expr]:
|
|
320
|
+
yield from call.args
|
|
321
|
+
yield from (keyword.value for keyword in call.keywords if keyword.arg is not None)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _plain_call(statement: ast.stmt) -> ast.Call | None:
|
|
325
|
+
if (
|
|
326
|
+
isinstance(statement, (ast.Expr, ast.Assign, ast.AnnAssign))
|
|
327
|
+
and isinstance(statement.value, ast.Call)
|
|
328
|
+
and isinstance(statement.value.func, (ast.Name, ast.Attribute))
|
|
329
|
+
):
|
|
330
|
+
return statement.value
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _assignment_name(statement: ast.stmt) -> str | None:
|
|
335
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):
|
|
336
|
+
return statement.targets[0].id
|
|
337
|
+
if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
|
|
338
|
+
return statement.target.id
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _root_name(expression: ast.expr) -> str | None:
|
|
343
|
+
while isinstance(expression, ast.Attribute):
|
|
344
|
+
expression = expression.value
|
|
345
|
+
return expression.id if isinstance(expression, ast.Name) else None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _executable_body(function: Function) -> list[ast.stmt]:
|
|
349
|
+
body = function.body
|
|
350
|
+
if (
|
|
351
|
+
body
|
|
352
|
+
and isinstance(body[0], ast.Expr)
|
|
353
|
+
and isinstance(body[0].value, ast.Constant)
|
|
354
|
+
and isinstance(body[0].value.value, str)
|
|
355
|
+
):
|
|
356
|
+
return body[1:]
|
|
357
|
+
return body
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _scope_bindings(scope: ast.Module | Function) -> Counter[str]:
|
|
361
|
+
names: Counter[str] = Counter()
|
|
362
|
+
if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
363
|
+
arguments = scope.args
|
|
364
|
+
names.update(arg.arg for arg in (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs))
|
|
365
|
+
names.update(arg.arg for arg in (arguments.vararg, arguments.kwarg) if arg is not None)
|
|
366
|
+
for statement in scope.body:
|
|
367
|
+
_collect_bindings(statement, names)
|
|
368
|
+
if isinstance(scope, ast.Module):
|
|
369
|
+
names.update(_global_writes(scope))
|
|
370
|
+
return names
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _global_writes(tree: ast.Module) -> Counter[str]:
|
|
374
|
+
if not any(isinstance(node, ast.Global) for node in ast.walk(tree)):
|
|
375
|
+
return Counter()
|
|
376
|
+
writes: Counter[str] = Counter()
|
|
377
|
+
for node in ast.walk(tree):
|
|
378
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
379
|
+
continue
|
|
380
|
+
names: Counter[str] = Counter()
|
|
381
|
+
declarations: set[str] = set()
|
|
382
|
+
for statement in node.body:
|
|
383
|
+
_collect_bindings(statement, names)
|
|
384
|
+
declarations.update(_global_names(statement))
|
|
385
|
+
writes.update(dict.fromkeys(declarations & names.keys(), 2))
|
|
386
|
+
return writes
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _global_names(node: ast.AST) -> Iterator[str]:
|
|
390
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
391
|
+
return
|
|
392
|
+
if isinstance(node, ast.Global):
|
|
393
|
+
yield from node.names
|
|
394
|
+
for child in ast.iter_child_nodes(node):
|
|
395
|
+
yield from _global_names(child)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _collect_bindings(node: ast.AST, names: Counter[str]) -> None:
|
|
399
|
+
match node:
|
|
400
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
|
|
401
|
+
names[name] += 1
|
|
402
|
+
for expression in _definition_expressions(node):
|
|
403
|
+
_collect_bindings(expression, names)
|
|
404
|
+
return
|
|
405
|
+
case ast.Lambda():
|
|
406
|
+
for expression in _definition_expressions(node):
|
|
407
|
+
_collect_bindings(expression, names)
|
|
408
|
+
return
|
|
409
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
410
|
+
names.update(alias.asname or alias.name.partition(".")[0] for alias in aliases)
|
|
411
|
+
case (
|
|
412
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
413
|
+
| ast.ExceptHandler(name=str(name))
|
|
414
|
+
| ast.MatchAs(name=str(name))
|
|
415
|
+
| ast.MatchStar(name=str(name))
|
|
416
|
+
| ast.MatchMapping(rest=str(name))
|
|
417
|
+
):
|
|
418
|
+
names[name] += 1
|
|
419
|
+
case ast.comprehension():
|
|
420
|
+
_collect_bindings(node.iter, names)
|
|
421
|
+
for condition in node.ifs:
|
|
422
|
+
_collect_bindings(condition, names)
|
|
423
|
+
return
|
|
424
|
+
case _:
|
|
425
|
+
pass
|
|
426
|
+
for child in ast.iter_child_nodes(node):
|
|
427
|
+
_collect_bindings(child, names)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _definition_expressions(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) -> list[ast.AST]:
|
|
431
|
+
if isinstance(node, ast.ClassDef):
|
|
432
|
+
return [*node.decorator_list, *node.bases, *node.keywords]
|
|
433
|
+
defaults: list[ast.AST] = [
|
|
434
|
+
*node.args.defaults,
|
|
435
|
+
*(default for default in node.args.kw_defaults if default is not None),
|
|
436
|
+
]
|
|
437
|
+
return defaults if isinstance(node, ast.Lambda) else [*node.decorator_list, *defaults]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _has_wildcard(tree: ast.Module) -> bool:
|
|
441
|
+
return any(
|
|
442
|
+
isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names) for node in ast.walk(tree)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _root_mutated(tree: ast.Module, root: str) -> bool:
|
|
447
|
+
return any(
|
|
448
|
+
isinstance(node, (ast.Attribute, ast.Subscript))
|
|
449
|
+
and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
450
|
+
and _root_name(node.value) == root
|
|
451
|
+
for node in ast.walk(tree)
|
|
452
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.14.4 → sarj_python_lint-1.15.1}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|