pyrefactor 1.0.7__tar.gz → 1.0.9__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 (51) hide show
  1. {pyrefactor-1.0.7/src/pyrefactor.egg-info → pyrefactor-1.0.9}/PKG-INFO +14 -7
  2. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/README.md +13 -6
  3. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/pyproject.toml +2 -2
  4. pyrefactor-1.0.9/src/pyrefactor/__init__.py +20 -0
  5. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/__main__.py +13 -6
  6. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/_version.py +8 -1
  7. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/analyzer.py +2 -1
  8. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/ast_visitor.py +0 -1
  9. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/config.py +89 -31
  10. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/boolean_logic.py +21 -16
  11. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/comparisons.py +45 -90
  12. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/complexity.py +0 -8
  13. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/context_manager.py +3 -37
  14. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/control_flow.py +12 -36
  15. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/dict_operations.py +66 -60
  16. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/duplication.py +3 -1
  17. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/loops.py +26 -54
  18. pyrefactor-1.0.9/src/pyrefactor/detectors/performance.py +309 -0
  19. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/reporter.py +1 -1
  20. {pyrefactor-1.0.7 → pyrefactor-1.0.9/src/pyrefactor.egg-info}/PKG-INFO +14 -7
  21. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_analyzer.py +101 -9
  22. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_cli.py +79 -7
  23. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_complexity_detector.py +0 -126
  24. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_config.py +147 -20
  25. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_config_discovery.py +28 -14
  26. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_dict_operations_detector.py +15 -0
  27. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_duplication_detector.py +7 -8
  28. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_integration.py +18 -20
  29. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_loops_detector.py +2 -5
  30. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_performance_detector.py +126 -6
  31. pyrefactor-1.0.9/tests/test_version.py +104 -0
  32. pyrefactor-1.0.7/src/pyrefactor/__init__.py +0 -5
  33. pyrefactor-1.0.7/src/pyrefactor/detectors/performance.py +0 -201
  34. pyrefactor-1.0.7/tests/test_version.py +0 -50
  35. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/LICENSE.md +0 -0
  36. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/setup.cfg +0 -0
  37. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/detectors/__init__.py +0 -0
  38. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/models.py +0 -0
  39. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor/py.typed +0 -0
  40. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/SOURCES.txt +0 -0
  41. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
  42. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/entry_points.txt +0 -0
  43. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/requires.txt +0 -0
  44. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/src/pyrefactor.egg-info/top_level.txt +0 -0
  45. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_ast_visitor.py +0 -0
  46. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_boolean_logic_detector.py +0 -0
  47. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_comparisons_detector.py +0 -0
  48. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_context_manager_detector.py +0 -0
  49. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_control_flow_detector.py +0 -0
  50. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_models.py +0 -0
  51. {pyrefactor-1.0.7 → pyrefactor-1.0.9}/tests/test_reporter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrefactor
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement
5
5
  Author: tboy1337
6
6
  Maintainer: tboy1337
@@ -70,7 +70,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
70
70
  ## Detectors
71
71
 
72
72
  - **Complexity**: High cyclomatic complexity functions
73
- - **Performance**: String concatenation in loops, uncached calls, inefficient operations
73
+ - **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
74
74
  - **Boolean Logic**: Overcomplicated boolean expressions
75
75
  - **Loops**: Nested loops, invariant code, comprehension opportunities
76
76
  - **Duplication**: Duplicate code blocks
@@ -145,7 +145,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
145
145
  exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
146
146
 
147
147
  [tool.pyrefactor.complexity]
148
- max_complexity = 10
148
+ enabled = true
149
+ max_cyclomatic_complexity = 10
150
+ max_branches = 10
151
+ max_nesting_depth = 3
152
+ max_function_lines = 50
153
+ max_arguments = 5
154
+ max_local_variables = 15
149
155
 
150
156
  [tool.pyrefactor.performance]
