sarj-python-lint 0.81.14__tar.gz → 0.81.16__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-0.81.14 → sarj_python_lint-0.81.16}/PKG-INFO +1 -1
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/pyproject.toml +1 -1
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_repeated_test_body.py +46 -12
- sarj_python_lint-0.81.16/src/sarj_python_lint/rules/unused_test_factory_option.py +206 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/.gitignore +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/LICENSE +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/README.md +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.81.
|
|
3
|
+
Version: 0.81.16
|
|
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/
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -154,6 +154,7 @@ from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarrati
|
|
|
154
154
|
from sarj_python_lint.rules.typed_error_reasons import TypedErrorReasons
|
|
155
155
|
from sarj_python_lint.rules.uncontrolled_randomness_in_test import UncontrolledRandomnessInTest
|
|
156
156
|
from sarj_python_lint.rules.unused_mock_setup import UnusedMockSetup
|
|
157
|
+
from sarj_python_lint.rules.unused_test_factory_option import UnusedTestFactoryOption
|
|
157
158
|
|
|
158
159
|
|
|
159
160
|
if TYPE_CHECKING:
|
|
@@ -219,6 +220,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
219
220
|
NoFrozenAfterValidatorFieldWrite.id: NoFrozenAfterValidatorFieldWrite,
|
|
220
221
|
NoRepeatedTestBody.id: NoRepeatedTestBody,
|
|
221
222
|
UnusedMockSetup.id: UnusedMockSetup,
|
|
223
|
+
UnusedTestFactoryOption.id: UnusedTestFactoryOption,
|
|
222
224
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
223
225
|
PreferOrPattern.id: PreferOrPattern,
|
|
224
226
|
RequirePortForService.id: RequirePortForService,
|
|
@@ -44,6 +44,7 @@ _MIN_STATEMENTS = 3
|
|
|
44
44
|
|
|
45
45
|
# Members of a normalized-body group before it counts as copy-paste.
|
|
46
46
|
_MIN_GROUP = 2
|
|
47
|
+
_MIN_SHORT_CASE_GROUP = 3
|
|
47
48
|
|
|
48
49
|
# Small source-checker pairs often read better as individually named contracts.
|
|
49
50
|
# A longer uninterrupted run is a case table whose repeated shell obscures the cases.
|
|
@@ -114,21 +115,22 @@ class NoRepeatedTestBody(Rule):
|
|
|
114
115
|
limitations=(
|
|
115
116
|
"Only substantial sibling test bodies in one non-generated module are compared.",
|
|
116
117
|
"A run of at least five two-statement embedded-source checker cases is compared because the source documents are natural parameter values.",
|
|
118
|
+
"At least three consecutive call-and-literal-equality cases with corroborating behavior names are compared even with only two statements.",
|
|
117
119
|
"Meaningful docstring or comment differences keep tests distinct.",
|
|
118
120
|
"Two-test groups with varying literals require corroborating behavior names; long scenario prose and distinct API resources remain separate contracts.",
|
|
119
121
|
),
|
|
120
122
|
examples=(
|
|
121
123
|
RuleExample(
|
|
122
|
-
example_id="
|
|
123
|
-
title="
|
|
124
|
+
example_id="short-literal-cases",
|
|
125
|
+
title="Consecutive short cases repeat the same call and assertion",
|
|
124
126
|
outcome=ExampleOutcome.MATCH,
|
|
125
127
|
files=(
|
|
126
128
|
ExampleFile.python(
|
|
127
|
-
"tests/
|
|
128
|
-
'def
|
|
129
|
+
"tests/test_parser.py",
|
|
130
|
+
'def test_parse_one():\n result = parse("a")\n assert result == 1\n\ndef test_parse_two():\n result = parse("b")\n assert result == 2\n\ndef test_parse_three():\n result = parse("c")\n assert result == 3\n',
|
|
129
131
|
),
|
|
130
132
|
),
|
|
131
|
-
focus_path=PurePosixPath("tests/
|
|
133
|
+
focus_path=PurePosixPath("tests/test_parser.py"),
|
|
132
134
|
expected_count=1,
|
|
133
135
|
public=True,
|
|
134
136
|
),
|
|
@@ -349,7 +351,7 @@ def _duplicate_groups(tree: ast.Module, source: str) -> list[list[_Shape]]:
|
|
|
349
351
|
if in_test_case or _uses_unittest_api(node):
|
|
350
352
|
continue
|
|
351
353
|
outline = _Outline(node, container)
|
|
352
|
-
if outline.statements < _MIN_STATEMENTS and not outline.embedded_source_checker:
|
|
354
|
+
if outline.statements < _MIN_STATEMENTS and not (outline.embedded_source_checker or _is_short_case(node)):
|
|
353
355
|
continue
|
|
354
356
|
outlines.setdefault(outline.key, []).append(outline)
|
|
355
357
|
|
|
@@ -366,11 +368,22 @@ def _duplicate_groups(tree: ast.Module, source: str) -> list[list[_Shape]]:
|
|
|
366
368
|
shape = _Shape(outline.node, comments, imports)
|
|
367
369
|
groups.setdefault(shape.key, []).append(shape)
|
|
368
370
|
for members in groups.values():
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
if all(_is_short_case(member.node) for member in members):
|
|
372
|
+
candidates = [
|
|
373
|
+
run
|
|
374
|
+
for run in _consecutive_groups(members, positions)
|
|
375
|
+
if len(run) >= _MIN_SHORT_CASE_GROUP
|
|
376
|
+
and _test_name_tokens(run[0].node.name).intersection(
|
|
377
|
+
*(_test_name_tokens(member.node.name) for member in run[1:])
|
|
378
|
+
)
|
|
379
|
+
- _WEAK_TEST_NAME_TOKENS
|
|
380
|
+
]
|
|
381
|
+
else:
|
|
382
|
+
candidates = (
|
|
383
|
+
_consecutive_embedded_source_groups(members, positions)
|
|
384
|
+
if all(member.embedded_source_checker for member in members)
|
|
385
|
+
else [members]
|
|
386
|
+
)
|
|
374
387
|
found.extend(
|
|
375
388
|
candidate
|
|
376
389
|
for candidate in candidates
|
|
@@ -391,13 +404,21 @@ def duplicate_test_owner_ids(tree: ast.Module, source: str) -> frozenset[int]:
|
|
|
391
404
|
|
|
392
405
|
|
|
393
406
|
def _consecutive_embedded_source_groups(members: list[_Shape], positions: dict[int, int]) -> list[list[_Shape]]:
|
|
407
|
+
return [
|
|
408
|
+
run
|
|
409
|
+
for run in _consecutive_groups(members, positions)
|
|
410
|
+
if len(run) >= _MIN_EMBEDDED_SOURCE_GROUP and _shares_embedded_source_signal(run)
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _consecutive_groups(members: list[_Shape], positions: dict[int, int]) -> list[list[_Shape]]:
|
|
394
415
|
ordered = sorted(members, key=lambda member: positions[id(member.node)])
|
|
395
416
|
runs: list[list[_Shape]] = []
|
|
396
417
|
for member in ordered:
|
|
397
418
|
if not runs or positions[id(member.node)] != positions[id(runs[-1][-1].node)] + 1:
|
|
398
419
|
runs.append([])
|
|
399
420
|
runs[-1].append(member)
|
|
400
|
-
return
|
|
421
|
+
return runs
|
|
401
422
|
|
|
402
423
|
|
|
403
424
|
def _shares_embedded_source_signal(members: list[_Shape]) -> bool:
|
|
@@ -546,6 +567,19 @@ def _body_without_docstring(node: ast.FunctionDef | ast.AsyncFunctionDef) -> lis
|
|
|
546
567
|
return body
|
|
547
568
|
|
|
548
569
|
|
|
570
|
+
def _is_short_case(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
571
|
+
match _body_without_docstring(node):
|
|
572
|
+
case [
|
|
573
|
+
ast.Assign(targets=[ast.Name(id=name)], value=ast.Call()),
|
|
574
|
+
ast.Assert(
|
|
575
|
+
test=ast.Compare(left=ast.Name(id=result), ops=[ast.Eq()], comparators=[ast.Constant()]), msg=None
|
|
576
|
+
),
|
|
577
|
+
]:
|
|
578
|
+
return name == result
|
|
579
|
+
case _:
|
|
580
|
+
return False
|
|
581
|
+
|
|
582
|
+
|
|
549
583
|
def _is_embedded_source_checker(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
550
584
|
match _body_without_docstring(node):
|
|
551
585
|
case [
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, TypeGuard, final
|
|
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
|
+
parse_or_none,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._ast_index import nodes
|
|
21
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_MIN_INVARIANT_CALLS = 2
|
|
29
|
+
|
|
30
|
+
_REFLECTION = frozenset({"globals", "locals", "vars", "eval", "exec", "getattr", "__import__", "__all__"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@final
|
|
34
|
+
class UnusedTestFactoryOption(Rule):
|
|
35
|
+
id = "unused-test-factory-option"
|
|
36
|
+
code = "SARJ443"
|
|
37
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
38
|
+
summary="A private test factory exposes a literal option that visible callers never vary.",
|
|
39
|
+
rationale="Unused customization obscures the values that actually distinguish test scenarios.",
|
|
40
|
+
remediation="Keep the value in the factory's construction instead of exposing an unused option; retain it if external callers need it.",
|
|
41
|
+
category=RuleCategory.TESTING,
|
|
42
|
+
autofix=AutofixPolicy.NONE,
|
|
43
|
+
limitations=(
|
|
44
|
+
"Only module-level private _make_ and _build_ helpers consisting of a single return call are considered.",
|
|
45
|
+
"Only literal defaults with direct, unambiguous calls in this file are reported. Explicit options require at least two calls with the same effective literal value. Decorators, rebinding, callable escapes, reflection, and argument unpacking exclude the helper.",
|
|
46
|
+
"Cross-module callers cannot be proven absent. Shared helpers require an exact suppression; this advisory never autofixes signatures or changes default evaluation timing.",
|
|
47
|
+
),
|
|
48
|
+
examples=(
|
|
49
|
+
RuleExample(
|
|
50
|
+
example_id="invariant-size-option",
|
|
51
|
+
title="Repeated calls never vary the option",
|
|
52
|
+
outcome=ExampleOutcome.MATCH,
|
|
53
|
+
files=(
|
|
54
|
+
ExampleFile.python(
|
|
55
|
+
"tests/test_widget.py",
|
|
56
|
+
"def _make_widget(*, size=3):\n return Widget(size=size)\n\n_make_widget()\n_make_widget(size=3)\n",
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
60
|
+
expected_count=1,
|
|
61
|
+
public=True,
|
|
62
|
+
),
|
|
63
|
+
RuleExample(
|
|
64
|
+
example_id="exercised-size-option",
|
|
65
|
+
title="Keep customization that a caller exercises",
|
|
66
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
67
|
+
files=(
|
|
68
|
+
ExampleFile.python(
|
|
69
|
+
"tests/test_widget.py",
|
|
70
|
+
"def _make_widget(*, size=3):\n return Widget(size=size)\n\ndef test_widget():\n assert _make_widget(size=4)\n",
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
focus_path=PurePosixPath("tests/test_widget.py"),
|
|
74
|
+
expected_count=0,
|
|
75
|
+
public=True,
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
description = documentation.summary
|
|
80
|
+
|
|
81
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
82
|
+
if not is_test_path(path) or is_generated(path, source) or not ("_make_" in source or "_build_" in source):
|
|
83
|
+
return []
|
|
84
|
+
tree = parse_or_none(path, source)
|
|
85
|
+
if tree is None or (
|
|
86
|
+
any(node.id in _REFLECTION for node in nodes(tree, ast.Name))
|
|
87
|
+
or any(node.attr in _REFLECTION or node.attr == "__dict__" for node in nodes(tree, ast.Attribute))
|
|
88
|
+
or any(node.name in _REFLECTION or node.name == "*" for node in nodes(tree, ast.alias))
|
|
89
|
+
):
|
|
90
|
+
return []
|
|
91
|
+
lines = source.splitlines()
|
|
92
|
+
findings: list[Diagnostic] = []
|
|
93
|
+
for function in tree.body:
|
|
94
|
+
if not _is_factory(function):
|
|
95
|
+
continue
|
|
96
|
+
calls = _direct_calls(tree, function)
|
|
97
|
+
if not calls:
|
|
98
|
+
continue
|
|
99
|
+
bound = _bound_calls(function, calls)
|
|
100
|
+
if bound is None:
|
|
101
|
+
continue
|
|
102
|
+
supplied = {name for arguments in bound for name in arguments}
|
|
103
|
+
positional = [*function.args.posonlyargs, *function.args.args]
|
|
104
|
+
defaults = [
|
|
105
|
+
*zip(positional[len(positional) - len(function.args.defaults) :], function.args.defaults, strict=True),
|
|
106
|
+
*zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True),
|
|
107
|
+
]
|
|
108
|
+
used = {node.id for node in nodes(function, ast.Name) if isinstance(node.ctx, ast.Load)}
|
|
109
|
+
for argument, default in defaults:
|
|
110
|
+
if argument.arg not in used or not isinstance(default, ast.Constant):
|
|
111
|
+
continue
|
|
112
|
+
if default.value is not None and not isinstance(default.value, (str, bytes, int, float)):
|
|
113
|
+
continue
|
|
114
|
+
explicitly_supplied = argument.arg in supplied
|
|
115
|
+
if explicitly_supplied:
|
|
116
|
+
values = [arguments.get(argument.arg, default) for arguments in bound]
|
|
117
|
+
if len(values) < _MIN_INVARIANT_CALLS or any(
|
|
118
|
+
not isinstance(value, ast.Constant) or ast.dump(value) != ast.dump(values[0])
|
|
119
|
+
for value in values
|
|
120
|
+
):
|
|
121
|
+
continue
|
|
122
|
+
if is_suppressed(lines, argument.lineno, self.code):
|
|
123
|
+
continue
|
|
124
|
+
findings.append(
|
|
125
|
+
Diagnostic(
|
|
126
|
+
path=path,
|
|
127
|
+
line=argument.lineno,
|
|
128
|
+
col=argument.col_offset + 1,
|
|
129
|
+
code=self.code,
|
|
130
|
+
severity=Severity.WARNING,
|
|
131
|
+
message=(
|
|
132
|
+
f"Direct callers in this file always use the same literal for `{function.name}.{argument.arg}`; "
|
|
133
|
+
"consider keeping that value in the factory instead of repeating an invariant option. Retain it if external callers need it."
|
|
134
|
+
if explicitly_supplied
|
|
135
|
+
else f"No direct caller in this file supplies `{function.name}.{argument.arg}`; keep its literal value in the factory instead of exposing an unused option. Retain it if external callers need it."
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return sorted(findings, key=lambda item: (item.line, item.col))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _is_factory(node: ast.stmt) -> TypeGuard[ast.FunctionDef]:
|
|
143
|
+
return (
|
|
144
|
+
isinstance(node, ast.FunctionDef)
|
|
145
|
+
and node.name.startswith(("_make_", "_build_"))
|
|
146
|
+
and not node.decorator_list
|
|
147
|
+
and node.args.vararg is None
|
|
148
|
+
and node.args.kwarg is None
|
|
149
|
+
and len(node.body) == 1
|
|
150
|
+
and isinstance(node.body[0], ast.Return)
|
|
151
|
+
and isinstance(node.body[0].value, ast.Call)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _direct_calls(tree: ast.Module, function: ast.FunctionDef) -> list[ast.Call]:
|
|
156
|
+
name = function.name
|
|
157
|
+
if sum(node.name == name for node in nodes(tree, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) != 1:
|
|
158
|
+
return []
|
|
159
|
+
if any(node.arg == name for node in nodes(tree, ast.arg)):
|
|
160
|
+
return []
|
|
161
|
+
if any((node.asname or node.name.split(".")[0]) == name for node in nodes(tree, ast.alias)):
|
|
162
|
+
return []
|
|
163
|
+
if any(node.attr == name for node in nodes(tree, ast.Attribute)):
|
|
164
|
+
return []
|
|
165
|
+
if any(node.value == name for node in nodes(tree, ast.Constant)):
|
|
166
|
+
return []
|
|
167
|
+
if any(node.name == name for node in nodes(tree, ast.MatchAs, ast.MatchStar, ast.ExceptHandler)):
|
|
168
|
+
return []
|
|
169
|
+
if any(node.rest == name for node in nodes(tree, ast.MatchMapping)):
|
|
170
|
+
return []
|
|
171
|
+
references = [node for node in nodes(tree, ast.Name) if node.id == name]
|
|
172
|
+
calls = [node for node in nodes(tree, ast.Call) if isinstance(node.func, ast.Name) and node.func.id == name]
|
|
173
|
+
if not calls or len(references) != len(calls) or any(not isinstance(node.ctx, ast.Load) for node in references):
|
|
174
|
+
return []
|
|
175
|
+
if any(
|
|
176
|
+
any(isinstance(arg, ast.Starred) for arg in call.args) or any(kw.arg is None for kw in call.keywords)
|
|
177
|
+
for call in calls
|
|
178
|
+
):
|
|
179
|
+
return []
|
|
180
|
+
if any(function.lineno <= call.lineno <= (function.end_lineno or function.lineno) for call in calls):
|
|
181
|
+
return []
|
|
182
|
+
return calls
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _bound_calls(function: ast.FunctionDef, calls: list[ast.Call]) -> list[dict[str, ast.expr]] | None:
|
|
186
|
+
positional = [*function.args.posonlyargs, *function.args.args]
|
|
187
|
+
keywords = {arg.arg for arg in (*function.args.args, *function.args.kwonlyargs)}
|
|
188
|
+
required = {arg.arg for arg in positional[: len(positional) - len(function.args.defaults)]}
|
|
189
|
+
required.update(
|
|
190
|
+
arg.arg
|
|
191
|
+
for arg, default in zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True)
|
|
192
|
+
if default is None
|
|
193
|
+
)
|
|
194
|
+
bound: list[dict[str, ast.expr]] = []
|
|
195
|
+
for call in calls:
|
|
196
|
+
if len(call.args) > len(positional):
|
|
197
|
+
return None
|
|
198
|
+
arguments = {arg.arg: value for arg, value in zip(positional[: len(call.args)], call.args, strict=True)}
|
|
199
|
+
for keyword in call.keywords:
|
|
200
|
+
if keyword.arg not in keywords or keyword.arg in arguments:
|
|
201
|
+
return None
|
|
202
|
+
arguments[keyword.arg] = keyword.value
|
|
203
|
+
if not required <= arguments.keys():
|
|
204
|
+
return None
|
|
205
|
+
bound.append(arguments)
|
|
206
|
+
return bound
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/_python_target.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/__init__.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_comments.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_fastapi.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_imports.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.14 → sarj_python_lint-0.81.16}/src/sarj_python_lint/rules/stepdown.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
|