pyrefactor 1.0.6__tar.gz → 1.0.7__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.6/src/pyrefactor.egg-info → pyrefactor-1.0.7}/PKG-INFO +48 -9
  2. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/README.md +31 -4
  3. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/pyproject.toml +30 -9
  4. pyrefactor-1.0.7/src/pyrefactor/__init__.py +5 -0
  5. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/__main__.py +16 -11
  6. pyrefactor-1.0.7/src/pyrefactor/_version.py +33 -0
  7. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/analyzer.py +86 -50
  8. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/ast_visitor.py +121 -10
  9. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/config.py +165 -12
  10. pyrefactor-1.0.7/src/pyrefactor/detectors/boolean_logic.py +154 -0
  11. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/comparisons.py +6 -3
  12. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/complexity.py +9 -6
  13. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/context_manager.py +21 -17
  14. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/duplication.py +2 -2
  15. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/performance.py +48 -114
  16. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/models.py +2 -0
  17. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/reporter.py +1 -1
  18. {pyrefactor-1.0.6 → pyrefactor-1.0.7/src/pyrefactor.egg-info}/PKG-INFO +48 -9
  19. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/SOURCES.txt +5 -1
  20. pyrefactor-1.0.7/src/pyrefactor.egg-info/requires.txt +16 -0
  21. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_analyzer.py +90 -4
  22. pyrefactor-1.0.7/tests/test_ast_visitor.py +82 -0
  23. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_boolean_logic_detector.py +4 -8
  24. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_cli.py +16 -3
  25. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_comparisons_detector.py +6 -6
  26. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_complexity_detector.py +7 -6
  27. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_config.py +104 -0
  28. pyrefactor-1.0.7/tests/test_config_discovery.py +64 -0
  29. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_context_manager_detector.py +18 -5
  30. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_models.py +61 -0
  31. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_reporter.py +56 -0
  32. pyrefactor-1.0.7/tests/test_version.py +50 -0
  33. pyrefactor-1.0.6/src/pyrefactor/__init__.py +0 -3
  34. pyrefactor-1.0.6/src/pyrefactor/detectors/boolean_logic.py +0 -231
  35. pyrefactor-1.0.6/src/pyrefactor.egg-info/requires.txt +0 -1
  36. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/LICENSE.md +0 -0
  37. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/setup.cfg +0 -0
  38. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/__init__.py +0 -0
  39. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/control_flow.py +0 -0
  40. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/dict_operations.py +0 -0
  41. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/detectors/loops.py +0 -0
  42. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor/py.typed +0 -0
  43. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
  44. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/entry_points.txt +0 -0
  45. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/src/pyrefactor.egg-info/top_level.txt +0 -0
  46. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_control_flow_detector.py +0 -0
  47. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_dict_operations_detector.py +0 -0
  48. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_duplication_detector.py +0 -0
  49. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_integration.py +0 -0
  50. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_loops_detector.py +0 -0
  51. {pyrefactor-1.0.6 → pyrefactor-1.0.7}/tests/test_performance_detector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrefactor
3
- Version: 1.0.6
3
+ Version: 1.0.7
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
@@ -20,11 +20,9 @@ Classifier: Operating System :: POSIX :: Linux
20
20
  Classifier: Operating System :: MacOS
21
21
  Classifier: Programming Language :: Python
22
22
  Classifier: Programming Language :: Python :: 3
23
- Classifier: Programming Language :: Python :: 3.9
24
- Classifier: Programming Language :: Python :: 3.10
25
- Classifier: Programming Language :: Python :: 3.11
26
23
  Classifier: Programming Language :: Python :: 3.12
27
24
  Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
28
26
  Classifier: Programming Language :: Python :: 3 :: Only
29
27
  Classifier: Programming Language :: Python :: Implementation :: CPython
30
28
  Classifier: Topic :: Software Development
@@ -35,17 +33,31 @@ Classifier: Topic :: Utilities
35
33
  Classifier: Typing :: Typed
