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