sarj-python-lint 0.45.0__tar.gz → 0.46.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 (103) hide show
  1. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/README.md +1 -1
  3. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/pyproject.toml +1 -1
  4. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/__main__.py +1 -0
  5. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_comments.py +2 -1
  6. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_prose_budget.py +17 -2
  7. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +32 -20
  8. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/.gitignore +0 -0
  9. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  11. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_secret_names.py +0 -0
  12. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_version.py +0 -0
  13. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/py.typed +0 -0
  14. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/ratchet.py +0 -0
  15. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rule_base.py +0 -0
  16. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  17. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  18. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  19. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  20. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  21. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  22. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  23. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  24. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  25. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  26. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  27. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
  28. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  29. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  30. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  31. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  32. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  33. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  34. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
  35. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
  36. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  37. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  38. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  39. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  40. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  41. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  42. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  43. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  44. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  45. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  46. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  47. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  48. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  49. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  50. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  51. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  52. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  53. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  54. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  55. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  56. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  57. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  58. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  59. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  60. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  61. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  62. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  63. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  64. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  65. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  66. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  67. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  68. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  69. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  70. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  71. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  72. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  73. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  74. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  75. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  76. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  77. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  78. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  79. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
  80. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  81. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_single_sentence_comment.py +0 -0
  82. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  83. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  84. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  85. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  86. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  87. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  88. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  89. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  90. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  91. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  92. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  93. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  94. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  95. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  96. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  97. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
  98. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  99. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  100. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  101. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  102. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
  103. {sarj_python_lint-0.45.0 → sarj_python_lint-0.46.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.45.0
3
+ Version: 0.46.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
@@ -28,7 +28,7 @@ uv tool install sarj-python-lint
28
28
 
29
29
  ```yaml
30
30
  - repo: https://github.com/sarj-ai/standards
31
- rev: python-v0.45.0
31
+ rev: python-v0.46.0
32
32
  hooks:
33
33
  - id: sarj-no-sequential-await
34
34
  - id: sarj-inefficient-string-concat-in-loop
@@ -10,7 +10,7 @@ uv tool install sarj-python-lint
10
10
 
11
11
  ```yaml
12
12
  - repo: https://github.com/sarj-ai/standards
13
- rev: python-v0.45.0
13
+ rev: python-v0.46.0
14
14
  hooks:
15
15
  - id: sarj-no-sequential-await
16
16
  - id: sarj-inefficient-string-concat-in-loop
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.45.0"
3
+ version = "0.46.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" }]
@@ -25,6 +25,7 @@ SKIP_DIR_NAMES = {
25
25
  "__pycache__",
26
26
  ".pytest_cache",
27
27
  ".ruff_cache",
28
+ ".uv-cache",
28
29
  ".mypy_cache",
29
30
  ".turbo",
30
31
  ".yarn",
@@ -9,6 +9,7 @@ import tokenize
9
9
  from typing import TYPE_CHECKING, NamedTuple
10
10
 
11
11
  from sarj_python_lint.rule_base import parse_or_none
12
+ from sarj_python_lint.rules._ast_index import nodes
12
13
 
13
14
 
14
15
  if TYPE_CHECKING:
@@ -498,7 +499,7 @@ def statement_comment_walls(
498
499
  return {}
499
500
  comments = {line: (col, body) for line, col, body in standalone}
500
501
  walls: dict[int, frozenset[int]] = {}
501
- for owner in ast.walk(tree):
502
+ for owner in nodes(tree, ast.AST):
502
503
  for value in _owned_statement_lists(owner):
503
504
  statements = [item for item in value if _is_wall_statement(item)]
504
505
  if len(statements) < _WALL_MIN_STATEMENTS:
@@ -10,6 +10,7 @@ import tokenize
10
10
  from typing import TYPE_CHECKING, Final
11
11
 
12
12
  from sarj_python_lint.rule_base import parse_or_none
13
+ from sarj_python_lint.rules._ast_index import nodes
13
14
  from sarj_python_lint.rules._docstrings import PROMPT_DECORATOR_MARKERS, decorator_markers, sections
14
15
  from sarj_python_lint.rules._paths import is_generated
15
16
 
@@ -53,6 +54,9 @@ class ProseGroup:
53
54
  typed_sections: frozenset[str] = frozenset()
54
55
 
55
56
 
57
+ _last_groups: tuple[str, str, tuple[ProseGroup, ...]] | None = None
58
+
59
+
56
60
  def sentence_units(text: str) -> int:
57
61
  """Count prose sentences and unpunctuated list items deterministically."""
58
62
  text = _without_examples_metadata(text)
@@ -98,6 +102,17 @@ def _without_examples_metadata(text: str) -> str:
98
102
 
99
103
  def groups(path: Path, source: str) -> list[ProseGroup]:
100
104
  """Extract docstrings and contiguous own-line comment runs from one file."""
105
+ global _last_groups # ruff: ignore[global-statement] -- rules run sequentially per file.
106
+ path_key = str(path)
107
+ if _last_groups is not None and _last_groups[0] == path_key and _last_groups[1] is source:
108
+ return list(_last_groups[2])
109
+ extracted = _extract_groups(path, source)
110
+ _last_groups = (path_key, source, tuple(extracted))
111
+ return extracted
112
+
113
+
114
+ def _extract_groups(path: Path, source: str) -> list[ProseGroup]:
115
+ """Perform the shared parse and tokenization once for adjacent prose rules."""
101
116
  if is_generated(path, source):
102
117
  return []
103
118
  tree = parse_or_none(path, source)
@@ -140,8 +155,8 @@ def _comment_run(run: list[tokenize.TokenInfo]) -> ProseGroup:
140
155
 
141
156
  def _docstring_groups(tree: ast.Module) -> list[ProseGroup]:
142
157
  out: list[ProseGroup] = []
143
- for node in ast.walk(tree):
144
- if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) or not node.body:
158
+ for node in nodes(tree, ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef):
159
+ if not node.body:
145
160
  continue
146
161
  first = node.body[0]
147
162
  if not (
@@ -52,6 +52,12 @@ class _Binding:
52
52
  symbol: str | None
53
53
 
54
54
 
55
+ @dataclass(frozen=True, slots=True)
56
+ class _HiddenParameter:
57
+ parameter: ast.arg
58
+ uses_boolean_or: bool
59
+
60
+
55
61
  @final
56
62
  class NoHiddenConstructorFallback(Rule):
57
63
  id = "no-hidden-constructor-fallback"
@@ -88,19 +94,24 @@ class NoHiddenConstructorFallback(Rule):
88
94
  hidden = _hidden_parameters(init, resolver)
89
95
  if not hidden or not _has_composition_call(path, class_node.name):
90
96
  continue
91
- names = ", ".join(f"`{parameter.arg}`" for parameter in hidden)
97
+ names = ", ".join(f"`{match.parameter.arg}`" for match in hidden)
92
98
  noun = "parameter" if len(hidden) == 1 else "parameters"
93
99
  verb = "falls" if len(hidden) == 1 else "fall"
100
+ falsey_warning = (
101
+ " A boolean `or` also treats explicit falsey values as omitted."
102
+ if any(match.uses_boolean_or for match in hidden)
103
+ else ""
104
+ )
94
105
  diagnostics.append(
95
106
  Diagnostic(
96
107
  path=path,
97
- line=hidden[0].lineno,
98
- col=hidden[0].col_offset + 1,
108
+ line=hidden[0].parameter.lineno,
109
+ col=hidden[0].parameter.col_offset + 1,
99
110
  code=self.code,
100
111
  message=(
101
112
  f"Constructor {noun} {names} {verb} back to application settings when omitted; "
102
- "make the argument required and resolve the fallback at the call site or composition root. "
103
- "A boolean `or` also treats explicit falsey values as omitted."
113
+ "make the argument required and resolve the fallback at the call site or composition root."
114
+ f"{falsey_warning}"
104
115
  ),
105
116
  severity=Severity.WARNING,
106
117
  )
@@ -115,7 +126,7 @@ def _is_descriptor(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
115
126
  def _hidden_parameters(
116
127
  init: ast.FunctionDef | ast.AsyncFunctionDef,
117
128
  resolver: _RuntimeConfigResolver,
118
- ) -> list[ast.arg]:
129
+ ) -> list[_HiddenParameter]:
119
130
  candidates = {
120
131
  parameter.arg: parameter
121
132
  for parameter, default in zip(init.args.kwonlyargs, init.args.kw_defaults, strict=True)
@@ -124,15 +135,16 @@ def _hidden_parameters(
124
135
  if not candidates:
125
136
  return []
126
137
 
127
- hidden: set[str] = set()
138
+ hidden: dict[str, bool] = {}
128
139
  rebound: set[str] = set()
129
140
  shadowed = _argument_names(init.args)
130
141
  for statement in init.body:
131
142
  available = candidates.keys() - rebound
132
- hidden.update(_statement_fallbacks(statement, available, resolver, shadowed))
143
+ for name, uses_boolean_or in _statement_fallbacks(statement, available, resolver, shadowed).items():
144
+ hidden[name] = hidden.get(name, False) or uses_boolean_or
133
145
  rebound.update(_directly_bound_names(statement) & candidates.keys())
134
146
  shadowed.update(_directly_bound_names(statement))
135
- return [parameter for name, parameter in candidates.items() if name in hidden]
147
+ return [_HiddenParameter(parameter, hidden[name]) for name, parameter in candidates.items() if name in hidden]
136
148
 
137
149
 
138
150
  def _statement_fallbacks(
@@ -140,16 +152,16 @@ def _statement_fallbacks(
140
152
  candidates: set[str],
141
153
  resolver: _RuntimeConfigResolver,
142
154
  shadowed: set[str],
143
- ) -> set[str]:
155
+ ) -> dict[str, bool]:
144
156
  if isinstance(statement, ast.Assign):
145
157
  return _expression_fallbacks(statement.value, candidates, resolver, shadowed)
146
158
  if isinstance(statement, ast.AnnAssign) and statement.value is not None:
147
159
  return _expression_fallbacks(statement.value, candidates, resolver, shadowed)
148
160
  if not isinstance(statement, ast.If) or statement.orelse or len(statement.body) != 1:
149
- return set()
161
+ return {}
150
162
  parameter = _none_comparison_parameter(statement.test, candidates, expect_not=False)
151
163
  if parameter is None:
152
- return set()
164
+ return {}
153
165
  body = statement.body[0]
154
166
  if isinstance(body, ast.Assign):
155
167
  value = body.value if len(body.targets) == 1 and _is_name(body.targets[0], parameter) else None
@@ -157,7 +169,7 @@ def _statement_fallbacks(
157
169
  value = body.value if _is_name(body.target, parameter) else None
158
170
  else:
159
171
  value = None
160
- return {parameter} if value is not None and resolver.is_runtime_config(value, shadowed) else set()
172
+ return {parameter: False} if value is not None and resolver.is_runtime_config(value, shadowed) else {}
161
173
 
162
174
 
163
175
  def _expression_fallbacks(
@@ -165,15 +177,15 @@ def _expression_fallbacks(
165
177
  candidates: set[str],
166
178
  resolver: _RuntimeConfigResolver,
167
179
  shadowed: set[str],
168
- ) -> set[str]:
180
+ ) -> dict[str, bool]:
169
181
  if isinstance(expression, ast.BoolOp) and isinstance(expression.op, ast.Or):
170
182
  match expression.values:
171
183
  case [ast.Name(id=parameter), fallback] if parameter in candidates:
172
- return {parameter} if resolver.is_runtime_config(fallback, shadowed) else set()
184
+ return {parameter: True} if resolver.is_runtime_config(fallback, shadowed) else {}
173
185
  case _:
174
- return set()
186
+ return {}
175
187
  if not isinstance(expression, ast.IfExp):
176
- return set()
188
+ return {}
177
189
  not_none = _none_comparison_parameter(expression.test, candidates, expect_not=True)
178
190
  if (
179
191
  not_none is not None
@@ -181,7 +193,7 @@ def _expression_fallbacks(
181
193
  and expression.body.id == not_none
182
194
  and resolver.is_runtime_config(expression.orelse, shadowed)
183
195
  ):
184
- return {not_none}
196
+ return {not_none: False}
185
197
  is_none = _none_comparison_parameter(expression.test, candidates, expect_not=False)
186
198
  if (
187
199
  is_none is not None
@@ -189,8 +201,8 @@ def _expression_fallbacks(
189
201
  and expression.orelse.id == is_none
190
202
  and resolver.is_runtime_config(expression.body, shadowed)
191
203
  ):
192
- return {is_none}
193
- return set()
204
+ return {is_none: False}
205
+ return {}
194
206
 
195
207
 
196
208
  def _none_comparison_parameter(test: ast.expr, candidates: set[str], *, expect_not: bool) -> str | None: