pysfi 0.1.7__py3-none-any.whl → 0.1.11__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.
Files changed (55) hide show
  1. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/METADATA +11 -9
  2. pysfi-0.1.11.dist-info/RECORD +60 -0
  3. pysfi-0.1.11.dist-info/entry_points.txt +28 -0
  4. sfi/__init__.py +1 -1
  5. sfi/alarmclock/alarmclock.py +40 -40
  6. sfi/bumpversion/__init__.py +1 -1
  7. sfi/cleanbuild/cleanbuild.py +155 -0
  8. sfi/condasetup/condasetup.py +116 -0
  9. sfi/docscan/__init__.py +1 -1
  10. sfi/docscan/docscan.py +407 -103
  11. sfi/docscan/docscan_gui.py +1282 -596
  12. sfi/docscan/lang/eng.py +152 -0
  13. sfi/docscan/lang/zhcn.py +170 -0
  14. sfi/filedate/filedate.py +185 -112
  15. sfi/gittool/__init__.py +2 -0
  16. sfi/gittool/gittool.py +401 -0
  17. sfi/llmclient/llmclient.py +592 -0
  18. sfi/llmquantize/llmquantize.py +480 -0
  19. sfi/llmserver/llmserver.py +335 -0
  20. sfi/makepython/makepython.py +31 -30
  21. sfi/pdfsplit/pdfsplit.py +173 -173
  22. sfi/pyarchive/pyarchive.py +418 -0
  23. sfi/pyembedinstall/pyembedinstall.py +629 -0
  24. sfi/pylibpack/__init__.py +0 -0
  25. sfi/pylibpack/pylibpack.py +1457 -0
  26. sfi/pylibpack/rules/numpy.json +22 -0
  27. sfi/pylibpack/rules/pymupdf.json +10 -0
  28. sfi/pylibpack/rules/pyqt5.json +19 -0
  29. sfi/pylibpack/rules/pyside2.json +23 -0
  30. sfi/pylibpack/rules/scipy.json +23 -0
  31. sfi/pylibpack/rules/shiboken2.json +24 -0
  32. sfi/pyloadergen/pyloadergen.py +512 -227
  33. sfi/pypack/__init__.py +0 -0
  34. sfi/pypack/pypack.py +1142 -0
  35. sfi/pyprojectparse/__init__.py +0 -0
  36. sfi/pyprojectparse/pyprojectparse.py +500 -0
  37. sfi/pysourcepack/pysourcepack.py +308 -0
  38. sfi/quizbase/__init__.py +0 -0
  39. sfi/quizbase/quizbase.py +828 -0
  40. sfi/quizbase/quizbase_gui.py +987 -0
  41. sfi/regexvalidate/__init__.py +0 -0
  42. sfi/regexvalidate/regex_help.html +284 -0
  43. sfi/regexvalidate/regexvalidate.py +468 -0
  44. sfi/taskkill/taskkill.py +0 -2
  45. sfi/workflowengine/__init__.py +0 -0
  46. sfi/workflowengine/workflowengine.py +444 -0
  47. pysfi-0.1.7.dist-info/RECORD +0 -31
  48. pysfi-0.1.7.dist-info/entry_points.txt +0 -15
  49. sfi/embedinstall/embedinstall.py +0 -418
  50. sfi/projectparse/projectparse.py +0 -152
  51. sfi/pypacker/fspacker.py +0 -91
  52. {pysfi-0.1.7.dist-info → pysfi-0.1.11.dist-info}/WHEEL +0 -0
  53. /sfi/{embedinstall → docscan/lang}/__init__.py +0 -0
  54. /sfi/{projectparse → llmquantize}/__init__.py +0 -0
  55. /sfi/{pypacker → pyembedinstall}/__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)