sarj-python-lint 0.81.5__tar.gz → 0.81.6__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 (126) hide show
  1. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -1
  4. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -1
  5. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -1
  6. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +1 -1
  7. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +4 -4
  8. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +1 -1
  9. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +60 -6
  10. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/.gitignore +0 -0
  11. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/LICENSE +0 -0
  12. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/README.md +0 -0
  13. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/__init__.py +0 -0
  14. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/__main__.py +0 -0
  15. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_analysis_session.py +0 -0
  16. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_python_target.py +0 -0
  18. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  19. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_secret_names.py +0 -0
  20. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  26. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_comments.py +0 -0
  27. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  28. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  29. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
  30. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  31. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_imports.py +0 -0
  32. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_logging.py +0 -0
  33. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_paths.py +0 -0
  34. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
  35. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  36. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
  37. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_registry.py +0 -0
  38. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_sql.py +0 -0
  39. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  40. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  41. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  42. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  43. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  44. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  45. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  46. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  47. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  48. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  49. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  50. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  51. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  52. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  53. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  54. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  55. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  56. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  57. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  58. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  59. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  60. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  61. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  62. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  63. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  64. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  65. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  66. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  67. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  68. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  69. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  70. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  71. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  72. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  73. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  74. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  75. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  76. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  77. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  78. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  79. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  80. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  81. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  82. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  83. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  84. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  85. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  86. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  87. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  88. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  89. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  90. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  91. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  92. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  93. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  94. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  95. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  96. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  97. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  98. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  99. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  100. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  101. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  102. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  103. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  104. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  105. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  106. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  107. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  108. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  109. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  110. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  111. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  112. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  113. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  114. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  115. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  116. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  117. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  118. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  119. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
  120. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  121. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  122. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  123. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  124. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  125. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  126. {sarj_python_lint-0.81.5 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.81.5
3
+ Version: 0.81.6
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.81.5"
3
+ version = "0.81.6"
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" }]
@@ -200,7 +200,7 @@ def _query_argument(call: ast.Call) -> ast.expr | None:
200
200
  tokens = frozenset(part for part in receiver.split("_") if part)
201
201
  if (
202
202
  receiver not in _QUERY_RECEIVER_TOKENS
203
- and not tokens.intersection(_QUERY_RECEIVER_TOKENS)
203
+ and tokens.isdisjoint(_QUERY_RECEIVER_TOKENS)
204
204
  and not any(receiver.endswith(suffix) for suffix in ("conn", "cur", "cursor", "session"))
205
205
  ):
206
206
  return None
@@ -761,7 +761,7 @@ def _representation_fixture_path(node: ast.AST) -> bool:
761
761
  if not isinstance(child, ast.Constant) or not isinstance(child.value, str):
762
762
  continue
763
763
  parts = {part.lower() for part in child.value.replace("\\", "/").split("/")}
764
- if parts & _REPRESENTATION_DIRS:
764
+ if not parts.isdisjoint(_REPRESENTATION_DIRS):
765
765
  return True
766
766
  return False
767
767
 
@@ -121,7 +121,7 @@ def _public_contract_docstring_lines(path: Path, source: str) -> frozenset[int]:
121
121
  protocol_method = isinstance(owner, ast.ClassDef) and any(
122
122
  _terminal_name(base) in _PUBLIC_CONTRACT_BASES for base in owner.bases
123
123
  )
124
- if decorators & _PUBLIC_CONTRACT_DECORATORS or protocol_method:
124
+ if not decorators.isdisjoint(_PUBLIC_CONTRACT_DECORATORS) or protocol_method:
125
125
  excluded.add(first.lineno)
126
126
  return frozenset(excluded)
127
127
 
@@ -647,7 +647,7 @@ def _schema_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> fr
647
647
  _resolve_imported_name(base.value if isinstance(base, ast.Subscript) else base, imports)
648
648
  for base in class_node.bases
649
649
  }
