pysfi 0.1.10__py3-none-any.whl → 0.1.12__py3-none-any.whl
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.
- {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/METADATA +9 -7
- pysfi-0.1.12.dist-info/RECORD +62 -0
- {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/entry_points.txt +13 -2
- sfi/__init__.py +1 -1
- sfi/alarmclock/alarmclock.py +40 -40
- sfi/bumpversion/__init__.py +1 -1
- sfi/cleanbuild/cleanbuild.py +155 -0
- sfi/condasetup/condasetup.py +116 -0
- sfi/docdiff/docdiff.py +238 -0
- sfi/docscan/__init__.py +1 -1
- sfi/docscan/docscan_gui.py +1 -1
- sfi/docscan/lang/eng.py +152 -152
- sfi/docscan/lang/zhcn.py +170 -170
- sfi/filedate/filedate.py +185 -112
- sfi/gittool/__init__.py +2 -0
- sfi/gittool/gittool.py +401 -0
- sfi/llmclient/llmclient.py +592 -0
- sfi/llmquantize/llmquantize.py +480 -0
- sfi/llmserver/llmserver.py +335 -0
- sfi/makepython/makepython.py +2 -2
- sfi/pdfsplit/pdfsplit.py +4 -4
- sfi/pyarchive/pyarchive.py +418 -0
- sfi/pyembedinstall/__init__.py +0 -0
- sfi/pyembedinstall/pyembedinstall.py +629 -0
- sfi/pylibpack/pylibpack.py +813 -269
- sfi/pylibpack/rules/numpy.json +22 -0
- sfi/pylibpack/rules/pymupdf.json +10 -0
- sfi/pylibpack/rules/pyqt5.json +19 -0
- sfi/pylibpack/rules/pyside2.json +23 -0
- sfi/pylibpack/rules/scipy.json +23 -0
- sfi/pylibpack/rules/shiboken2.json +24 -0
- sfi/pyloadergen/pyloadergen.py +271 -572
- sfi/pypack/pypack.py +822 -471
- sfi/pyprojectparse/__init__.py +0 -0
- sfi/pyprojectparse/pyprojectparse.py +500 -0
- sfi/pysourcepack/pysourcepack.py +308 -369
- sfi/quizbase/__init__.py +0 -0
- sfi/quizbase/quizbase.py +828 -0
- sfi/quizbase/quizbase_gui.py +987 -0
- sfi/regexvalidate/__init__.py +0 -0
- sfi/regexvalidate/regex_help.html +284 -0
- sfi/regexvalidate/regexvalidate.py +468 -0
- sfi/taskkill/taskkill.py +0 -2
- pysfi-0.1.10.dist-info/RECORD +0 -39
- sfi/embedinstall/embedinstall.py +0 -478
- sfi/projectparse/projectparse.py +0 -152
- {pysfi-0.1.10.dist-info → pysfi-0.1.12.dist-info}/WHEEL +0 -0
- /sfi/{embedinstall → llmclient}/__init__.py +0 -0
- /sfi/{projectparse → llmquantize}/__init__.py +0 -0
|
File without changes
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Parse pyproject.toml files in directory, supports multiple projects.
|
|
2
|
+
|
|
3
|
+
This module provides classes and functions for parsing pyproject.toml files
|
|
4
|
+
and extracting project metadata. It offers two main classes:
|
|
5
|
+
|
|
6
|
+
- Project: Represents a single Python project with its metadata and dependencies
|
|
7
|
+
- Solution: Represents a collection of projects found in a directory tree
|
|
8
|
+
|
|
9
|
+
The module supports loading from TOML files, JSON files, and directory scanning,
|
|
10
|
+
with caching capabilities for improved performance.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import datetime
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from functools import cached_property
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
if sys.version_info >= (3, 11):
|
|
28
|
+
import tomllib
|
|
29
|
+
else:
|
|
30
|
+
import tomli as tomllib # type: ignore
|
|
31
|
+
|
|
32
|
+
__all__ = ["Project", "Solution"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
cwd = Path.cwd()
|
|
38
|
+
|
|
39
|
+
# Precompiled regex for dependency name extraction (optimization)
|
|
40
|
+
_DEP_NAME_PATTERN = re.compile(r"^([a-zA-Z0-9._-]+)")
|
|
41
|
+
|
|
42
|
+
# Qt-related keywords and dependencies for faster detection
|
|
43
|
+
_QT_DEPENDENCIES: frozenset[str] = frozenset((
|
|
44
|
+
"Qt",
|
|
45
|
+
"PySide",
|
|
46
|
+
"PyQt",
|
|
47
|
+
"PySide2",
|
|
48
|
+
"PySide6",
|
|
49
|
+
"PyQt5",
|
|
50
|
+
"PyQt6",
|
|
51
|
+
"Qt5",
|
|
52
|
+
"Qt6",
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# GUI-related keywords for faster detection
|
|
56
|
+
_GUI_KEYWORDS: frozenset[str] = frozenset(("gui", "desktop"))
|
|
57
|
+
|
|
58
|
+
# Required attributes for project validation (module-level constant for performance)
|
|
59
|
+
_REQUIRED_ATTRS: frozenset[str] = frozenset(("name", "version", "description"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class Project:
|
|
64
|
+
"""Represents a single Python project parsed from a pyproject.toml file.
|
|
65
|
+
|
|
66
|
+
This class encapsulates all metadata and configuration information from a Python project's
|
|
67
|
+
pyproject.toml file. It provides convenient access to project properties and utility methods
|
|
68
|
+
for analyzing project characteristics such as dependencies and GUI frameworks used.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
name: The name of the Python package
|
|
72
|
+
version: The version string of the package
|
|
73
|
+
description: Brief description of the package
|
|
74
|
+
readme: Path to the project's README file
|
|
75
|
+
requires_python: Python version requirement (e.g., ">=3.8")
|
|
76
|
+
dependencies: List of required dependencies
|
|
77
|
+
optional_dependencies: Dictionary of optional dependency groups
|
|
78
|
+
scripts: Dictionary of console script entry points
|
|
79
|
+
entry_points: Dictionary of other entry points
|
|
80
|
+
authors: List of author information
|
|
81
|
+
license: License information
|
|
82
|
+
keywords: List of keywords/tags for the package
|
|
83
|
+
classifiers: List of trove classifiers
|
|
84
|
+
urls: Dictionary of project URLs (homepage, repository, etc.)
|
|
85
|
+
build_backend: Build backend system used (e.g., "setuptools.build_meta")
|
|
86
|
+
requires: List of build system requirements
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
version: str
|
|
91
|
+
description: str
|
|
92
|
+
readme: str
|
|
93
|
+
requires_python: str
|
|
94
|
+
dependencies: list[str]
|
|
95
|
+
optional_dependencies: dict[str, list[str]]
|
|
96
|
+
scripts: dict[str, str]
|
|
97
|
+
entry_points: dict[str, Any]
|
|
98
|
+
authors: list[dict[str, Any]]
|
|
99
|
+
license: str
|
|
100
|
+
keywords: list[str]
|
|
101
|
+
classifiers: list[str]
|
|
102
|
+
urls: dict[str, str]
|
|
103
|
+
build_backend: str
|
|
104
|
+
requires: list[str]
|
|
105
|
+
|
|
106
|
+
@cached_property
|
|
107
|
+
def normalized_name(self) -> str:
|
|
108
|
+
return self.name.replace("-", "_")
|
|
109
|
+
|
|
110
|
+
@cached_property
|
|
111
|
+
def dep_names(self) -> set[str]:
|
|
112
|
+
"""Extract normalized dependency names from the dependencies list.
|
|
113
|
+
|
|
114
|
+
This method parses dependency strings to extract just the package names,
|
|
115
|
+
removing version specifiers, extras, and environment markers.
|
|
116
|
+
Handles complex dependency formats like: package[extra]>=1.0; python_version>="3.8"
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Set of normalized dependency package names.
|
|
120
|
+
"""
|
|
121
|
+
# Use set comprehension directly to avoid intermediate list
|
|
122
|
+
return {
|
|
123
|
+
match.group(1)
|
|
124
|
+
if (match := _DEP_NAME_PATTERN.match(main_part))
|
|
125
|
+
else main_part
|
|
126
|
+
for dep in self.dependencies
|
|
127
|
+
if (main_part := dep.split(";")[0].strip())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@cached_property
|
|
131
|
+
def qt_deps(self) -> set[str]:
|
|
132
|
+
"""Cached intersection of dependencies with Qt packages.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Set of Qt-related dependency names found in the project.
|
|
136
|
+
"""
|
|
137
|
+
return self.dep_names & _QT_DEPENDENCIES
|
|
138
|
+
|
|
139
|
+
@cached_property
|
|
140
|
+
def has_qt(self) -> bool:
|
|
141
|
+
return bool(self.qt_deps)
|
|
142
|
+
|
|
143
|
+
@cached_property
|
|
144
|
+
def qt_libname(self) -> str | None:
|
|
145
|
+
return next(iter(self.qt_deps), None)
|
|
146
|
+
|
|
147
|
+
@cached_property
|
|
148
|
+
def is_gui(self) -> bool:
|
|
149
|
+
# Use set intersection for O(1) lookup instead of O(n) with any()
|
|
150
|
+
return bool(set(self.keywords) & _GUI_KEYWORDS)
|
|
151
|
+
|
|
152
|
+
@cached_property
|
|
153
|
+
def loader_type(self) -> str:
|
|
154
|
+
return "gui" if self.is_gui else "console"
|
|
155
|
+
|
|
156
|
+
def __repr__(self):
|
|
157
|
+
return f"<Project {self.name} v{self.version} py{self.requires_python} deps{self.dependencies}>"
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_toml_file(cls, toml_file: Path) -> Project:
|
|
161
|
+
"""Create a Project instance by parsing a pyproject.toml file.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
toml_file: Path to the pyproject.toml file to parse
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A Project instance containing the parsed project data.
|
|
168
|
+
Returns an empty Project if the file doesn't exist or is invalid.
|
|
169
|
+
"""
|
|
170
|
+
if not toml_file.is_file():
|
|
171
|
+
logger.error(f"{toml_file} does not exist")
|
|
172
|
+
return Project.from_dict({})
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with open(toml_file, "rb") as f:
|
|
176
|
+
data = tomllib.load(f)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Error parsing {toml_file}: {e}")
|
|
179
|
+
return Project.from_dict({})
|
|
180
|
+
|
|
181
|
+
if "project" not in data:
|
|
182
|
+
logger.error(f"No project section in {toml_file}")
|
|
183
|
+
return Project.from_dict({})
|
|
184
|
+
|
|
185
|
+
if "name" not in data["project"]:
|
|
186
|
+
logger.error(f"No name in project section of {toml_file}")
|
|
187
|
+
return Project.from_dict({})
|
|
188
|
+
|
|
189
|
+
return Project.from_dict({**data["project"], **data.get("build-system", {})})
|
|
190
|
+
|
|
191
|
+
@cached_property
|
|
192
|
+
def raw_data(self) -> dict[str, Any]:
|
|
193
|
+
"""Convert the Project instance to a dictionary for JSON serialization.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Dictionary representation of the Project instance suitable for JSON serialization.
|
|
197
|
+
"""
|
|
198
|
+
return {
|
|
199
|
+
"name": self.name,
|
|
200
|
+
"version": self.version,
|
|
201
|
+
"description": self.description,
|
|
202
|
+
"readme": self.readme,
|
|
203
|
+
"requires_python": self.requires_python,
|
|
204
|
+
"dependencies": self.dependencies,
|
|
205
|
+
"optional_dependencies": self.optional_dependencies,
|
|
206
|
+
"scripts": self.scripts,
|
|
207
|
+
"entry_points": self.entry_points,
|
|
208
|
+
"authors": self.authors,
|
|
209
|
+
"license": self.license,
|
|
210
|
+
"keywords": self.keywords,
|
|
211
|
+
"classifiers": self.classifiers,
|
|
212
|
+
"urls": self.urls,
|
|
213
|
+
# build-system data
|
|
214
|
+
"build_backend": self.build_backend,
|
|
215
|
+
"requires": self.requires,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def from_dict(cls, data: dict[str, Any]) -> Project:
|
|
220
|
+
"""Create a Project instance from a dictionary of project attributes.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
data: Dictionary containing project attributes
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A Project instance initialized with the provided data.
|
|
227
|
+
"""
|
|
228
|
+
return cls(
|
|
229
|
+
name=data.get("name", ""),
|
|
230
|
+
version=data.get("version", ""),
|
|
231
|
+
description=data.get("description", ""),
|
|
232
|
+
readme=data.get("readme", ""),
|
|
233
|
+
requires_python=data.get("requires_python", ""),
|
|
234
|
+
dependencies=data.get("dependencies", []),
|
|
235
|
+
optional_dependencies=data.get("optional_dependencies", {}),
|
|
236
|
+
scripts=data.get("scripts", {}),
|
|
237
|
+
entry_points=data.get("entry_points", {}),
|
|
238
|
+
authors=data.get("authors", []),
|
|
239
|
+
license=data.get("license", ""),
|
|
240
|
+
keywords=data.get("keywords", []),
|
|
241
|
+
classifiers=data.get("classifiers", []),
|
|
242
|
+
urls=data.get("urls", {}),
|
|
243
|
+
build_backend=data.get("build_backend", ""),
|
|
244
|
+
requires=data.get("requires", []),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True)
|
|
249
|
+
class Solution:
|
|
250
|
+
"""Represents a collection of Python projects found in a directory tree.
|
|
251
|
+
|
|
252
|
+
This class manages multiple Project instances discovered by scanning a directory
|
|
253
|
+
for pyproject.toml files. It provides methods to load projects from various sources
|
|
254
|
+
(TOML files, JSON files, or directory scans) and handles project data persistence
|
|
255
|
+
by saving/loading to/from projects.json files.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
root_dir: The root directory where project scanning originated
|
|
259
|
+
projects: Dictionary mapping project names to their Project instances
|
|
260
|
+
start_time: Float representing the start time for performance measurement
|
|
261
|
+
time_stamp: Datetime object representing when the solution was created
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
root_dir: Path
|
|
265
|
+
projects: dict[str, Project]
|
|
266
|
+
update: bool = False
|
|
267
|
+
start_time: float = field(default_factory=time.perf_counter)
|
|
268
|
+
time_stamp: datetime.datetime = field(default_factory=datetime.datetime.now)
|
|
269
|
+
|
|
270
|
+
def __repr__(self):
|
|
271
|
+
return f"""
|
|
272
|
+
<Solution root={self.root_dir}
|
|
273
|
+
projects: {len(self.projects)}
|
|
274
|
+
update: {self.update}
|
|
275
|
+
time_used: {self.elapsed_time:.4f}s
|
|
276
|
+
timestamp: {self.time_stamp}
|
|
277
|
+
>"""
|
|
278
|
+
|
|
279
|
+
@cached_property
|
|
280
|
+
def elapsed_time(self) -> float:
|
|
281
|
+
"""Calculate and cache the elapsed time since start."""
|
|
282
|
+
return time.perf_counter() - self.start_time
|
|
283
|
+
|
|
284
|
+
def __post_init__(self):
|
|
285
|
+
logger.info(f"\t - Loaded {len(self.projects)} projects from {self.root_dir}")
|
|
286
|
+
# Only log brief summary to avoid overly verbose output
|
|
287
|
+
logger.info(
|
|
288
|
+
f"\t - Solution created in {self.elapsed_time:.4f}s at {self.time_stamp}"
|
|
289
|
+
)
|
|
290
|
+
self._write_project_json()
|
|
291
|
+
|
|
292
|
+
@cached_property
|
|
293
|
+
def json_file(self) -> Path:
|
|
294
|
+
"""Path to the cache file where project data is stored.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Path to projects.json file in the root directory.
|
|
298
|
+
"""
|
|
299
|
+
return self.root_dir / "projects.json"
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def from_toml_files(
|
|
303
|
+
cls, root_dir: Path, toml_files: list[Path], update: bool = False
|
|
304
|
+
) -> Solution:
|
|
305
|
+
"""Create a Solution instance by parsing multiple pyproject.toml files.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
root_dir: Root directory where the projects are located
|
|
309
|
+
toml_files: List of paths to pyproject.toml files to parse
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
A Solution instance containing all parsed projects.
|
|
313
|
+
"""
|
|
314
|
+
projects: dict[str, Project] = {}
|
|
315
|
+
for toml_file in toml_files:
|
|
316
|
+
if not toml_file.is_file():
|
|
317
|
+
logger.warning(f"Warning: {toml_file} is not a file")
|
|
318
|
+
continue
|
|
319
|
+
project = Project.from_toml_file(toml_file)
|
|
320
|
+
if not project.name:
|
|
321
|
+
logger.warning(
|
|
322
|
+
f"Warning: {toml_file} does not contain project information"
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
projects[project.name] = project
|
|
326
|
+
|
|
327
|
+
return cls(root_dir=root_dir, projects=projects, update=update)
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def from_json_data(
|
|
331
|
+
cls, root_dir: Path, json_data: dict[str, Any], update: bool = False
|
|
332
|
+
) -> Solution:
|
|
333
|
+
"""Create a Solution instance from JSON data.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
root_dir: Root directory for the project collection
|
|
337
|
+
json_data: Dictionary containing project data in JSON format
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
A Solution instance containing projects parsed from the JSON data.
|
|
341
|
+
"""
|
|
342
|
+
projects = {}
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
for key, value in json_data.items():
|
|
346
|
+
if isinstance(value, dict):
|
|
347
|
+
# Check if the value has a "project" section (from pyproject.toml parsing)
|
|
348
|
+
if "project" in value:
|
|
349
|
+
project_data = value.get("project", {})
|
|
350
|
+
projects[key] = Project.from_dict(project_data)
|
|
351
|
+
else:
|
|
352
|
+
# Check if the value contains direct project attributes
|
|
353
|
+
# Use set intersection with module-level constant for faster attribute checking
|
|
354
|
+
if value.keys() & _REQUIRED_ATTRS:
|
|
355
|
+
projects[key] = Project.from_dict(value)
|
|
356
|
+
else:
|
|
357
|
+
# No project section or recognizable project attributes found
|
|
358
|
+
logger.warning(f"No project information found in {key}")
|
|
359
|
+
projects[key] = Project.from_dict({})
|
|
360
|
+
else:
|
|
361
|
+
projects[key] = value
|
|
362
|
+
except (TypeError, ValueError) as e:
|
|
363
|
+
logger.error(f"Error loading project data from JSON data: {e}")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.error(f"Unknown error loading project data from JSON data: {e}")
|
|
366
|
+
|
|
367
|
+
return cls(root_dir=root_dir, projects=projects, update=update)
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def from_json_file(cls, json_file: Path, update: bool = False) -> Solution:
|
|
371
|
+
"""Create a Solution instance from a JSON file.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
json_file: Path to the JSON file containing project data
|
|
375
|
+
update: If True, forces re-parsing even if cache exists
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
A Solution instance containing projects parsed from the JSON file.
|
|
379
|
+
"""
|
|
380
|
+
if not json_file.is_file():
|
|
381
|
+
logger.error(f"Error: {json_file} is not a file")
|
|
382
|
+
return cls(root_dir=json_file.parent, projects={})
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
logger.debug(f"Loading project data from {json_file}...")
|
|
386
|
+
with json_file.open("r", encoding="utf-8") as f:
|
|
387
|
+
loaded_data = json.load(f)
|
|
388
|
+
except (OSError, json.JSONDecodeError, KeyError) as e:
|
|
389
|
+
logger.error(f"Error loading project data from {json_file}: {e}")
|
|
390
|
+
return cls(root_dir=json_file.parent, projects={})
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Unknown error loading project data from {json_file}: {e}")
|
|
393
|
+
return cls(root_dir=json_file.parent, projects={})
|
|
394
|
+
|
|
395
|
+
logger.debug(f"\t - Loaded project data from {json_file}")
|
|
396
|
+
return cls.from_json_data(json_file.parent, loaded_data, update=update)
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def from_directory(cls, root_dir: Path, update: bool = False) -> Solution:
|
|
400
|
+
"""Create a Solution instance by scanning a directory for pyproject.toml files.
|
|
401
|
+
|
|
402
|
+
This method recursively searches the given directory for pyproject.toml files,
|
|
403
|
+
parses each one, and creates Project instances from them. If a projects.json
|
|
404
|
+
cache file exists and update is False, it will load from the cache instead.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
root_dir: Directory to scan for pyproject.toml files
|
|
408
|
+
update: If True, forces re-parsing even if cache exists
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
A Solution instance containing all discovered projects.
|
|
412
|
+
"""
|
|
413
|
+
if not root_dir.is_dir():
|
|
414
|
+
logger.error(f"Error: {root_dir} is not a directory")
|
|
415
|
+
return cls(root_dir=root_dir, projects={})
|
|
416
|
+
|
|
417
|
+
# Use walrus operator to avoid intermediate variable
|
|
418
|
+
if not update and (project_json := root_dir / "projects.json").is_file():
|
|
419
|
+
return cls.from_json_file(project_json, update=update)
|
|
420
|
+
|
|
421
|
+
logger.debug(f"Parsing pyproject.toml in {root_dir}...")
|
|
422
|
+
toml_files = list(root_dir.rglob("pyproject.toml"))
|
|
423
|
+
return cls.from_toml_files(root_dir, toml_files, update=update)
|
|
424
|
+
|
|
425
|
+
def _write_project_json(self):
|
|
426
|
+
"""Write the project data to a projects.json file for caching.
|
|
427
|
+
|
|
428
|
+
This method serializes the project data to JSON format and saves it
|
|
429
|
+
to a cache file named projects.json in the root directory. This enables
|
|
430
|
+
faster loading on subsequent runs unless the update flag is set.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
update: If True, forces writing even if the file already exists
|
|
434
|
+
"""
|
|
435
|
+
# Cache json_file reference to avoid repeated cached_property access
|
|
436
|
+
json_file = self.json_file
|
|
437
|
+
if json_file.exists() and not self.update:
|
|
438
|
+
logger.info(
|
|
439
|
+
f"\t - Skip write project data file {json_file}, already exists"
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
# Convert Project objects to dictionaries for JSON serialization
|
|
445
|
+
# Use dict comprehension for better performance
|
|
446
|
+
serializable_data = {
|
|
447
|
+
key: project_data.raw_data
|
|
448
|
+
if isinstance(project_data, Project)
|
|
449
|
+
else project_data
|
|
450
|
+
for key, project_data in self.projects.items()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
with json_file.open("w", encoding="utf-8") as f:
|
|
454
|
+
json.dump(serializable_data, f, indent=2, ensure_ascii=False)
|
|
455
|
+
except (OSError, json.JSONDecodeError, KeyError) as e:
|
|
456
|
+
logger.error(f"Error writing output to {json_file}: {e}")
|
|
457
|
+
return
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.error(f"Unknown error writing output to {json_file}: {e}")
|
|
460
|
+
return
|
|
461
|
+
else:
|
|
462
|
+
logger.info(f"Output written to {json_file}")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
466
|
+
"""Create and return an argument parser for the pyproject.toml parser tool.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Configured ArgumentParser instance with all supported command-line options.
|
|
470
|
+
"""
|
|
471
|
+
parser = argparse.ArgumentParser(
|
|
472
|
+
description="Parse pyproject.toml files in a directory and analyze Python projects"
|
|
473
|
+
)
|
|
474
|
+
parser.add_argument(
|
|
475
|
+
"directory",
|
|
476
|
+
type=str,
|
|
477
|
+
nargs="?",
|
|
478
|
+
default=str(cwd),
|
|
479
|
+
help="Directory to scan for pyproject.toml files (default: current directory)",
|
|
480
|
+
)
|
|
481
|
+
parser.add_argument(
|
|
482
|
+
"--debug", "-d", action="store_true", help="Enable debug logging output"
|
|
483
|
+
)
|
|
484
|
+
parser.add_argument(
|
|
485
|
+
"--update",
|
|
486
|
+
"-u",
|
|
487
|
+
action="store_true",
|
|
488
|
+
help="Force update by re-parsing projects instead of using cache",
|
|
489
|
+
)
|
|
490
|
+
return parser
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def main() -> None:
|
|
494
|
+
parser = create_parser()
|
|
495
|
+
args = parser.parse_args()
|
|
496
|
+
|
|
497
|
+
if args.debug:
|
|
498
|
+
logger.setLevel(logging.DEBUG)
|
|
499
|
+
|
|
500
|
+
Solution.from_directory(Path(args.directory), update=args.update)
|