sarj-python-lint 0.29.0__tar.gz → 0.30.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.
Files changed (91) hide show
  1. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_implicit_attribute_access.py +52 -1
  4. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/.gitignore +0 -0
  5. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/README.md +0 -0
  6. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/__init__.py +0 -0
  7. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/__main__.py +0 -0
  8. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  9. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/_secret_names.py +0 -0
  10. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/_version.py +0 -0
  11. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/py.typed +0 -0
  12. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/ratchet.py +0 -0
  13. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rule_base.py +0 -0
  14. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  15. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  16. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  17. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  18. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  19. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  20. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  21. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  22. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  23. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  24. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
  25. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  26. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  27. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
  28. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
  29. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  30. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  31. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  32. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  33. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  34. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  35. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  36. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  37. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  38. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  39. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  40. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  41. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  42. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  43. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  44. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +0 -0
  45. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  46. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  47. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  48. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  49. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  50. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  51. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  52. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  53. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  54. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  55. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  56. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  57. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  58. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  59. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  60. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  61. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  62. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  63. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  64. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +0 -0
  65. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  66. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  67. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  68. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  69. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  70. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
  71. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  72. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  73. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  74. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  75. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  76. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  77. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  78. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  79. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  80. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  81. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  82. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  83. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  84. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  85. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
  86. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  87. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  88. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  89. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  90. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
  91. {sarj_python_lint-0.29.0 → sarj_python_lint-0.30.0}/src/sarj_python_lint/rules/zero_assertion_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sarj-python-lint
3
- Version: 0.29.0
3
+ Version: 0.30.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
6
6
  Project-URL: Repository, https://github.com/sarj-ai/standards
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.29.0"
3
+ version = "0.30.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" }]
@@ -44,6 +44,34 @@ _EXCLUDED_BASES = {
44
44
  }
45
45
 
46
46
 
47
+ # Typing constructs subscripted with string literals. `Literal["x"]` is a type,
48
+ # not a lookup, so the rule's advice ("parse declaratively with Pydantic") is
49
+ # nonsensical there -- the annotation already IS the declarative schema.
50
+ _TYPE_SUBSCRIPTS = frozenset(
51
+ {
52
+ "Literal",
53
+ "Annotated",
54
+ "TypedDict",
55
+ "NamedTuple",
56
+ "Field",
57
+ "Doc",
58
+ "Required",
59
+ "NotRequired",
60
+ "ReadOnly",
61
+ }
62
+ )
63
+
64
+
65
+ def _looks_like_route_or_url(value: str) -> bool:
66
+ """Report whether a `.get()` argument is a route path or URL rather than a key.
67
+
68
+ Returns:
69
+ True for `"/users/{id}"`-shaped paths and anything carrying a scheme.
70
+
71
+ """
72
+ return value.startswith("/") or "://" in value
73
+
74
+
47
75
  def _get_base_name(node: ast.expr) -> str | None:
48
76
  if isinstance(node, ast.Name):
49
77
  return node.id
@@ -100,6 +128,14 @@ def _get_key(node: ast.Call) -> str | None:
100
128
  first = node.args[0]
101
129
  if not isinstance(first, ast.Constant) or not isinstance(first.value, str):
102
130
  return None
131
+ # `.get()` is also the HTTP verb and the route-registration decorator, and
132
+ # both take a string first argument, so the method name alone cannot tell
133
+ # them from a mapping lookup. The ARGUMENT can: a URL or a route path is not
134
+ # a dictionary key. Measured on bulbul + noura-be, this shape was 168 of the
135
+ # rule's 1,756 findings (9.6%) -- `@router.get("/available-events")` and
136
+ # `await self.http_client.get(url)` were reported as implicit schema access.
137
+ if _looks_like_route_or_url(first.value):
138
+ return None
103
139
  return None if _get_base_name(func.value) in _EXCLUDED_BASES else first.value
104
140
 
105
141
 
@@ -111,10 +147,25 @@ def _subscript_key(node: ast.Subscript) -> str | None:
111
147
  the base is one of the excluded receivers.
112
148
 
113
149
  """
150
+ # Writing to a mapping is the opposite of the defect. This rule is about
151
+ # PLUCKING fields out of a payload whose schema is already known -- building
152
+ # a dict up key by key (`field_dict["x"] = x`, `params["status"] = ...`) is
153
+ # ordinary construction, and a Pydantic model does not replace it. Measured
154
+ # on bulbul + noura-be this was 503 of 1,756 findings (28.6%), the single
155
+ # largest source, and every sampled instance was an assignment target.
156
+ if isinstance(node.ctx, (ast.Store, ast.Del)):
157
+ return None
158
+ # `Literal["a"]`, `Annotated[T, "..."]` and friends are type expressions that
159
+ # merely LOOK like subscripts. They are not dictionary access at all, and no
160
+ # Pydantic model can replace them -- `Literal["user"]` IS the schema. 470 of
161
+ # 1,756 findings (26.8%) were this, second only to assignment targets.
162
+ base_name = _get_base_name(node.value)
163
+ if base_name in _TYPE_SUBSCRIPTS:
164
+ return None
114
165
  index = node.slice
115
166
  if not isinstance(index, ast.Constant) or not isinstance(index.value, str):
116
167
  return None
117
- return None if _get_base_name(node.value) in _EXCLUDED_BASES else index.value
168
+ return None if base_name in _EXCLUDED_BASES else index.value
118
169
 
119
170
 
120
171
  def _is_test_path(path: Path) -> bool: