pysfi 0.1.0__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.
@@ -0,0 +1,152 @@
1
+ """Parse pyproject.toml files in directory, supports multiple projects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import logging
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+
12
+ if sys.version_info >= (3, 11):
13
+ import tomllib
14
+ else:
15
+ import tomli as tomllib # type: ignore
16
+
17
+ __all__ = ["parse_project_data"]
18
+
19
+
20
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
21
+ logger = logging.getLogger(__name__)
22
+ cwd = Path.cwd()
23
+
24
+
25
+ def parse_project_data(directory: Path, recursive: bool = False) -> dict:
26
+ """Parse pyproject.toml file in directory and return project data.
27
+
28
+ Returns:
29
+ dict: Project data.
30
+ """
31
+ data = _parse_pyproject(directory, recursive=recursive)
32
+ if not data:
33
+ return {}
34
+ return _extract_project_info(data)
35
+
36
+
37
+ def _parse_pyproject(directory: Path, recursive: bool = False) -> dict[str, dict]:
38
+ """Parse pyproject.toml file in directory and return raw data."""
39
+ data = {}
40
+ if recursive:
41
+ for pyproject_path in directory.rglob("pyproject.toml"):
42
+ with pyproject_path.open("rb") as f:
43
+ data[pyproject_path.parent.stem] = tomllib.load(f)
44
+ else:
45
+ pyproject_path = directory / "pyproject.toml"
46
+ if not pyproject_path.is_file():
47
+ logger.error(f"No pyproject.toml found in {directory}")
48
+ return {}
49
+
50
+ with pyproject_path.open("rb") as f:
51
+ logger.debug(f"Parsing {pyproject_path}")
52
+ data[pyproject_path.parent.stem] = tomllib.load(f)
53
+
54
+ logger.debug(f"Parsed {len(data)} pyproject.toml files, data: {data}")
55
+ return data
56
+
57
+
58
+ def _extract_project_info(data: dict) -> dict:
59
+ """Extract commonly used project information from parsed data."""
60
+ if not data:
61
+ logger.error("No data to extract")
62
+ return {}
63
+
64
+ project_info = {}
65
+ for key, value in data.items():
66
+ if "project" in value:
67
+ project = value.get("project", {})
68
+ build_system = value.get("build-system", {})
69
+ project_info.setdefault(
70
+ key,
71
+ {
72
+ "name": project.get("name"),
73
+ "version": project.get("version"),
74
+ "description": project.get("description"),
75
+ "readme": project.get("readme"),
76
+ "requires_python": project.get("requires-python"),
77
+ "dependencies": project.get("dependencies", []),
78
+ "optional_dependencies": project.get("optional-dependencies", {}),
79
+ "scripts": project.get("scripts", {}),
80
+ "entry_points": project.get("entry-points", {}),
81
+ "authors": project.get("authors", []),
82
+ "license": project.get("license"),
83
+ "keywords": project.get("keywords", []),
84
+ "classifiers": project.get("classifiers", []),
85
+ "urls": project.get("urls", {}),
86
+ "build_backend": build_system.get("build-backend"),
87
+ "requires": build_system.get("requires", []),
88
+ },
89
+ )
90
+ else:
91
+ logger.warning(f"No project information found in {key}")
92
+ project_info.setdefault(key, {})
93
+
94
+ logger.debug(f"Extracted {len(project_info)} projects, info: {project_info}")
95
+ return project_info
96
+
97
+
98
+ def _check_directory(directory: str) -> bool:
99
+ """Check if directory is valid."""
100
+ if not directory:
101
+ logger.error("Error: No directory specified")
102
+ return False
103
+
104
+ dir_path = Path(directory)
105
+ if not dir_path.is_dir():
106
+ logger.error(f"Error: {dir_path} is not a directory")
107
+ return False
108
+
109
+ return True
110
+
111
+
112
+ def main():
113
+ parser = argparse.ArgumentParser()
114
+ parser.add_argument("--directory", "-D", type=str, default=str(cwd), help="Directory to parse")
115
+ parser.add_argument("--debug", "-d", action="store_true", help="Debug mode")
116
+ parser.add_argument("--recursive", "-r", action="store_true", help="Recursively parse subdirectories")
117
+ parser.add_argument("--show", "-s", action="store_true", help="Show parsed data")
118
+ parser.add_argument("--output", "-o", type=str, default="projects.json", help="Output file path")
119
+
120
+ args = parser.parse_args()
121
+ if args.debug:
122
+ logger.setLevel(logging.DEBUG)
123
+
124
+ if not _check_directory(args.directory):
125
+ return
126
+
127
+ output_path = (cwd / args.output).with_suffix(".json")
128
+ if args.show:
129
+ if output_path.is_file():
130
+ logger.info(f"Loading output from `{output_path}`:")
131
+ with output_path.open("r", encoding="utf-8") as f:
132
+ output_data = json.load(f)
133
+ logger.info(json.dumps(output_data, indent=2, ensure_ascii=False, sort_keys=True))
134
+ return
135
+ else:
136
+ logger.debug(f"No json file found at {output_path}, continue parsing...")
137
+
138
+ t0 = time.perf_counter()
139
+ logger.info(f"Parsing pyproject.toml in {args.directory}")
140
+ output_data = parse_project_data(Path(args.directory), recursive=args.recursive)
141
+ if args.show:
142
+ logger.info(json.dumps(output_data, indent=2, ensure_ascii=False, sort_keys=True))
143
+ return
144
+
145
+ try:
146
+ with output_path.open("w", encoding="utf-8") as f:
147
+ json.dump(output_data, f, indent=2, ensure_ascii=False)
148
+ except Exception as e:
149
+ logger.error(f"Error writing output to {output_path}: {e}")
150
+ return
151
+ else:
152
+ logger.info(f"Output written to {output_path}, took {time.perf_counter() - t0:.4f}s")