36
34
  Classifier: Environment :: Console
37
35
  Classifier: Natural Language :: English
38
- Requires-Python: >=3.9
36
+ Requires-Python: >=3.12
39
37
  Description-Content-Type: text/markdown
40
38
  License-File: LICENSE.md
41
39
  Requires-Dist: colorama
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest; extra == "dev"
42
+ Requires-Dist: pytest-timeout; extra == "dev"
43
+ Requires-Dist: pytest-xdist; extra == "dev"
44
+ Requires-Dist: pytest-mock; extra == "dev"
45
+ Requires-Dist: pytest-cov; extra == "dev"
46
+ Requires-Dist: pylint; extra == "dev"
47
+ Requires-Dist: mypy; extra == "dev"
48
+ Requires-Dist: autopep8; extra == "dev"
49
+ Requires-Dist: black; extra == "dev"
50
+ Requires-Dist: isort; extra == "dev"
51
+ Requires-Dist: types-colorama; extra == "dev"
52
+ Requires-Dist: bandit; extra == "dev"
53
+ Requires-Dist: safety; extra == "dev"
42
54
  Dynamic: license-file
43
55
 
44
56
  # PyRefactor
45
57
 
46
58
  A Python refactoring and optimization linter that uses AST analysis to identify performance issues, complexity problems, and code improvements.
47
59
 
48
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
60
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
49
61
 
50
62
  ## Features
51
63
 
@@ -87,7 +99,7 @@ cd PyRefactor
87
99
  pip install -e .
88
100
  ```
89
101
 
90
- **Requirements**: Python 3.9+
102
+ **Requirements**: Python 3.12+
91
103
 
92
104
  ## Usage
93
105
 
@@ -197,7 +209,7 @@ jobs:
197
209
  - uses: actions/checkout@v3
198
210
  - uses: actions/setup-python@v4
199
211
  with:
200
- python-version: '3.9'
212
+ python-version: '3.12'
201
213
  - run: pip install pyrefactor
202
214
  - run: pyrefactor --min-severity medium src/
203
215
  ```
@@ -206,8 +218,35 @@ jobs:
206
218
 
207
219
  Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
208
220
 
221
+ ### Development
222
+
223
+ Install the package with development dependencies:
224
+
225
+ ```bash
226
+ pip install -e ".[dev]"
227
+ ```
228
+
229
+ Alternatively:
230
+
231
+ ```bash
232
+ pip install -e .
233
+ pip install -r requirements-dev.txt
234
+ ```
235
+
236
+ Run the local verification script (formatting, type checks, lint, security scan, tests):
237
+
238
+ ```bash
239
+ py scripts/verify.py
240
+ ```
241
+
242
+ Run tests directly:
243
+
244
+ ```bash
245
+ pytest
246
+ ```
247
+
209
248
  1. Follow existing code style (Black, isort)
210
- 2. Add tests for new features (>95% coverage)
249
+ 2. Add tests for new features (>90% coverage)
211
250
  3. Run type checking and linting
212
251
 
213
252
  ## License
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Python refactoring and optimization linter that uses AST analysis to identify performance issues, complexity problems, and code improvements.
4
4
 
5
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
6
6
 
7
7
  ## Features
8
8
 
@@ -44,7 +44,7 @@ cd PyRefactor
44
44
  pip install -e .
45
45
  ```
46
46
 
47
- **Requirements**: Python 3.9+
47
+ **Requirements**: Python 3.12+
48
48
 
49
49
  ## Usage
50
50
 
@@ -154,7 +154,7 @@ jobs:
154
154
  - uses: actions/checkout@v3
155
155
  - uses: actions/setup-python@v4
156
156
  with:
157
- python-version: '3.9'
157
+ python-version: '3.12'
158
158
  - run: pip install pyrefactor
159
159
  - run: pyrefactor --min-severity medium src/
160
160
  ```
@@ -163,8 +163,35 @@ jobs:
163
163
 
164
164
  Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
