pyrefactor 1.0.6__tar.gz → 1.0.8__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 (54) hide show
  1. {pyrefactor-1.0.6/src/pyrefactor.egg-info → pyrefactor-1.0.8}/PKG-INFO +61 -15
  2. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/README.md +44 -10
  3. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/pyproject.toml +31 -10
  4. pyrefactor-1.0.8/src/pyrefactor/__init__.py +5 -0
  5. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/__main__.py +24 -13
  6. pyrefactor-1.0.8/src/pyrefactor/_version.py +40 -0
  7. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/analyzer.py +88 -51
  8. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/ast_visitor.py +121 -10
  9. pyrefactor-1.0.8/src/pyrefactor/config.py +423 -0
  10. pyrefactor-1.0.8/src/pyrefactor/detectors/boolean_logic.py +159 -0
  11. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/comparisons.py +6 -3
  12. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/complexity.py +9 -6
  13. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/context_manager.py +21 -17
  14. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/duplication.py +5 -3
  15. pyrefactor-1.0.8/src/pyrefactor/detectors/performance.py +336 -0
  16. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/models.py +2 -0
  17. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/reporter.py +2 -2
  18. {pyrefactor-1.0.6 → pyrefactor-1.0.8/src/pyrefactor.egg-info}/PKG-INFO +61 -15
  19. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/SOURCES.txt +5 -1
  20. pyrefactor-1.0.8/src/pyrefactor.egg-info/requires.txt +16 -0
  21. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_analyzer.py +108 -8
  22. pyrefactor-1.0.8/tests/test_ast_visitor.py +82 -0
  23. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_boolean_logic_detector.py +4 -8
  24. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_cli.py +88 -3
  25. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_comparisons_detector.py +6 -6
  26. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_complexity_detector.py +7 -132
  27. pyrefactor-1.0.8/tests/test_config.py +303 -0
  28. pyrefactor-1.0.8/tests/test_config_discovery.py +62 -0
  29. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_context_manager_detector.py +18 -5
  30. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_integration.py +18 -20
  31. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_models.py +61 -0
  32. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_performance_detector.py +123 -0
  33. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_reporter.py +56 -0
  34. pyrefactor-1.0.8/tests/test_version.py +87 -0
  35. pyrefactor-1.0.6/src/pyrefactor/__init__.py +0 -3
  36. pyrefactor-1.0.6/src/pyrefactor/config.py +0 -224
  37. pyrefactor-1.0.6/src/pyrefactor/detectors/boolean_logic.py +0 -231
  38. pyrefactor-1.0.6/src/pyrefactor/detectors/performance.py +0 -267
  39. pyrefactor-1.0.6/src/pyrefactor.egg-info/requires.txt +0 -1
  40. pyrefactor-1.0.6/tests/test_config.py +0 -132
  41. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/LICENSE.md +0 -0
  42. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/setup.cfg +0 -0
  43. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/__init__.py +0 -0
  44. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/control_flow.py +0 -0
  45. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/dict_operations.py +0 -0
  46. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/detectors/loops.py +0 -0
  47. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor/py.typed +0 -0
  48. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/dependency_links.txt +0 -0
  49. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/entry_points.txt +0 -0
  50. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/src/pyrefactor.egg-info/top_level.txt +0 -0
  51. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_control_flow_detector.py +0 -0
  52. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_dict_operations_detector.py +0 -0
  53. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_duplication_detector.py +0 -0
  54. {pyrefactor-1.0.6 → pyrefactor-1.0.8}/tests/test_loops_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.8
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
 
@@ -58,7 +70,7 @@ A Python refactoring and optimization linter that uses AST analysis to identify
58
70
  ## Detectors
59
71
 
60
72
  - **Complexity**: High cyclomatic complexity functions
61
- - **Performance**: String concatenation in loops, uncached calls, inefficient operations
73
+ - **Performance**: String concatenation in loops (thresholded), repeated uncached calls in loops, inefficient operations
62
74
  - **Boolean Logic**: Overcomplicated boolean expressions
63
75
  - **Loops**: Nested loops, invariant code, comprehension opportunities
64
76
  - **Duplication**: Duplicate code blocks
@@ -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
 
@@ -133,7 +145,13 @@ Configure via TOML file (e.g., `pyproject.toml`):
133
145
  exclude_patterns = ["__pycache__", ".venv", "build", "dist"]
