sarj-python-lint 1.24.0__tar.gz → 1.25.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.24.0 → sarj_python_lint-1.25.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +227 -122
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/.gitignore +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/LICENSE +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/README.md +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.24.0 → sarj_python_lint-1.25.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.25.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/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from collections import defaultdict
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from enum import StrEnum
|
|
6
7
|
from io import StringIO
|
|
@@ -21,11 +22,11 @@ from sarj_python_lint.rule_base import (
|
|
|
21
22
|
Severity,
|
|
22
23
|
is_suppressed,
|
|
23
24
|
)
|
|
24
|
-
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
25
|
+
from sarj_python_lint.rules._ast_index import children, walk as walk_ast
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
if TYPE_CHECKING:
|
|
28
|
-
from collections.abc import Iterator
|
|
29
|
+
from collections.abc import Iterable, Iterator
|
|
29
30
|
|
|
30
31
|
from sarj_python_lint._file_context import PythonFileContext
|
|
31
32
|
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
@@ -74,6 +75,15 @@ class _LoopReplacement:
|
|
|
74
75
|
allow_multiline: bool = False
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class _OwnerBindings:
|
|
80
|
+
names: dict[str, list[ast.Name | ast.arg]]
|
|
81
|
+
external: frozenset[str]
|
|
82
|
+
local_nodes: frozenset[int]
|
|
83
|
+
captured: frozenset[str]
|
|
84
|
+
observes_namespace: bool
|
|
85
|
+
|
|
86
|
+
|
|
77
87
|
@final
|
|
78
88
|
class PreferCollectionComprehension(Rule):
|
|
79
89
|
id = "prefer-collection-comprehension"
|
|
@@ -96,13 +106,14 @@ class PreferCollectionComprehension(Rule):
|
|
|
96
106
|
"Only function-local, adjacent empty initializers and one synchronous single-purpose loop are checked.",
|
|
97
107
|
(
|
|
98
108
|
"The rule fills gaps left by Ruff PERF401, PERF403, and FURB142: derived dict projections, "
|
|
99
|
-
"constructor-valued dict projections, destructured list projections, narrowly filtered computed list candidates, and filtered set "
|
|
109
|
+
"constructor-valued and filtered dict projections, destructured list projections, narrowly filtered computed list candidates, and filtered set "
|
|
100
110
|
"builders."
|
|
101
111
|
),
|
|
102
112
|
(
|
|
103
113
|
"Comments, loop-target leakage, try blocks, aliases, complex projections, and replacements that "
|
|
104
114
|
"do not fit a 120-column line or a simple formatter-style multiline comprehension are excluded. "
|
|
105
|
-
"
|
|
115
|
+
"Namespace observers, explicit inspect/sys frame access, and collections referenced by nested functions "
|
|
116
|
+
"are excluded. No autofix is offered because dict key/value evaluation order can differ."
|
|
106
117
|
),
|
|
107
118
|
(
|
|
108
119
|
"Attribute projections can invoke properties or descriptors, so reviewers should keep the loop "
|
|
@@ -241,32 +252,51 @@ class PreferCollectionComprehension(Rule):
|
|
|
241
252
|
source_lines = context.source_lines
|
|
242
253
|
comments = _comment_lines(source)
|
|
243
254
|
diagnostics: list[Diagnostic] = []
|
|
255
|
+
set_shadowed = _set_is_shadowed(tree, node_index=context.node_index)
|
|
256
|
+
namespace_observers = _namespace_observer_calls(context)
|
|
244
257
|
for owner in (node for node in context.nodes(ast.AST) if isinstance(node, _Callable)):
|
|
245
|
-
for
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
256
|
-
if finding is None:
|
|
257
|
-
continue
|
|
258
|
-
diagnostics.append(
|
|
259
|
-
Diagnostic(
|
|
260
|
-
path=path,
|
|
261
|
-
line=loop.lineno,
|
|
262
|
-
col=loop.col_offset + 1,
|
|
263
|
-
code=self.code,
|
|
264
|
-
message=_message(finding),
|
|
265
|
-
)
|
|
258
|
+
for finding, loop in _owner_candidates(
|
|
259
|
+
owner, source_lines, comments, namespace_observers, set_shadowed=set_shadowed
|
|
260
|
+
):
|
|
261
|
+
diagnostics.append(
|
|
262
|
+
Diagnostic(
|
|
263
|
+
path=path,
|
|
264
|
+
line=loop.lineno,
|
|
265
|
+
col=loop.col_offset + 1,
|
|
266
|
+
code=self.code,
|
|
267
|
+
message=_message(finding),
|
|
266
268
|
)
|
|
269
|
+
)
|
|
267
270
|
return sorted(diagnostics, key=lambda diagnostic: (diagnostic.line, diagnostic.col))
|
|
268
271
|
|
|
269
272
|
|
|
273
|
+
def _owner_candidates(
|
|
274
|
+
owner: _Callable,
|
|
275
|
+
source_lines: list[str],
|
|
276
|
+
comments: frozenset[int],
|
|
277
|
+
namespace_observers: frozenset[int],
|
|
278
|
+
*,
|
|
279
|
+
set_shadowed: bool,
|
|
280
|
+
) -> Iterator[tuple[_Candidate, ast.For]]:
|
|
281
|
+
bindings = None
|
|
282
|
+
for block in _statement_blocks(owner.body):
|
|
283
|
+
for init, loop in pairwise(block):
|
|
284
|
+
if not isinstance(init, _Init) or not isinstance(loop, ast.For) or _initialized_collection(init) is None:
|
|
285
|
+
continue
|
|
286
|
+
if bindings is None:
|
|
287
|
+
bindings = _owner_bindings(owner, namespace_observers)
|
|
288
|
+
finding = _candidate(
|
|
289
|
+
bindings,
|
|
290
|
+
init=init,
|
|
291
|
+
loop=loop,
|
|
292
|
+
source_lines=source_lines,
|
|
293
|
+
comments=comments,
|
|
294
|
+
set_shadowed=set_shadowed,
|
|
295
|
+
)
|
|
296
|
+
if finding is not None:
|
|
297
|
+
yield finding, loop
|
|
298
|
+
|
|
299
|
+
|
|
270
300
|
def _statement_blocks(body: list[ast.stmt]) -> Iterator[list[ast.stmt]]:
|
|
271
301
|
yield body
|
|
272
302
|
for statement in body:
|
|
@@ -286,14 +316,13 @@ def _statement_blocks(body: list[ast.stmt]) -> Iterator[list[ast.stmt]]:
|
|
|
286
316
|
|
|
287
317
|
|
|
288
318
|
def _candidate(
|
|
289
|
-
|
|
290
|
-
owner: _Callable,
|
|
319
|
+
bindings: _OwnerBindings,
|
|
291
320
|
*,
|
|
292
321
|
init: ast.stmt,
|
|
293
322
|
loop: ast.stmt,
|
|
294
323
|
source_lines: list[str],
|
|
295
324
|
comments: frozenset[int],
|
|
296
|
-
|
|
325
|
+
set_shadowed: bool,
|
|
297
326
|
) -> _Candidate | None:
|
|
298
327
|
if not isinstance(init, _Init) or not isinstance(loop, ast.For) or loop.orelse:
|
|
299
328
|
return None
|
|
@@ -301,7 +330,9 @@ def _candidate(
|
|
|
301
330
|
if initialized is None:
|
|
302
331
|
return None
|
|
303
332
|
name = initialized.name
|
|
304
|
-
if
|
|
333
|
+
if bindings.observes_namespace or name in bindings.captured:
|
|
334
|
+
return None
|
|
335
|
+
if initialized.kind is _CollectionKind.SET and set_shadowed:
|
|
305
336
|
return None
|
|
306
337
|
if _has_comment(init, loop, comments) or is_suppressed(
|
|
307
338
|
source_lines, loop.lineno, PreferCollectionComprehension.code
|
|
@@ -310,19 +341,19 @@ def _candidate(
|
|
|
310
341
|
bound_names = _bound_names(loop.target)
|
|
311
342
|
if not bound_names or _contains_starred(loop.target) or name in bound_names:
|
|
312
343
|
return None
|
|
313
|
-
if _target_binding_is_observable(
|
|
344
|
+
if _target_binding_is_observable(bindings, bound_names, loop):
|
|
314
345
|
return None
|
|
315
|
-
if (
|
|
316
|
-
_loads_name(loop.iter, name)
|
|
317
|
-
or _contains_prohibited_expression(loop.iter)
|
|
318
|
-
or _declares_external(owner, frozenset({name}))
|
|
319
|
-
):
|
|
346
|
+
if _loads_name(loop.iter, name) or _contains_prohibited_expression(loop.iter) or name in bindings.external:
|
|
320
347
|
return None
|
|
321
348
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
349
|
+
try:
|
|
350
|
+
replacement = _loop_replacement(loop, name, initialized.kind, bound_names, bindings)
|
|
351
|
+
fits = replacement is not None and _replacement_fits(
|
|
352
|
+
init, name, replacement.expression, allow_multiline=replacement.allow_multiline
|
|
353
|
+
)
|
|
354
|
+
except RecursionError, SyntaxError:
|
|
355
|
+
return None
|
|
356
|
+
if replacement is None or not fits:
|
|
326
357
|
return None
|
|
327
358
|
return _Candidate(kind=replacement.kind, name=name, action=replacement.action)
|
|
328
359
|
|
|
@@ -354,40 +385,56 @@ def _loop_replacement(
|
|
|
354
385
|
name: str,
|
|
355
386
|
initialized_kind: _CollectionKind,
|
|
356
387
|
bound_names: frozenset[str],
|
|
357
|
-
|
|
388
|
+
bindings: _OwnerBindings,
|
|
358
389
|
) -> _LoopReplacement | None:
|
|
359
|
-
if initialized_kind is _CollectionKind.DICT
|
|
360
|
-
|
|
361
|
-
case ast.Assign(targets=[ast.Subscript(value=ast.Name(id=target), slice=key)], value=value) if (
|
|
362
|
-
target == name
|
|
363
|
-
):
|
|
364
|
-
if _valid_dict_projection(key, value, name, bound_names):
|
|
365
|
-
return _LoopReplacement(
|
|
366
|
-
_CollectionKind.DICT,
|
|
367
|
-
f"{{{ast.unparse(key)}: {ast.unparse(value)} for {ast.unparse(loop.target)} "
|
|
368
|
-
f"in {ast.unparse(loop.iter)}}}",
|
|
369
|
-
allow_multiline=isinstance(value, ast.Call),
|
|
370
|
-
)
|
|
371
|
-
case _:
|
|
372
|
-
pass
|
|
390
|
+
if initialized_kind is _CollectionKind.DICT:
|
|
391
|
+
return _dict_replacement(loop, name, bound_names)
|
|
373
392
|
if initialized_kind is _CollectionKind.LIST:
|
|
374
|
-
replacement = _list_replacement(loop, name, bound_names,
|
|
393
|
+
replacement = _list_replacement(loop, name, bound_names, bindings)
|
|
375
394
|
if replacement is not None:
|
|
376
395
|
return replacement
|
|
377
|
-
if initialized_kind is _CollectionKind.SET
|
|
396
|
+
if initialized_kind is _CollectionKind.SET:
|
|
378
397
|
return _set_replacement(loop, name, bound_names)
|
|
379
398
|
return None
|
|
380
399
|
|
|
381
400
|
|
|
401
|
+
def _dict_replacement(loop: ast.For, name: str, bound_names: frozenset[str]) -> _LoopReplacement | None:
|
|
402
|
+
condition = ""
|
|
403
|
+
filtered = _filtered_statement_parts(loop.body)
|
|
404
|
+
if filtered is not None:
|
|
405
|
+
statement, test, inverted = filtered
|
|
406
|
+
if _loads_name(test, name) or _contains_prohibited_expression(test):
|
|
407
|
+
return None
|
|
408
|
+
condition = f" if {_condition_source(test, inverted=inverted)}"
|
|
409
|
+
elif len(loop.body) == 1:
|
|
410
|
+
statement = loop.body[0]
|
|
411
|
+
else:
|
|
412
|
+
return None
|
|
413
|
+
match statement:
|
|
414
|
+
case ast.Assign(targets=[ast.Subscript(value=ast.Name(id=target), slice=key)], value=value) if target == name:
|
|
415
|
+
pass
|
|
416
|
+
case _:
|
|
417
|
+
return None
|
|
418
|
+
if not _valid_dict_projection(key, value, name, bound_names):
|
|
419
|
+
return None
|
|
420
|
+
return _LoopReplacement(
|
|
421
|
+
_CollectionKind.DICT,
|
|
422
|
+
f"{{{ast.unparse(key)}: {ast.unparse(value)} for {ast.unparse(loop.target)} "
|
|
423
|
+
f"in {_comprehension_expression_source(loop.iter)}{condition}}}",
|
|
424
|
+
"filters and populates" if filtered is not None else "populates",
|
|
425
|
+
allow_multiline=isinstance(value, ast.Call),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
382
429
|
def _list_replacement(
|
|
383
|
-
loop: ast.For, name: str, bound_names: frozenset[str],
|
|
430
|
+
loop: ast.For, name: str, bound_names: frozenset[str], bindings: _OwnerBindings
|
|
384
431
|
) -> _LoopReplacement | None:
|
|
385
432
|
if len(bound_names) > 1 and len(loop.body) == 1:
|
|
386
433
|
expression = _single_method_argument(loop.body[0], name, "append")
|
|
387
434
|
if expression is not None and not _loads_name(expression, name) and _simple_projection(expression, bound_names):
|
|
388
435
|
return _LoopReplacement(
|
|
389
436
|
_CollectionKind.LIST,
|
|
390
|
-
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {
|
|
437
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {_comprehension_expression_source(loop.iter)}]",
|
|
391
438
|
)
|
|
392
439
|
|
|
393
440
|
filtered = _filtered_list_parts(loop.body, name)
|
|
@@ -402,32 +449,36 @@ def _list_replacement(
|
|
|
402
449
|
condition = _condition_source(test, inverted=inverted)
|
|
403
450
|
return _LoopReplacement(
|
|
404
451
|
_CollectionKind.LIST,
|
|
405
|
-
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {
|
|
452
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {_comprehension_expression_source(loop.iter)} "
|
|
406
453
|
f"if {condition}]",
|
|
407
454
|
"filters and appends to",
|
|
408
455
|
allow_multiline=True,
|
|
409
456
|
)
|
|
410
457
|
|
|
411
|
-
return _computed_list_replacement(loop, name, bound_names,
|
|
458
|
+
return _computed_list_replacement(loop, name, bound_names, bindings)
|
|
412
459
|
|
|
413
460
|
|
|
414
|
-
def
|
|
461
|
+
def _filtered_statement_parts(body: list[ast.stmt]) -> tuple[ast.stmt, ast.expr, bool] | None:
|
|
415
462
|
match body:
|
|
416
463
|
case [ast.If(test=test, body=[statement], orelse=[])]:
|
|
417
|
-
|
|
418
|
-
if expression is not None:
|
|
419
|
-
return expression, test, False
|
|
464
|
+
return statement, test, False
|
|
420
465
|
case [ast.If(test=test, body=[ast.Continue()], orelse=[]), statement]:
|
|
421
|
-
|
|
422
|
-
if expression is not None:
|
|
423
|
-
return expression, test, True
|
|
466
|
+
return statement, test, True
|
|
424
467
|
case _:
|
|
425
|
-
|
|
426
|
-
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _filtered_list_parts(body: list[ast.stmt], name: str) -> tuple[ast.expr, ast.expr, bool] | None:
|
|
472
|
+
parts = _filtered_statement_parts(body)
|
|
473
|
+
if parts is None:
|
|
474
|
+
return None
|
|
475
|
+
statement, test, inverted = parts
|
|
476
|
+
expression = _single_method_argument(statement, name, "append")
|
|
477
|
+
return (expression, test, inverted) if expression is not None else None
|
|
427
478
|
|
|
428
479
|
|
|
429
480
|
def _computed_list_replacement(
|
|
430
|
-
loop: ast.For, name: str, bound_names: frozenset[str],
|
|
481
|
+
loop: ast.For, name: str, bound_names: frozenset[str], bindings: _OwnerBindings
|
|
431
482
|
) -> _LoopReplacement | None:
|
|
432
483
|
match loop.body:
|
|
433
484
|
case [ast.Assign(targets=[ast.Name(id=candidate)], value=value), *tail]:
|
|
@@ -437,7 +488,7 @@ def _computed_list_replacement(
|
|
|
437
488
|
|
|
438
489
|
if candidate == name or candidate in bound_names:
|
|
439
490
|
return None
|
|
440
|
-
if _target_binding_is_observable(
|
|
491
|
+
if _target_binding_is_observable(bindings, frozenset({candidate}), loop):
|
|
441
492
|
return None
|
|
442
493
|
if _loads_name(value, name) or _contains_prohibited_expression(value):
|
|
443
494
|
return None
|
|
@@ -459,7 +510,7 @@ def _computed_list_replacement(
|
|
|
459
510
|
condition = assignment if condition_kind == "truthy" else f"{assignment} is not None"
|
|
460
511
|
return _LoopReplacement(
|
|
461
512
|
_CollectionKind.LIST,
|
|
462
|
-
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {
|
|
513
|
+
f"[{ast.unparse(expression)} for {ast.unparse(loop.target)} in {_comprehension_expression_source(loop.iter)} if {condition}]",
|
|
463
514
|
"computes, filters, and appends to",
|
|
464
515
|
allow_multiline=True,
|
|
465
516
|
)
|
|
@@ -517,9 +568,14 @@ def _dotted_name(node: ast.expr) -> bool:
|
|
|
517
568
|
return False
|
|
518
569
|
|
|
519
570
|
|
|
571
|
+
def _comprehension_expression_source(iterable: ast.expr) -> str:
|
|
572
|
+
source = ast.unparse(iterable)
|
|
573
|
+
return f"({source})" if isinstance(iterable, ast.IfExp) else source
|
|
574
|
+
|
|
575
|
+
|
|
520
576
|
def _condition_source(test: ast.expr, *, inverted: bool) -> str:
|
|
521
577
|
if not inverted:
|
|
522
|
-
return
|
|
578
|
+
return _comprehension_expression_source(test)
|
|
523
579
|
return ast.unparse(ast.UnaryOp(op=ast.Not(), operand=test))
|
|
524
580
|
|
|
525
581
|
|
|
@@ -597,41 +653,95 @@ def _contains_prohibited_expression(node: ast.AST) -> bool:
|
|
|
597
653
|
return any(isinstance(item, _PROHIBITED_EXPRESSION_NODES) for item in walk_ast(node))
|
|
598
654
|
|
|
599
655
|
|
|
600
|
-
def
|
|
601
|
-
|
|
656
|
+
def _owner_bindings(owner: _Callable, namespace_observers: frozenset[int]) -> _OwnerBindings:
|
|
657
|
+
nodes = tuple(walk_ast(owner))
|
|
658
|
+
names: defaultdict[str, list[ast.Name | ast.arg]] = defaultdict(list)
|
|
659
|
+
for node in nodes:
|
|
660
|
+
if isinstance(node, ast.Name):
|
|
661
|
+
names[node.id].append(node)
|
|
662
|
+
elif isinstance(node, ast.arg):
|
|
663
|
+
names[node.arg].append(node)
|
|
664
|
+
return _OwnerBindings(
|
|
665
|
+
names=dict(names),
|
|
666
|
+
external=frozenset(
|
|
667
|
+
name for node in nodes if isinstance(node, (ast.Global, ast.Nonlocal)) for name in node.names
|
|
668
|
+
),
|
|
669
|
+
local_nodes=frozenset(_comprehension_local_nodes(nodes, frozenset(names))),
|
|
670
|
+
captured=_captured_names(owner),
|
|
671
|
+
observes_namespace=any(id(node) in namespace_observers for node in nodes),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _namespace_observer_calls(context: PythonFileContext) -> frozenset[int]:
|
|
676
|
+
observers = {
|
|
677
|
+
"builtins": {"locals", "vars", "eval", "exec"},
|
|
678
|
+
"inspect": {"currentframe"},
|
|
679
|
+
"sys": {"_getframe"},
|
|
680
|
+
}
|
|
681
|
+
# Explicit aliases remain conservative exclusions even when another scope shadows them.
|
|
682
|
+
modules: defaultdict[str, set[str]] = defaultdict(set)
|
|
683
|
+
symbols = observers["builtins"].copy()
|
|
684
|
+
for node in context.nodes(ast.Import):
|
|
685
|
+
for alias in node.names:
|
|
686
|
+
if alias.name in observers:
|
|
687
|
+
modules[alias.asname or alias.name].update(observers[alias.name])
|
|
688
|
+
for node in context.nodes(ast.ImportFrom):
|
|
689
|
+
if node.module not in observers:
|
|
690
|
+
continue
|
|
691
|
+
symbols.update(alias.asname or alias.name for alias in node.names if alias.name in observers[node.module])
|
|
692
|
+
return frozenset(
|
|
693
|
+
id(call) for call in context.nodes(ast.Call) if _is_namespace_observer(call.func, symbols, modules)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _is_namespace_observer(function: ast.expr, symbols: set[str], modules: dict[str, set[str]]) -> bool:
|
|
698
|
+
match function:
|
|
699
|
+
case ast.Name(id=name):
|
|
700
|
+
return name in symbols
|
|
701
|
+
case ast.Attribute(value=ast.Name(id=module), attr=symbol):
|
|
702
|
+
return module in modules and symbol in modules[module]
|
|
703
|
+
case _:
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _captured_names(owner: _Callable) -> frozenset[str]:
|
|
708
|
+
captured: set[str] = set()
|
|
709
|
+
pending: list[tuple[ast.AST, bool]] = [(owner, False)]
|
|
710
|
+
while pending:
|
|
711
|
+
node, nested = pending.pop()
|
|
712
|
+
nested = nested or (node is not owner and isinstance(node, (_Callable, ast.Lambda, ast.ClassDef)))
|
|
713
|
+
if nested and isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
714
|
+
captured.add(node.id)
|
|
715
|
+
pending.extend((child, nested) for child in children(node))
|
|
716
|
+
return frozenset(captured)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _target_binding_is_observable(bindings: _OwnerBindings, names: frozenset[str], loop: ast.For) -> bool:
|
|
720
|
+
if not bindings.external.isdisjoint(names) or any(_loads_name(loop.iter, name) for name in names):
|
|
602
721
|
return True
|
|
603
722
|
end_line = loop.end_lineno or loop.lineno
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
and node.id in names
|
|
613
|
-
and (
|
|
614
|
-
getattr(node, "lineno", loop.lineno) < loop.lineno
|
|
615
|
-
or (getattr(node, "lineno", 0) > end_line and isinstance(node.ctx, (ast.Load, ast.Del)))
|
|
616
|
-
)
|
|
617
|
-
):
|
|
618
|
-
return True
|
|
723
|
+
for name in names:
|
|
724
|
+
for node in bindings.names.get(name, []):
|
|
725
|
+
if isinstance(node, ast.arg):
|
|
726
|
+
return True
|
|
727
|
+
if id(node) in bindings.local_nodes:
|
|
728
|
+
continue
|
|
729
|
+
if node.lineno < loop.lineno or (node.lineno > end_line and isinstance(node.ctx, (ast.Load, ast.Del))):
|
|
730
|
+
return True
|
|
619
731
|
return False
|
|
620
732
|
|
|
621
733
|
|
|
622
|
-
def _comprehension_local_nodes(
|
|
734
|
+
def _comprehension_local_nodes(nodes: Iterable[ast.AST], names: frozenset[str]) -> set[int]:
|
|
623
735
|
local_nodes: set[int] = set()
|
|
624
|
-
for node in
|
|
736
|
+
for node in nodes:
|
|
625
737
|
if not isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
|
|
626
738
|
continue
|
|
627
|
-
#
|
|
628
|
-
if len(node.generators) != 1:
|
|
629
|
-
continue
|
|
739
|
+
# Only first-generator bindings are local before every later generator expression.
|
|
630
740
|
bound = _bound_names(node.generators[0].target) & names
|
|
631
741
|
if not bound:
|
|
632
742
|
continue
|
|
633
743
|
# 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]
|
|
744
|
+
scoped: list[ast.AST] = [node.generators[0].target, *node.generators[0].ifs, *node.generators[1:]]
|
|
635
745
|
scoped.extend((node.key, node.value) if isinstance(node, ast.DictComp) else (node.elt,))
|
|
636
746
|
local_nodes.update(
|
|
637
747
|
id(child)
|
|
@@ -642,12 +752,6 @@ def _comprehension_local_nodes(owner: _Callable, names: frozenset[str]) -> set[i
|
|
|
642
752
|
return local_nodes
|
|
643
753
|
|
|
644
754
|
|
|
645
|
-
def _declares_external(owner: _Callable, names: frozenset[str]) -> bool:
|
|
646
|
-
return any(
|
|
647
|
-
isinstance(node, (ast.Global, ast.Nonlocal)) and not names.isdisjoint(node.names) for node in walk_ast(owner)
|
|
648
|
-
)
|
|
649
|
-
|
|
650
|
-
|
|
651
755
|
def _set_is_shadowed(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
|
|
652
756
|
for node in walk_ast(tree, index=node_index):
|
|
653
757
|
match node:
|
|
@@ -673,6 +777,7 @@ def _set_is_shadowed(tree: ast.Module, *, node_index: NodeIndex | None = None) -
|
|
|
673
777
|
|
|
674
778
|
|
|
675
779
|
def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multiline: bool = False) -> bool:
|
|
780
|
+
comprehension = ast.parse(expression, mode="eval").body
|
|
676
781
|
prefix = f"{name} = "
|
|
677
782
|
if isinstance(init, ast.AnnAssign):
|
|
678
783
|
prefix = f"{name}: {ast.unparse(init.annotation)} = "
|
|
@@ -681,7 +786,6 @@ def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multilin
|
|
|
681
786
|
if not allow_multiline:
|
|
682
787
|
return False
|
|
683
788
|
if expression.startswith("{"):
|
|
684
|
-
comprehension = ast.parse(expression, mode="eval").body
|
|
685
789
|
if not isinstance(comprehension, ast.DictComp) or not isinstance(comprehension.value, ast.Call):
|
|
686
790
|
return False
|
|
687
791
|
call = comprehension.value
|
|
@@ -690,7 +794,8 @@ def _replacement_fits(init: _Init, name: str, expression: str, *, allow_multilin
|
|
|
690
794
|
f"{ast.unparse(comprehension.key)}: {ast.unparse(call.func)}(",
|
|
691
795
|
*(f" {ast.unparse(argument)}," for argument in call.args),
|
|
692
796
|
*(f" {keyword.arg}={ast.unparse(keyword.value)}," for keyword in call.keywords),
|
|
693
|
-
f"for {ast.unparse(generator.target)} in {
|
|
797
|
+
f"for {ast.unparse(generator.target)} in {_comprehension_expression_source(generator.iter)}",
|
|
798
|
+
*(f"if {_comprehension_expression_source(test)}" for test in generator.ifs),
|
|
694
799
|
)
|
|
695
800
|
return init.col_offset + len(prefix) + 1 <= _MAX_REPLACEMENT_WIDTH and all(
|
|
696
801
|
init.col_offset + 4 + len(line) <= _MAX_REPLACEMENT_WIDTH for line in lines
|
|
@@ -718,24 +823,24 @@ def _has_comment(init: _Init, loop: ast.For, comments: frozenset[int]) -> bool:
|
|
|
718
823
|
|
|
719
824
|
|
|
720
825
|
def _set_replacement(loop: ast.For, name: str, bound_names: frozenset[str]) -> _LoopReplacement | None:
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
826
|
+
parts = _filtered_statement_parts(loop.body)
|
|
827
|
+
if parts is None:
|
|
828
|
+
return None
|
|
829
|
+
statement, test, inverted = parts
|
|
830
|
+
expression = _single_method_argument(statement, name, "add")
|
|
831
|
+
if (
|
|
832
|
+
expression is None
|
|
833
|
+
or _loads_name(test, name)
|
|
834
|
+
or _loads_name(expression, name)
|
|
835
|
+
or not _simple_projection(expression, bound_names)
|
|
836
|
+
or _contains_prohibited_expression(test)
|
|
837
|
+
):
|
|
838
|
+
return None
|
|
839
|
+
return _LoopReplacement(
|
|
840
|
+
_CollectionKind.SET,
|
|
841
|
+
f"{{{ast.unparse(expression)} for {ast.unparse(loop.target)} "
|
|
842
|
+
f"in {_comprehension_expression_source(loop.iter)} if {_condition_source(test, inverted=inverted)}}}",
|
|
843
|
+
)
|
|
739
844
|
|
|
740
845
|
|
|
741
846
|
def _message(finding: _Candidate) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/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.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_copy_on_write.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_dict_key_views.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.24.0 → sarj_python_lint-1.25.1}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|