165
165
 
166
+ ### Development
167
+
168
+ Install the package with development dependencies:
169
+
170
+ ```bash
171
+ pip install -e ".[dev]"
172
+ ```
173
+
174
+ Alternatively:
175
+
176
+ ```bash
177
+ pip install -e .
178
+ pip install -r requirements-dev.txt
179
+ ```
180
+
181
+ Run the local verification script (formatting, type checks, lint, security scan, tests):
182
+
183
+ ```bash
184
+ py scripts/verify.py
185
+ ```
186
+
187
+ Run tests directly:
188
+
189
+ ```bash
190
+ pytest
191
+ ```
192
+
166
193
  1. Follow existing code style (Black, isort)
167
- 2. Add tests for new features (>95% coverage)
194
+ 2. Add tests for new features (>90% coverage)
168
195
  3. Run type checking and linting
169
196
 
170
197
  ## License
@@ -1,16 +1,16 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyrefactor"
7
- dynamic = ["version"]
7
+ version = "1.0.7"
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
12
  license = {text = "Commercial Restricted License (CRL)"}
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.12"
14
14
  keywords = [
15
15
  "refactoring",
16
16
  "linter",
@@ -38,11 +38,9 @@ classifiers = [
38
38
  "Operating System :: MacOS",
39
39
  "Programming Language :: Python",
40
40
  "Programming Language :: Python :: 3",
41
- "Programming Language :: Python :: 3.9",
42
- "Programming Language :: Python :: 3.10",
43
- "Programming Language :: Python :: 3.11",
44
41
  "Programming Language :: Python :: 3.12",
45
42
  "Programming Language :: Python :: 3.13",
43
+ "Programming Language :: Python :: 3.14",
46
44
  "Programming Language :: Python :: 3 :: Only",
47
45
  "Programming Language :: Python :: Implementation :: CPython",
48
46
  "Topic :: Software Development",
@@ -58,6 +56,23 @@ dependencies = [
58
56
  "colorama",
59
57
  ]
60
58
 
59
+ [project.optional-dependencies]
60
+ dev = [
61
+ "pytest",
62
+ "pytest-timeout",
63
+ "pytest-xdist",
64
+ "pytest-mock",
65
+ "pytest-cov",
66
+ "pylint",
67
+ "mypy",
68
+ "autopep8",
69
+ "black",
70
+ "isort",
71
+ "types-colorama",
72
+ "bandit",
73
+ "safety",
74
+ ]
75
+
61
76
  [project.urls]
62
77
  Homepage = "https://github.com/tboy1337/PyRefactor"
63
78
  Repository = "https://github.com/tboy1337/PyRefactor"
@@ -68,15 +83,20 @@ Documentation = "https://github.com/tboy1337/PyRefactor#readme"
68
83
  [project.scripts]
69
84
  pyrefactor = "pyrefactor.__main__:main"
70
85
 
71
- [tool.setuptools.dynamic]
72
- version = {attr = "pyrefactor.__version__"}
86
+ [tool.setuptools]
87
+ package-dir = {"" = "src"}
88
+
89
+ [tool.setuptools.packages.find]
90
+ where = ["src"]
91
+ include = ["pyrefactor*"]
92
+ exclude = ["tests*"]
73
93
 
74
94
  [tool.setuptools.package-data]
75
95
  pyrefactor = ["py.typed"]
76
96
 
77
97
  [tool.black]
78
98
  line-length = 88
79
- target-version = ['py39', 'py310', 'py311', 'py312', 'py313', 'py314']
99
+ target-version = ['py312']
80
100
 
81
101
  [tool.isort]
82
102
  profile = "black"
@@ -86,3 +106,4 @@ include_trailing_comma = true
86
106
  force_grid_wrap = 0
87
107
  use_parentheses = true
88
108
  ensure_newline_before_comments = true
109
+ known_first_party = ["pyrefactor"]
@@ -0,0 +1,5 @@
1
+ """PyRefactor - A Python refactoring and optimization linter."""
2
+
3
+ from pyrefactor._version import get_version
4
+
5
+ __version__ = get_version()
@@ -65,7 +65,11 @@ def _add_parser_arguments(parser: argparse.ArgumentParser) -> None:
65
65
  help="Minimum severity level to report (default: info)",
66
66
  )
67
67
  parser.add_argument(
68
- "-j", "--jobs", type=int, default=4, help="Number of parallel jobs (default: 4)"
68
+ "-j",
69
+ "--jobs",
70
+ type=int,
71
+ default=4,
72
+ help="Number of parallel jobs (default: 4, minimum 1)",
69
73
  )
70
74
  parser.add_argument(
71
75
  "-v", "--verbose", action="store_true", help="Enable verbose logging"
@@ -102,13 +106,13 @@ def parse_arguments() -> Args:
102
106
  # Convert to our typed class
103
107
  # Note: argparse returns Any type for namespace attributes
104
108
  return Args(
105
- paths=namespace.paths, # type: ignore[misc]
106
- config=namespace.config, # type: ignore[misc]
107
- group_by=namespace.group_by, # type: ignore[misc]
108
- min_severity=namespace.min_severity, # type: ignore[misc]
109
- jobs=namespace.jobs, # type: ignore[misc]
110
- verbose=namespace.verbose, # type: ignore[misc]
111
- version=namespace.version, # type: ignore[misc]
109
+ paths=namespace.paths,
110
+ config=namespace.config,
111
+ group_by=namespace.group_by,
112
+ min_severity=namespace.min_severity,
113
+ jobs=namespace.jobs,
114
+ verbose=namespace.verbose,
115
+ version=namespace.version,
112
116
  )
113
117
 
114
118
 
@@ -150,12 +154,12 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
150
154
 
151
155
 
152
156
  def _analyze_files_safely(
153
- analyzer: Analyzer, paths: list[Path]
157
+ analyzer: Analyzer, paths: list[Path], max_workers: int
154
158
  ) -> Optional[AnalysisResult]:
155
159
  """Analyze files and handle errors. Returns result or None on error."""
156
160
  try:
157
161
  logger.info("Analyzing %d path(s)...", len(paths))
158
- return analyzer.analyze_files(paths)
162
+ return analyzer.analyze_files(paths, max_workers=max_workers)
159
163
  except Exception as e:
160
164
  logger.error("Error during analysis: %s", e)
161
165
  return None
@@ -210,8 +214,9 @@ def main() -> int:
210
214
  return 2
211
215
 
212
216
  # Create analyzer and analyze files
217
+ max_workers = max(1, args.jobs)
213
218
  analyzer = Analyzer(config)
214
- result = _analyze_files_safely(analyzer, paths)
219
+ result = _analyze_files_safely(analyzer, paths, max_workers)
215
220
  if result is None:
216
221
  return 2
217
222
 
@@ -0,0 +1,33 @@
1
+ """Package version resolution."""
2
+
3
+ from functools import lru_cache
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from pathlib import Path
6
+
7
+ _PACKAGE_NAME = "pyrefactor"
8
+
9
+
10
+ def _pyproject_path() -> Path:
11
+ """Return the repository pyproject.toml path."""
12
+ return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
13
+
14
+
15
+ @lru_cache(maxsize=1)
16
+ def _fallback_version() -> str:
17
+ """Read version from pyproject.toml when the package is not installed."""
18
+ pyproject = _pyproject_path()
19
+ if not pyproject.is_file():
20
+ return "unknown"
21
+ for line in pyproject.read_text(encoding="utf-8").splitlines():
22
+ stripped = line.strip()
23
+ if stripped.startswith("version = "):
24
+ return stripped.split("=", 1)[1].strip().strip('"').strip("'")
25
+ return "unknown"
26
+
27
+
28
+ def get_version() -> str:
29
+ """Return the installed package version, falling back to pyproject.toml."""
30
+ try:
31
+ return version(_PACKAGE_NAME)
32
+ except PackageNotFoundError:
33
+ return _fallback_version()
@@ -2,8 +2,9 @@
2
2
 
3
3
  import ast
4
4
  import concurrent.futures
5
+ import fnmatch
5
6
  import logging
6
- from pathlib import Path
7
+ from pathlib import Path, PurePosixPath
7
8
 
8
9
  from .ast_visitor import BaseDetector
9
10
  from .config import Config
@@ -22,6 +23,9 @@ from .models import AnalysisResult, FileAnalysis
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
26
+ # Maximum file size to read for analysis (10 MB)
27
+ MAX_FILE_BYTES = 10 * 1024 * 1024
28
+
25
29
 
26
30
  class Analyzer:
27
31
  """Main analyzer that orchestrates all detectors."""
@@ -33,16 +37,11 @@ class Analyzer:
33
37
  def _create_detectors(
34
38
  self, file_path: str, source_lines: list[str]
35
39
  ) -> list[BaseDetector]:
36
- """Create all enabled detectors for a file.
37
-
38
- Factory method to consolidate detector initialization and reduce duplication.
39
- """
40
+ """Create all enabled detectors for a file."""
40
41
  detectors: list[BaseDetector] = []
41
42
 
42
- # Complexity detector (always enabled)
43
43
  detectors.append(ComplexityDetector(self.config, file_path, source_lines))
44
44
 
45
- # Conditionally enabled detectors
46
45
  detector_configs = [
47
46
  (self.config.performance.enabled, PerformanceDetector),
48
47
  (self.config.boolean_logic.enabled, BooleanLogicDetector),
@@ -60,24 +59,42 @@ class Analyzer:
60
59
 
61
60
  return detectors
62
61
 
62
+ def _read_source(self, file_path: Path) -> tuple[str, list[str]] | str:
63
+ """Read source from a file, returning an error message on failure."""
64
+ try:
65
+ file_size = file_path.stat().st_size
66
+ if file_size > MAX_FILE_BYTES:
67
+ return (
68
+ f"File exceeds maximum size of {MAX_FILE_BYTES} bytes "
69
+ f"({file_size} bytes)"
70
+ )
71
+
72
+ source_code = file_path.read_text(encoding="utf-8")
73
+ return source_code, source_code.splitlines()
74
+ except UnicodeDecodeError:
75
+ return "File is not valid UTF-8 text"
76
+ except OSError as e:
77
+ return f"Error reading file: {e}"
78
+
63
79
  def analyze_file(self, file_path: Path) -> FileAnalysis:
64
80
  """Analyze a single Python file."""
65
81
  analysis = FileAnalysis(file_path=str(file_path))
66
82
 
67
83
  try:
68
- # Read the file
69
- source_code = file_path.read_text(encoding="utf-8")
70
- source_lines = source_code.splitlines()
84
+ read_result = self._read_source(file_path)
85
+ if isinstance(read_result, str):
86
+ analysis.parse_error = read_result
87
+ return analysis
88
+
89
+ source_code, source_lines = read_result
71
90
  analysis.lines_of_code = len(source_lines)
72
91
 
73
- # Parse the AST
74
92
  try:
75
93
  tree = ast.parse(source_code, filename=str(file_path))
76
94
  except SyntaxError as e:
77
95
  analysis.parse_error = f"Syntax error: {e}"
78
96
  return analysis
79
97
 
80
- # Create and run all enabled detectors
81
98
  detectors = self._create_detectors(str(file_path), source_lines)
82
99
  self._run_detectors(detectors, tree, analysis, file_path)
83
100
 
@@ -114,18 +131,54 @@ class Analyzer:
114
131
  """Analyze all Python files in a directory."""
115
132
  result = AnalysisResult()
116
133
 
117
- # Find all Python files
118
134
  python_files = list(directory.rglob("*.py"))
119
-
120
- # Filter excluded patterns
121
135
  python_files = self._filter_excluded_files(python_files)
122
136
 
123
137
  if not python_files:
124
138
  logger.warning("No Python files found in %s", directory)
125
139
  return result
126
140
 
127
- # Analyze files in parallel
128
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
141
+ return self._analyze_paths_parallel(python_files, max_workers, result)
142
+
143
+ def analyze_files(
144
+ self, file_paths: list[Path], max_workers: int = 4
145
+ ) -> AnalysisResult:
146
+ """Analyze a list of Python files and directories."""
147
+ result = AnalysisResult()
148
+ paths_to_analyze: list[Path] = []
149
+
150
+ for file_path in file_paths:
151
+ if file_path.is_file():
152
+ if file_path.suffix == ".py" and not self._is_excluded(file_path):
153
+ paths_to_analyze.append(file_path)
154
+ elif file_path.is_dir():
155
+ python_files = [
156
+ path
157
+ for path in file_path.rglob("*.py")
158
+ if not self._is_excluded(path)
159
+ ]
160
+ paths_to_analyze.extend(python_files)
161
+
162
+ if not paths_to_analyze:
163
+ return result
164
+
165
+ return self._analyze_paths_parallel(paths_to_analyze, max_workers, result)
166
+
167
+ def _analyze_paths_parallel(
168
+ self,
169
+ python_files: list[Path],
170
+ max_workers: int,
171
+ result: AnalysisResult,
172
+ ) -> AnalysisResult:
173
+ """Analyze multiple Python files in parallel."""
174
+ workers = max(1, max_workers)
175
+
176
+ if workers == 1 or len(python_files) == 1:
177
+ for file_path in python_files:
178
+ result.add_file_analysis(self.analyze_file(file_path))
179
+ return result
180
+
181
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
129
182
  future_to_file = {
130
183
  executor.submit(self.analyze_file, file_path): file_path
131
184
  for file_path in python_files
@@ -147,39 +200,22 @@ class Analyzer:
147
200
 
148
201
  return result
149
202
 
150
- def analyze_files(self, file_paths: list[Path]) -> AnalysisResult:
151
- """Analyze a list of Python files."""
152
- result = AnalysisResult()
153
-
154
- for file_path in file_paths:
155
- self._process_path(file_path, result)
156
-
157
- return result
158
-
159
- def _process_path(self, file_path: Path, result: AnalysisResult) -> None:
160
- """Process a single file or directory path.
161
-
162
- Args:
163
- file_path: Path to file or directory
164
- result: AnalysisResult to add analyses to
165
- """
166
- if file_path.is_file():
167
- analysis = self.analyze_file(file_path)
168
- result.add_file_analysis(analysis)
169
- elif file_path.is_dir():
170
- dir_result = self.analyze_directory(file_path)
171
- for analysis in dir_result.file_analyses:
172
- result.add_file_analysis(analysis)
203
+ def _is_excluded(self, file_path: Path) -> bool:
204
+ """Check if a file matches any exclusion pattern."""
205
+ if not self.config.exclude_patterns:
206
+ return False
207
+
208
+ posix_path = PurePosixPath(file_path.as_posix())
209
+ for pattern in self.config.exclude_patterns:
210
+ normalized = pattern.replace("\\", "/")
211
+ if posix_path.match(normalized):
212
+ return True
213
+ if fnmatch.fnmatch(posix_path.as_posix(), normalized):
214
+ return True
215
+ if fnmatch.fnmatch(file_path.name, normalized):
216
+ return True
217
+ return False
173
218
 
174
219
  def _filter_excluded_files(self, files: list[Path]) -> list[Path]:
175
220
  """Filter out files matching exclusion patterns."""
176
- if not self.config.exclude_patterns:
177
- return files
178
-
179
- return [
180
- file_path
181
- for file_path in files
182
- if not any(
183
- pattern in str(file_path) for pattern in self.config.exclude_patterns
184
- )
185
- ]
221
+ return [file_path for file_path in files if not self._is_excluded(file_path)]