650
- if base_names & (_KNOWN_SCHEMA_BASES | consumed):
650
+ if not base_names.isdisjoint(_KNOWN_SCHEMA_BASES | consumed):
651
651
  consumed.add(name)
652
652
  changed = True
653
653
  return frozenset(id(classes[name]) for name in consumed)
@@ -299,15 +299,15 @@ def _name_role(identifier: str) -> _Role:
299
299
  def _is_authenticator_name(identifier: str) -> bool:
300
300
  tokens = identifier_tokens(identifier)
301
301
  token_set = set(tokens)
302
- is_auth_header = "header" in token_set and bool(token_set & {"auth", "authorization"})
302
+ is_auth_header = "header" in token_set and not token_set.isdisjoint({"auth", "authorization"})
303
303
  if not is_secret_name(identifier) and not is_auth_header:
304
304
  return False
305
305
  if (token_set == {"token"} and not identifier.isupper()) or not tokens:
306
306
  return False
307
- if tokens[-1] in _DESCRIPTOR_WORDS or token_set & _CATEGORY_WORDS:
307
+ if tokens[-1] in _DESCRIPTOR_WORDS or not token_set.isdisjoint(_CATEGORY_WORDS):
308
308
  return False
309
- return bool(
310
- token_set & _AUTH_WORDS
309
+ return (
310
+ not token_set.isdisjoint(_AUTH_WORDS)
311
311
  or is_auth_header
312
312
  or any(left == "api" and right == "key" for left, right in pairwise(tokens))
313
313
  )
@@ -431,7 +431,7 @@ def _missing_unit(statement: ast.Assign | ast.AnnAssign, name: str, comment: str
431
431
  # A name carrying a different unit is contradictory, but appending a second
432
432
  # suffix would worsen it; leave the conflict to a dedicated future rule.
433
433
  mentioned_name_tokens = frozenset(token for unit in mentioned for token in unit.name_tokens)
434
- if declaration_tokens & (_CONFLICT_UNIT_NAME_TOKENS - mentioned_name_tokens):
434
+ if not declaration_tokens.isdisjoint(_CONFLICT_UNIT_NAME_TOKENS - mentioned_name_tokens):
435
435
  return None
436
436
  # A rationale can mention several quantities; once the name identifies one,
437
437
  # guessing that another quantity is the assigned value is unsafe.
@@ -23,6 +23,24 @@ if TYPE_CHECKING:
23
23
  from pathlib import Path
24
24
 
25
25
 
26
+ _TRACKED_BUILTINS = frozenset(
27
+ {
28
+ "bool",
29
+ "dict",
30
+ "enumerate",
31
+ "filter",
32
+ "frozenset",
33
+ "list",
34
+ "map",
35
+ "range",
36
+ "reversed",
37
+ "set",
38
+ "tuple",
39
+ "zip",
40
+ }
41
+ )
42
+
43
+
26
44
  @final
27
45
  class PreferSetIsdisjoint(Rule):
28
46
  id = "prefer-set-isdisjoint"
@@ -35,7 +53,7 @@ class PreferSetIsdisjoint(Rule):
35
53
  autofix=AutofixPolicy.SUGGESTION,
36
54
  limitations=(
37
55
  "Built-in set identity must be proven from a literal, comprehension, constructor, or one dominating local assignment.",
38
- "Both intersection operands must be proven built-in sets; annotations, parameters, attributes, subclasses, branch-merged bindings, stored intersections, and generated files are excluded.",
56
+ "For binary `&`, the left operand must be a proven built-in set and the right operand must be a literal, comprehension, built-in iterable constructor, standard no-argument collection view, or proven set; annotations, parameters, attributes, subclasses, branch-merged bindings, stored intersections, and generated files are excluded.",
39
57
  "The suggestion is intentionally not an autofix because short-circuiting may make custom element equality or hashing side effects observable.",
40
58
  ),
41
59
  examples=(
@@ -185,6 +203,8 @@ class _Scanner:
185
203
  if isinstance(expression, ast.UnaryOp) and isinstance(expression.op, ast.Not):
186
204
  negated = True
187
205
  candidate = expression.operand
206
+ if isinstance(candidate, ast.Call) and _is_builtin_bool_call(candidate, self.shadowed):
207
+ candidate = candidate.args[0]
188
208
  if _is_intersection(candidate, safe_exact, self.shadowed):
189
209
  self._report(candidate, negated=negated)
190
210
 
@@ -246,7 +266,7 @@ class _Scanner:
246
266
 
247
267
  def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
248
268
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitAnd):
249
- return _is_exact_set(node.left, exact, shadowed) and _is_exact_set(node.right, exact, shadowed)
269
+ return _is_exact_set(node.left, exact, shadowed) and _is_proven_iterable(node.right, exact, shadowed)
250
270
  return (
251
271
  isinstance(node, ast.Call)
252
272
  and isinstance(node.func, ast.Attribute)
@@ -254,7 +274,17 @@ def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str])
254
274
  and len(node.args) == 1
255
275
  and not node.keywords
256
276
  and _is_exact_set(node.func.value, exact, shadowed)
257
- and _is_exact_set(node.args[0], exact, shadowed)
277
+ )
278
+
279
+
280
+ def _is_builtin_bool_call(node: ast.expr, shadowed: frozenset[str]) -> bool:
281
+ return (
282
+ isinstance(node, ast.Call)
283
+ and isinstance(node.func, ast.Name)
284
+ and node.func.id == "bool"
285
+ and node.func.id not in shadowed
286
+ and len(node.args) == 1
287
+ and not node.keywords
258
288
  )