134
146
 
135
147
  [tool.pyrefactor.complexity]
136
- 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
137
155
 
138
156
  [tool.pyrefactor.performance]
139
157
  enabled = true
@@ -142,16 +160,15 @@ min_duplicate_calls = 3
142
160
 
143
161
  [tool.pyrefactor.boolean_logic]
144
162
  enabled = true
145
- min_depth = 3
163
+ max_boolean_operators = 3
146
164
 
147
165
  [tool.pyrefactor.loops]
148
166
  enabled = true
149
- max_nesting = 3
150
167
 
151
168
  [tool.pyrefactor.duplication]
152
169
  enabled = true
153
- min_lines = 5
154
- similarity_threshold = 0.8
170
+ min_duplicate_lines = 5
171
+ similarity_threshold = 0.85
155
172
 
156
173
  [tool.pyrefactor.context_manager]
157
174
  enabled = true
@@ -168,6 +185,8 @@ enabled = true
168
185
 
169
186
  Configuration is searched in: `--config` → `pyproject.toml` → `pyrefactor.ini` → defaults
170
187
 
188
+ **Note:** The PyPI package version (`pyproject.toml`) may differ from GitHub release build numbers used for standalone executables.
189
+
171
190
  ## CI/CD Integration
172
191
 
173
192
  ### Pre-commit Hook
@@ -197,7 +216,7 @@ jobs:
197
216
  - uses: actions/checkout@v3
198
217
  - uses: actions/setup-python@v4
199
218
  with:
200
- python-version: '3.9'
219
+ python-version: '3.12'
201
220
  - run: pip install pyrefactor
202
221
  - run: pyrefactor --min-severity medium src/
203
222
  ```
@@ -206,8 +225,35 @@ jobs:
206
225
 
207
226
  Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
208
227
 
228
+ ### Development
229
+
230
+ Install the package with development dependencies:
231
+
232
+ ```bash
233
+ pip install -e ".[dev]"
234
+ ```
235
+
236
+ Alternatively:
237
+
238
+ ```bash
239
+ pip install -e .
240
+ pip install -r requirements-dev.txt
241
+ ```
242
+
243
+ Run the local verification script (formatting, type checks, lint, security scan, tests):
244
+
245
+ ```bash
246
+ py scripts/verify.py
247
+ ```
248
+
249
+ Run tests directly:
250
+
251
+ ```bash
252
+ pytest
253
+ ```
254
+
209
255
  1. Follow existing code style (Black, isort)
210
- 2. Add tests for new features (>95% coverage)
256
+ 2. Add tests for new features (>90% coverage)
211
257
  3. Run type checking and linting
212
258
 
213
259
  ## 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
 
@@ -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
@@ -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
 
@@ -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
@@ -154,7 +161,7 @@ jobs:
154
161
  - uses: actions/checkout@v3
155
162
  - uses: actions/setup-python@v4
156
163
  with:
157
- python-version: '3.9'
164
+ python-version: '3.12'
158
165
  - run: pip install pyrefactor
159
166
  - run: pyrefactor --min-severity medium src/
160
167
  ```
@@ -163,8 +170,35 @@ jobs:
163
170
 
164
171
  Contributions are welcome! This project is under a Commercial Restricted License (CRL). For commercial use, contact the copyright holder.
165
172
 
173
+ ### Development
174
+
175
+ Install the package with development dependencies:
176
+
177
+ ```bash
178
+ pip install -e ".[dev]"
179
+ ```
180
+
181
+ Alternatively:
182
+
183
+ ```bash
184
+ pip install -e .
185
+ pip install -r requirements-dev.txt
186
+ ```
187
+
188
+ Run the local verification script (formatting, type checks, lint, security scan, tests):
189
+
190
+ ```bash
191
+ py scripts/verify.py
192
+ ```
193
+
194
+ Run tests directly:
195
+
196
+ ```bash
197
+ pytest
198
+ ```
199
+
166
200
  1. Follow existing code style (Black, isort)
167
- 2. Add tests for new features (>95% coverage)
201
+ 2. Add tests for new features (>90% coverage)
168
202
  3. Run type checking and linting
169
203
 
