pysfi 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → pysfi-0.1.13.dist-info}/METADATA +1 -1
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/RECORD +35 -27
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/entry_points.txt +2 -0
- sfi/__init__.py +5 -3
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +5 -3
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cli.py +12 -2
- sfi/condasetup/__init__.py +1 -0
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +1 -1
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan_gui.py +150 -46
- sfi/img2pdf/__init__.py +0 -0
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +39 -11
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +507 -124
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +571 -465
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +494 -965
- sfi/pyprojectparse/pyprojectparse.py +328 -28
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- sfi/workflowengine/workflowengine.py +225 -122
- {pysfi-0.1.12.dist-info → pysfi-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -16,6 +16,7 @@ import argparse
|
|
|
16
16
|
import datetime
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
|
+
import platform
|
|
19
20
|
import re
|
|
20
21
|
import sys
|
|
21
22
|
import time
|
|
@@ -35,9 +36,12 @@ __all__ = ["Project", "Solution"]
|
|
|
35
36
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
36
37
|
logger = logging.getLogger(__name__)
|
|
37
38
|
cwd = Path.cwd()
|
|
39
|
+
is_windows = platform.system() == "Windows"
|
|
38
40
|
|
|
39
41
|
# Precompiled regex for dependency name extraction (optimization)
|
|
40
42
|
_DEP_NAME_PATTERN = re.compile(r"^([a-zA-Z0-9._-]+)")
|
|
43
|
+
_EXTRA_PATTERN = re.compile(r"\[([^\]]+)\]")
|
|
44
|
+
_VERSION_PATTERN = re.compile(r"[<>=!~].*$")
|
|
41
45
|
|
|
42
46
|
# Qt-related keywords and dependencies for faster detection
|
|
43
47
|
_QT_DEPENDENCIES: frozenset[str] = frozenset((
|
|
@@ -59,6 +63,73 @@ _GUI_KEYWORDS: frozenset[str] = frozenset(("gui", "desktop"))
|
|
|
59
63
|
_REQUIRED_ATTRS: frozenset[str] = frozenset(("name", "version", "description"))
|
|
60
64
|
|
|
61
65
|
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Dependency:
|
|
68
|
+
"""Represents a Python package dependency."""
|
|
69
|
+
|
|
70
|
+
name: str = ""
|
|
71
|
+
version: str | None = None
|
|
72
|
+
extras: set[str] = field(default_factory=set)
|
|
73
|
+
requires: set[str] = field(default_factory=set)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def from_str(dep_str: str) -> Dependency:
|
|
77
|
+
"""Create a Dependency instance from a dependency string.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
dep_str (str): The dependency string in the format "name[extras]version".
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dependency: The created Dependency instance.
|
|
84
|
+
"""
|
|
85
|
+
# Parse the dependency string to extract name, extras, and version
|
|
86
|
+
name = ""
|
|
87
|
+
version = None # Use None initially, will be set if version is found
|
|
88
|
+
extras = set()
|
|
89
|
+
|
|
90
|
+
# First, extract extras if present (text within square brackets)
|
|
91
|
+
extras_match = _EXTRA_PATTERN.search(dep_str)
|
|
92
|
+
if extras_match:
|
|
93
|
+
extras_str = extras_match.group(1)
|
|
94
|
+
extras = {extra.strip() for extra in extras_str.split(",")}
|
|
95
|
+
# Remove the extras part from the string for further processing
|
|
96
|
+
dep_str = dep_str[: extras_match.start()] + dep_str[extras_match.end() :]
|
|
97
|
+
|
|
98
|
+
# Extract version specifier (starts with comparison operators like >=, <=, ==, etc.)
|
|
99
|
+
version_match = _VERSION_PATTERN.search(dep_str)
|
|
100
|
+
if version_match:
|
|
101
|
+
version = version_match.group()
|
|
102
|
+
dep_str = dep_str[: version_match.start()].strip()
|
|
103
|
+
|
|
104
|
+
# Remaining part is the name
|
|
105
|
+
name = dep_str.strip()
|
|
106
|
+
|
|
107
|
+
return Dependency(name=name, version=version, extras=extras, requires=set())
|
|
108
|
+
|
|
109
|
+
def __post_init__(self):
|
|
110
|
+
# Normalize the name after initialization
|
|
111
|
+
# This ensures that names like "Requests-OAuthLib" become "requests_oauthlib"
|
|
112
|
+
object.__setattr__(self, "name", self.name.lower().replace("-", "_"))
|
|
113
|
+
|
|
114
|
+
@cached_property
|
|
115
|
+
def normalized_name(self) -> str:
|
|
116
|
+
"""Return the normalized name of the dependency (same as name since it's already normalized)."""
|
|
117
|
+
return self.name
|
|
118
|
+
|
|
119
|
+
def __str__(self) -> str:
|
|
120
|
+
"""String representation of dependency."""
|
|
121
|
+
# Pre-cache version to avoid repeated evaluation
|
|
122
|
+
version_str = self.version or ""
|
|
123
|
+
if self.extras:
|
|
124
|
+
# Pre-cache sorted extras to avoid repeated sorting
|
|
125
|
+
sorted_extras = ",".join(sorted(self.extras))
|
|
126
|
+
return f"{self.name}[{sorted_extras}]{version_str}"
|
|
127
|
+
return f"{self.name}{version_str}"
|
|
128
|
+
|
|
129
|
+
def __hash__(self) -> int:
|
|
130
|
+
return hash((self.name, self.version, frozenset(self.extras)))
|
|
131
|
+
|
|
132
|
+
|
|
62
133
|
@dataclass(frozen=True)
|
|
63
134
|
class Project:
|
|
64
135
|
"""Represents a single Python project parsed from a pyproject.toml file.
|
|
@@ -84,6 +155,8 @@ class Project:
|
|
|
84
155
|
urls: Dictionary of project URLs (homepage, repository, etc.)
|
|
85
156
|
build_backend: Build backend system used (e.g., "setuptools.build_meta")
|
|
86
157
|
requires: List of build system requirements
|
|
158
|
+
toml_path: Path to the pyproject.toml file
|
|
159
|
+
solution_root_dir: Root directory of the solution (for multi-project setups)
|
|
87
160
|
"""
|
|
88
161
|
|
|
89
162
|
name: str
|
|
@@ -102,11 +175,99 @@ class Project:
|
|
|
102
175
|
urls: dict[str, str]
|
|
103
176
|
build_backend: str
|
|
104
177
|
requires: list[str]
|
|
178
|
+
toml_path: Path
|
|
179
|
+
solution_root_dir: Path | None = None
|
|
180
|
+
|
|
181
|
+
@cached_property
|
|
182
|
+
def min_python_version(self) -> str | None:
|
|
183
|
+
"""Extract the minimum Python version from requires_python.
|
|
184
|
+
|
|
185
|
+
Parses the requires_python string to extract the minimum version requirement.
|
|
186
|
+
Supports formats like ">=3.8", ">3.7", "~=3.9", etc. Returns None if no
|
|
187
|
+
minimum version is specified or if the format is unrecognized.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The minimum Python version as a string, or None if not found.
|
|
191
|
+
"""
|
|
192
|
+
if not self.requires_python:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
# Pattern to match minimum Python version requirements
|
|
196
|
+
# Examples: >=3.8, >3.7, ~=3.9, ==3.8.*, etc.
|
|
197
|
+
patterns = [
|
|
198
|
+
r">=(\d+\.\d+(?:\.\d+)?)", # >=3.8, >=3.8.1
|
|
199
|
+
r">(\d+\.\d+(?:\.\d+)?)", # >3.7
|
|
200
|
+
r"~=?(\d+\.\d+(?:\.\d+)?)", # ~=3.9, ~3.9
|
|
201
|
+
r"==?(\d+\.\d+(?:\.\d+)?)(?:\.\*)?", # ==3.8, ==3.8.*, =3.8
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for pattern in patterns:
|
|
205
|
+
match = re.search(pattern, self.requires_python)
|
|
206
|
+
if match:
|
|
207
|
+
return match.group(1)
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
@cached_property
|
|
212
|
+
def root_dir(self) -> Path:
|
|
213
|
+
"""Return the root directory of the project."""
|
|
214
|
+
if not self.toml_path.is_file():
|
|
215
|
+
# Return current directory if toml_path is not set or invalid
|
|
216
|
+
# This can happen when Project is created with default values
|
|
217
|
+
return Path()
|
|
218
|
+
|
|
219
|
+
return self.toml_path.parent
|
|
220
|
+
|
|
221
|
+
@cached_property
|
|
222
|
+
def dist_dir(self) -> Path:
|
|
223
|
+
"""Return the distribution directory of the project.
|
|
224
|
+
|
|
225
|
+
In multi-project solutions, returns solution_root_dir/dist.
|
|
226
|
+
In single-project setups, returns project_root_dir/dist.
|
|
227
|
+
"""
|
|
228
|
+
if self.solution_root_dir:
|
|
229
|
+
dist_dir = self.solution_root_dir / "dist"
|
|
230
|
+
else:
|
|
231
|
+
dist_dir = self.root_dir / "dist"
|
|
232
|
+
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
return dist_dir
|
|
234
|
+
|
|
235
|
+
@cached_property
|
|
236
|
+
def extension(self) -> str:
|
|
237
|
+
return ".exe" if is_windows else ""
|
|
238
|
+
|
|
239
|
+
@cached_property
|
|
240
|
+
def exe_name(self) -> str:
|
|
241
|
+
"""Return the executable name of the project."""
|
|
242
|
+
return f"{self.name}{self.extension}"
|
|
243
|
+
|
|
244
|
+
@cached_property
|
|
245
|
+
def exe_path(self) -> Path:
|
|
246
|
+
"""Return the executable path of the project."""
|
|
247
|
+
return self.dist_dir / f"{self.name}{self.extension}"
|
|
248
|
+
|
|
249
|
+
@cached_property
|
|
250
|
+
def runtime_dir(self) -> Path:
|
|
251
|
+
"""Return the runtime directory of the project."""
|
|
252
|
+
runtime_dir = self.dist_dir / "runtime"
|
|
253
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
return runtime_dir
|
|
255
|
+
|
|
256
|
+
@cached_property
|
|
257
|
+
def lib_dir(self) -> Path:
|
|
258
|
+
"""Return the lib directory of the project."""
|
|
259
|
+
lib_dir = self.dist_dir / "site-packages"
|
|
260
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
261
|
+
return lib_dir
|
|
105
262
|
|
|
106
263
|
@cached_property
|
|
107
264
|
def normalized_name(self) -> str:
|
|
108
265
|
return self.name.replace("-", "_")
|
|
109
266
|
|
|
267
|
+
@cached_property
|
|
268
|
+
def converted_dependencies(self) -> set[Dependency]:
|
|
269
|
+
return {Dependency.from_str(dep) for dep in self.dependencies}
|
|
270
|
+
|
|
110
271
|
@cached_property
|
|
111
272
|
def dep_names(self) -> set[str]:
|
|
112
273
|
"""Extract normalized dependency names from the dependencies list.
|
|
@@ -118,7 +279,6 @@ class Project:
|
|
|
118
279
|
Returns:
|
|
119
280
|
Set of normalized dependency package names.
|
|
120
281
|
"""
|
|
121
|
-
# Use set comprehension directly to avoid intermediate list
|
|
122
282
|
return {
|
|
123
283
|
match.group(1)
|
|
124
284
|
if (match := _DEP_NAME_PATTERN.match(main_part))
|
|
@@ -134,7 +294,10 @@ class Project:
|
|
|
134
294
|
Returns:
|
|
135
295
|
Set of Qt-related dependency names found in the project.
|
|
136
296
|
"""
|
|
137
|
-
|
|
297
|
+
# Pre-cache the sets to avoid attribute lookup overhead
|
|
298
|
+
dep_names_set = self.dep_names
|
|
299
|
+
qt_deps_set = _QT_DEPENDENCIES
|
|
300
|
+
return dep_names_set & qt_deps_set
|
|
138
301
|
|
|
139
302
|
@cached_property
|
|
140
303
|
def has_qt(self) -> bool:
|
|
@@ -147,7 +310,8 @@ class Project:
|
|
|
147
310
|
@cached_property
|
|
148
311
|
def is_gui(self) -> bool:
|
|
149
312
|
# Use set intersection for O(1) lookup instead of O(n) with any()
|
|
150
|
-
|
|
313
|
+
# Also consider projects with Qt dependencies as GUI applications
|
|
314
|
+
return bool(set(self.keywords) & _GUI_KEYWORDS) or self.has_qt
|
|
151
315
|
|
|
152
316
|
@cached_property
|
|
153
317
|
def loader_type(self) -> str:
|
|
@@ -169,24 +333,46 @@ class Project:
|
|
|
169
333
|
"""
|
|
170
334
|
if not toml_file.is_file():
|
|
171
335
|
logger.error(f"{toml_file} does not exist")
|
|
172
|
-
return Project.
|
|
336
|
+
return Project._from_empty_dict()
|
|
173
337
|
|
|
174
338
|
try:
|
|
175
339
|
with open(toml_file, "rb") as f:
|
|
176
340
|
data = tomllib.load(f)
|
|
177
341
|
except Exception as e:
|
|
178
342
|
logger.error(f"Error parsing {toml_file}: {e}")
|
|
179
|
-
return Project.
|
|
343
|
+
return Project._from_empty_dict()
|
|
180
344
|
|
|
181
345
|
if "project" not in data:
|
|
182
346
|
logger.error(f"No project section in {toml_file}")
|
|
183
|
-
return Project.
|
|
347
|
+
return Project._from_empty_dict()
|
|
184
348
|
|
|
185
349
|
if "name" not in data["project"]:
|
|
186
350
|
logger.error(f"No name in project section of {toml_file}")
|
|
187
|
-
return Project.
|
|
351
|
+
return Project._from_empty_dict()
|
|
352
|
+
|
|
353
|
+
# Extract project data and build system data separately for performance
|
|
354
|
+
project_data = data["project"]
|
|
355
|
+
build_system_data = data.get("build-system", {})
|
|
356
|
+
|
|
357
|
+
# Merge data efficiently
|
|
358
|
+
merged_data = project_data.copy() # Start with project data
|
|
188
359
|
|
|
189
|
-
|
|
360
|
+
# Handle TOML field names that use hyphens but Python attributes use underscores
|
|
361
|
+
if "requires-python" in merged_data:
|
|
362
|
+
merged_data["requires_python"] = merged_data.pop("requires-python")
|
|
363
|
+
|
|
364
|
+
merged_data.update({"toml_path": toml_file})
|
|
365
|
+
merged_data.update(build_system_data) # Add build system data
|
|
366
|
+
|
|
367
|
+
return Project._from_dict(merged_data)
|
|
368
|
+
|
|
369
|
+
def pack_source(self):
|
|
370
|
+
"""Pack source code and resources to dist/src directory."""
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
def pack_embed_python(self):
|
|
374
|
+
"""Pack Python runtime to dist/runtime directory."""
|
|
375
|
+
pass
|
|
190
376
|
|
|
191
377
|
@cached_property
|
|
192
378
|
def raw_data(self) -> dict[str, Any]:
|
|
@@ -216,7 +402,12 @@ class Project:
|
|
|
216
402
|
}
|
|
217
403
|
|
|
218
404
|
@classmethod
|
|
219
|
-
def
|
|
405
|
+
def _from_empty_dict(cls) -> Project:
|
|
406
|
+
"""Create a Project instance from an empty dictionary."""
|
|
407
|
+
return cls._from_dict({})
|
|
408
|
+
|
|
409
|
+
@classmethod
|
|
410
|
+
def _from_dict(cls, data: dict[str, Any]) -> Project:
|
|
220
411
|
"""Create a Project instance from a dictionary of project attributes.
|
|
221
412
|
|
|
222
413
|
Args:
|
|
@@ -242,6 +433,8 @@ class Project:
|
|
|
242
433
|
urls=data.get("urls", {}),
|
|
243
434
|
build_backend=data.get("build_backend", ""),
|
|
244
435
|
requires=data.get("requires", []),
|
|
436
|
+
toml_path=data.get("toml_path", Path("")),
|
|
437
|
+
solution_root_dir=data.get("solution_root_dir"),
|
|
245
438
|
)
|
|
246
439
|
|
|
247
440
|
|
|
@@ -268,27 +461,41 @@ class Solution:
|
|
|
268
461
|
time_stamp: datetime.datetime = field(default_factory=datetime.datetime.now)
|
|
269
462
|
|
|
270
463
|
def __repr__(self):
|
|
271
|
-
return
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def elapsed_time(self) -> float:
|
|
281
|
-
"""Calculate and cache the elapsed time since start."""
|
|
282
|
-
return time.perf_counter() - self.start_time
|
|
464
|
+
return (
|
|
465
|
+
f"<Solution(\n"
|
|
466
|
+
f" root_dir={self.root_dir!r},\n"
|
|
467
|
+
f" projects: {len(self.projects)},\n"
|
|
468
|
+
f" update={self.update!r},\n"
|
|
469
|
+
f" time_used={self.elapsed_time:.4f}s,\n"
|
|
470
|
+
f" timestamp={self.time_stamp!r}\n"
|
|
471
|
+
f")>"
|
|
472
|
+
)
|
|
283
473
|
|
|
284
474
|
def __post_init__(self):
|
|
285
|
-
logger.info(f"\t - Loaded {len(self.projects)} projects from {self.root_dir}")
|
|
286
475
|
# Only log brief summary to avoid overly verbose output
|
|
287
476
|
logger.info(
|
|
288
|
-
f"
|
|
477
|
+
f"Solution: {len(self.projects)} projects from {self.root_dir}, "
|
|
478
|
+
f"created in {self.elapsed_time:.4f}s at {self.time_stamp:%Y-%m-%d %H:%M:%S}"
|
|
289
479
|
)
|
|
290
480
|
self._write_project_json()
|
|
291
481
|
|
|
482
|
+
@cached_property
|
|
483
|
+
def elapsed_time(self) -> float:
|
|
484
|
+
"""Calculate and cache the elapsed time since start."""
|
|
485
|
+
return time.perf_counter() - self.start_time
|
|
486
|
+
|
|
487
|
+
@cached_property
|
|
488
|
+
def dependencies(self) -> set[str]:
|
|
489
|
+
"""Get a set of all dependencies for all projects in the solution.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Set of dependency package names.
|
|
493
|
+
"""
|
|
494
|
+
# Use set comprehension for better performance
|
|
495
|
+
return {
|
|
496
|
+
dep for project in self.projects.values() for dep in project.dependencies
|
|
497
|
+
}
|
|
498
|
+
|
|
292
499
|
@cached_property
|
|
293
500
|
def json_file(self) -> Path:
|
|
294
501
|
"""Path to the cache file where project data is stored.
|
|
@@ -298,6 +505,69 @@ class Solution:
|
|
|
298
505
|
"""
|
|
299
506
|
return self.root_dir / "projects.json"
|
|
300
507
|
|
|
508
|
+
def find_matching_projects(self, pattern: str) -> list[str]:
|
|
509
|
+
"""Find all projects matching the given pattern (case-insensitive).
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
pattern: Pattern to match (substring, case-insensitive)
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
List of matching project names
|
|
516
|
+
"""
|
|
517
|
+
if not pattern:
|
|
518
|
+
return []
|
|
519
|
+
|
|
520
|
+
lower_pattern = pattern.lower()
|
|
521
|
+
return [name for name in self.projects if lower_pattern in name.lower()]
|
|
522
|
+
|
|
523
|
+
def resolve_project_name(self, project_name: str | None) -> str | None:
|
|
524
|
+
"""Resolve project name with fuzzy matching support.
|
|
525
|
+
|
|
526
|
+
Resolution strategy:
|
|
527
|
+
1. If project_name is None: auto-select if only one project exists
|
|
528
|
+
2. Exact match: return if project name exists
|
|
529
|
+
3. Fuzzy match: find projects containing the given substring (case-insensitive)
|
|
530
|
+
4. If multiple fuzzy matches: return None (ambiguous)
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
project_name: Project name to resolve, or None for auto-selection
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Resolved project name, or None if resolution fails
|
|
537
|
+
|
|
538
|
+
Examples:
|
|
539
|
+
>>> solution.resolve_project_name(None) # Auto-select if single project
|
|
540
|
+
'myproject'
|
|
541
|
+
>>> solution.resolve_project_name('docscan') # Exact match
|
|
542
|
+
'docscan'
|
|
543
|
+
>>> solution.resolve_project_name('doc') # Fuzzy match
|
|
544
|
+
'docscan'
|
|
545
|
+
>>> solution.resolve_project_name('scan') # Fuzzy match
|
|
546
|
+
'docscan'
|
|
547
|
+
"""
|
|
548
|
+
# Auto-select if only one project
|
|
549
|
+
if not project_name:
|
|
550
|
+
if len(self.projects) == 1:
|
|
551
|
+
return next(iter(self.projects.keys()))
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
# Exact match (case-sensitive)
|
|
555
|
+
if project_name in self.projects:
|
|
556
|
+
return project_name
|
|
557
|
+
|
|
558
|
+
# Fuzzy match (case-insensitive substring matching)
|
|
559
|
+
lower_name = project_name.lower()
|
|
560
|
+
matches = [name for name in self.projects if lower_name in name.lower()]
|
|
561
|
+
|
|
562
|
+
if len(matches) == 1:
|
|
563
|
+
return matches[0]
|
|
564
|
+
elif len(matches) > 1:
|
|
565
|
+
# Multiple matches found - ambiguous
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
# No match found
|
|
569
|
+
return None
|
|
570
|
+
|
|
301
571
|
@classmethod
|
|
302
572
|
def from_toml_files(
|
|
303
573
|
cls, root_dir: Path, toml_files: list[Path], update: bool = False
|
|
@@ -322,6 +592,31 @@ class Solution:
|
|
|
322
592
|
f"Warning: {toml_file} does not contain project information"
|
|
323
593
|
)
|
|
324
594
|
continue
|
|
595
|
+
|
|
596
|
+
# For multi-project solutions, set solution_root_dir
|
|
597
|
+
# Create new Project with solution_root_dir set
|
|
598
|
+
if len(toml_files) > 1:
|
|
599
|
+
project = Project(
|
|
600
|
+
name=project.name,
|
|
601
|
+
version=project.version,
|
|
602
|
+
description=project.description,
|
|
603
|
+
readme=project.readme,
|
|
604
|
+
requires_python=project.requires_python,
|
|
605
|
+
dependencies=project.dependencies,
|
|
606
|
+
optional_dependencies=project.optional_dependencies,
|
|
607
|
+
scripts=project.scripts,
|
|
608
|
+
entry_points=project.entry_points,
|
|
609
|
+
authors=project.authors,
|
|
610
|
+
license=project.license,
|
|
611
|
+
keywords=project.keywords,
|
|
612
|
+
classifiers=project.classifiers,
|
|
613
|
+
urls=project.urls,
|
|
614
|
+
build_backend=project.build_backend,
|
|
615
|
+
requires=project.requires,
|
|
616
|
+
toml_path=project.toml_path,
|
|
617
|
+
solution_root_dir=root_dir,
|
|
618
|
+
)
|
|
619
|
+
|
|
325
620
|
projects[project.name] = project
|
|
326
621
|
|
|
327
622
|
return cls(root_dir=root_dir, projects=projects, update=update)
|
|
@@ -347,16 +642,16 @@ class Solution:
|
|
|
347
642
|
# Check if the value has a "project" section (from pyproject.toml parsing)
|
|
348
643
|
if "project" in value:
|
|
349
644
|
project_data = value.get("project", {})
|
|
350
|
-
projects[key] = Project.
|
|
645
|
+
projects[key] = Project._from_dict(project_data)
|
|
351
646
|
else:
|
|
352
647
|
# Check if the value contains direct project attributes
|
|
353
648
|
# Use set intersection with module-level constant for faster attribute checking
|
|
354
649
|
if value.keys() & _REQUIRED_ATTRS:
|
|
355
|
-
projects[key] = Project.
|
|
650
|
+
projects[key] = Project._from_dict(value)
|
|
356
651
|
else:
|
|
357
652
|
# No project section or recognizable project attributes found
|
|
358
653
|
logger.warning(f"No project information found in {key}")
|
|
359
|
-
projects[key] = Project.
|
|
654
|
+
projects[key] = Project._from_empty_dict()
|
|
360
655
|
else:
|
|
361
656
|
projects[key] = value
|
|
362
657
|
except (TypeError, ValueError) as e:
|
|
@@ -441,8 +736,7 @@ class Solution:
|
|
|
441
736
|
return
|
|
442
737
|
|
|
443
738
|
try:
|
|
444
|
-
#
|
|
445
|
-
# Use dict comprehension for better performance
|
|
739
|
+
# Pre-cache raw_data access to avoid repeated property access
|
|
446
740
|
serializable_data = {
|
|
447
741
|
key: project_data.raw_data
|
|
448
742
|
if isinstance(project_data, Project)
|
|
@@ -491,6 +785,12 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
491
785
|
|
|
492
786
|
|
|
493
787
|
def main() -> None:
|
|
788
|
+
"""Main entry point for the pyproject.toml parser tool.
|
|
789
|
+
|
|
790
|
+
Parses command line arguments and creates a Solution instance by scanning
|
|
791
|
+
the specified directory for pyproject.toml files. Uses cached data if
|
|
792
|
+
available unless the update flag is set.
|
|
793
|
+
"""
|
|
494
794
|
parser = create_parser()
|
|
495
795
|
args = parser.parse_args()
|
|
496
796
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|