sarj-python-lint 1.22.1__tar.gz → 1.24.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.22.1 → sarj_python_lint-1.24.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +27 -14
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +103 -14
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +95 -4
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/.gitignore +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/LICENSE +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/README.md +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/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.24.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/
|
|
@@ -133,18 +133,8 @@ class NoPsycopgExecutionOutsideInjectedOwner(Rule):
|
|
|
133
133
|
or _is_operational_path(path)
|
|
134
134
|
):
|
|
135
135
|
return []
|
|
136
|
-
tree = context.tree
|
|
137
|
-
if not isinstance(tree, ast.Module):
|
|
138
|
-
return []
|
|
139
|
-
types = _type_index(tree)
|
|
140
|
-
ownership = {
|
|
141
|
-
node: _class_ownership(node, types) for node in context.nodes(ast.AST) if isinstance(node, ast.ClassDef)
|
|
142
|
-
}
|
|
143
136
|
source_lines = context.source_lines
|
|
144
|
-
calls
|
|
145
|
-
for scope in _function_scopes(tree):
|
|
146
|
-
owned = ownership[scope.owner] if scope.owner is not None else _ClassOwnership(frozenset(), frozenset())
|
|
147
|
-
calls.extend(_FunctionAnalyzer(types, owned).analyze(scope.function))
|
|
137
|
+
calls = psycopg_execution_calls(context, include_injected=False, include_probes=False)
|
|
148
138
|
unique = {(call.lineno, call.col_offset): call for call in calls}
|
|
149
139
|
return [
|
|
150
140
|
Diagnostic(
|
|
@@ -163,6 +153,25 @@ class NoPsycopgExecutionOutsideInjectedOwner(Rule):
|
|
|
163
153
|
]
|
|
164
154
|
|
|
165
155
|
|
|
156
|
+
def psycopg_execution_calls(
|
|
157
|
+
context: PythonFileContext, *, include_injected: bool, include_probes: bool
|
|
158
|
+
) -> list[ast.Call]:
|
|
159
|
+
tree = context.tree
|
|
160
|
+
if tree is None:
|
|
161
|
+
return []
|
|
162
|
+
types = _type_index(tree)
|
|
163
|
+
ownership = {node: _class_ownership(node, types) for node in context.nodes(ast.ClassDef)}
|
|
164
|
+
calls: list[ast.Call] = []
|
|
165
|
+
for scope in _function_scopes(tree):
|
|
166
|
+
owned = ownership[scope.owner] if scope.owner is not None else _ClassOwnership(frozenset(), frozenset())
|
|
167
|
+
calls.extend(
|
|
168
|
+
_FunctionAnalyzer(types, owned, include_injected=include_injected, include_probes=include_probes).analyze(
|
|
169
|
+
scope.function
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return calls
|
|
173
|
+
|
|
174
|
+
|
|
166
175
|
def _type_index(tree: ast.Module) -> _TypeIndex:
|
|
167
176
|
counts = _module_binding_counts(tree)
|
|
168
177
|
aliases = _module_type_aliases(tree, counts)
|
|
@@ -416,9 +425,13 @@ def _function_scopes(tree: ast.Module) -> list[_FunctionScope]:
|
|
|
416
425
|
|
|
417
426
|
@final
|
|
418
427
|
class _FunctionAnalyzer:
|
|
419
|
-
def __init__(
|
|
428
|
+
def __init__(
|
|
429
|
+
self, types: _TypeIndex, ownership: _ClassOwnership, *, include_injected: bool, include_probes: bool
|
|
430
|
+
) -> None:
|
|
420
431
|
self._types = types
|
|
421
432
|
self._ownership = ownership
|
|
433
|
+
self._allowed_origins = frozenset(_Origin) if include_injected else frozenset({_Origin.EXTERNAL})
|
|
434
|
+
self._include_probes = include_probes
|
|
422
435
|
self._findings: list[ast.Call] = []
|
|
423
436
|
self._shadowed: set[str] = set()
|
|
424
437
|
|
|
@@ -578,8 +591,8 @@ class _FunctionAnalyzer:
|
|
|
578
591
|
if (
|
|
579
592
|
isinstance(node.func, ast.Attribute)
|
|
580
593
|
and node.func.attr in _EXECUTE_METHODS
|
|
581
|
-
and analyzer._expression_origin(node.func.value, state)
|
|
582
|
-
and not _is_connection_probe(node)
|
|
594
|
+
and analyzer._expression_origin(node.func.value, state) in analyzer._allowed_origins
|
|
595
|
+
and (analyzer._include_probes or not _is_connection_probe(node))
|
|
583
596
|
):
|
|
584
597
|
analyzer._findings.append(node)
|
|
585
598
|
self.generic_visit(node)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from pathlib import PurePosixPath
|
|
5
|
+
import re
|
|
5
6
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
7
|
|
|
7
8
|
from sarj_python_lint.rule_base import (
|
|
@@ -14,9 +15,12 @@ from sarj_python_lint.rule_base import (
|
|
|
14
15
|
RuleDocumentation,
|
|
15
16
|
RuleExample,
|
|
16
17
|
Severity,
|
|
18
|
+
is_suppressed,
|
|
17
19
|
)
|
|
18
20
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
19
21
|
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
|
+
from sarj_python_lint.rules._sql import sql_string_value, strip_sql_noise
|
|
23
|
+
from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import psycopg_execution_calls
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
if TYPE_CHECKING:
|
|
@@ -26,6 +30,17 @@ if TYPE_CHECKING:
|
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
_POOL_TYPES = frozenset({"AsyncConnectionPool", "ConnectionPool"})
|
|
33
|
+
_SQL_METHODS = frozenset(
|
|
34
|
+
{"execute", "executemany", "executescript", "exec_driver_sql", "fetch", "fetchrow", "fetchval", "query", "command"}
|
|
35
|
+
)
|
|
36
|
+
_QUERY_KEYWORDS = frozenset({"query", "sql", "statement", "command", "operation"})
|
|
37
|
+
_SQL_START = re.compile(
|
|
38
|
+
r"\A\s*(?:SELECT\b|WITH\s+\S+[\s\S]*?\bAS\s*\(|INSERT\s+INTO\b|"
|
|
39
|
+
r"UPDATE\s+\S+[\s\S]*?\bSET\b|DELETE\s+FROM\b|TRUNCATE\b|"
|
|
40
|
+
r"(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX|SCHEMA|VIEW|FUNCTION|TRIGGER)\b|"
|
|
41
|
+
r"LOCK\s+TABLE\b|SET\s+(?:LOCAL|TRANSACTION)\b)",
|
|
42
|
+
re.IGNORECASE,
|
|
43
|
+
)
|
|
29
44
|
|
|
30
45
|
|
|
31
46
|
@final
|
|
@@ -34,34 +49,35 @@ class NoRawConnectionInTests(Rule):
|
|
|
34
49
|
code = "SARJ429"
|
|
35
50
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
36
51
|
default_level=Severity.ERROR,
|
|
37
|
-
summary="Do not acquire raw database connections in tests.",
|
|
52
|
+
summary="Do not execute raw SQL or acquire raw database connections in tests.",
|
|
38
53
|
rationale=(
|
|
39
54
|
"Tests that reach through a pool couple assertions and setup to persistence internals, bypass the "
|
|
40
55
|
"application boundary, and duplicate transaction ownership."
|
|
41
56
|
),
|
|
42
57
|
remediation=(
|
|
43
|
-
"Exercise the owning store/service API
|
|
44
|
-
"suppression for
|
|
58
|
+
"Exercise the owning store/service API. Do not move SQL into a test-only probe to hide it. Use an exact "
|
|
59
|
+
"SARJ429 suppression for deliberate database bootstrap, migration or transaction fault testing."
|
|
45
60
|
),
|
|
46
61
|
category=RuleCategory.TESTING,
|
|
47
62
|
autofix=AutofixPolicy.NONE,
|
|
63
|
+
aliases=("no-raw-sql-in-tests",),
|
|
48
64
|
limitations=(
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
65
|
+
"Raw SQL execution is checked in tests, conftest.py, shared test-support helpers and migration tests; generated code is excluded. Standalone SQL examples and store/service calls are not execution.",
|
|
66
|
+
"Import-proven Psycopg connection/pool flows, including constructor-injected members, aliases and dynamic query arguments, are followed. SQL-shaped literals in execute/query/fetch-like calls are also checked across drivers, including concatenation, f-strings and import-proven SQL/text constructors.",
|
|
67
|
+
"Opaque non-Psycopg builders and interprocedural receiver flows are not inferred. Raw connection acquisition keeps its existing fixture/support exclusions when no SQL execution is found in that scope.",
|
|
52
68
|
),
|
|
53
69
|
examples=(
|
|
54
70
|
RuleExample(
|
|
55
|
-
example_id="test-
|
|
56
|
-
title="
|
|
71
|
+
example_id="test-probe-executes-sql",
|
|
72
|
+
title="An injected test probe still bypasses the owning store",
|
|
57
73
|
outcome=ExampleOutcome.MATCH,
|
|
58
74
|
files=(
|
|
59
75
|
ExampleFile.python(
|
|
60
|
-
"tests/
|
|
61
|
-
"
|
|
76
|
+
"tests/fixtures/probe.py",
|
|
77
|
+
"from psycopg_pool import AsyncConnectionPool\n\nclass Probe:\n def __init__(self, pool: AsyncConnectionPool):\n self._pool = pool\n\n async def seed(self):\n async with self._pool.connection() as conn:\n await conn.execute('INSERT INTO jobs (id) VALUES (%s)', (job_id,))\n",
|
|
62
78
|
),
|
|
63
79
|
),
|
|
64
|
-
focus_path=PurePosixPath("tests/
|
|
80
|
+
focus_path=PurePosixPath("tests/fixtures/probe.py"),
|
|
65
81
|
expected_count=1,
|
|
66
82
|
public=True,
|
|
67
83
|
),
|
|
@@ -86,6 +102,20 @@ class NoRawConnectionInTests(Rule):
|
|
|
86
102
|
@override
|
|
87
103
|
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
88
104
|
path = context.path
|
|
105
|
+
if context.generated or context.tree is None:
|
|
106
|
+
return []
|
|
107
|
+
sql_calls = _test_sql_calls(context)
|
|
108
|
+
diagnostics = [
|
|
109
|
+
Diagnostic(
|
|
110
|
+
path=path,
|
|
111
|
+
line=node.lineno,
|
|
112
|
+
col=node.col_offset + 1,
|
|
113
|
+
code=self.code,
|
|
114
|
+
message="test code executes raw SQL; use the owning store/service API or an exact SARJ429 exception for deliberate database-level testing",
|
|
115
|
+
)
|
|
116
|
+
for node in sql_calls
|
|
117
|
+
if not is_suppressed(context.source_lines, node.lineno, self.code)
|
|
118
|
+
]
|
|
89
119
|
excluded_path = any(
|
|
90
120
|
(
|
|
91
121
|
not is_test_path(path),
|
|
@@ -95,17 +125,19 @@ class NoRawConnectionInTests(Rule):
|
|
|
95
125
|
_is_migration_test(path),
|
|
96
126
|
)
|
|
97
127
|
)
|
|
98
|
-
if excluded_path
|
|
99
|
-
return
|
|
128
|
+
if excluded_path:
|
|
129
|
+
return sorted(diagnostics, key=lambda item: (item.line, item.col))
|
|
100
130
|
tree = context.tree
|
|
101
131
|
if tree is None:
|
|
102
132
|
return []
|
|
103
|
-
|
|
133
|
+
sql_scopes = {_containing_scope(context, node) for node in sql_calls}
|
|
104
134
|
scopes: list[ast.Module | ast.FunctionDef | ast.AsyncFunctionDef] = [
|
|
105
135
|
tree,
|
|
106
136
|
*(node for node in context.nodes(ast.AST) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))),
|
|
107
137
|
]
|
|
108
138
|
for scope in scopes:
|
|
139
|
+
if scope in sql_scopes:
|
|
140
|
+
continue
|
|
109
141
|
pool_names = _proven_pool_names(scope)
|
|
110
142
|
diagnostics.extend(
|
|
111
143
|
Diagnostic(
|
|
@@ -129,6 +161,63 @@ class NoRawConnectionInTests(Rule):
|
|
|
129
161
|
return sorted(diagnostics, key=lambda item: (item.line, item.col))
|
|
130
162
|
|
|
131
163
|
|
|
164
|
+
def _test_sql_calls(context: PythonFileContext) -> list[ast.Call]:
|
|
165
|
+
path = context.path
|
|
166
|
+
if not (is_test_path(path) or is_test_support_path(path) or "test_support" in path.parts):
|
|
167
|
+
return []
|
|
168
|
+
candidates = [
|
|
169
|
+
node
|
|
170
|
+
for node in context.nodes(ast.Call)
|
|
171
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr in _SQL_METHODS
|
|
172
|
+
]
|
|
173
|
+
if not candidates:
|
|
174
|
+
return []
|
|
175
|
+
calls: list[ast.Call] = []
|
|
176
|
+
for node in candidates:
|
|
177
|
+
value = _execution_sql(node, context)
|
|
178
|
+
if value is not None and _SQL_START.search(strip_sql_noise(value)):
|
|
179
|
+
calls.append(node)
|
|
180
|
+
if any(
|
|
181
|
+
isinstance(node.func, ast.Attribute) and node.func.attr in {"execute", "executemany"} and node not in calls
|
|
182
|
+
for node in candidates
|
|
183
|
+
):
|
|
184
|
+
calls.extend(psycopg_execution_calls(context, include_injected=True, include_probes=True))
|
|
185
|
+
unique = {(call.lineno, call.col_offset): call for call in calls}
|
|
186
|
+
return sorted(unique.values(), key=lambda item: (item.lineno, item.col_offset))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _execution_sql(call: ast.Call, context: PythonFileContext) -> str | None:
|
|
190
|
+
if call.args:
|
|
191
|
+
return _literal_sql(call.args[0], context)
|
|
192
|
+
for keyword in call.keywords:
|
|
193
|
+
if keyword.arg in _QUERY_KEYWORDS:
|
|
194
|
+
return _literal_sql(keyword.value, context)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _literal_sql(node: ast.expr | None, context: PythonFileContext) -> str | None:
|
|
199
|
+
if node is None:
|
|
200
|
+
return None
|
|
201
|
+
if isinstance(node, ast.Call):
|
|
202
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == "format":
|
|
203
|
+
return _literal_sql(node.func.value, context)
|
|
204
|
+
if node.args and (
|
|
205
|
+
context.imports.resolves(node.func, sources=frozenset({"psycopg.sql"}), symbol="SQL")
|
|
206
|
+
or context.imports.resolves(node.func, sources=frozenset({"sqlalchemy"}), symbol="text")
|
|
207
|
+
):
|
|
208
|
+
return sql_string_value(node.args[0])
|
|
209
|
+
return sql_string_value(node)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _containing_scope(context: PythonFileContext, node: ast.AST) -> ast.AST:
|
|
213
|
+
current = node
|
|
214
|
+
while (parent := context.parents.get(current)) is not None:
|
|
215
|
+
if isinstance(parent, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
216
|
+
return parent
|
|
217
|
+
current = parent
|
|
218
|
+
return current
|
|
219
|
+
|
|
220
|
+
|
|
132
221
|
def _is_non_collected_test_support(path: Path) -> bool:
|
|
133
222
|
return "tests" in path.parts and not (path.name.startswith("test_") or path.name.endswith("_test.py"))
|
|
134
223
|
|
|
@@ -96,7 +96,7 @@ class PreferCollectionComprehension(Rule):
|
|
|
96
96
|
"Only function-local, adjacent empty initializers and one synchronous single-purpose loop are checked.",
|
|
97
97
|
(
|
|
98
98
|
"The rule fills gaps left by Ruff PERF401, PERF403, and FURB142: derived dict projections, "
|
|
99
|
-
"destructured list projections, narrowly filtered computed list candidates, and filtered set "
|
|
99
|
+
"constructor-valued dict projections, destructured list projections, narrowly filtered computed list candidates, and filtered set "
|
|
100
100
|
"builders."
|
|
101
101
|
),
|
|
102
102
|
(
|
|
@@ -115,6 +115,41 @@ class PreferCollectionComprehension(Rule):
|
|
|
115
115
|
),
|
|
116
116
|
),
|
|
117
117
|
examples=(
|
|
118
|
+
RuleExample(
|
|
119
|
+
example_id="constructor-dict-projection-loop",
|
|
120
|
+
title="Build constructor-valued dictionaries directly",
|
|
121
|
+
outcome=ExampleOutcome.MATCH,
|
|
122
|
+
files=(
|
|
123
|
+
ExampleFile.python(
|
|
124
|
+
"app/views.py",
|
|
125
|
+
"def views(rows, labels):\n"
|
|
126
|
+
" result = {}\n"
|
|
127
|
+
" for row in rows:\n"
|
|
128
|
+
" result[row.id] = EntryView(id=row.id, label=labels.get(row.id))\n"
|
|
129
|
+
" return result\n",
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
focus_path=PurePosixPath("app/views.py"),
|
|
133
|
+
expected_count=1,
|
|
134
|
+
public=True,
|
|
135
|
+
scenario="constructor-dict",
|
|
136
|
+
),
|
|
137
|
+
RuleExample(
|
|
138
|
+
example_id="constructor-dict-comprehension",
|
|
139
|
+
title="Keep constructor-valued dictionaries declarative",
|
|
140
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
141
|
+
files=(
|
|
142
|
+
ExampleFile.python(
|
|
143
|
+
"app/views.py",
|
|
144
|
+
"def views(rows, labels):\n"
|
|
145
|
+
" return {row.id: EntryView(id=row.id, label=labels.get(row.id)) for row in rows}\n",
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
focus_path=PurePosixPath("app/views.py"),
|
|
149
|
+
expected_count=0,
|
|
150
|
+
public=True,
|
|
151
|
+
scenario="constructor-dict",
|
|
152
|
+
),
|
|
118
153
|
RuleExample(
|
|
119
154
|
example_id="derived-dict-projection-loop",
|
|
120
155
|
title="Build a projected dictionary directly",
|
|
@@ -331,6 +366,7 @@ def _loop_replacement(
|
|
|
331
366
|
_CollectionKind.DICT,
|
|
332
367
|
f"{{{ast.unparse(key)}: {ast.unparse(value)} for {ast.unparse(loop.target)} "
|
|
333
368
|
f"in {ast.unparse(loop.iter)}}}",
|
|
369
|
+
allow_multiline=isinstance(value, ast.Call),
|
|
334
370
|
)
|
|
335
371
|
case _:
|
|
336
372
|
pass
|
|
@@ -520,8 +556,20 @@ def _valid_dict_projection(key: ast.expr, value: ast.expr, name: str, bound_name
|
|
|
520
556
|
return False
|
|
521
557
|
return (
|
|
522
558
|
_simple_projection(key, bound_names)
|
|
523
|
-
and _simple_projection(value, bound_names)
|
|
524
|
-
and (_is_derived(key) or _is_derived(value))
|
|
559
|
+
and (_simple_projection(value, bound_names) or _constructor_projection(value))
|
|
560
|
+
and (_is_derived(key) or _is_derived(value) or isinstance(value, ast.Call))
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _constructor_projection(node: ast.expr) -> bool:
|
|
565
|
+
if not isinstance(node, ast.Call) or _contains_prohibited_expression(node):
|
|
566
|
+
return False
|
|
567
|
+
return all(
|
|
568
|
+
_dotted_name(call.func)
|
|
569
|
+
and not any(isinstance(argument, ast.Starred) for argument in call.args)
|
|
570
|
+
and all(keyword.arg is not None for keyword in call.keywords)
|
|
571
|
+
for call in walk_ast(node)
|
|
572
|
+
if isinstance(call, ast.Call)
|
|
525
573
|
)
|
|
526
574
|
|
|
527
575
|
|
|
@@ -553,9 +601,12 @@ def _target_binding_is_observable(owner: _Callable, names: frozenset[str], loop:
|
|
|
553
601
|
if _declares_external(owner, names) or any(_loads_name(loop.iter, name) for name in names):
|
|
554
602
|
return True
|
|
555
603
|
end_line = loop.end_lineno or loop.lineno
|
|
604
|
+
comprehension_locals = _comprehension_local_nodes(owner, names)
|
|
556
605
|
for node in walk_ast(owner):
|
|
557
606
|
if isinstance(node, ast.arg) and node.arg in names:
|
|
558
607
|
return True
|
|
608
|
+
if id(node) in comprehension_locals:
|
|
609
|
+
continue
|
|
559
610
|
if (
|
|
560
611
|
isinstance(node, ast.Name)
|
|
561
612
|
and node.id in names
|
|
@@ -568,6 +619,29 @@ def _target_binding_is_observable(owner: _Callable, names: frozenset[str], loop:
|
|
|
568
619
|
return False
|
|
569
620
|
|
|
570
621
|
|
|
622
|
+
def _comprehension_local_nodes(owner: _Callable, names: frozenset[str]) -> set[int]:
|
|
623
|
+
local_nodes: set[int] = set()
|
|
624
|
+
for node in walk_ast(owner):
|
|
625
|
+
if not isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
|
|
626
|
+
continue
|
|
627
|
+
# Multiple generators can read an outer binding before a later generator shadows it.
|
|
628
|
+
if len(node.generators) != 1:
|
|
629
|
+
continue
|
|
630
|
+
bound = _bound_names(node.generators[0].target) & names
|
|
631
|
+
if not bound:
|
|
632
|
+
continue
|
|
633
|
+
# The first iterable runs in the surrounding scope; targets and remaining expressions are local.
|
|
634
|
+
scoped: list[ast.AST] = [node.generators[0].target, *node.generators[0].ifs]
|
|
635
|
+
scoped.extend((node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,))
|
|
636
|
+
local_nodes.update(
|
|
637
|
+
id(child)
|
|
638
|
+
for expression in scoped
|
|
639
|
+
for child in walk_ast(expression)
|
|
640
|
+
if isinstance(child, ast.Name) and child.id in bound
|
|
641
|
+
)
|
|
642
|
+
return local_nodes
|
|
643
|
+
|
|
644
|
+
|
|
571
645
|
def _declares_external(owner: _Callable, names: frozenset[str]) -> bool:
|
|
572
646
|
return any(
|
|
573
647
|
isinstance(node, (ast.Global, ast.Nonlocal)) and not names.isdisjoint(node.names) for node in walk_ast(owner)
|
|
@@ -604,7 +678,24 @@ def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multilin
|
|
|
604
678
|
prefix = f"{name}: {ast.unparse(init.annotation)} = "
|
|
605
679
|
if init.col_offset + len(prefix) + len(expression) <= _MAX_REPLACEMENT_WIDTH:
|
|
606
680
|
return True
|
|
607
|
-
if not allow_multiline
|
|
681
|
+
if not allow_multiline:
|
|
682
|
+
return False
|
|
683
|
+
if expression.startswith("{"):
|
|
684
|
+
comprehension = ast.parse(expression, mode="eval").body
|
|
685
|
+
if not isinstance(comprehension, ast.DictComp) or not isinstance(comprehension.value, ast.Call):
|
|
686
|
+
return False
|
|
687
|
+
call = comprehension.value
|
|
688
|
+
generator = comprehension.generators[0]
|
|
689
|
+
lines = (
|
|
690
|
+
f"{ast.unparse(comprehension.key)}: {ast.unparse(call.func)}(",
|
|
691
|
+
*(f" {ast.unparse(argument)}," for argument in call.args),
|
|
692
|
+
*(f" {keyword.arg}={ast.unparse(keyword.value)}," for keyword in call.keywords),
|
|
693
|
+
f"for {ast.unparse(generator.target)} in {ast.unparse(generator.iter)}",
|
|
694
|
+
)
|
|
695
|
+
return init.col_offset + len(prefix) + 1 <= _MAX_REPLACEMENT_WIDTH and all(
|
|
696
|
+
init.col_offset + 4 + len(line) <= _MAX_REPLACEMENT_WIDTH for line in lines
|
|
697
|
+
)
|
|
698
|
+
if not (expression.startswith("[") and expression.endswith("]")):
|
|
608
699
|
return False
|
|
609
700
|
content = expression[1:-1]
|
|
610
701
|
return (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.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.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_set_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/_test_provenance.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/mock_without_spec.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.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_dunder_all.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
|
|
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.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/no_select_star.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_class_row.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
|
|
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.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/prefer_str_enum.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
|
|
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
|
|
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.22.1 → sarj_python_lint-1.24.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|