sarj-python-lint 0.77.0__tar.gz → 0.78.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/PKG-INFO +6 -6
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/README.md +2 -2
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/pyproject.toml +4 -4
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/__main__.py +5 -1
- sarj_python_lint-0.78.0/src/sarj_python_lint/_analysis_session.py +11 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rule_base.py +7 -2
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_first_party.py +80 -47
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_paths.py +9 -13
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_project_index.py +11 -5
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +9 -7
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +87 -24
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +7 -9
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/preserve_enum_types.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +1 -1
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/.gitignore +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/LICENSE +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.78.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
7
|
-
Project-URL: Changelog, https://github.com/sarj-ai/standards/releases
|
|
8
|
-
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
9
|
-
Project-URL: Issues, https://github.com/sarj-ai/standards/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/sarj-ai/code-standards/releases
|
|
8
|
+
Project-URL: Repository, https://github.com/sarj-ai/code-standards
|
|
9
|
+
Project-URL: Issues, https://github.com/sarj-ai/code-standards/issues
|
|
10
10
|
Author: sarj-ai
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
22
|
-
<!-- Generated by `
|
|
22
|
+
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
23
23
|
|
|
24
24
|
# sarj-python-lint
|
|
25
25
|
|
|
@@ -33,4 +33,4 @@ uv tool install sarj-python-lint
|
|
|
33
33
|
sarj-python-lint --help
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
|
|
36
|
+
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Generated by `
|
|
1
|
+
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
2
2
|
|
|
3
3
|
# sarj-python-lint
|
|
4
4
|
|
|
@@ -12,4 +12,4 @@ uv tool install sarj-python-lint
|
|
|
12
12
|
sarj-python-lint --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
|
|
15
|
+
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.78.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -24,9 +24,9 @@ sarj-ratchet = "sarj_python_lint._ratchet_cli:main"
|
|
|
24
24
|
[project.urls]
|
|
25
25
|
Homepage = "https://code-standards.sarj.ai/rules/python/"
|
|
26
26
|
Documentation = "https://code-standards.sarj.ai/rules/python/"
|
|
27
|
-
Changelog = "https://github.com/sarj-ai/standards/releases"
|
|
28
|
-
Repository = "https://github.com/sarj-ai/standards"
|
|
29
|
-
Issues = "https://github.com/sarj-ai/standards/issues"
|
|
27
|
+
Changelog = "https://github.com/sarj-ai/code-standards/releases"
|
|
28
|
+
Repository = "https://github.com/sarj-ai/code-standards"
|
|
29
|
+
Issues = "https://github.com/sarj-ai/code-standards/issues"
|
|
30
30
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
@@ -10,6 +10,7 @@ from types import MappingProxyType
|
|
|
10
10
|
from typing import NamedTuple
|
|
11
11
|
|
|
12
12
|
from sarj_python_lint import __version__
|
|
13
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
13
14
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
14
15
|
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Severity, is_suppressed
|
|
15
16
|
from sarj_python_lint.rules import REGISTRY
|
|
@@ -77,6 +78,9 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
77
78
|
sys.stderr.write(f"available: {', '.join(sorted(REGISTRY))}\n")
|
|
78
79
|
raise SystemExit(2)
|
|
79
80
|
rules = [REGISTRY[rid]() for rid in rule_ids]
|
|
81
|
+
session = AnalysisSession()
|
|
82
|
+
for rule in rules:
|
|
83
|
+
rule.prepare_session(session)
|
|
80
84
|
clear_path_caches()
|
|
81
85
|
expanded = _expand_paths(paths)
|
|
82
86
|
loaded: dict[Path, str] = {}
|
|
@@ -87,7 +91,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
87
91
|
continue
|
|
88
92
|
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
89
93
|
if project_rules:
|
|
90
|
-
indexes = ProjectIndexSet.build(expanded, loaded)
|
|
94
|
+
indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
|
|
91
95
|
for rule in project_rules:
|
|
92
96
|
rule.prepare(indexes)
|
|
93
97
|
diags: list[Diagnostic] = []
|
|
@@ -12,11 +12,12 @@ from typing import TYPE_CHECKING, ClassVar, Final, Self
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Sequence
|
|
14
14
|
|
|
15
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
15
16
|
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# Each rule points directly to its executable examples.
|
|
19
|
-
REPO_BLOB: Final = "https://github.com/sarj-ai/standards/blob/main"
|
|
20
|
+
REPO_BLOB: Final = "https://github.com/sarj-ai/code-standards/blob/main"
|
|
20
21
|
TESTS_DIR: Final = "packages/python/tests/rules"
|
|
21
22
|
|
|
22
23
|
|
|
@@ -237,6 +238,10 @@ class Rule(ABC):
|
|
|
237
238
|
code: str
|
|
238
239
|
description: str
|
|
239
240
|
documentation: ClassVar[RuleDocumentation | None] = None
|
|
241
|
+
_analysis_session: AnalysisSession | None = None
|
|
242
|
+
|
|
243
|
+
def prepare_session(self, session: AnalysisSession) -> None:
|
|
244
|
+
self._analysis_session = session
|
|
240
245
|
|
|
241
246
|
@abstractmethod
|
|
242
247
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
@@ -281,7 +286,7 @@ class Rule(ABC):
|
|
|
281
286
|
return () if spec is None else spec.public_examples
|
|
282
287
|
|
|
283
288
|
|
|
284
|
-
class ProjectRule(Rule):
|
|
289
|
+
class ProjectRule(Rule, ABC):
|
|
285
290
|
_project_indexes: ProjectIndexSet | None = None
|
|
286
291
|
|
|
287
292
|
def prepare(self, indexes: ProjectIndexSet) -> None:
|
{sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from functools import lru_cache
|
|
6
5
|
import sys
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, final
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
@@ -53,22 +52,78 @@ _MAX_DIRS_SCANNED = 3000
|
|
|
53
52
|
_MAX_ANCESTORS = 24
|
|
54
53
|
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
@final
|
|
56
|
+
class FirstPartyFacts:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._project_roots: dict[Path, Path | None] = {}
|
|
59
|
+
self._roots: dict[Path, frozenset[str]] = {}
|
|
60
|
+
self._packages: dict[Path, tuple[tuple[str, Path], ...]] = {}
|
|
61
|
+
|
|
62
|
+
def project_root(self, path: Path) -> Path | None:
|
|
63
|
+
if path in self._project_roots:
|
|
64
|
+
return self._project_roots[path]
|
|
65
|
+
resolved = _resolved(path)
|
|
66
|
+
if resolved is None:
|
|
67
|
+
self._project_roots[path] = None
|
|
68
|
+
return None
|
|
69
|
+
ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
|
|
70
|
+
for ancestor in ancestors:
|
|
71
|
+
if (ancestor / _GIT_MARKER).exists():
|
|
72
|
+
self._project_roots[path] = ancestor
|
|
73
|
+
return ancestor
|
|
74
|
+
# No VCS boundary: take the OUTERMOST directory of the unbroken run of
|
|
75
|
+
# pyproject.toml ancestors, which is the workspace root in a uv workspace.
|
|
76
|
+
outermost: Path | None = None
|
|
77
|
+
for ancestor in ancestors:
|
|
78
|
+
if not (ancestor / _PROJECT_MARKER).exists():
|
|
79
|
+
if outermost is not None:
|
|
80
|
+
break
|
|
81
|
+
continue
|
|
82
|
+
outermost = ancestor
|
|
83
|
+
self._project_roots[path] = outermost
|
|
84
|
+
return outermost
|
|
85
|
+
|
|
86
|
+
def first_party_roots(self, root: Path) -> frozenset[str]:
|
|
87
|
+
cached = self._roots.get(root)
|
|
88
|
+
if cached is not None:
|
|
89
|
+
return cached
|
|
90
|
+
roots = frozenset(name for name, _ in self.first_party_packages(root))
|
|
91
|
+
self._roots[root] = roots
|
|
92
|
+
return roots
|
|
93
|
+
|
|
94
|
+
def first_party_packages(self, root: Path) -> tuple[tuple[str, Path], ...]:
|
|
95
|
+
cached = self._packages.get(root)
|
|
96
|
+
if cached is not None:
|
|
97
|
+
return cached
|
|
98
|
+
packages = _scan_first_party_packages(root)
|
|
99
|
+
self._packages[root] = packages
|
|
100
|
+
return packages
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fresh_facts(facts: FirstPartyFacts | None) -> FirstPartyFacts:
|
|
104
|
+
# Compatibility helper calls are independent requests. The CLI supplies one
|
|
105
|
+
# explicit instance so scans are still shared within a full analysis.
|
|
106
|
+
return facts if facts is not None else FirstPartyFacts()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_first_party_module(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
110
|
+
facts = _fresh_facts(facts)
|
|
57
111
|
top = module.partition(".")[0]
|
|
58
112
|
if not top or top in sys.stdlib_module_names:
|
|
59
113
|
return False
|
|
60
|
-
root =
|
|
114
|
+
root = facts.project_root(path)
|
|
61
115
|
if root is None:
|
|
62
116
|
# Unresolved ownership biases to third-party to avoid unfixable findings.
|
|
63
117
|
return False
|
|
64
|
-
return top in
|
|
118
|
+
return top in facts.first_party_roots(root)
|
|
65
119
|
|
|
66
120
|
|
|
67
|
-
def own_top_package(path: Path) -> str | None:
|
|
121
|
+
def own_top_package(path: Path, *, facts: FirstPartyFacts | None = None) -> str | None:
|
|
122
|
+
facts = _fresh_facts(facts)
|
|
68
123
|
resolved = _resolved(path)
|
|
69
124
|
if resolved is None:
|
|
70
125
|
return None
|
|
71
|
-
root =
|
|
126
|
+
root = facts.project_root(resolved)
|
|
72
127
|
top: str | None = None
|
|
73
128
|
for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
|
|
74
129
|
if (ancestor / "__init__.py").exists():
|
|
@@ -76,30 +131,33 @@ def own_top_package(path: Path) -> str | None:
|
|
|
76
131
|
if ancestor == root:
|
|
77
132
|
break
|
|
78
133
|
if top is None and root is not None:
|
|
79
|
-
roots =
|
|
134
|
+
roots = facts.first_party_roots(root)
|
|
80
135
|
if len(roots) == 1:
|
|
81
136
|
return next(iter(roots))
|
|
82
137
|
return top
|
|
83
138
|
|
|
84
139
|
|
|
85
|
-
def same_distribution(module: str, path: Path) -> bool:
|
|
86
|
-
|
|
140
|
+
def same_distribution(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
141
|
+
facts = _fresh_facts(facts)
|
|
142
|
+
ours = distribution_root(path, facts=facts)
|
|
87
143
|
if ours is None:
|
|
88
144
|
return False
|
|
89
|
-
root =
|
|
145
|
+
root = facts.project_root(path)
|
|
90
146
|
if root is None:
|
|
91
147
|
return False
|
|
92
148
|
top = module.partition(".")[0]
|
|
93
149
|
return any(
|
|
94
|
-
name == top and distribution_root(package_dir) == ours
|
|
150
|
+
name == top and distribution_root(package_dir, facts=facts) == ours
|
|
151
|
+
for name, package_dir in facts.first_party_packages(root)
|
|
95
152
|
)
|
|
96
153
|
|
|
97
154
|
|
|
98
|
-
def distribution_root(path: Path) -> Path | None:
|
|
155
|
+
def distribution_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
|
|
156
|
+
facts = _fresh_facts(facts)
|
|
99
157
|
resolved = _resolved(path)
|
|
100
158
|
if resolved is None:
|
|
101
159
|
return None
|
|
102
|
-
root =
|
|
160
|
+
root = facts.project_root(resolved)
|
|
103
161
|
for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
|
|
104
162
|
if any((ancestor / marker).exists() for marker in _DISTRIBUTION_MARKERS):
|
|
105
163
|
return ancestor
|
|
@@ -108,19 +166,21 @@ def distribution_root(path: Path) -> Path | None:
|
|
|
108
166
|
return None
|
|
109
167
|
|
|
110
168
|
|
|
111
|
-
def has_first_party_source(module: str, path: Path) -> bool:
|
|
169
|
+
def has_first_party_source(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
170
|
+
facts = _fresh_facts(facts)
|
|
112
171
|
top, _, rest = module.partition(".")
|
|
113
|
-
root =
|
|
172
|
+
root = facts.project_root(path)
|
|
114
173
|
if root is None:
|
|
115
174
|
return False
|
|
116
175
|
segments = rest.split(".") if rest else []
|
|
117
176
|
return any(
|
|
118
|
-
name == top and _declares_module(package_dir, segments)
|
|
177
|
+
name == top and _declares_module(package_dir, segments)
|
|
178
|
+
for name, package_dir in facts.first_party_packages(root)
|
|
119
179
|
)
|
|
120
180
|
|
|
121
181
|
|
|
122
|
-
def project_root(path: Path) -> Path | None:
|
|
123
|
-
return
|
|
182
|
+
def project_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
|
|
183
|
+
return _fresh_facts(facts).project_root(path)
|
|
124
184
|
|
|
125
185
|
|
|
126
186
|
def _declares_module(package_dir: Path, segments: list[str]) -> bool:
|
|
@@ -138,34 +198,7 @@ def _resolved(path: Path) -> Path | None:
|
|
|
138
198
|
return None
|
|
139
199
|
|
|
140
200
|
|
|
141
|
-
|
|
142
|
-
def _project_root(path: Path) -> Path | None:
|
|
143
|
-
resolved = _resolved(path)
|
|
144
|
-
if resolved is None:
|
|
145
|
-
return None
|
|
146
|
-
ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
|
|
147
|
-
for ancestor in ancestors:
|
|
148
|
-
if (ancestor / _GIT_MARKER).exists():
|
|
149
|
-
return ancestor
|
|
150
|
-
# No VCS boundary: take the OUTERMOST directory of the unbroken run of
|
|
151
|
-
# pyproject.toml ancestors, which is the workspace root in a uv workspace.
|
|
152
|
-
outermost: Path | None = None
|
|
153
|
-
for ancestor in ancestors:
|
|
154
|
-
if not (ancestor / _PROJECT_MARKER).exists():
|
|
155
|
-
if outermost is not None:
|
|
156
|
-
break
|
|
157
|
-
continue
|
|
158
|
-
outermost = ancestor
|
|
159
|
-
return outermost
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@lru_cache(maxsize=32)
|
|
163
|
-
def _first_party_roots(root: Path) -> frozenset[str]:
|
|
164
|
-
return frozenset(name for name, _ in _first_party_packages(root))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@lru_cache(maxsize=32)
|
|
168
|
-
def _first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
|
|
201
|
+
def _scan_first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
|
|
169
202
|
found: list[tuple[str, Path]] = []
|
|
170
203
|
queue: list[tuple[Path, int]] = [(root, 0)]
|
|
171
204
|
scanned = 0
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
-
from functools import lru_cache
|
|
5
4
|
from io import StringIO
|
|
6
5
|
import re
|
|
7
6
|
import tokenize
|
|
@@ -103,16 +102,6 @@ def _leading_header_bodies(source: str) -> list[str]:
|
|
|
103
102
|
return bodies
|
|
104
103
|
|
|
105
104
|
|
|
106
|
-
@lru_cache(maxsize=256)
|
|
107
|
-
def _is_codegen_root(directory: Path) -> bool:
|
|
108
|
-
return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@lru_cache(maxsize=256)
|
|
112
|
-
def _is_repo_root(directory: Path) -> bool:
|
|
113
|
-
return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
|
|
114
|
-
|
|
115
|
-
|
|
116
105
|
def is_generated_path(path: Path) -> bool:
|
|
117
106
|
if any(part.lower() in _GENERATED_DIR_NAMES for part in path.parts):
|
|
118
107
|
return True
|
|
@@ -128,9 +117,16 @@ def is_generated_path(path: Path) -> bool:
|
|
|
128
117
|
return False
|
|
129
118
|
|
|
130
119
|
|
|
120
|
+
def _is_codegen_root(directory: Path) -> bool:
|
|
121
|
+
return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_repo_root(directory: Path) -> bool:
|
|
125
|
+
return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
|
|
126
|
+
|
|
127
|
+
|
|
131
128
|
def clear_path_caches() -> None:
|
|
132
|
-
|
|
133
|
-
_is_repo_root.cache_clear()
|
|
129
|
+
"""Compatibility no-op; path topology is intentionally observed fresh."""
|
|
134
130
|
|
|
135
131
|
|
|
136
132
|
def is_generated(path: Path, source: str) -> bool:
|
{sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -10,7 +10,7 @@ import re
|
|
|
10
10
|
from types import MappingProxyType
|
|
11
11
|
from typing import TYPE_CHECKING, final
|
|
12
12
|
|
|
13
|
-
from sarj_python_lint.rules._first_party import project_root
|
|
13
|
+
from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -111,8 +111,14 @@ class ProjectIndexSet:
|
|
|
111
111
|
self._nominals = MappingProxyType({key: frozenset(value) for key, value in nominals.items()})
|
|
112
112
|
|
|
113
113
|
@classmethod
|
|
114
|
-
def build(
|
|
115
|
-
|
|
114
|
+
def build(
|
|
115
|
+
cls,
|
|
116
|
+
paths: Sequence[Path],
|
|
117
|
+
loaded: Mapping[Path, str],
|
|
118
|
+
*,
|
|
119
|
+
facts: FirstPartyFacts | None = None,
|
|
120
|
+
) -> Self:
|
|
121
|
+
roots = _project_roots(paths, facts=facts)
|
|
116
122
|
sources: dict[Path, str] = {}
|
|
117
123
|
for path, source in loaded.items():
|
|
118
124
|
try:
|
|
@@ -283,11 +289,11 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
|
|
|
283
289
|
return ".".join(parts)
|
|
284
290
|
|
|
285
291
|
|
|
286
|
-
def _project_roots(paths: Sequence[Path]) -> tuple[Path, ...]:
|
|
292
|
+
def _project_roots(paths: Sequence[Path], *, facts: FirstPartyFacts | None = None) -> tuple[Path, ...]:
|
|
287
293
|
candidates: set[Path] = set()
|
|
288
294
|
for path in paths:
|
|
289
295
|
try:
|
|
290
|
-
root = project_root(path.resolve())
|
|
296
|
+
root = project_root(path.resolve(), facts=facts)
|
|
291
297
|
except OSError:
|
|
292
298
|
continue
|
|
293
299
|
if root is not None:
|
|
@@ -127,7 +127,7 @@ class CreatedAtOrderRequiresTiebreaker(Rule):
|
|
|
127
127
|
line=node.lineno,
|
|
128
128
|
col=node.col_offset + 1,
|
|
129
129
|
code=self.code,
|
|
130
|
-
severity=Severity.
|
|
130
|
+
severity=Severity.ERROR,
|
|
131
131
|
message=(
|
|
132
132
|
"Store query leaves `created_at` as its final ordering item; add a stable tie-break key "
|
|
133
133
|
"after it, such as `id` (for example, `ORDER BY created_at DESC, id DESC`)."
|
|
@@ -130,7 +130,7 @@ class GetDelegatesToGetMany(Rule):
|
|
|
130
130
|
f"`{bulk.name}([key])` so singleton and bulk reads share one contract, or document why "
|
|
131
131
|
"their semantics differ."
|
|
132
132
|
),
|
|
133
|
-
severity=Severity.
|
|
133
|
+
severity=Severity.ERROR,
|
|
134
134
|
)
|
|
135
135
|
)
|
|
136
136
|
return diagnostics
|
|
@@ -150,7 +150,7 @@ class NegativeOnlyHttpStatusAssertion(Rule):
|
|
|
150
150
|
line=node.lineno,
|
|
151
151
|
col=node.col_offset + 1,
|
|
152
152
|
code=self.code,
|
|
153
|
-
severity=Severity.
|
|
153
|
+
severity=Severity.ERROR,
|
|
154
154
|
message=(
|
|
155
155
|
"this assertion proves only that the response avoided one server-error shape; assert the "
|
|
156
156
|
"intended exact status and, when relevant, its domain payload or side effect."
|
|
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
)
|
|
18
18
|
from sarj_python_lint.rules._ast_index import nodes
|
|
19
19
|
from sarj_python_lint.rules._first_party import (
|
|
20
|
+
FirstPartyFacts,
|
|
20
21
|
has_first_party_source,
|
|
21
22
|
is_first_party_module,
|
|
22
23
|
own_top_package,
|
|
@@ -84,11 +85,12 @@ class NoFirstPartyPrivateImport(Rule):
|
|
|
84
85
|
tree = parse_or_none(path, source)
|
|
85
86
|
if tree is None:
|
|
86
87
|
return []
|
|
87
|
-
|
|
88
|
+
facts = self._analysis_session.first_party if self._analysis_session is not None else FirstPartyFacts()
|
|
89
|
+
own_top = own_top_package(path, facts=facts)
|
|
88
90
|
diags = [
|
|
89
91
|
Diagnostic(path=path, line=hit.line, col=hit.col, code=self.code, message=_message(hit.module, hit.name))
|
|
90
92
|
for hit in _private_imports(tree)
|
|
91
|
-
if _is_ours(hit.module, path, own_top) and not _is_our_own_internals(hit, path)
|
|
93
|
+
if _is_ours(hit.module, path, own_top, facts) and not _is_our_own_internals(hit, path, facts)
|
|
92
94
|
]
|
|
93
95
|
diags.sort(key=lambda d: (d.line, d.col))
|
|
94
96
|
return diags
|
|
@@ -106,12 +108,12 @@ class _PrivateImport:
|
|
|
106
108
|
names_public: bool
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def _is_our_own_internals(hit: _PrivateImport, path: Path) -> bool:
|
|
111
|
+
def _is_our_own_internals(hit: _PrivateImport, path: Path, facts: FirstPartyFacts) -> bool:
|
|
110
112
|
if not hit.is_segment:
|
|
111
113
|
return False
|
|
112
|
-
if not has_first_party_source(hit.module, path):
|
|
114
|
+
if not has_first_party_source(hit.module, path, facts=facts):
|
|
113
115
|
return True
|
|
114
|
-
return hit.names_public and same_distribution(hit.module, path)
|
|
116
|
+
return hit.names_public and same_distribution(hit.module, path, facts=facts)
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
def _message(module: str, name: str) -> str:
|
|
@@ -122,11 +124,11 @@ def _message(module: str, name: str) -> str:
|
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
|
|
125
|
-
def _is_ours(module: str, path: Path, own_top: str | None) -> bool:
|
|
127
|
+
def _is_ours(module: str, path: Path, own_top: str | None, facts: FirstPartyFacts) -> bool:
|
|
126
128
|
top = module.partition(".")[0]
|
|
127
129
|
if own_top is not None and top == own_top:
|
|
128
130
|
return False
|
|
129
|
-
return is_first_party_module(module, path)
|
|
131
|
+
return is_first_party_module(module, path, facts=facts)
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
def _private_imports(tree: ast.Module) -> list[_PrivateImport]:
|