sarj-python-lint 1.6.1__tar.gz → 1.7.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.6.1 → sarj_python_lint-1.7.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_registry.py +4 -0
- sarj_python_lint-1.7.1/src/sarj_python_lint/rules/_resource_provenance.py +184 -0
- sarj_python_lint-1.7.1/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +101 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +22 -5
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +353 -63
- sarj_python_lint-1.7.1/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +143 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/.gitignore +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/LICENSE +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/README.md +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.6.1 → sarj_python_lint-1.7.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.7.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/
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from types import MappingProxyType
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
from sarj_python_lint.rules.async_cleanup_registered_synchronously import AsyncCleanupRegisteredSynchronously
|
|
6
7
|
from sarj_python_lint.rules.async_mock_call_without_await_assertion import AsyncMockCallWithoutAwaitAssertion
|
|
7
8
|
from sarj_python_lint.rules.complex_postgres_query_requires_architecture_review import (
|
|
8
9
|
ComplexPostgresQueryRequiresArchitectureReview,
|
|
@@ -171,6 +172,7 @@ from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDele
|
|
|
171
172
|
from sarj_python_lint.rules.store_insert_requires_on_conflict import (
|
|
172
173
|
StoreInsertRequiresOnConflict,
|
|
173
174
|
)
|
|
175
|
+
from sarj_python_lint.rules.subprocess_kill_without_reap import SubprocessKillWithoutReap
|
|
174
176
|
from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
|
|
175
177
|
from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
|
|
176
178
|
from sarj_python_lint.rules.typed_error_reasons import TypedErrorReasons
|
|
@@ -186,6 +188,8 @@ if TYPE_CHECKING:
|
|
|
186
188
|
|
|
187
189
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
188
190
|
{
|
|
191
|
+
AsyncCleanupRegisteredSynchronously.id: AsyncCleanupRegisteredSynchronously,
|
|
192
|
+
SubprocessKillWithoutReap.id: SubprocessKillWithoutReap,
|
|
189
193
|
NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
|
|
190
194
|
RequireExplicitPsycopgTransaction.id: RequireExplicitPsycopgTransaction,
|
|
191
195
|
AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SCOPES = (
|
|
13
|
+
ast.Module,
|
|
14
|
+
ast.FunctionDef,
|
|
15
|
+
ast.AsyncFunctionDef,
|
|
16
|
+
ast.ClassDef,
|
|
17
|
+
ast.Lambda,
|
|
18
|
+
ast.ListComp,
|
|
19
|
+
ast.SetComp,
|
|
20
|
+
ast.DictComp,
|
|
21
|
+
ast.GeneratorExp,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@final
|
|
26
|
+
class ResourceProvenance:
|
|
27
|
+
def __init__(self, context: PythonFileContext) -> None:
|
|
28
|
+
self.context = context
|
|
29
|
+
self.bindings: dict[ast.AST | None, dict[str, list[ast.AST]]] = defaultdict(lambda: defaultdict(list))
|
|
30
|
+
self.reads: dict[str, list[ast.Name]] = defaultdict(list)
|
|
31
|
+
self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
|
|
32
|
+
self.wildcard = False
|
|
33
|
+
self.mutated: set[str] = set()
|
|
34
|
+
for node in context.nodes(ast.AST):
|
|
35
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
36
|
+
self.reads[node.id].append(node)
|
|
37
|
+
|
|
38
|
+
for name in _bound_names(node):
|
|
39
|
+
self.bindings[self.scope(node)][name].append(node)
|
|
40
|
+
self.wildcard |= name == "*"
|
|
41
|
+
if isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store | ast.Del):
|
|
42
|
+
root = _root_name(node)
|
|
43
|
+
if root is not None:
|
|
44
|
+
self.mutated.add(root)
|
|
45
|
+
|
|
46
|
+
def scope(self, node: ast.AST) -> ast.AST | None:
|
|
47
|
+
parent = self.context.parents.get(node)
|
|
48
|
+
while parent is not None and not isinstance(parent, _SCOPES):
|
|
49
|
+
parent = self.context.parents.get(parent)
|
|
50
|
+
return parent
|
|
51
|
+
|
|
52
|
+
def binding(self, name: str, at: ast.AST) -> ast.AST | None:
|
|
53
|
+
scope = self.scope(at)
|
|
54
|
+
while scope is not None:
|
|
55
|
+
nodes = self.bindings.get(scope, {}).get(name)
|
|
56
|
+
if nodes:
|
|
57
|
+
return nodes[0] if len(nodes) == 1 else None
|
|
58
|
+
scope = self.scope(scope)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def imported(self, node: ast.expr, qualified: str) -> bool:
|
|
62
|
+
root = _root_name(node)
|
|
63
|
+
if self.wildcard or root is None or root in self.mutated:
|
|
64
|
+
return False
|
|
65
|
+
binding = self.binding(root, node)
|
|
66
|
+
for read in self.reads.get(root, ()):
|
|
67
|
+
parent = self.context.parents.get(read)
|
|
68
|
+
if isinstance(parent, ast.Attribute | ast.ExceptHandler):
|
|
69
|
+
continue
|
|
70
|
+
if not (isinstance(parent, ast.Call) and parent.func is read):
|
|
71
|
+
return False
|
|
72
|
+
return (
|
|
73
|
+
isinstance(binding, ast.alias)
|
|
74
|
+
and isinstance(self.scope(binding), ast.Module)
|
|
75
|
+
and self.context.imports.resolved_qualified_name(node) == qualified
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def constructor(self, name: ast.Name, at: ast.AST) -> ast.Call | None:
|
|
79
|
+
binding = self.binding(name.id, at)
|
|
80
|
+
if not isinstance(binding, ast.Name):
|
|
81
|
+
return None
|
|
82
|
+
statement = self.context.parents.get(binding)
|
|
83
|
+
if isinstance(statement, ast.withitem):
|
|
84
|
+
owner = self.context.parents.get(statement)
|
|
85
|
+
if (
|
|
86
|
+
binding is statement.optional_vars
|
|
87
|
+
and self.contains(owner, at)
|
|
88
|
+
and isinstance(statement.context_expr, ast.Call)
|
|
89
|
+
):
|
|
90
|
+
return statement.context_expr
|
|
91
|
+
return None
|
|
92
|
+
value = self.assignment_value(binding, at)
|
|
93
|
+
return value if isinstance(value, ast.Call) else None
|
|
94
|
+
|
|
95
|
+
def assignment_value(self, binding: ast.Name, at: ast.AST) -> ast.expr | None:
|
|
96
|
+
statement = self.context.parents.get(binding)
|
|
97
|
+
if not isinstance(statement, ast.Assign | ast.AnnAssign):
|
|
98
|
+
return None
|
|
99
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
|
|
100
|
+
return None
|
|
101
|
+
if not isinstance(self.context.parents.get(statement), ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
102
|
+
return None
|
|
103
|
+
return statement.value if statement.lineno < getattr(at, "lineno", 0) else None
|
|
104
|
+
|
|
105
|
+
def async_function(self, value: ast.expr, at: ast.AST) -> ast.AsyncFunctionDef | None:
|
|
106
|
+
seen: set[str] = set()
|
|
107
|
+
while isinstance(value, ast.Name) and value.id not in seen:
|
|
108
|
+
seen.add(value.id)
|
|
109
|
+
if value.id in self.mutated:
|
|
110
|
+
return None
|
|
111
|
+
binding = self.binding(value.id, at)
|
|
112
|
+
if isinstance(binding, ast.AsyncFunctionDef):
|
|
113
|
+
owner = self.context.parents.get(binding)
|
|
114
|
+
if not isinstance(owner, ast.Module | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
115
|
+
return None
|
|
116
|
+
if binding.decorator_list or any(
|
|
117
|
+
isinstance(node, ast.Yield | ast.YieldFrom) for node in ast.walk(binding)
|
|
118
|
+
):
|
|
119
|
+
return None
|
|
120
|
+
return binding
|
|
121
|
+
if not isinstance(binding, ast.Name):
|
|
122
|
+
return None
|
|
123
|
+
resolved = self.assignment_value(binding, at)
|
|
124
|
+
if resolved is None:
|
|
125
|
+
return None
|
|
126
|
+
value, at = resolved, binding
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def unescaped(self, name: ast.Name, at: ast.AST, *, allow_context: bool = False) -> bool:
|
|
130
|
+
binding = self.binding(name.id, at)
|
|
131
|
+
if binding is None or name.id in self.mutated:
|
|
132
|
+
return False
|
|
133
|
+
key = (binding, allow_context)
|
|
134
|
+
if key not in self.escape_cache:
|
|
135
|
+
self.escape_cache[key] = all(
|
|
136
|
+
self._method_or_context_read(read, allow_context=allow_context)
|
|
137
|
+
for read in self.reads.get(name.id, ())
|
|
138
|
+
if self.binding(read.id, read) is binding
|
|
139
|
+
)
|
|
140
|
+
return self.escape_cache[key]
|
|
141
|
+
|
|
142
|
+
def _method_or_context_read(self, read: ast.Name, *, allow_context: bool) -> bool:
|
|
143
|
+
parent = self.context.parents.get(read)
|
|
144
|
+
if allow_context and isinstance(parent, ast.withitem) and parent.context_expr is read:
|
|
145
|
+
return True
|
|
146
|
+
if not isinstance(parent, ast.Attribute) or parent.value is not read:
|
|
147
|
+
return False
|
|
148
|
+
call = self.context.parents.get(parent)
|
|
149
|
+
return isinstance(call, ast.Call) and call.func is parent
|
|
150
|
+
|
|
151
|
+
def contains(self, owner: ast.AST | None, node: ast.AST) -> bool:
|
|
152
|
+
while node in self.context.parents:
|
|
153
|
+
node = self.context.parents[node]
|
|
154
|
+
if node is owner:
|
|
155
|
+
return True
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _root_name(node: ast.expr) -> str | None:
|
|
160
|
+
while isinstance(node, ast.Attribute):
|
|
161
|
+
node = node.value
|
|
162
|
+
return node.id if isinstance(node, ast.Name) else None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _bound_names(node: ast.AST) -> tuple[str, ...]:
|
|
166
|
+
match node:
|
|
167
|
+
case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
|
|
168
|
+
return (name,)
|
|
169
|
+
case ast.alias():
|
|
170
|
+
return (node.asname or node.name.partition(".")[0],)
|
|
171
|
+
case (
|
|
172
|
+
ast.FunctionDef(name=name)
|
|
173
|
+
| ast.AsyncFunctionDef(name=name)
|
|
174
|
+
| ast.ClassDef(name=name)
|
|
175
|
+
| ast.ExceptHandler(name=str() as name)
|
|
176
|
+
| ast.MatchAs(name=str() as name)
|
|
177
|
+
| ast.MatchStar(name=str() as name)
|
|
178
|
+
| ast.MatchMapping(rest=str() as name)
|
|
179
|
+
):
|
|
180
|
+
return (name,)
|
|
181
|
+
case ast.Global(names=names) | ast.Nonlocal(names=names):
|
|
182
|
+
return tuple(names)
|
|
183
|
+
case _:
|
|
184
|
+
return ()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_BAD = "from contextlib import AsyncExitStack\nasync def close():\n await release()\nasync def run():\n async with AsyncExitStack() as stack:\n stack.callback(close)\n"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncCleanupRegisteredSynchronously(Rule):
|
|
30
|
+
id: str = "async-cleanup-registered-synchronously"
|
|
31
|
+
code: str = "SARJ463"
|
|
32
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
33
|
+
default_level=Severity.WARNING,
|
|
34
|
+
summary="Do not register a proven async cleanup function with ExitStack.callback.",
|
|
35
|
+
rationale="The synchronous callback API discards the returned coroutine without awaiting it, so the cleanup body never runs.",
|
|
36
|
+
remediation="Use AsyncExitStack and register the coroutine function with push_async_callback.",
|
|
37
|
+
category=RuleCategory.CORRECTNESS,
|
|
38
|
+
autofix=AutofixPolicy.NONE,
|
|
39
|
+
limitations=(
|
|
40
|
+
"Requires a proven contextlib ExitStack or AsyncExitStack constructor and an undecorated, visible async function, optionally through simple function aliases.",
|
|
41
|
+
"Unknown methods/callables, decorated functions, async generators, conditional bindings, transferred stacks, wildcard imports, and generated files are excluded.",
|
|
42
|
+
"Resource aliases and interprocedural mutation are not inferred. No autofix is offered because executing previously dropped cleanup can raise new exceptions.",
|
|
43
|
+
),
|
|
44
|
+
examples=(
|
|
45
|
+
RuleExample(
|
|
46
|
+
example_id="dropped-cleanup",
|
|
47
|
+
title="Synchronous callbacks do not await cleanup",
|
|
48
|
+
outcome=ExampleOutcome.MATCH,
|
|
49
|
+
files=(ExampleFile.python("app/resources.py", _BAD),),
|
|
50
|
+
focus_path=PurePosixPath("app/resources.py"),
|
|
51
|
+
expected_count=1,
|
|
52
|
+
public=True,
|
|
53
|
+
),
|
|
54
|
+
RuleExample(
|
|
55
|
+
example_id="awaited-cleanup",
|
|
56
|
+
title="Register cleanup with the async API",
|
|
57
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
58
|
+
files=(
|
|
59
|
+
ExampleFile.python("app/resources.py", _BAD.replace("stack.callback", "stack.push_async_callback")),
|
|
60
|
+
),
|
|
61
|
+
focus_path=PurePosixPath("app/resources.py"),
|
|
62
|
+
expected_count=0,
|
|
63
|
+
public=True,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
description: str = documentation.summary
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
71
|
+
if "callback" not in context.source or context.generated or context.tree is None:
|
|
72
|
+
return []
|
|
73
|
+
provenance = ResourceProvenance(context)
|
|
74
|
+
findings: list[Diagnostic] = []
|
|
75
|
+
for call in context.nodes(ast.Call):
|
|
76
|
+
if not _wrong_callback(call, provenance) or is_suppressed(context.source_lines, call.lineno, self.code):
|
|
77
|
+
continue
|
|
78
|
+
findings.append(
|
|
79
|
+
Diagnostic(
|
|
80
|
+
path=context.path,
|
|
81
|
+
line=call.lineno,
|
|
82
|
+
col=call.col_offset + 1,
|
|
83
|
+
code=self.code,
|
|
84
|
+
severity=Severity.WARNING,
|
|
85
|
+
message="synchronous ExitStack.callback does not await this async cleanup function; use AsyncExitStack.push_async_callback",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
return findings
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wrong_callback(call: ast.Call, provenance: ResourceProvenance) -> bool:
|
|
92
|
+
if not isinstance(call.func, ast.Attribute) or call.func.attr != "callback" or not call.args:
|
|
93
|
+
return False
|
|
94
|
+
receiver = call.func.value
|
|
95
|
+
if not isinstance(receiver, ast.Name) or not provenance.unescaped(receiver, call, allow_context=True):
|
|
96
|
+
return False
|
|
97
|
+
constructor = provenance.constructor(receiver, call)
|
|
98
|
+
if constructor is None or constructor.args or constructor.keywords:
|
|
99
|
+
return False
|
|
100
|
+
known = any(provenance.imported(constructor.func, f"contextlib.{name}") for name in ("ExitStack", "AsyncExitStack"))
|
|
101
|
+
return known and provenance.async_function(call.args[0], call) is not None
|
|
@@ -36,13 +36,13 @@ class PreferRegexFullmatch(Rule):
|
|
|
36
36
|
code: str = "SARJ460"
|
|
37
37
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
38
38
|
default_level=Severity.WARNING,
|
|
39
|
-
summary="An anchored regex
|
|
39
|
+
summary="An anchored regex validation predicate does not require a full-string match.",
|
|
40
40
|
rationale="The regex `$` anchor can match before a final newline, so an anchored match can accept a value that was not fully validated.",
|
|
41
41
|
remediation="Use `re.fullmatch` or the compiled pattern's `fullmatch` method for whole-value validation; review whether a terminal newline is intentionally allowed.",
|
|
42
42
|
category=RuleCategory.CORRECTNESS,
|
|
43
43
|
autofix=AutofixPolicy.NONE,
|
|
44
44
|
limitations=(
|
|
45
|
-
"
|
|
45
|
+
"Checks direct negative match/is-None rejection guards and returns of bool(match), not match, or match is/is-not None. Match-object returns, extraction, search, and compound conditions are excluded.",
|
|
46
46
|
"Patterns must be valid string or bytes literals bounded by `^` or `\\A` and an unescaped outer `$`; top-level alternation, inline extension groups other than noncapturing groups, and flags other than literal zero are excluded.",
|
|
47
47
|
"Only imported standard-library re functions and unique, unconditional module or same-function compile bindings are resolved. Rebinding, import shadowing, escaped regex objects or modules, explicit match positions, tests, and generated files are excluded.",
|
|
48
48
|
"Binding checks are deliberately file-wide, so unrelated same-named locals can suppress findings. Dynamic monkeypatching and interprocedural changes are not inferred.",
|
|
@@ -85,8 +85,8 @@ class PreferRegexFullmatch(Rule):
|
|
|
85
85
|
)
|
|
86
86
|
compiled = _compiled_patterns(context, bindings, mutated)
|
|
87
87
|
findings: list[Diagnostic] = []
|
|
88
|
-
for guard in context.nodes(ast.If):
|
|
89
|
-
call =
|
|
88
|
+
for guard in context.nodes(ast.If, ast.Return):
|
|
89
|
+
call = _validation_call(guard, context, bindings)
|
|
90
90
|
if call is None or is_suppressed(context.source_lines, call.lineno, self.code):
|
|
91
91
|
continue
|
|
92
92
|
pattern = _match_pattern(context, call, bindings, mutated, compiled)
|
|
@@ -98,13 +98,30 @@ class PreferRegexFullmatch(Rule):
|
|
|
98
98
|
line=call.lineno,
|
|
99
99
|
col=call.col_offset + 1,
|
|
100
100
|
code=self.code,
|
|
101
|
-
message="anchored match is used
|
|
101
|
+
message="anchored match is used as a validation predicate, but `$` can accept a final newline; use fullmatch for whole-value validation and review newline handling.",
|
|
102
102
|
severity=Severity.WARNING,
|
|
103
103
|
)
|
|
104
104
|
)
|
|
105
105
|
return sorted(findings, key=lambda item: (item.line, item.col))
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _validation_call(node: ast.If | ast.Return, context: PythonFileContext, bindings: Counter[str]) -> ast.Call | None:
|
|
109
|
+
if isinstance(node, ast.If):
|
|
110
|
+
return _rejection_call(node)
|
|
111
|
+
match node.value:
|
|
112
|
+
case (
|
|
113
|
+
ast.UnaryOp(op=ast.Not(), operand=ast.Call() as call)
|
|
114
|
+
| ast.Compare(left=ast.Call() as call, ops=[ast.Is() | ast.IsNot()], comparators=[ast.Constant(value=None)])
|
|
115
|
+
):
|
|
116
|
+
return call
|
|
117
|
+
case ast.Call(func=ast.Name(id="bool"), args=[ast.Call() as call], keywords=[]):
|
|
118
|
+
if context.imports.builtin_is_unshadowed("bool") and bindings["bool"] == 0:
|
|
119
|
+
return call
|
|
120
|
+
case _:
|
|
121
|
+
pass
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
108
125
|
def _rejection_call(guard: ast.If) -> ast.Call | None:
|
|
109
126
|
if len(guard.body) != 1 or not isinstance(guard.body[0], ast.Raise):
|
|
110
127
|
return None
|