151
157
  enabled = true
@@ -154,16 +160,15 @@ min_duplicate_calls = 3
154
160
 
155
161
  [tool.pyrefactor.boolean_logic]
156
162
  enabled = true
157
- min_depth = 3
163
+ max_boolean_operators = 3
158
164
 
159
165
  [tool.pyrefactor.loops]
160
166
  enabled = true
161
- max_nesting = 3
162
167
 
163
168
  [tool.pyrefactor.duplication]
164
169
  enabled = true
165
- min_lines = 5
166
- similarity_threshold = 0.8
170
+ min_duplicate_lines = 5
171
+ similarity_threshold = 0.85
167
172
 
168
173
  [tool.pyrefactor.context_manager]
169
174
  enabled = true
@@ -180,6 +185,8 @@ enabled = true
180
185
 
181
186
  Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
182
187
 
188
+ **Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
189
+
183
190
  ## CI/CD Integration
184
191
 
185
192
  ### Pre-commit Hook
@@ -15,7 +15,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
15
15
  ## Detectors
16
16
 
17
17
  - **Complexity**: High cyclomatic complexity functions
18
- - **Performance**: String concatenation in loops, uncached calls, inefficient operations
18
+ - **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
19
19
  - **Boolean Logic**: Overcomplicated boolean expressions
20
20
  - **Loops**: Nested loops, invariant code, comprehension opportunities
21
21
  - **Duplication**: Duplicate code blocks
@@ -90,7 +90,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
90
90
  exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
91
91
 
92
92
  [tool.pyrefactor.complexity]
93
- max_complexity = 10
93
+ enabled = true
94
+ max_cyclomatic_complexity = 10
95
+ max_branches = 10
96
+ max_nesting_depth = 3
97
+ max_function_lines = 50
98
+ max_arguments = 5
99
+ max_local_variables = 15
94
100
 
95
101
  [tool.pyrefactor.performance]
96
102
  enabled = true
@@ -99,16 +105,15 @@ min_duplicate_calls = 3
99
105
 
100
106
  [tool.pyrefactor.boolean_logic]
101
107
  enabled = true
102
- min_depth = 3
108
+ max_boolean_operators = 3
103
109
 
104
110
  [tool.pyrefactor.loops]
105
111
  enabled = true
106
- max_nesting = 3
107
112
 
108
113
  [tool.pyrefactor.duplication]
109
114
  enabled = true
110
- min_lines = 5
111
- similarity_threshold = 0.8
115
+ min_duplicate_lines = 5
116
+ similarity_threshold = 0.85
112
117
 
113
118
  [tool.pyrefactor.context_manager]
114
119
  enabled = true
@@ -125,6 +130,8 @@ enabled = true
125
130
 
126
131
  Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
127
132
 
133
+ **Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
134
+
128
135
  ## CI/CD Integration
129
136
 
130
137
  ### Pre-commit Hook
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyrefactor"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  description = "A Python refactoring and optimization linter that analyzes code for performance issues, complexity problems, and opportunities for improvement"
9
9
  authors = [{name = "tboy1337"}]
10
10
  maintainers = [{name = "tboy1337"}]
11
11
  readme = "README.md"
12
- license = {text = "Commercial Restricted License (CRL)"}
12
+ license = { text = "Commercial Restricted License (CRL)" }
13
13
  requires-python = ">=3.12"
