sarj-python-lint 0.75.0__tar.gz → 0.76.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.75.0 → sarj_python_lint-0.76.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_registry.py +10 -0
- sarj_python_lint-0.76.0/src/sarj_python_lint/rules/no_conftest_test_module_import.py +105 -0
- sarj_python_lint-0.76.0/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +171 -0
- sarj_python_lint-0.76.0/src/sarj_python_lint/rules/no_redundant_literal_description.py +214 -0
- sarj_python_lint-0.76.0/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +283 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/.gitignore +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/LICENSE +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/README.md +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.76.0}/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.
|
|
3
|
+
Version: 0.76.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/
|
|
@@ -33,6 +33,7 @@ from sarj_python_lint.rules.no_aggregation_in_store_query import (
|
|
|
33
33
|
NoAggregationInStoreQuery,
|
|
34
34
|
)
|
|
35
35
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
36
|
+
from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
|
|
36
37
|
from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
37
38
|
NoCorsWildcardWithCredentials,
|
|
38
39
|
)
|
|
@@ -51,11 +52,13 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
|
51
52
|
)
|
|
52
53
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
53
54
|
from sarj_python_lint.rules.no_long_comment import NoLongComment
|
|
55
|
+
from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
|
|
54
56
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
55
57
|
from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
56
58
|
NoOptionalTenantPredicate,
|
|
57
59
|
)
|
|
58
60
|
from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
|
|
61
|
+
from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
|
|
59
62
|
from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
|
|
60
63
|
from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
|
|
61
64
|
from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
@@ -125,6 +128,9 @@ from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCall
|
|
|
125
128
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
126
129
|
RequireKeywordOnlySwapProneParams,
|
|
127
130
|
)
|
|
131
|
+
from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import (
|
|
132
|
+
RequireNoDecodeForSplittingSettingsField,
|
|
133
|
+
)
|
|
128
134
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
129
135
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
130
136
|
RequirePydanticForExternalJson,
|
|
@@ -169,6 +175,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
169
175
|
PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
|
|
170
176
|
PreferStructOverNamedtuple.id: PreferStructOverNamedtuple,
|
|
171
177
|
NoCommentCruft.id: NoCommentCruft,
|
|
178
|
+
NoConftestTestModuleImport.id: NoConftestTestModuleImport,
|
|
172
179
|
StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
|
|
173
180
|
SourceCoupledTest.id: SourceCoupledTest,
|
|
174
181
|
IacSourceCoupledTest.id: IacSourceCoupledTest,
|
|
@@ -226,6 +233,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
226
233
|
RestatedTestDocstring.id: RestatedTestDocstring,
|
|
227
234
|
TestPhaseLabelComment.id: TestPhaseLabelComment,
|
|
228
235
|
NoLongComment.id: NoLongComment,
|
|
236
|
+
NoNestedPydanticFieldValidator.id: NoNestedPydanticFieldValidator,
|
|
237
|
+
NoRedundantLiteralDescription.id: NoRedundantLiteralDescription,
|
|
229
238
|
NoTypedDocSections.id: NoTypedDocSections,
|
|
230
239
|
NoUnnecessaryDocstring.id: NoUnnecessaryDocstring,
|
|
231
240
|
PreferNominalIdTypes.id: PreferNominalIdTypes,
|
|
@@ -239,6 +248,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
239
248
|
PreserveDeclaredNominalId.id: PreserveDeclaredNominalId,
|
|
240
249
|
PreserveEnumTypes.id: PreserveEnumTypes,
|
|
241
250
|
RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
|
|
251
|
+
RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
|
|
242
252
|
NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
|
|
243
253
|
}
|
|
244
254
|
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, final, 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
|
+
parse_or_none,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@final
|
|
25
|
+
class NoConftestTestModuleImport(Rule):
|
|
26
|
+
id = "no-conftest-test-module-import"
|
|
27
|
+
code = "SARJ426"
|
|
28
|
+
documentation = RuleDocumentation(
|
|
29
|
+
summary="Do not import individual test modules from conftest.py.",
|
|
30
|
+
rationale=(
|
|
31
|
+
"pytest imports conftest for every test module in its scope; importing a specific test from conftest "
|
|
32
|
+
"turns that test into global collection infrastructure and creates cycles and order dependence."
|
|
33
|
+
),
|
|
34
|
+
remediation="Move shared fixtures or helpers into conftest.py or a dedicated support module.",
|
|
35
|
+
category=RuleCategory.TESTING,
|
|
36
|
+
autofix=AutofixPolicy.NONE,
|
|
37
|
+
limitations=(
|
|
38
|
+
"Only files named conftest.py are inspected.",
|
|
39
|
+
"A test module is recognized only when an explicit module-path component begins with `test_` or ends with `_test`.",
|
|
40
|
+
),
|
|
41
|
+
examples=(
|
|
42
|
+
RuleExample(
|
|
43
|
+
example_id="conftest-imports-test-module",
|
|
44
|
+
title="Conftest imports a specific test module",
|
|
45
|
+
outcome=ExampleOutcome.MATCH,
|
|
46
|
+
files=(ExampleFile.python("tests/conftest.py", "from tests.test_api import make_client\n"),),
|
|
47
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
48
|
+
expected_count=1,
|
|
49
|
+
public=True,
|
|
50
|
+
),
|
|
51
|
+
RuleExample(
|
|
52
|
+
example_id="conftest-imports-support-module",
|
|
53
|
+
title="Conftest imports dedicated support code",
|
|
54
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
55
|
+
files=(ExampleFile.python("tests/conftest.py", "from tests.helpers import make_client\n"),),
|
|
56
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
57
|
+
expected_count=0,
|
|
58
|
+
public=True,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
description = documentation.summary
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
66
|
+
if path.name != "conftest.py":
|
|
67
|
+
return []
|
|
68
|
+
tree = parse_or_none(path, source)
|
|
69
|
+
if tree is None:
|
|
70
|
+
return []
|
|
71
|
+
diagnostics: list[Diagnostic] = []
|
|
72
|
+
for statement in tree.body:
|
|
73
|
+
targets: list[str] = []
|
|
74
|
+
match statement:
|
|
75
|
+
case ast.Import(names=names):
|
|
76
|
+
targets.extend(alias.name for alias in names if _module_has_test_leaf(alias.name))
|
|
77
|
+
case ast.ImportFrom(module=module, names=names):
|
|
78
|
+
if module is not None and _module_has_test_leaf(module):
|
|
79
|
+
targets.append(module)
|
|
80
|
+
case _:
|
|
81
|
+
continue
|
|
82
|
+
if not targets:
|
|
83
|
+
continue
|
|
84
|
+
diagnostics.append(
|
|
85
|
+
Diagnostic(
|
|
86
|
+
path=path,
|
|
87
|
+
line=statement.lineno,
|
|
88
|
+
col=statement.col_offset + 1,
|
|
89
|
+
code=self.code,
|
|
90
|
+
message=(
|
|
91
|
+
f"conftest.py imports test module `{targets[0]}`; move shared fixtures or helpers to "
|
|
92
|
+
"conftest or a dedicated support module."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return diagnostics
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _module_has_test_leaf(module: str) -> bool:
|
|
100
|
+
parts = module.split(".")
|
|
101
|
+
return any(_is_test_module_name(part) for part in parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_test_module_name(name: str) -> bool:
|
|
105
|
+
return name.startswith("test_") or name.endswith("_test")
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, final, 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
|
+
parse_or_none,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
|
|
27
|
+
_PYDANTIC_SOURCES = frozenset({"pydantic", "pydantic.functional_validators"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
class NoNestedPydanticFieldValidator(Rule):
|
|
32
|
+
id = "no-nested-pydantic-field-validator"
|
|
33
|
+
code = "SARJ425"
|
|
34
|
+
documentation = RuleDocumentation(
|
|
35
|
+
summary="Do not place an outer Pydantic model's field validator inside a nested helper class.",
|
|
36
|
+
rationale=(
|
|
37
|
+
"A decorator indented into a nested class registers on that class, not on the outer Pydantic model, "
|
|
38
|
+
"so the declared field silently loses validation."
|
|
39
|
+
),
|
|
40
|
+
remediation="Move the field-validator method into the outer model class.",
|
|
41
|
+
category=RuleCategory.CORRECTNESS,
|
|
42
|
+
autofix=AutofixPolicy.NONE,
|
|
43
|
+
limitations=(
|
|
44
|
+
"Only direct BaseModel subclasses and validators directly owned by one nested class are inspected.",
|
|
45
|
+
"Nested classes with bases are excluded because inherited Pydantic fields cannot be resolved locally.",
|
|
46
|
+
"Test and generated files are excluded.",
|
|
47
|
+
),
|
|
48
|
+
examples=(
|
|
49
|
+
RuleExample(
|
|
50
|
+
example_id="validator-nested-in-config",
|
|
51
|
+
title="Validator accidentally belongs to Config",
|
|
52
|
+
outcome=ExampleOutcome.MATCH,
|
|
53
|
+
files=(
|
|
54
|
+
ExampleFile.python(
|
|
55
|
+
"app/models.py",
|
|
56
|
+
"from pydantic import BaseModel, field_validator\n\n"
|
|
57
|
+
"class Settings(BaseModel):\n"
|
|
58
|
+
" language: str\n"
|
|
59
|
+
" class Config:\n"
|
|
60
|
+
" extra = 'forbid'\n"
|
|
61
|
+
" @field_validator('language')\n"
|
|
62
|
+
" @classmethod\n"
|
|
63
|
+
" def normalize(cls, value):\n"
|
|
64
|
+
" return value.lower()\n",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
68
|
+
expected_count=1,
|
|
69
|
+
public=True,
|
|
70
|
+
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="validator-owned-by-model",
|
|
73
|
+
title="Validator belongs to the model",
|
|
74
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python(
|
|
77
|
+
"app/models.py",
|
|
78
|
+
"from pydantic import BaseModel, field_validator\n\n"
|
|
79
|
+
"class Settings(BaseModel):\n"
|
|
80
|
+
" language: str\n"
|
|
81
|
+
" @field_validator('language')\n"
|
|
82
|
+
" @classmethod\n"
|
|
83
|
+
" def normalize(cls, value):\n"
|
|
84
|
+
" return value.lower()\n",
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
88
|
+
expected_count=0,
|
|
89
|
+
public=True,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
description = documentation.summary
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
97
|
+
if is_test_path(path) or is_generated(path, source):
|
|
98
|
+
return []
|
|
99
|
+
tree = parse_or_none(path, source)
|
|
100
|
+
if tree is None:
|
|
101
|
+
return []
|
|
102
|
+
imports = ImportIndex.from_tree(tree)
|
|
103
|
+
diagnostics: list[Diagnostic] = []
|
|
104
|
+
for outer in (node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)):
|
|
105
|
+
if not _is_direct_model(outer, imports):
|
|
106
|
+
continue
|
|
107
|
+
outer_fields = _direct_fields(outer)
|
|
108
|
+
for nested in (statement for statement in outer.body if isinstance(statement, ast.ClassDef)):
|
|
109
|
+
# A based nested class may be an indirect Pydantic model whose inherited
|
|
110
|
+
# fields are unavailable to this file-local analysis. Only base-less
|
|
111
|
+
# Config/helper classes provide a deterministic ownership mistake.
|
|
112
|
+
if nested.bases:
|
|
113
|
+
continue
|
|
114
|
+
nested_fields = _direct_fields(nested)
|
|
115
|
+
for function in (
|
|
116
|
+
statement
|
|
117
|
+
for statement in nested.body
|
|
118
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
119
|
+
):
|
|
120
|
+
for decorator, fields in _field_validators(function, imports):
|
|
121
|
+
misplaced = sorted((fields & outer_fields) - nested_fields)
|
|
122
|
+
if not misplaced:
|
|
123
|
+
continue
|
|
124
|
+
diagnostics.append(
|
|
125
|
+
Diagnostic(
|
|
126
|
+
path=path,
|
|
127
|
+
line=decorator.lineno,
|
|
128
|
+
col=decorator.col_offset + 1,
|
|
129
|
+
code=self.code,
|
|
130
|
+
message=(
|
|
131
|
+
f"Validator for outer field(s) {', '.join(f'`{name}`' for name in misplaced)} "
|
|
132
|
+
f"is nested inside `{nested.name}`; move it to `{outer.name}`."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return diagnostics
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
140
|
+
return len(node.bases) == 1 and imports.resolves(
|
|
141
|
+
node.bases[0], sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _direct_fields(node: ast.ClassDef) -> frozenset[str]:
|
|
146
|
+
return frozenset(
|
|
147
|
+
statement.target.id
|
|
148
|
+
for statement in node.body
|
|
149
|
+
if isinstance(statement, ast.AnnAssign)
|
|
150
|
+
and isinstance(statement.target, ast.Name)
|
|
151
|
+
and not statement.target.id.startswith("_")
|
|
152
|
+
and statement.target.id != "model_config"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _field_validators(
|
|
157
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex
|
|
158
|
+
) -> tuple[tuple[ast.Call, frozenset[str]], ...]:
|
|
159
|
+
validators: list[tuple[ast.Call, frozenset[str]]] = []
|
|
160
|
+
for decorator in node.decorator_list:
|
|
161
|
+
if not isinstance(decorator, ast.Call) or not imports.resolves(
|
|
162
|
+
decorator.func, sources=_PYDANTIC_SOURCES, symbol="field_validator"
|
|
163
|
+
):
|
|
164
|
+
continue
|
|
165
|
+
fields = frozenset(
|
|
166
|
+
argument.value
|
|
167
|
+
for argument in decorator.args
|
|
168
|
+
if isinstance(argument, ast.Constant) and isinstance(argument.value, str)
|
|
169
|
+
)
|
|
170
|
+
validators.append((decorator, fields))
|
|
171
|
+
return tuple(validators)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
parse_or_none,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
|
|
28
|
+
_PYDANTIC_FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
|
|
29
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
30
|
+
_ENUM_SOURCES = frozenset({"enum"})
|
|
31
|
+
_DOMAIN_ONLY = re.compile(r"^\s*(?:must|should)\s+be(?:\s+one\s+of)?\s+(?P<body>.+?)\s*[.!]?\s*$", re.IGNORECASE)
|
|
32
|
+
_QUOTED_VALUE = re.compile(r"(['\"])(?P<value>.+?)\1")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class NoRedundantLiteralDescription(Rule):
|
|
37
|
+
id = "no-redundant-literal-description"
|
|
38
|
+
code = "SARJ423"
|
|
39
|
+
documentation = RuleDocumentation(
|
|
40
|
+
summary="Do not restate a Literal or Enum field's closed domain in its Pydantic description.",
|
|
41
|
+
rationale=(
|
|
42
|
+
"Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can "
|
|
43
|
+
"contradict the generated contract after a value changes."
|
|
44
|
+
),
|
|
45
|
+
remediation="Remove the domain-only description or replace it with rationale not encoded by the field type.",
|
|
46
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
47
|
+
autofix=AutofixPolicy.NONE,
|
|
48
|
+
limitations=(
|
|
49
|
+
"Only direct fields on direct BaseModel subclasses are inspected.",
|
|
50
|
+
"Enum fields are inspected only when their direct string-valued enum class is declared in the same module.",
|
|
51
|
+
"Descriptions are reported only when they begin with a narrow 'must be' or 'should be' restatement.",
|
|
52
|
+
"Test and generated files are excluded.",
|
|
53
|
+
),
|
|
54
|
+
examples=(
|
|
55
|
+
RuleExample(
|
|
56
|
+
example_id="literal-domain-restated",
|
|
57
|
+
title="Literal domain repeated in prose",
|
|
58
|
+
outcome=ExampleOutcome.MATCH,
|
|
59
|
+
files=(
|
|
60
|
+
ExampleFile.python(
|
|
61
|
+
"app/models.py",
|
|
62
|
+
"from typing import Literal\nfrom pydantic import BaseModel, Field\n\n"
|
|
63
|
+
"class Request(BaseModel):\n"
|
|
64
|
+
" kind: Literal['call'] = Field(description=\"Must be 'call'\")\n",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
68
|
+
expected_count=1,
|
|
69
|
+
public=True,
|
|
70
|
+
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="literal-rationale-described",
|
|
73
|
+
title="Description adds rationale",
|
|
74
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python(
|
|
77
|
+
"app/models.py",
|
|
78
|
+
"from typing import Literal\nfrom pydantic import BaseModel, Field\n\n"
|
|
79
|
+
"class Request(BaseModel):\n"
|
|
80
|
+
" kind: Literal['call'] = Field(description='Routes through the realtime provider.')\n",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
84
|
+
expected_count=0,
|
|
85
|
+
public=True,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
description = documentation.summary
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
93
|
+
if is_test_path(path) or is_generated(path, source):
|
|
94
|
+
return []
|
|
95
|
+
tree = parse_or_none(path, source)
|
|
96
|
+
if tree is None:
|
|
97
|
+
return []
|
|
98
|
+
imports = ImportIndex.from_tree(tree)
|
|
99
|
+
enum_domains = _local_enum_domains(tree, imports)
|
|
100
|
+
diagnostics: list[Diagnostic] = []
|
|
101
|
+
for model in (node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)):
|
|
102
|
+
if not _is_direct_model(model, imports):
|
|
103
|
+
continue
|
|
104
|
+
for statement in model.body:
|
|
105
|
+
if not isinstance(statement, ast.AnnAssign) or not isinstance(statement.target, ast.Name):
|
|
106
|
+
continue
|
|
107
|
+
domain = _closed_domain(statement.annotation, imports, enum_domains)
|
|
108
|
+
if domain is None:
|
|
109
|
+
continue
|
|
110
|
+
for field_call in _field_calls(statement, imports):
|
|
111
|
+
description = next(
|
|
112
|
+
(keyword.value for keyword in field_call.keywords if keyword.arg == "description"), None
|
|
113
|
+
)
|
|
114
|
+
if not (
|
|
115
|
+
isinstance(description, ast.Constant)
|
|
116
|
+
and isinstance(description.value, str)
|
|
117
|
+
and _repeats_domain(description.value, domain)
|
|
118
|
+
):
|
|
119
|
+
continue
|
|
120
|
+
diagnostics.append(
|
|
121
|
+
Diagnostic(
|
|
122
|
+
path=path,
|
|
123
|
+
line=description.lineno,
|
|
124
|
+
col=description.col_offset + 1,
|
|
125
|
+
code=self.code,
|
|
126
|
+
message=(
|
|
127
|
+
f"`{statement.target.id}` repeats its closed type domain in `description`; "
|
|
128
|
+
"remove the restatement or document rationale not present in the schema."
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
return diagnostics
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
136
|
+
return len(node.bases) == 1 and imports.resolves(
|
|
137
|
+
node.bases[0], sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _local_enum_domains(tree: ast.Module, imports: ImportIndex) -> dict[str, frozenset[str]]:
|
|
142
|
+
domains: dict[str, frozenset[str]] = {}
|
|
143
|
+
for node in tree.body:
|
|
144
|
+
if not isinstance(node, ast.ClassDef) or not any(
|
|
145
|
+
imports.resolves(base, sources=_ENUM_SOURCES, symbol=symbol)
|
|
146
|
+
for base in node.bases
|
|
147
|
+
for symbol in ("Enum", "IntEnum", "StrEnum")
|
|
148
|
+
):
|
|
149
|
+
continue
|
|
150
|
+
values: set[str] = set()
|
|
151
|
+
complete = True
|
|
152
|
+
for statement in node.body:
|
|
153
|
+
value = statement.value if isinstance(statement, (ast.Assign, ast.AnnAssign)) else None
|
|
154
|
+
if value is None:
|
|
155
|
+
continue
|
|
156
|
+
if not isinstance(value, ast.Constant) or not isinstance(value.value, str):
|
|
157
|
+
complete = False
|
|
158
|
+
break
|
|
159
|
+
values.add(value.value)
|
|
160
|
+
if complete and values:
|
|
161
|
+
domains[node.name] = frozenset(values)
|
|
162
|
+
return domains
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _closed_domain(
|
|
166
|
+
node: ast.expr, imports: ImportIndex, enum_domains: dict[str, frozenset[str]]
|
|
167
|
+
) -> frozenset[str] | None:
|
|
168
|
+
if isinstance(node, ast.Name):
|
|
169
|
+
return enum_domains.get(node.id)
|
|
170
|
+
if not isinstance(node, ast.Subscript):
|
|
171
|
+
return None
|
|
172
|
+
if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Literal"):
|
|
173
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
|
174
|
+
values: set[str] = set()
|
|
175
|
+
for member in members:
|
|
176
|
+
if not isinstance(member, ast.Constant) or not isinstance(member.value, str):
|
|
177
|
+
return None
|
|
178
|
+
values.add(member.value)
|
|
179
|
+
return frozenset(values)
|
|
180
|
+
if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Annotated"):
|
|
181
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
|
182
|
+
return _closed_domain(members[0], imports, enum_domains) if members else None
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _repeats_domain(description: str, domain: frozenset[str]) -> bool:
|
|
187
|
+
match = _DOMAIN_ONLY.fullmatch(description)
|
|
188
|
+
if match is None:
|
|
189
|
+
return False
|
|
190
|
+
body = match.group("body")
|
|
191
|
+
values = frozenset(item.group("value") for item in _QUOTED_VALUE.finditer(body))
|
|
192
|
+
if values != domain:
|
|
193
|
+
return False
|
|
194
|
+
residue = _QUOTED_VALUE.sub("", body)
|
|
195
|
+
residue = re.sub(r"\b(?:and|or)\b|[\s,\[\](){}]", "", residue, flags=re.IGNORECASE)
|
|
196
|
+
return not residue
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _field_calls(statement: ast.AnnAssign, imports: ImportIndex) -> tuple[ast.Call, ...]:
|
|
200
|
+
candidates: list[ast.expr] = []
|
|
201
|
+
if statement.value is not None:
|
|
202
|
+
candidates.append(statement.value)
|
|
203
|
+
annotation = statement.annotation
|
|
204
|
+
if isinstance(annotation, ast.Subscript) and imports.resolves(
|
|
205
|
+
annotation.value, sources=_TYPING_SOURCES, symbol="Annotated"
|
|
206
|
+
):
|
|
207
|
+
members = annotation.slice.elts if isinstance(annotation.slice, ast.Tuple) else (annotation.slice,)
|
|
208
|
+
candidates.extend(members[1:])
|
|
209
|
+
return tuple(
|
|
210
|
+
candidate
|
|
211
|
+
for candidate in candidates
|
|
212
|
+
if isinstance(candidate, ast.Call)
|
|
213
|
+
and imports.resolves(candidate.func, sources=_PYDANTIC_FIELD_SOURCES, symbol="Field")
|
|
214
|
+
)
|