170
204
  ## 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.8"
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)"}
13
- requires-python = ">=3.9"
12
+ license = { text = "Commercial Restricted License (CRL)" }
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
 
@@ -134,7 +138,10 @@ def _load_config(args: Args) -> Optional[Config]:
134
138
  logger.info("Loaded configuration: %s", config)
135
139
  return config
136
140
  except Exception as e:
137
- logger.error("Error loading configuration: %s", e)
141
+ if args.verbose:
142
+ logger.error("Error loading configuration: %s", e, exc_info=True)
143
+ else:
144
+ logger.error("Error loading configuration: %s", e)
138
145
  return None
139
146
 
140
147
 
@@ -150,14 +157,17 @@ def _validate_paths(args: Args) -> Optional[list[Path]]:
150
157
 
151
158
 
152
159
  def _analyze_files_safely(
153
- analyzer: Analyzer, paths: list[Path]
160
+ analyzer: Analyzer, paths: list[Path], max_workers: int, *, verbose: bool = False
154
161
  ) -> Optional[AnalysisResult]:
155
162
  """Analyze files and handle errors. Returns result or None on error."""
156
163
  try:
157
164
  logger.info("Analyzing %d path(s)...", len(paths))
158
- return analyzer.analyze_files(paths)
165
+ return analyzer.analyze_files(paths, max_workers=max_workers)
159
166
  except Exception as e:
160
- logger.error("Error during analysis: %s", e)
167
+ if verbose:
168
+ logger.error("Error during analysis: %s", e, exc_info=True)
169
+ else:
170
+ logger.error("Error during analysis: %s", e)
161
171
  return None
162
172
 
163
173
 
@@ -210,8 +220,9 @@ def main() -> int:
210
220
  return 2
211
221
 
212
222
  # Create analyzer and analyze files
223
+ max_workers = max(1, args.jobs)
213
224
  analyzer = Analyzer(config)
214
- result = _analyze_files_safely(analyzer, paths)
225
+ result = _analyze_files_safely(analyzer, paths, max_workers, verbose=args.verbose)
215
226
  if result is None:
216
227
  return 2
217
228
 