14
14
  keywords = [
15
15
  "refactoring",
@@ -0,0 +1,20 @@
1
+ """PyRefactor - A Python refactoring and optimization linter."""
2
+
3
+ from pyrefactor._version import get_version
4
+ from pyrefactor.analyzer import Analyzer
5
+ from pyrefactor.config import Config
6
+ from pyrefactor.models import AnalysisResult, FileAnalysis, Issue, Severity
7
+ from pyrefactor.reporter import ConsoleReporter
8
+
9
+ __version__ = get_version()
10
+
11
+ __all__ = [
12
+ "Analyzer",
13
+ "AnalysisResult",
14
+ "Config",
15
+ "ConsoleReporter",
16
+ "FileAnalysis",
17
+ "Issue",
18
+ "Severity",
19
+ "__version__",
20
+ ]
@@ -3,6 +3,7 @@
3
3
  import argparse
4
4
  import logging
5
5
  import sys
6
+ import tomllib
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path
8
9
  from typing import Optional
@@ -137,8 +138,11 @@ def _load_config(args: Args) -> Optional[Config]:
137
138
  config = Config.load(args.config)
138
139
  logger.info("Loaded configuration: %s", config)
139
140
  return config
140
- except Exception as e:
141
- logger.error("Error loading configuration: %s", e)
141
+ except (ValueError, OSError, tomllib.TOMLDecodeError) as e:
142
+ if args.verbose:
143
+ logger.error("Error loading configuration: %s", e, exc_info=True)
144
+ else:
145
+ logger.error("Error loading configuration: %s", e)
142
146
  return None
143
147
 
144
148
 
@@ -154,14 +158,17 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
154
158
 
155
159
 
156
160
  def _analyze_files_safely(
157
- analyzer: Analyzer, paths: list[Path], max_workers: int
161
+ analyzer: Analyzer, paths: list[Path], max_workers: int, *, verbose: bool = False
158
162
  ) -> Optional[AnalysisResult]:
159
163
  """Analyze files and handle errors. Returns result or None on error."""
160
164
  try:
161
165
  logger.info("Analyzing %d path(s)...", len(paths))
162
166
  return analyzer.analyze_files(paths, max_workers=max_workers)
163
- except Exception as e:
164
- logger.error("Error during analysis: %s", e)
167
+ except (OSError, RuntimeError) as e:
168
+ if verbose:
169
+ logger.error("Error during analysis: %s", e, exc_info=True)
170
+ else:
171
+ logger.error("Error during analysis: %s", e)
165
172
  return None
166
173
 
167
174
 
@@ -216,7 +223,7 @@ def main() -> int:
216
223
  # Create analyzer and analyze files
217
224
  max_workers = max(1, args.jobs)
218
225
  analyzer = Analyzer(config)
219
- result = _analyze_files_safely(analyzer, paths, max_workers)
226
+ result = _analyze_files_safely(analyzer, paths, max_workers, verbose=args.verbose)
220
227
  if result is None:
221
228
  return 2
222
229
 
@@ -1,5 +1,6 @@
1
1
  """Package version resolution."""
2
2
 
3
+ import sys
3
4
  from functools import lru_cache
4
5
  from importlib.metadata import PackageNotFoundError, version
5
6
  from pathlib import Path
@@ -8,7 +9,13 @@ _PACKAGE_NAME = "pyrefactor"
8
9
 
9
10
 
10
11
  def _pyproject_path() -> Path:
11
- """Return the repository pyproject.toml path."""
12
+ """Return the pyproject.toml path for version fallback."""
13
+ if getattr(sys, "frozen", False):
14
+ meipass = getattr(sys, "_MEIPASS", None)
15
+ if meipass:
16
+ bundled = Path(meipass) / "pyproject.toml"
17
+ if bundled.is_file():
18
+ return bundled
12
19
  return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
13
20
 
14
21
 
@@ -40,7 +40,8 @@ class Analyzer:
40
40
  """Create all enabled detectors for a file."""
41
41
  detectors: list[BaseDetector] = []
42
42
 
43
- detectors.append(ComplexityDetector(self.config, file_path, source_lines))
43
+ if self.config.complexity.enabled:
44
+ detectors.append(ComplexityDetector(self.config, file_path, source_lines))
44
45
 
45
46
  detector_configs = [
46
47
  (self.config.performance.enabled, PerformanceDetector),
@@ -39,7 +39,6 @@ class BaseDetector(ast.NodeVisitor, ABC):
39
39
  self.source_lines = source_lines
40
40
  self.issues: list[Issue] = []
41
41
  self.current_function: Union[ast.FunctionDef, ast.AsyncFunctionDef, None] = None
42
- self.nesting_level = 0
43
42
 
44
43
  @abstractmethod
45
44
  def get_detector_name(self) -> str:
@@ -11,6 +11,7 @@ from typing import Any, Mapping, Optional, Union
11
11
  class ComplexityConfig:
12
12
  """Configuration for complexity detector."""
13
13
 
14
+ enabled: bool = True
14
15
  max_branches: int = 10
15
16
  max_nesting_depth: int = 3
16
17
  max_function_lines: int = 50
@@ -24,6 +25,8 @@ class PerformanceConfig:
24
25
  """Configuration for performance detector."""
25
26
 
26
27
  enabled: bool = True
28
+ min_concatenations: int = 3
29
+ min_duplicate_calls: int = 3
27
30
 
28
31
 
29
32
  @dataclass
@@ -94,10 +97,14 @@ class Config:
94
97
  exclude_patterns: list[str] = field(default_factory=list)
95
98
 
96
99
  @staticmethod
97
- def _parse_complexity_config(config: configparser.ConfigParser) -> dict[str, int]:
100
+ def _parse_complexity_config(
101
+ config: configparser.ConfigParser,
102
+ ) -> dict[str, Union[int, bool]]:
98
103
  """Extract complexity configuration from config parser."""
99
- complexity_dict: dict[str, int] = {}
104
+ complexity_dict: dict[str, Union[int, bool]] = {}
100
105
  if config.has_section("complexity"):
106
+ if config.has_option("complexity", "enabled"):
107
+ complexity_dict["enabled"] = config.getboolean("complexity", "enabled")
101
108
  for key in [
102
109
  "max_branches",
103
110
  "max_nesting_depth",
@@ -110,6 +117,27 @@ class Config:
110
117
  complexity_dict[key] = config.getint("complexity", key)
111
118
  return complexity_dict
112
119
 
120
+ @staticmethod
121
+ def _parse_performance_config(
122
+ config: configparser.ConfigParser,
123
+ ) -> dict[str, Union[int, bool]]:
124
+ """Extract performance configuration from config parser."""
125
+ performance_dict: dict[str, Union[int, bool]] = {}
126
+ if config.has_section("performance"):
127
+ if config.has_option("performance", "enabled"):
128
+ performance_dict["enabled"] = config.getboolean(
129
+ "performance", "enabled"
130
+ )
131
+ if config.has_option("performance", "min_concatenations"):
132
+ performance_dict["min_concatenations"] = config.getint(
133
+ "performance", "min_concatenations"
134
+ )
135
+ if config.has_option("performance", "min_duplicate_calls"):
136
+ performance_dict["min_duplicate_calls"] = config.getint(
137
+ "performance", "min_duplicate_calls"
138
+ )
139
+ return performance_dict
140
+
113
141
  @staticmethod
114
142
  def _parse_duplication_config(
115
143
  config: configparser.ConfigParser,
@@ -207,6 +235,7 @@ class Config:
207
235
  raise ValueError("Invalid [tool.pyrefactor] section in configuration")
208
236
 
209
237
  complexity_fields = {
238
+ "enabled": bool,
210
239
  "max_branches": int,
211
240
  "max_nesting_depth": int,
212
241
  "max_function_lines": int,
@@ -214,6 +243,11 @@ class Config:
214
243
  "max_local_variables": int,
215
244
  "max_cyclomatic_complexity": int,
216
245
  }
246
+ performance_fields = {
247
+ "enabled": bool,
248
+ "min_concatenations": int,
249
+ "min_duplicate_calls": int,
250
+ }
217
251
  duplication_fields = {
218
252
  "enabled": bool,
219
253
  "min_duplicate_lines": int,
@@ -232,9 +266,7 @@ class Config:
232
266
  exclude_patterns = [str(pattern) for pattern in raw_exclude]
233
267
  elif isinstance(raw_exclude, str):
234
268
  exclude_patterns = [
235
- pattern.strip()
236
- for pattern in raw_exclude.split(",")
237
- if pattern.strip()
269
+ pattern.strip() for pattern in raw_exclude.split(",") if pattern.strip()
238
270
  ]
239
271
 
240
272
  return cls(
@@ -246,15 +278,21 @@ class Config:
246
278
  ),
247
279
  performance=PerformanceConfig(
248
280
  **cls._coerce_section(
249
- pyrefactor.get("performance", {})
250
- if isinstance(pyrefactor.get("performance"), dict)
251
- else {},
252
- enabled_only,
281
+ (
282
+ pyrefactor.get("performance", {})
283
+ if isinstance(pyrefactor.get("performance"), dict)
284
+ else {}
285
+ ),
286
+ performance_fields,
253
287
  )
254
288
  ),
255
289
  duplication=DuplicationConfig(
256
290
  **cls._coerce_section(
257
- duplication_section if isinstance(duplication_section, dict) else {},
291
+ (
292
+ duplication_section
293
+ if isinstance(duplication_section, dict)
294
+ else {}
295
+ ),
258
296
  duplication_fields,
259
297
  )
260
298
  ),
@@ -266,47 +304,66 @@ class Config:
266
304
  ),
267
305
  loops=LoopsConfig(
268
306
  **cls._coerce_section(
269
- pyrefactor.get("loops", {})
270
- if isinstance(pyrefactor.get("loops"), dict)
271
- else {},
307
+ (
308
+ pyrefactor.get("loops", {})
309
+ if isinstance(pyrefactor.get("loops"), dict)
310
+ else {}
311
+ ),
272
312
  enabled_only,
273
313
  )
274
314
  ),
275
315
  context_manager=ContextManagerConfig(
276
316
  **cls._coerce_section(
277
- pyrefactor.get("context_manager", {})
278
- if isinstance(pyrefactor.get("context_manager"), dict)
279
- else {},
317
+ (
318
+ pyrefactor.get("context_manager", {})
319
+ if isinstance(pyrefactor.get("context_manager"), dict)
320
+ else {}
321
+ ),
280
322
  enabled_only,
281
323
  )
282
324
  ),
283
325
  control_flow=ControlFlowConfig(
284
326
  **cls._coerce_section(
285
- pyrefactor.get("control_flow", {})
286
- if isinstance(pyrefactor.get("control_flow"), dict)
287
- else {},
327
+ (
328
+ pyrefactor.get("control_flow", {})
329
+ if isinstance(pyrefactor.get("control_flow"), dict)
330
+ else {}
331
+ ),
288
332
  enabled_only,
289
333
  )
290
334
  ),
291
335
  dict_operations=DictOperationsConfig(
292
336
  **cls._coerce_section(
293
- pyrefactor.get("dict_operations", {})
294
- if isinstance(pyrefactor.get("dict_operations"), dict)
295
- else {},
337
+ (
338
+ pyrefactor.get("dict_operations", {})
339
+ if isinstance(pyrefactor.get("dict_operations"), dict)
340
+ else {}
341
+ ),
296
342
  enabled_only,
297
343
  )
298
344
  ),
299
345
  comparisons=ComparisonsConfig(
300
346
  **cls._coerce_section(
301
- pyrefactor.get("comparisons", {})
302
- if isinstance(pyrefactor.get("comparisons"), dict)
303
- else {},
347
+ (
348
+ pyrefactor.get("comparisons", {})
349
+ if isinstance(pyrefactor.get("comparisons"), dict)
350
+ else {}
351
+ ),
304
352
  enabled_only,
305
353
  )
306
354
  ),
307
355
  exclude_patterns=exclude_patterns,
308
356
  )
309
357
 
358
+ @staticmethod
359
+ def _has_pyrefactor_config(data: dict[str, Any]) -> bool:
360
+ """Return True when parsed TOML contains a non-empty [tool.pyrefactor] table."""
361
+ tool_section = data.get("tool")
362
+ if not isinstance(tool_section, dict):
363
+ return False
364
+ pyrefactor = tool_section.get("pyrefactor")
365
+ return isinstance(pyrefactor, dict) and bool(pyrefactor)
366
+
310
367
  @classmethod
311
368
  def from_toml_file(cls, config_path: Path) -> "Config":
312
369
  """Load configuration from a TOML file."""
@@ -328,10 +385,8 @@ class Config:
328
385
  parser.read(config_path, encoding="utf-8")
329
386
 
330
387
  return cls(
331
- complexity=ComplexityConfig(**cls._parse_complexity_config(parser)),
332
- performance=PerformanceConfig(
333
- **cls._parse_enabled_flag(parser, "performance")
334
- ),
388
+ complexity=ComplexityConfig(**cls._parse_complexity_config(parser)), # type: ignore[arg-type]
389
+ performance=PerformanceConfig(**cls._parse_performance_config(parser)), # type: ignore[arg-type]
335
390
  duplication=DuplicationConfig(**cls._parse_duplication_config(parser)), # type: ignore[arg-type]
336
391
  boolean_logic=BooleanLogicConfig(**cls._parse_boolean_logic_config(parser)), # type: ignore[arg-type]
337
392
  loops=LoopsConfig(**cls._parse_enabled_flag(parser, "loops")),
@@ -367,8 +422,11 @@ class Config:
367
422
  return cls.from_file(config_path)
368
423
 
369
424
  pyproject = Path("pyproject.toml")
370
- if pyproject.exists():
371
- return cls.from_toml_file(pyproject)
425
+ if pyproject.is_file():
426
+ with pyproject.open("rb") as config_file:
427
+ data = tomllib.load(config_file)
428
+ if cls._has_pyrefactor_config(data):
429
+ return cls.from_toml_data(data)
372
430
 
373
431
  ini_file = Path("pyrefactor.ini")
374
432
  if ini_file.exists():
@@ -45,24 +45,29 @@ class BooleanLogicDetector(BaseDetector):
45
45
  self.generic_visit(node)
46
46
  return
47
47
 
48
+ self._check_boolean_singleton_comparison(node)
49
+ self.generic_visit(node)
50
+
51
+ def _check_boolean_singleton_comparison(self, node: ast.Compare) -> None:
52
+ """Report use of ``is`` when comparing to boolean constants."""
48
53
  for comparator in node.comparators:
49
- if isinstance(comparator, ast.Constant) and isinstance(
50
- comparator.value, bool
54
+ if not (
55
+ isinstance(comparator, ast.Constant)
56
+ and isinstance(comparator.value, bool)
51
57
  ):
52
- for op in node.ops:
53
- if isinstance(op, ast.Is):
54
- self.report_issue(
55
- node,
56
- severity=Severity.MEDIUM,
57
- rule_id="B004",
58
- message="Using 'is' for boolean comparison",
59
- suggestion=(
60
- "Use '==' for value comparison or use the boolean directly"
61
- ),
62
- )
63
- break
64
-
65
- self.generic_visit(node)
58
+ continue
59
+ if not any(isinstance(op, ast.Is) for op in node.ops):
60
+ continue
61
+ self.report_issue(
62
+ node,
63
+ severity=Severity.MEDIUM,
64
+ rule_id="B004",
65
+ message="Using 'is' for boolean comparison",
66
+ suggestion=(
67
+ "Use '==' for value comparison or use the boolean directly"
68
+ ),
69
+ )
70
+ return
66
71
 
67
72
  def visit_FunctionDef(
68
73
  self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]