sarj-python-lint 1.3.0__tar.gz → 1.3.3__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.3.0 → sarj_python_lint-1.3.3}/PKG-INFO +1 -1
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/pyproject.toml +1 -1
- sarj_python_lint-1.3.3/src/sarj_python_lint/rules/_set_fields.py +110 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +26 -8
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +71 -4
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +27 -9
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +2 -2
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/repeated_test_composition.py +18 -11
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +109 -5
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +24 -4
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/.gitignore +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/LICENSE +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/README.md +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.3.0 → sarj_python_lint-1.3.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
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/
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
7
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
12
|
+
from sarj_python_lint.rules._project_index import ClassSummary, ProjectIndexSet, SourceUnit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def declared_set_fields(
|
|
16
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef, project: ProjectIndexSet, context: PythonFileContext
|
|
17
|
+
) -> set[str]:
|
|
18
|
+
changed = _changed_roots(function)
|
|
19
|
+
fields: set[str] = set()
|
|
20
|
+
for argument in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
|
|
21
|
+
if argument.arg in changed or argument.annotation is None:
|
|
22
|
+
continue
|
|
23
|
+
summary = _field_class(argument.annotation, project, context)
|
|
24
|
+
if summary is None:
|
|
25
|
+
continue
|
|
26
|
+
owner = project.source_unit(summary.symbol.module)
|
|
27
|
+
if owner is None or owner.tree is None:
|
|
28
|
+
continue
|
|
29
|
+
for name, annotation in summary.fields.items():
|
|
30
|
+
if _builtin_set_annotation(annotation, owner) and _plain_field(owner, summary.symbol.name, name):
|
|
31
|
+
fields.add(f"{argument.arg}.{name}")
|
|
32
|
+
return fields
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _field_class(annotation: ast.expr, project: ProjectIndexSet, context: PythonFileContext) -> ClassSummary | None:
|
|
36
|
+
root = annotation
|
|
37
|
+
while isinstance(root, ast.Attribute):
|
|
38
|
+
root = root.value
|
|
39
|
+
if not isinstance(root, ast.Name) or any(
|
|
40
|
+
node.id == root.id and isinstance(node.ctx, (ast.Store, ast.Del)) for node in context.nodes(ast.Name)
|
|
41
|
+
):
|
|
42
|
+
return None
|
|
43
|
+
qualified = context.module_imports.resolved_qualified_name(annotation)
|
|
44
|
+
unit = project.unit(context.path)
|
|
45
|
+
if qualified is not None:
|
|
46
|
+
module, _, name = qualified.rpartition(".")
|
|
47
|
+
unit = project.source_unit(module)
|
|
48
|
+
annotation = ast.Name(id=name, ctx=ast.Load())
|
|
49
|
+
return project.class_for(unit, annotation) if unit is not None else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _builtin_set_annotation(annotation: ast.expr, unit: SourceUnit) -> bool:
|
|
53
|
+
if unit.tree is None:
|
|
54
|
+
return False
|
|
55
|
+
base = annotation.value if isinstance(annotation, ast.Subscript) else annotation
|
|
56
|
+
return (
|
|
57
|
+
isinstance(base, ast.Name)
|
|
58
|
+
and base.id in {"set", "frozenset"}
|
|
59
|
+
and ImportIndex.from_tree(unit.tree, module_scope_only=True).builtin_is_unshadowed(base.id)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _plain_field(unit: SourceUnit, class_name: str, name: str) -> bool:
|
|
64
|
+
if unit.tree is None:
|
|
65
|
+
return False
|
|
66
|
+
owner = next((node for node in unit.tree.body if isinstance(node, ast.ClassDef) and node.name == class_name), None)
|
|
67
|
+
if owner is None:
|
|
68
|
+
return False
|
|
69
|
+
for statement in owner.body:
|
|
70
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)) and statement.name in {
|
|
71
|
+
name,
|
|
72
|
+
"__getattribute__",
|
|
73
|
+
"__getattr__",
|
|
74
|
+
}:
|
|
75
|
+
return False
|
|
76
|
+
if isinstance(statement, ast.Assign) and any(
|
|
77
|
+
isinstance(target, ast.Name) and target.id in {name, "set", "frozenset"} for target in statement.targets
|
|
78
|
+
):
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _changed_roots(function: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
84
|
+
changed: set[str] = set()
|
|
85
|
+
for node in (node for statement in function.body for node in walk_ast(statement)):
|
|
86
|
+
match node:
|
|
87
|
+
case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
|
|
88
|
+
changed.add(name)
|
|
89
|
+
case ast.Attribute(ctx=ast.Store() | ast.Del()):
|
|
90
|
+
root = node.value
|
|
91
|
+
while isinstance(root, ast.Attribute):
|
|
92
|
+
root = root.value
|
|
93
|
+
if isinstance(root, ast.Name):
|
|
94
|
+
changed.add(root.id)
|
|
95
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
96
|
+
changed.update(alias.asname or alias.name.split(".")[0] for alias in aliases)
|
|
97
|
+
case (
|
|
98
|
+
ast.ExceptHandler(name=str(name))
|
|
99
|
+
| ast.MatchAs(name=str(name))
|
|
100
|
+
| ast.MatchStar(name=str(name))
|
|
101
|
+
| ast.MatchMapping(rest=str(name))
|
|
102
|
+
| ast.arg(arg=name)
|
|
103
|
+
| ast.FunctionDef(name=name)
|
|
104
|
+
| ast.AsyncFunctionDef(name=name)
|
|
105
|
+
| ast.ClassDef(name=name)
|
|
106
|
+
):
|
|
107
|
+
changed.add(name)
|
|
108
|
+
case _:
|
|
109
|
+
pass
|
|
110
|
+
return changed
|
|
@@ -91,7 +91,7 @@ class NoHiddenConstructorFallback(Rule):
|
|
|
91
91
|
remediation="Require the constructor argument and resolve any default at the composition root or call site.",
|
|
92
92
|
category=RuleCategory.ARCHITECTURE,
|
|
93
93
|
limitations=(
|
|
94
|
-
"Detection requires a proven local settings provider
|
|
94
|
+
"Detection requires a proven local settings provider and a first-party composition call; covers None fallbacks and settings attributes captured directly in module-level class constructor defaults.",
|
|
95
95
|
"Tests, generated files, migrations, descriptors, library environment fallbacks, and unconstructed classes are excluded.",
|
|
96
96
|
),
|
|
97
97
|
examples=(
|
|
@@ -186,7 +186,8 @@ class NoHiddenConstructorFallback(Rule):
|
|
|
186
186
|
)
|
|
187
187
|
if init is None or _is_descriptor(init):
|
|
188
188
|
continue
|
|
189
|
-
|
|
189
|
+
default_shadowed = _default_scope(class_node, init) if class_node in tree.body else None
|
|
190
|
+
hidden = _hidden_parameters(init, resolver, default_shadowed)
|
|
190
191
|
if not hidden or not _has_composition_call(path, class_node.name, first_party, facts):
|
|
191
192
|
continue
|
|
192
193
|
diagnostics.append(
|
|
@@ -208,23 +209,38 @@ def _is_descriptor(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
|
208
209
|
return any(_tail(decorator) in _DESCRIPTOR_DECORATORS for decorator in node.decorator_list)
|
|
209
210
|
|
|
210
211
|
|
|
212
|
+
def _default_scope(owner: ast.ClassDef, init: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
213
|
+
collector = _LocalBindingCollector()
|
|
214
|
+
for statement in owner.body:
|
|
215
|
+
if statement is init:
|
|
216
|
+
break
|
|
217
|
+
collector.visit(statement)
|
|
218
|
+
return collector.names
|
|
219
|
+
|
|
220
|
+
|
|
211
221
|
def _hidden_parameters(
|
|
212
222
|
init: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
213
223
|
resolver: _RuntimeConfigResolver,
|
|
224
|
+
default_shadowed: set[str] | None = None,
|
|
214
225
|
) -> list[_HiddenParameter]:
|
|
215
226
|
positional = (*init.args.posonlyargs, *init.args.args)
|
|
216
227
|
defaulted_positional = positional[-len(init.args.defaults) :] if init.args.defaults else ()
|
|
217
228
|
positional_defaults = zip(defaulted_positional, init.args.defaults, strict=True)
|
|
229
|
+
defaults = (*positional_defaults, *zip(init.args.kwonlyargs, init.args.kw_defaults, strict=True))
|
|
230
|
+
captured = [
|
|
231
|
+
_HiddenParameter(parameter, uses_boolean_or=False)
|
|
232
|
+
for parameter, default in defaults
|
|
233
|
+
if default is not None
|
|
234
|
+
and default_shadowed is not None
|
|
235
|
+
and resolver.is_runtime_config(default, default_shadowed)
|
|
236
|
+
]
|
|
218
237
|
candidates = {
|
|
219
238
|
parameter.arg: parameter
|
|
220
|
-
for parameter, default in
|
|
221
|
-
*positional_defaults,
|
|
222
|
-
*zip(init.args.kwonlyargs, init.args.kw_defaults, strict=True),
|
|
223
|
-
)
|
|
239
|
+
for parameter, default in defaults
|
|
224
240
|
if isinstance(default, ast.Constant) and default.value is None
|
|
225
241
|
}
|
|
226
242
|
if not candidates:
|
|
227
|
-
return
|
|
243
|
+
return captured
|
|
228
244
|
|
|
229
245
|
hidden: dict[str, bool] = {}
|
|
230
246
|
rebound: set[str] = set()
|
|
@@ -239,7 +255,9 @@ def _hidden_parameters(
|
|
|
239
255
|
hidden[name] = hidden.get(name, False) or uses_boolean_or
|
|
240
256
|
rebound.update(_directly_bound_names(statement) & candidates.keys())
|
|
241
257
|
shadowed.update(_directly_bound_names(statement))
|
|
242
|
-
return
|
|
258
|
+
return captured + [
|
|
259
|
+
_HiddenParameter(parameter, hidden[name]) for name, parameter in candidates.items() if name in hidden
|
|
260
|
+
]
|
|
243
261
|
|
|
244
262
|
|
|
245
263
|
def _statement_fallbacks(
|
|
@@ -57,7 +57,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
|
|
|
57
57
|
autofix=AutofixPolicy.NONE,
|
|
58
58
|
limitations=(
|
|
59
59
|
"Only maintained test paths are analyzed; generated files and production helpers are excluded.",
|
|
60
|
-
"The rule recognizes pytest monkeypatch and statically resolved unittest.mock or pytest-mock patch APIs.",
|
|
60
|
+
"The rule recognizes pytest monkeypatch fixtures, unambiguous local MonkeyPatch constructors and context handles, and statically resolved unittest.mock or pytest-mock patch APIs.",
|
|
61
61
|
"Environment, mapping, import-path, and working-directory mutations are intentionally allowed.",
|
|
62
62
|
"Untyped handles are recognized only on pytest tests or fixtures; directly parametrized values are excluded.",
|
|
63
63
|
"A nested function that captures a monkeypatch handle from an outer scope is not inferred.",
|
|
@@ -109,10 +109,13 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
|
|
|
109
109
|
return []
|
|
110
110
|
imports = context.module_imports
|
|
111
111
|
parents = parent_map(tree, index=context.node_index)
|
|
112
|
+
has_factory = "MonkeyPatch" in context.source and any(
|
|
113
|
+
_creates_monkeypatch(call, imports) for call in context.nodes(ast.Call)
|
|
114
|
+
)
|
|
112
115
|
replacements = [
|
|
113
116
|
(call, f"monkeypatch.{_operation(call)}")
|
|
114
117
|
for function in _functions(tree, node_index=context.node_index)
|
|
115
|
-
for call in _attribute_mutations(function, imports, parents)
|
|
118
|
+
for call in _attribute_mutations(function, imports, parents, has_factory=has_factory)
|
|
116
119
|
]
|
|
117
120
|
replacements.extend((call, label) for call, label in _patch_calls(tree, imports, node_index=context.node_index))
|
|
118
121
|
diagnostics = [
|
|
@@ -264,9 +267,11 @@ def _attribute_mutations(
|
|
|
264
267
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
265
268
|
imports: ImportIndex,
|
|
266
269
|
parents: Mapping[ast.AST, ast.AST],
|
|
270
|
+
*,
|
|
271
|
+
has_factory: bool,
|
|
267
272
|
) -> list[ast.Call]:
|
|
268
273
|
parameters = _parameter_handles(function, imports, parents)
|
|
269
|
-
if not parameters:
|
|
274
|
+
if not parameters and not has_factory:
|
|
270
275
|
return []
|
|
271
276
|
nodes = tuple(_lexical_body_nodes(function))
|
|
272
277
|
handles = set(parameters)
|
|
@@ -278,11 +283,73 @@ def _attribute_mutations(
|
|
|
278
283
|
and isinstance(node.func, ast.Attribute)
|
|
279
284
|
and node.func.attr in _ATTRIBUTE_MUTATIONS
|
|
280
285
|
and isinstance(node.func.value, ast.Name)
|
|
281
|
-
and
|
|
286
|
+
and (
|
|
287
|
+
node.func.value.id in handles
|
|
288
|
+
or _constructed_reference(
|
|
289
|
+
node.func.value.id, node, function, nodes=nodes, imports=imports, parents=parents, seen=frozenset()
|
|
290
|
+
)
|
|
291
|
+
)
|
|
282
292
|
and not _parameter_rebound_before(node, node.func.value.id, nodes, parameters)
|
|
283
293
|
]
|
|
284
294
|
|
|
285
295
|
|
|
296
|
+
def _creates_monkeypatch(call: ast.Call, imports: ImportIndex) -> bool:
|
|
297
|
+
callee = call.func
|
|
298
|
+
if isinstance(callee, ast.Attribute) and callee.attr == "context":
|
|
299
|
+
callee = callee.value
|
|
300
|
+
return imports.resolves(callee, sources=_PYTEST, symbol="MonkeyPatch")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _constructed_reference(
|
|
304
|
+
name: str,
|
|
305
|
+
use: ast.AST,
|
|
306
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
307
|
+
*,
|
|
308
|
+
nodes: tuple[ast.AST, ...],
|
|
309
|
+
imports: ImportIndex,
|
|
310
|
+
parents: Mapping[ast.AST, ast.AST],
|
|
311
|
+
seen: frozenset[str],
|
|
312
|
+
) -> bool:
|
|
313
|
+
if name in seen or name in {arg.arg for arg in _arguments(function.args)}:
|
|
314
|
+
return False
|
|
315
|
+
definitions = [node for node in nodes if name in _bound_targets(node)]
|
|
316
|
+
if len(definitions) != 1 or _position(definitions[0]) >= _position(use):
|
|
317
|
+
return False
|
|
318
|
+
definition = definitions[0]
|
|
319
|
+
value: ast.expr | None = None
|
|
320
|
+
if isinstance(definition, (ast.Assign, ast.AnnAssign)) and parents.get(definition) is function:
|
|
321
|
+
value = definition.value
|
|
322
|
+
elif isinstance(definition, ast.withitem) and _within(use, parents.get(definition), parents):
|
|
323
|
+
expression = definition.context_expr
|
|
324
|
+
if (
|
|
325
|
+
isinstance(expression, ast.Call)
|
|
326
|
+
and isinstance(expression.func, ast.Attribute)
|
|
327
|
+
and expression.func.attr == "context"
|
|
328
|
+
):
|
|
329
|
+
value = expression.func.value
|
|
330
|
+
if imports.resolves(value, sources=_PYTEST, symbol="MonkeyPatch"):
|
|
331
|
+
value = ast.Call(func=value, args=[], keywords=[])
|
|
332
|
+
if isinstance(value, ast.Name):
|
|
333
|
+
return _constructed_reference(
|
|
334
|
+
value.id, definition, function, nodes=nodes, imports=imports, parents=parents, seen=seen | {name}
|
|
335
|
+
)
|
|
336
|
+
return (
|
|
337
|
+
isinstance(value, ast.Call)
|
|
338
|
+
and imports.resolves(value.func, sources=_PYTEST, symbol="MonkeyPatch")
|
|
339
|
+
and not _import_shadowed(value.func, parents, {})
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _within(node: ast.AST, ancestor: ast.AST | None, parents: Mapping[ast.AST, ast.AST]) -> bool:
|
|
344
|
+
if ancestor is None:
|
|
345
|
+
return False
|
|
346
|
+
while (parent := parents.get(node)) is not None:
|
|
347
|
+
if parent is ancestor:
|
|
348
|
+
return True
|
|
349
|
+
node = parent
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
286
353
|
def _parameter_handles(
|
|
287
354
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
288
355
|
imports: ImportIndex,
|
|
@@ -9,7 +9,7 @@ from sarj_python_lint.rule_base import (
|
|
|
9
9
|
Diagnostic,
|
|
10
10
|
ExampleFile,
|
|
11
11
|
ExampleOutcome,
|
|
12
|
-
|
|
12
|
+
ProjectRule,
|
|
13
13
|
RuleCategory,
|
|
14
14
|
RuleDocumentation,
|
|
15
15
|
RuleExample,
|
|
@@ -17,6 +17,8 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
is_suppressed,
|
|
18
18
|
)
|
|
19
19
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
20
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
21
|
+
from sarj_python_lint.rules._set_fields import declared_set_fields
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
if TYPE_CHECKING:
|
|
@@ -45,7 +47,7 @@ _TRACKED_BUILTINS = frozenset(
|
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
@final
|
|
48
|
-
class PreferSetIsdisjoint(
|
|
50
|
+
class PreferSetIsdisjoint(ProjectRule):
|
|
49
51
|
id = "prefer-set-isdisjoint"
|
|
50
52
|
code = "SARJ431"
|
|
51
53
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
@@ -56,19 +58,19 @@ class PreferSetIsdisjoint(Rule):
|
|
|
56
58
|
category=RuleCategory.STYLE,
|
|
57
59
|
autofix=AutofixPolicy.SUGGESTION,
|
|
58
60
|
limitations=(
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
+
"Set receivers must come from a literal, comprehension, constructor, dominating local assignment, or an unchanged parameter's directly declared first-party set/frozenset field.",
|
|
62
|
+
"Declared fields rely on the annotated contract. Unknown or union field types, properties, explicit owner/field reassignment, branch-merged bindings, stored intersections, and generated files are excluded. The other operand must be a known iterable.",
|
|
61
63
|
"The suggestion is intentionally not an autofix because short-circuiting may make custom element equality or hashing side effects observable.",
|
|
62
64
|
),
|
|
63
65
|
examples=(
|
|
64
66
|
RuleExample(
|
|
65
67
|
example_id="discarded-intersection",
|
|
66
|
-
title="
|
|
68
|
+
title="A declared set field is used only for an overlap test",
|
|
67
69
|
outcome=ExampleOutcome.MATCH,
|
|
68
70
|
files=(
|
|
69
71
|
ExampleFile.python(
|
|
70
72
|
"app/policy.py",
|
|
71
|
-
"
|
|
73
|
+
"class AccessCase:\n tags: frozenset[str]\n\ndef accepts(case: AccessCase):\n if case.tags & {'read', 'write'}:\n allow()\n",
|
|
72
74
|
),
|
|
73
75
|
),
|
|
74
76
|
focus_path=PurePosixPath("app/policy.py"),
|
|
@@ -102,7 +104,12 @@ class PreferSetIsdisjoint(Rule):
|
|
|
102
104
|
tree = context.tree
|
|
103
105
|
if tree is None:
|
|
104
106
|
return []
|
|
105
|
-
|
|
107
|
+
project = context.session.project or ProjectIndexSet.single(path, source)
|
|
108
|
+
fields = {
|
|
109
|
+
node: declared_set_fields(node, project, context)
|
|
110
|
+
for node in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef)
|
|
111
|
+
}
|
|
112
|
+
scanner = _Scanner(path, context.source_lines, _shadowed_builtins(tree, node_index=context.node_index), fields)
|
|
106
113
|
scanner.scan_body(tree.body, set())
|
|
107
114
|
scanner.diagnostics.sort(key=lambda item: (item.line, item.col))
|
|
108
115
|
return scanner.diagnostics
|
|
@@ -110,12 +117,19 @@ class PreferSetIsdisjoint(Rule):
|
|
|
110
117
|
|
|
111
118
|
@final
|
|
112
119
|
class _Scanner:
|
|
113
|
-
def __init__(
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
path: Path,
|
|
123
|
+
source_lines: list[str],
|
|
124
|
+
shadowed: frozenset[str],
|
|
125
|
+
fields: dict[ast.FunctionDef | ast.AsyncFunctionDef, set[str]],
|
|
126
|
+
) -> None:
|
|
114
127
|
self.path = path
|
|
115
128
|
self.source_lines = source_lines
|
|
116
129
|
self.shadowed = shadowed
|
|
117
130
|
self.diagnostics: list[Diagnostic] = []
|
|
118
131
|
self.reported: set[int] = set()
|
|
132
|
+
self.fields = fields
|
|
119
133
|
|
|
120
134
|
def scan_body(self, body: list[ast.stmt], exact: set[str]) -> None:
|
|
121
135
|
local = set(exact)
|
|
@@ -139,7 +153,9 @@ class _Scanner:
|
|
|
139
153
|
case ast.Assert(test=test):
|
|
140
154
|
self._scan_boolean(test, exact)
|
|
141
155
|
self._scan_embedded(test, exact)
|
|
142
|
-
case ast.FunctionDef() | ast.AsyncFunctionDef()
|
|
156
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
157
|
+
self.scan_body(statement.body, self.fields.get(statement, set()))
|
|
158
|
+
case ast.ClassDef():
|
|
143
159
|
self.scan_body(statement.body, set())
|
|
144
160
|
case ast.For() | ast.AsyncFor():
|
|
145
161
|
self._scan_embedded(statement.iter, exact)
|
|
@@ -292,6 +308,8 @@ def _is_builtin_bool_call(node: ast.expr, shadowed: frozenset[str]) -> bool:
|
|
|
292
308
|
|
|
293
309
|
|
|
294
310
|
def _is_exact_set(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
|
|
311
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
|
|
312
|
+
return f"{node.value.id}.{node.attr}" in exact
|
|
295
313
|
if isinstance(node, ast.Set | ast.SetComp):
|
|
296
314
|
return True
|
|
297
315
|
if isinstance(node, ast.Name):
|
|
@@ -272,11 +272,11 @@ class _NameUsage(ast.NodeVisitor):
|
|
|
272
272
|
|
|
273
273
|
@override
|
|
274
274
|
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
275
|
-
|
|
275
|
+
"""Skip the deferred lambda body when tracking immediate name usage."""
|
|
276
276
|
|
|
277
277
|
@override
|
|
278
278
|
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
|
|
279
|
-
|
|
279
|
+
"""Skip the deferred generator body when tracking immediate name usage."""
|
|
280
280
|
|
|
281
281
|
def visit_Import(self, node: ast.Import) -> None:
|
|
282
282
|
self.rebound |= any((alias.asname or alias.name.split(".", maxsplit=1)[0]) == self.name for alias in node.names)
|
|
@@ -61,7 +61,7 @@ class RepeatedTestComposition(ProjectRule):
|
|
|
61
61
|
"Requires three sibling tests with equivalent wiring of at least two stored and invoked first-party ABC/Protocol dependencies.",
|
|
62
62
|
"Collaborators must resolve to equivalent fixture parameters or first-party constructor expressions, optionally through unambiguous local aliases.",
|
|
63
63
|
"Requires an unconditional construction followed by a subject method call; dynamic arguments, parametrized tests, constructor validation, multiple instances, mutated dependencies, fixtures, helpers, generated files, and inherited test classes are excluded.",
|
|
64
|
-
"HTTPX detection requires a managed AsyncClient with an inline ASGITransport and equivalent options except its app.",
|
|
64
|
+
"HTTPX detection requires a managed AsyncClient with an inline or unchanged local ASGITransport and equivalent options except its app; unambiguous constructor aliases are accepted.",
|
|
65
65
|
),
|
|
66
66
|
examples=(
|
|
67
67
|
RuleExample(
|
|
@@ -159,8 +159,11 @@ def _candidate(
|
|
|
159
159
|
callee = unalias(node.func, aliases)
|
|
160
160
|
if shadowed(callee, available.parameters | available.blocked):
|
|
161
161
|
return None
|
|
162
|
-
if context.imports.resolves(
|
|
163
|
-
|
|
162
|
+
if context.imports.resolves(callee, sources=_HTTPX, symbol="AsyncClient"):
|
|
163
|
+
blocked = available.parameters | available.blocked
|
|
164
|
+
changed = changed_before(function, node, aliases)
|
|
165
|
+
stable = {name: value for name, value in aliases.items() if name not in changed}
|
|
166
|
+
key = _client_key(context, node, stable, blocked) if _unconditional(context, node, function) else None
|
|
164
167
|
return "httpx.AsyncClient", key
|
|
165
168
|
spec = constructor(context, callee, project)
|
|
166
169
|
if spec is None:
|
|
@@ -238,9 +241,9 @@ def _subject_called(context: PythonFileContext, function: Function, call: ast.Ca
|
|
|
238
241
|
)
|
|
239
242
|
|
|
240
243
|
|
|
241
|
-
def _client_key(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
def _client_key(
|
|
245
|
+
context: PythonFileContext, call: ast.Call, aliases: dict[str, ast.expr], blocked: frozenset[str]
|
|
246
|
+
) -> str | None:
|
|
244
247
|
parent = context.parents.get(call)
|
|
245
248
|
if (
|
|
246
249
|
not isinstance(parent, ast.withitem)
|
|
@@ -259,7 +262,7 @@ def _client_key(context: PythonFileContext, call: ast.Call) -> str | None:
|
|
|
259
262
|
return None
|
|
260
263
|
options.append((keyword.arg, ast.dump(keyword.value)))
|
|
261
264
|
continue
|
|
262
|
-
transport_options = _transport_options(context, keyword.value)
|
|
265
|
+
transport_options = _transport_options(context, unalias(keyword.value, aliases), aliases, blocked)
|
|
263
266
|
if transport_options is None:
|
|
264
267
|
return None
|
|
265
268
|
found_transport = True
|
|
@@ -267,10 +270,14 @@ def _client_key(context: PythonFileContext, call: ast.Call) -> str | None:
|
|
|
267
270
|
return repr(sorted(options)) if found_transport else None
|
|
268
271
|
|
|
269
272
|
|
|
270
|
-
def _transport_options(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
):
|
|
273
|
+
def _transport_options(
|
|
274
|
+
context: PythonFileContext, transport: ast.expr, aliases: dict[str, ast.expr], blocked: frozenset[str]
|
|
275
|
+
) -> list[tuple[str, str]] | None:
|
|
276
|
+
if not isinstance(transport, ast.Call):
|
|
277
|
+
return None
|
|
278
|
+
earlier = {name: value for name, value in aliases.items() if value.lineno < transport.lineno}
|
|
279
|
+
callee = unalias(transport.func, earlier)
|
|
280
|
+
if shadowed(callee, blocked) or not context.imports.resolves(callee, sources=_HTTPX, symbol="ASGITransport"):
|
|
274
281
|
return None
|
|
275
282
|
if transport.args or any(option.arg is None for option in transport.keywords):
|
|
276
283
|
return None
|