259
289
 
260
290
 
@@ -273,6 +303,30 @@ def _is_exact_set(node: ast.expr, exact: set[str], shadowed: frozenset[str]) ->
273
303
  )
274
304
 
275
305
 
306
+ def _is_proven_iterable(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
307
+ if _is_exact_set(node, exact, shadowed):
308
+ return True
309
+ if isinstance(
310
+ node,
311
+ ast.List | ast.Tuple | ast.Dict | ast.ListComp | ast.DictComp | ast.GeneratorExp | ast.Constant,
312
+ ):
313
+ return not isinstance(node, ast.Constant) or isinstance(node.value, str | bytes)
314
+ if (
315
+ isinstance(node, ast.Call)
316
+ and isinstance(node.func, ast.Attribute)
317
+ and node.func.attr in {"items", "keys", "values"}
318
+ and not node.args
319
+ and not node.keywords
320
+ ):
321
+ return True
322
+ return (
323
+ isinstance(node, ast.Call)
324
+ and isinstance(node.func, ast.Name)
325
+ and node.func.id in {"dict", "enumerate", "filter", "list", "map", "range", "reversed", "tuple", "zip"}
326
+ and node.func.id not in shadowed
327
+ )
328
+
329
+
276
330
  def _stored_names(node: ast.AST) -> set[str]:
277
331
  return {child.id for child in ast.walk(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
278
332
 
@@ -297,10 +351,10 @@ def _argument_names(arguments: ast.arguments) -> set[str]:
297
351
  def _shadowed_builtins(tree: ast.Module) -> frozenset[str]:
298
352
  shadowed: set[str] = set()
299
353
  for node in ast.walk(tree):
300
- if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in {"set", "frozenset"}:
354
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in _TRACKED_BUILTINS:
301
355
  shadowed.add(node.id)
302
- elif isinstance(node, ast.arg) and node.arg in {"set", "frozenset"}:
356
+ elif isinstance(node, ast.arg) and node.arg in _TRACKED_BUILTINS:
303
357
  shadowed.add(node.arg)
304
- elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in {"set", "frozenset"}:
358
+ elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in _TRACKED_BUILTINS:
305
359
  shadowed.add(node.asname or node.name.split(".")[0])
306
360
  return frozenset(shadowed)