@@ -0,0 +1,40 @@
1
+ """Package version resolution."""
2
+
3
+ import sys
4
+ from functools import lru_cache
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from pathlib import Path
7
+
8
+ _PACKAGE_NAME = "pyrefactor"
9
+
10
+
11
+ def _pyproject_path() -> 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
19
+ return Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
20
+
21
+
22
+ @lru_cache(maxsize=1)
23
+ def _fallback_version() -> str:
24
+ """Read version from pyproject.toml when the package is not installed."""
25
+ pyproject = _pyproject_path()
26
+ if not pyproject.is_file():
27
+ return "unknown"
28
+ for line in pyproject.read_text(encoding="utf-8").splitlines():
29
+ stripped = line.strip()
30
+ if stripped.startswith("version = "):
31
+ return stripped.split("=", 1)[1].strip().strip('"').strip("'")
32
+ return "unknown"
33
+
34
+
35
+ def get_version() -> str:
36
+ """Return the installed package version, falling back to pyproject.toml."""
37
+ try:
38
+ return version(_PACKAGE_NAME)
39
+ except PackageNotFoundError:
40
+ 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,12 @@ 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
- 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
- # Conditionally enabled detectors
46
46
  detector_configs = [
47
47
  (self.config.performance.enabled, PerformanceDetector),
48
48
  (self.config.boolean_logic.enabled, BooleanLogicDetector),
@@ -60,24 +60,42 @@ class Analyzer:
60
60
 
61
61
  return detectors
62
62
 
63
+ def _read_source(self, file_path: Path) -> tuple[str, list[str]] | str:
64
+ """Read source from a file, returning an error message on failure."""
65
+ try:
66
+ file_size = file_path.stat().st_size
67
+ if file_size > MAX_FILE_BYTES:
68
+ return (
69
+ f"File exceeds maximum size of {MAX_FILE_BYTES} bytes "
70
+ f"({file_size} bytes)"
71
+ )
72
+
73
+ source_code = file_path.read_text(encoding="utf-8")
74
+ return source_code, source_code.splitlines()
75
+ except UnicodeDecodeError:
76
+ return "File is not valid UTF-8 text"
77
+ except OSError as e:
78
+ return f"Error reading file: {e}"
79
+
63
80
  def analyze_file(self, file_path: Path) -> FileAnalysis:
64
81
  """Analyze a single Python file."""
65
82
  analysis = FileAnalysis(file_path=str(file_path))
66
83
 
67
84
  try:
68
- # Read the file
69
- source_code = file_path.read_text(encoding="utf-8")
70
- source_lines = source_code.splitlines()
85
+ read_result = self._read_source(file_path)
86
+ if isinstance(read_result, str):
87
+ analysis.parse_error = read_result
88
+ return analysis
89
+
90
+ source_code, source_lines = read_result
71
91
  analysis.lines_of_code = len(source_lines)
72
92
 
73
- # Parse the AST
74
93
  try:
75
94
  tree = ast.parse(source_code, filename=str(file_path))
76
95
  except SyntaxError as e:
77
96
  analysis.parse_error = f"Syntax error: {e}"
78
97
  return analysis
79
98
 
80
- # Create and run all enabled detectors
81
99
  detectors = self._create_detectors(str(file_path), source_lines)
82
100
  self._run_detectors(detectors, tree, analysis, file_path)
83
101
 
@@ -114,18 +132,54 @@ class Analyzer:
114
132
  """Analyze all Python files in a directory."""
115
133
  result = AnalysisResult()
116
134
 
117
- # Find all Python files
118
135
  python_files = list(directory.rglob("*.py"))
119
-
120
- # Filter excluded patterns
121
136
  python_files = self._filter_excluded_files(python_files)
122
137
 
123
138
  if not python_files:
124
139
  logger.warning("No Python files found in %s", directory)
125
140
  return result
126
141
 
127
- # Analyze files in parallel
128
- with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
142
+ return self._analyze_paths_parallel(python_files, max_workers, result)
143
+
144
+ def analyze_files(
145
+ self, file_paths: list[Path], max_workers: int = 4
146
+ ) -> AnalysisResult:
147
+ """Analyze a list of Python files and directories."""
148
+ result = AnalysisResult()
149
+ paths_to_analyze: list[Path] = []
150
+
151
+ for file_path in file_paths:
152
+ if file_path.is_file():
153
+ if file_path.suffix == ".py" and not self._is_excluded(file_path):
154
+ paths_to_analyze.append(file_path)
155
+ elif file_path.is_dir():
156
+ python_files = [
157
+ path
158
+ for path in file_path.rglob("*.py")
159
+ if not self._is_excluded(path)
160
+ ]
161
+ paths_to_analyze.extend(python_files)
162
+
163
+ if not paths_to_analyze:
164
+ return result
165
+
166
+ return self._analyze_paths_parallel(paths_to_analyze, max_workers, result)
167
+
168
+ def _analyze_paths_parallel(
169
+ self,
170
+ python_files: list[Path],
171
+ max_workers: int,
172
+ result: AnalysisResult,
173
+ ) -> AnalysisResult:
174
+ """Analyze multiple Python files in parallel."""
175
+ workers = max(1, max_workers)
176
+
177
+ if workers == 1 or len(python_files) == 1:
178
+ for file_path in python_files:
179
+ result.add_file_analysis(self.analyze_file(file_path))
180
+ return result
181
+
182
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
129
183
  future_to_file = {
130
184
  executor.submit(self.analyze_file, file_path): file_path
131
185
  for file_path in python_files
@@ -147,39 +201,22 @@ class Analyzer:
147
201
 
148
202
  return result
149
203
 
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)
204
+ def _is_excluded(self, file_path: Path) -> bool:
205
+ """Check if a file matches any exclusion pattern."""
206
+ if not self.config.exclude_patterns:
207
+ return False
208
+
209
+ posix_path = PurePosixPath(file_path.as_posix())
210
+ for pattern in self.config.exclude_patterns:
211
+ normalized = pattern.replace("\\", "/")
212
+ if posix_path.match(normalized):
213
+ return True
214
+ if fnmatch.fnmatch(posix_path.as_posix(), normalized):
215
+ return True
216
+ if fnmatch.fnmatch(file_path.name, normalized):
217
+ return True
218
+ return False
173
219
 
174
220
  def _filter_excluded_files(self, files: list[Path]) -> list[Path]:
175
221
  """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
- ]
222
+ return [file_path for file_path in files if not self._is_excluded(file_path)]