sarj-python-lint 1.20.1__tar.gz → 1.20.2__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.20.1 → sarj_python_lint-1.20.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/pyproject.toml +1 -1
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_resource_provenance.py +8 -2
- sarj_python_lint-1.20.2/src/sarj_python_lint/rules/prefer_native_string_check.py +486 -0
- sarj_python_lint-1.20.1/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -169
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/.gitignore +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/LICENSE +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/README.md +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.20.1 → sarj_python_lint-1.20.2}/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.20.
|
|
3
|
+
Version: 1.20.2
|
|
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/
|
|
@@ -28,6 +28,7 @@ class ResourceProvenance:
|
|
|
28
28
|
self.context = context
|
|
29
29
|
self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
|
|
30
30
|
self.reads: dict[str, list[ast.Name]] = defaultdict(list)
|
|
31
|
+
self._reads_by_binding: dict[str, dict[ast.AST, list[ast.Name]]] = {}
|
|
31
32
|
self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
|
|
32
33
|
self.wildcard = False
|
|
33
34
|
self.mutated: set[str] = set()
|
|
@@ -132,10 +133,15 @@ class ResourceProvenance:
|
|
|
132
133
|
return False
|
|
133
134
|
key = (binding, allow_context)
|
|
134
135
|
if key not in self.escape_cache:
|
|
136
|
+
if name.id not in self._reads_by_binding:
|
|
137
|
+
grouped: dict[ast.AST, list[ast.Name]] = defaultdict(list)
|
|
138
|
+
for read in self.reads.get(name.id, ()):
|
|
139
|
+
if (read_binding := self.binding(read.id, read)) is not None:
|
|
140
|
+
grouped[read_binding].append(read)
|
|
141
|
+
self._reads_by_binding[name.id] = grouped
|
|
135
142
|
self.escape_cache[key] = all(
|
|
136
143
|
self._method_or_context_read(read, allow_context=allow_context)
|
|
137
|
-
for read in self.
|
|
138
|
-
if self.binding(read.id, read) is binding
|
|
144
|
+
for read in self._reads_by_binding[name.id].get(binding, ())
|
|
139
145
|
)
|
|
140
146
|
return self.escape_cache[key]
|
|
141
147
|
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
21
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_PYDANTIC = frozenset({"pydantic", "pydantic.type_adapter"})
|
|
29
|
+
_BUILTINS = frozenset({"builtins"})
|
|
30
|
+
_PYTEST = frozenset({"pytest"})
|
|
31
|
+
_VALIDATION_ERROR = frozenset({"pydantic", "pydantic_core"})
|
|
32
|
+
_MODULES = _PYDANTIC | _BUILTINS | _PYTEST | _VALIDATION_ERROR
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class _ImportReference:
|
|
37
|
+
index: ImportIndex
|
|
38
|
+
root_key: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@final
|
|
42
|
+
class PreferNativeStringCheck(Rule):
|
|
43
|
+
id = "prefer-native-string-check"
|
|
44
|
+
code = "SARJ480"
|
|
45
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
46
|
+
default_level=Severity.WARNING,
|
|
47
|
+
summary="Use native string assertions instead of unconstrained Pydantic coercion.",
|
|
48
|
+
rationale="An unconstrained TypeAdapter(str).validate_python(...) obscures a simple string check and can decode bytes instead of rejecting them.",
|
|
49
|
+
remediation="Use isinstance(value, str) and the appropriate failure path; in tests, assert the type. Keep structured validation at the model or JSON boundary. Preserve intentional coercion or ValidationError contracts explicitly.",
|
|
50
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
51
|
+
autofix=AutofixPolicy.NONE,
|
|
52
|
+
limitations=(
|
|
53
|
+
"Only plain built-in str adapters used by validate_python inside an assertion without validation options are reported. Configured adapters, constrained types, unions, models, containers, other scalar types and JSON/serialization operations are excluded.",
|
|
54
|
+
"Scoped import aliases and unescaped unconditional module or local adapter bindings are recognized, including independent functions reusing a name. Conditional, reassigned, indirect or class/comprehension/parameter/global/nonlocal cached bindings are excluded, as are shadowed symbols, wildcard imports and imports with visible mutation or escape.",
|
|
55
|
+
"Only assertion tests are checked: messages and lambda or generator expressions are excluded. Proven pytest.raises(ValidationError) contexts are preserved; other intentional byte-decoding or exception contracts need a local reasoned suppression. No automatic replacement can preserve those contracts.",
|
|
56
|
+
),
|
|
57
|
+
examples=(
|
|
58
|
+
RuleExample(
|
|
59
|
+
example_id="adapter-for-string-check",
|
|
60
|
+
title="A string assertion unnecessarily constructs a schema validator",
|
|
61
|
+
outcome=ExampleOutcome.MATCH,
|
|
62
|
+
files=(
|
|
63
|
+
ExampleFile.python(
|
|
64
|
+
"tests/test_receipt.py",
|
|
65
|
+
'from pydantic import TypeAdapter\ndef test_receipt():\n assert TypeAdapter(str).validate_python(receipt["id"]) == "abc"\n',
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
focus_path=PurePosixPath("tests/test_receipt.py"),
|
|
69
|
+
expected_count=1,
|
|
70
|
+
public=True,
|
|
71
|
+
),
|
|
72
|
+
RuleExample(
|
|
73
|
+
example_id="native-string-assertion",
|
|
74
|
+
title="A native assertion rejects unexpected types and narrows the value",
|
|
75
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
76
|
+
files=(
|
|
77
|
+
ExampleFile.python(
|
|
78
|
+
"tests/test_receipt.py",
|
|
79
|
+
'def test_receipt():\n value = receipt["id"]\n assert isinstance(value, str)\n assert value == "abc"\n',
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
focus_path=PurePosixPath("tests/test_receipt.py"),
|
|
83
|
+
expected_count=0,
|
|
84
|
+
public=True,
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
description = documentation.summary
|
|
89
|
+
|
|
90
|
+
@override
|
|
91
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
92
|
+
if (
|
|
93
|
+
context.generated
|
|
94
|
+
or not {"vendor", "vendored", "third_party"}.isdisjoint(context.path.parts)
|
|
95
|
+
or "validate_python" not in context.source
|
|
96
|
+
or context.tree is None
|
|
97
|
+
):
|
|
98
|
+
return []
|
|
99
|
+
provenance = ResourceProvenance(context)
|
|
100
|
+
imports = {
|
|
101
|
+
statement: ImportIndex.from_tree(ast.Module(body=[statement], type_ignores=[]))
|
|
102
|
+
for statement in (*context.nodes(ast.Import), *context.nodes(ast.ImportFrom))
|
|
103
|
+
}
|
|
104
|
+
unsafe_imports = _unsafe_imports(context, provenance, imports)
|
|
105
|
+
uncertain_names = _uncertain_cached_names(context, provenance)
|
|
106
|
+
mutated_bindings = _mutated_cached_bindings(context, provenance)
|
|
107
|
+
findings: list[Diagnostic] = []
|
|
108
|
+
for call in context.nodes(ast.Call):
|
|
109
|
+
receiver = _validation_receiver(call)
|
|
110
|
+
if receiver is None:
|
|
111
|
+
continue
|
|
112
|
+
if not _inside_assertion(call, context) or _expects_validation_error(
|
|
113
|
+
call, context, provenance, imports, unsafe_imports
|
|
114
|
+
):
|
|
115
|
+
continue
|
|
116
|
+
if isinstance(receiver, ast.Name):
|
|
117
|
+
receiver = _cached_constructor(receiver, call, provenance, uncertain_names, mutated_bindings)
|
|
118
|
+
if receiver is None or not _plain_string_adapter(receiver, context, provenance, imports, unsafe_imports):
|
|
119
|
+
continue
|
|
120
|
+
if is_suppressed(context.source_lines, call.lineno, self.code):
|
|
121
|
+
continue
|
|
122
|
+
findings.append(
|
|
123
|
+
Diagnostic(
|
|
124
|
+
path=context.path,
|
|
125
|
+
line=call.lineno,
|
|
126
|
+
col=call.col_offset + 1,
|
|
127
|
+
code=self.code,
|
|
128
|
+
severity=Severity.WARNING,
|
|
129
|
+
message="plain str adapter in an assertion; use isinstance(value, str) with an explicit failure path, or preserve the intentional Pydantic contract explicitly",
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cached_constructor(
|
|
136
|
+
receiver: ast.Name,
|
|
137
|
+
at: ast.Call,
|
|
138
|
+
provenance: ResourceProvenance,
|
|
139
|
+
uncertain_names: set[str],
|
|
140
|
+
mutated_bindings: set[ast.AST | None],
|
|
141
|
+
) -> ast.Call | None:
|
|
142
|
+
if (
|
|
143
|
+
receiver.id in uncertain_names
|
|
144
|
+
or provenance.binding(receiver.id, at) in mutated_bindings
|
|
145
|
+
or not provenance.unescaped(receiver, at)
|
|
146
|
+
):
|
|
147
|
+
return None
|
|
148
|
+
return provenance.constructor(receiver, at)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _validation_receiver(call: ast.Call) -> ast.expr | None:
|
|
152
|
+
if (
|
|
153
|
+
isinstance(call.func, ast.Attribute)
|
|
154
|
+
and call.func.attr == "validate_python"
|
|
155
|
+
and len(call.args) == 1
|
|
156
|
+
and not call.keywords
|
|
157
|
+
and not isinstance(call.args[0], ast.Starred)
|
|
158
|
+
):
|
|
159
|
+
return call.func.value
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _uncertain_cached_names(context: PythonFileContext, provenance: ResourceProvenance) -> set[str]:
|
|
164
|
+
uncertain_names = _nonlocal_names(context)
|
|
165
|
+
uncertain_names.update(
|
|
166
|
+
name
|
|
167
|
+
for scope, bindings in provenance.bindings.items()
|
|
168
|
+
if isinstance(scope, ast.ClassDef | ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp)
|
|
169
|
+
for name in bindings
|
|
170
|
+
)
|
|
171
|
+
uncertain_names.update(argument.arg for argument in context.nodes(ast.arg))
|
|
172
|
+
return uncertain_names
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _mutated_cached_bindings(context: PythonFileContext, provenance: ResourceProvenance) -> set[ast.AST | None]:
|
|
176
|
+
return {
|
|
177
|
+
provenance.binding(call.func.value.id, call)
|
|
178
|
+
for call in context.nodes(ast.Call)
|
|
179
|
+
if isinstance(call.func, ast.Attribute)
|
|
180
|
+
and isinstance(call.func.value, ast.Name)
|
|
181
|
+
and call.func.attr in {"__init__", "__setattr__", "__delattr__"}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _plain_string_adapter(
|
|
186
|
+
node: ast.expr,
|
|
187
|
+
context: PythonFileContext,
|
|
188
|
+
provenance: ResourceProvenance,
|
|
189
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
190
|
+
unsafe_imports: set[str],
|
|
191
|
+
) -> bool:
|
|
192
|
+
if not isinstance(node, ast.Call):
|
|
193
|
+
return False
|
|
194
|
+
function = node.func.value if isinstance(node.func, ast.Subscript) else node.func
|
|
195
|
+
if not _resolves(function, context, provenance, imports, unsafe_imports, sources=_PYDANTIC, symbol="TypeAdapter"):
|
|
196
|
+
return False
|
|
197
|
+
value = _single_value(node, "type")
|
|
198
|
+
if value is None:
|
|
199
|
+
return False
|
|
200
|
+
return (
|
|
201
|
+
isinstance(value, ast.Name)
|
|
202
|
+
and value.id == "str"
|
|
203
|
+
and context.imports.builtin_is_unshadowed("str")
|
|
204
|
+
and _lexical_binding("str", value, provenance) is None
|
|
205
|
+
and "builtins" not in unsafe_imports
|
|
206
|
+
and "builtins.str" not in unsafe_imports
|
|
207
|
+
) or _resolves(value, context, provenance, imports, unsafe_imports, sources=_BUILTINS, symbol="str")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _single_value(call: ast.Call, keyword: str) -> ast.expr | None:
|
|
211
|
+
if len(call.args) == 1 and not call.keywords and not isinstance(call.args[0], ast.Starred):
|
|
212
|
+
return call.args[0]
|
|
213
|
+
if not call.args and len(call.keywords) == 1 and call.keywords[0].arg == keyword:
|
|
214
|
+
return call.keywords[0].value
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _resolves(
|
|
219
|
+
node: ast.expr,
|
|
220
|
+
context: PythonFileContext,
|
|
221
|
+
provenance: ResourceProvenance,
|
|
222
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
223
|
+
unsafe_imports: set[str],
|
|
224
|
+
*,
|
|
225
|
+
sources: frozenset[str],
|
|
226
|
+
symbol: str,
|
|
227
|
+
) -> bool:
|
|
228
|
+
reference = _import_reference(node, context, provenance, imports)
|
|
229
|
+
if reference is None or provenance.wildcard:
|
|
230
|
+
return False
|
|
231
|
+
index = reference.index
|
|
232
|
+
root_key = reference.root_key
|
|
233
|
+
qualified = index.resolved_qualified_name(node)
|
|
234
|
+
return (
|
|
235
|
+
qualified is not None
|
|
236
|
+
and root_key not in unsafe_imports
|
|
237
|
+
and (root_key not in _MODULES or qualified.rsplit(".", 1)[0] not in unsafe_imports)
|
|
238
|
+
and _import_key(*qualified.rsplit(".", 1)) not in unsafe_imports
|
|
239
|
+
and index.resolves(node, sources=sources, symbol=symbol)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _lexical_binding(name: str, at: ast.AST, provenance: ResourceProvenance) -> ast.AST | None:
|
|
244
|
+
initial = scope = provenance.scope(at)
|
|
245
|
+
while scope is not None:
|
|
246
|
+
if (scope is initial or not isinstance(scope, ast.ClassDef)) and (
|
|
247
|
+
nodes := provenance.bindings.get(scope, {}).get(name)
|
|
248
|
+
):
|
|
249
|
+
return nodes[0] if len(nodes) == 1 else None
|
|
250
|
+
scope = provenance.scope(scope)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _import_key(module: str, symbol: str | None) -> str:
|
|
255
|
+
if symbol == "TypeAdapter" and module in _PYDANTIC:
|
|
256
|
+
return f"pydantic.{symbol}"
|
|
257
|
+
if symbol == "ValidationError" and module in _VALIDATION_ERROR:
|
|
258
|
+
return f"pydantic_core.{symbol}"
|
|
259
|
+
return f"{module}.{symbol}" if symbol is not None else module
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _import_reference(
|
|
263
|
+
node: ast.expr,
|
|
264
|
+
context: PythonFileContext,
|
|
265
|
+
provenance: ResourceProvenance,
|
|
266
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
267
|
+
) -> _ImportReference | None:
|
|
268
|
+
root = node
|
|
269
|
+
while isinstance(root, ast.Attribute):
|
|
270
|
+
root = root.value
|
|
271
|
+
if not isinstance(root, ast.Name):
|
|
272
|
+
return None
|
|
273
|
+
binding = _lexical_binding(root.id, node, provenance)
|
|
274
|
+
owner = context.parents.get(binding) if isinstance(binding, ast.alias) else None
|
|
275
|
+
index = imports.get(owner) if isinstance(owner, ast.Import | ast.ImportFrom) else None
|
|
276
|
+
target = index.bindings.get(root.id) if index is not None else None
|
|
277
|
+
if index is None or target is None:
|
|
278
|
+
return None
|
|
279
|
+
return _ImportReference(index, _import_key(target.module, target.symbol))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _reference_key(
|
|
283
|
+
node: ast.expr,
|
|
284
|
+
context: PythonFileContext,
|
|
285
|
+
provenance: ResourceProvenance,
|
|
286
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
287
|
+
) -> str | None:
|
|
288
|
+
reference = _import_reference(node, context, provenance, imports)
|
|
289
|
+
if reference is None:
|
|
290
|
+
return None
|
|
291
|
+
index = reference.index
|
|
292
|
+
root_key = reference.root_key
|
|
293
|
+
if qualified := index.resolved_qualified_name(node):
|
|
294
|
+
return qualified if qualified in _MODULES else _import_key(*qualified.rsplit(".", 1))
|
|
295
|
+
return root_key
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _unsafe_imports(
|
|
299
|
+
context: PythonFileContext,
|
|
300
|
+
provenance: ResourceProvenance,
|
|
301
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
302
|
+
) -> set[str]:
|
|
303
|
+
unsafe = _mutated_imports(context, provenance, imports)
|
|
304
|
+
for read in (*context.nodes(ast.Name), *context.nodes(ast.Attribute)):
|
|
305
|
+
if not isinstance(read.ctx, ast.Load):
|
|
306
|
+
continue
|
|
307
|
+
key = _reference_key(read, context, provenance, imports)
|
|
308
|
+
if key is None or not _import_escapes(read, key, context, provenance, imports):
|
|
309
|
+
continue
|
|
310
|
+
unsafe.add(key)
|
|
311
|
+
if key in _PYDANTIC:
|
|
312
|
+
unsafe.add("pydantic.TypeAdapter")
|
|
313
|
+
if key in _VALIDATION_ERROR:
|
|
314
|
+
unsafe.add("pydantic_core.ValidationError")
|
|
315
|
+
if key == "pytest":
|
|
316
|
+
unsafe.add("pytest.raises")
|
|
317
|
+
return unsafe
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _mutated_imports(
|
|
321
|
+
context: PythonFileContext,
|
|
322
|
+
provenance: ResourceProvenance,
|
|
323
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
324
|
+
) -> set[str]:
|
|
325
|
+
unsafe = _rebound_imports(context, imports)
|
|
326
|
+
for attribute in context.nodes(ast.Attribute):
|
|
327
|
+
if (isinstance(attribute.ctx, ast.Store | ast.Del) or attribute.attr == "__dict__") and (
|
|
328
|
+
key := _reference_key(attribute.value, context, provenance, imports)
|
|
329
|
+
) is not None:
|
|
330
|
+
_mark_unsafe_import(key, attribute, context, imports, unsafe)
|
|
331
|
+
for call in context.nodes(ast.Call):
|
|
332
|
+
if (
|
|
333
|
+
isinstance(call.func, ast.Attribute)
|
|
334
|
+
and call.func.attr in {"__setattr__", "__delattr__"}
|
|
335
|
+
and (key := _reference_key(call.func.value, context, provenance, imports)) is not None
|
|
336
|
+
):
|
|
337
|
+
_mark_unsafe_import(key, call, context, imports, unsafe)
|
|
338
|
+
if (
|
|
339
|
+
call.args
|
|
340
|
+
and _builtin_mutation(call, context, provenance, imports)
|
|
341
|
+
and (key := _reference_key(call.args[0], context, provenance, imports)) is not None
|
|
342
|
+
):
|
|
343
|
+
_mark_unsafe_import(key, call, context, imports, unsafe)
|
|
344
|
+
return unsafe
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _rebound_imports(
|
|
348
|
+
context: PythonFileContext,
|
|
349
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
350
|
+
) -> set[str]:
|
|
351
|
+
uncertain_names = _nonlocal_names(context)
|
|
352
|
+
return {
|
|
353
|
+
_import_key(target.module, target.symbol)
|
|
354
|
+
for index in imports.values()
|
|
355
|
+
for name, target in index.bindings.items()
|
|
356
|
+
if name in uncertain_names
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _nonlocal_names(context: PythonFileContext) -> set[str]:
|
|
361
|
+
return {
|
|
362
|
+
name for statement in (*context.nodes(ast.Global), *context.nodes(ast.Nonlocal)) for name in statement.names
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _import_escapes(
|
|
367
|
+
read: ast.expr,
|
|
368
|
+
key: str,
|
|
369
|
+
context: PythonFileContext,
|
|
370
|
+
provenance: ResourceProvenance,
|
|
371
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
372
|
+
) -> bool:
|
|
373
|
+
parent = context.parents.get(read)
|
|
374
|
+
if key == "pydantic.TypeAdapter":
|
|
375
|
+
return not (
|
|
376
|
+
isinstance(parent, ast.Attribute)
|
|
377
|
+
or (isinstance(parent, ast.Subscript) and parent.value is read)
|
|
378
|
+
or (isinstance(parent, ast.Call) and parent.func is read)
|
|
379
|
+
or (isinstance(parent, ast.AnnAssign) and parent.annotation is read)
|
|
380
|
+
)
|
|
381
|
+
if key not in _MODULES or isinstance(parent, ast.Attribute | ast.ExceptHandler):
|
|
382
|
+
return False
|
|
383
|
+
return not (
|
|
384
|
+
isinstance(parent, ast.Call)
|
|
385
|
+
and (parent.func is read or _builtin_mutation(parent, context, provenance, imports))
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _builtin_mutation(
|
|
390
|
+
call: ast.Call,
|
|
391
|
+
context: PythonFileContext,
|
|
392
|
+
provenance: ResourceProvenance,
|
|
393
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
394
|
+
) -> bool:
|
|
395
|
+
return (
|
|
396
|
+
isinstance(call.func, ast.Name)
|
|
397
|
+
and call.func.id in {"setattr", "delattr"}
|
|
398
|
+
and context.imports.builtin_is_unshadowed(call.func.id)
|
|
399
|
+
and _lexical_binding(call.func.id, call.func, provenance) is None
|
|
400
|
+
) or _reference_key(call.func, context, provenance, imports) in {
|
|
401
|
+
"builtins.setattr",
|
|
402
|
+
"builtins.delattr",
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _mark_unsafe_import(
|
|
407
|
+
key: str,
|
|
408
|
+
at: ast.AST,
|
|
409
|
+
context: PythonFileContext,
|
|
410
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
411
|
+
unsafe: set[str],
|
|
412
|
+
) -> None:
|
|
413
|
+
unsafe.add(key)
|
|
414
|
+
if key not in _MODULES:
|
|
415
|
+
return
|
|
416
|
+
for statement, index in imports.items():
|
|
417
|
+
if isinstance(context.parents.get(statement), ast.Module) and (statement.lineno, statement.col_offset) < (
|
|
418
|
+
getattr(at, "lineno", 0),
|
|
419
|
+
getattr(at, "col_offset", 0),
|
|
420
|
+
):
|
|
421
|
+
continue
|
|
422
|
+
unsafe.update(
|
|
423
|
+
_import_key(target.module, target.symbol)
|
|
424
|
+
for target in index.bindings.values()
|
|
425
|
+
if target.symbol is not None and target.module == key
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _inside_assertion(call: ast.Call, context: PythonFileContext) -> bool:
|
|
430
|
+
child: ast.AST = call
|
|
431
|
+
parent = context.parents.get(call)
|
|
432
|
+
while parent is not None:
|
|
433
|
+
if isinstance(parent, ast.Assert):
|
|
434
|
+
return child is parent.test
|
|
435
|
+
if isinstance(parent, (ast.Lambda, ast.GeneratorExp, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
436
|
+
return False
|
|
437
|
+
child = parent
|
|
438
|
+
parent = context.parents.get(parent)
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _expects_validation_error(
|
|
443
|
+
call: ast.Call,
|
|
444
|
+
context: PythonFileContext,
|
|
445
|
+
provenance: ResourceProvenance,
|
|
446
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
447
|
+
unsafe_imports: set[str],
|
|
448
|
+
) -> bool:
|
|
449
|
+
parent = context.parents.get(call)
|
|
450
|
+
while parent is not None and not isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
451
|
+
if isinstance(parent, ast.With | ast.AsyncWith) and any(
|
|
452
|
+
_raises_validation_error(item.context_expr, context, provenance, imports, unsafe_imports)
|
|
453
|
+
for item in parent.items
|
|
454
|
+
):
|
|
455
|
+
return True
|
|
456
|
+
parent = context.parents.get(parent)
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _raises_validation_error(
|
|
461
|
+
value: ast.expr,
|
|
462
|
+
context: PythonFileContext,
|
|
463
|
+
provenance: ResourceProvenance,
|
|
464
|
+
imports: dict[ast.Import | ast.ImportFrom, ImportIndex],
|
|
465
|
+
unsafe_imports: set[str],
|
|
466
|
+
) -> bool:
|
|
467
|
+
if (
|
|
468
|
+
not isinstance(value, ast.Call)
|
|
469
|
+
or len(value.args) > 1
|
|
470
|
+
or not _resolves(value.func, context, provenance, imports, unsafe_imports, sources=_PYTEST, symbol="raises")
|
|
471
|
+
):
|
|
472
|
+
return False
|
|
473
|
+
expected = (
|
|
474
|
+
value.args[0]
|
|
475
|
+
if value.args
|
|
476
|
+
else next((keyword.value for keyword in value.keywords if keyword.arg == "expected_exception"), None)
|
|
477
|
+
)
|
|
478
|
+
if expected is None:
|
|
479
|
+
return False
|
|
480
|
+
types = expected.elts if isinstance(expected, ast.Tuple) else (expected,)
|
|
481
|
+
return any(
|
|
482
|
+
_resolves(
|
|
483
|
+
item, context, provenance, imports, unsafe_imports, sources=_VALIDATION_ERROR, symbol="ValidationError"
|
|
484
|
+
)
|
|
485
|
+
for item in types
|
|
486
|
+
)
|