python-package-folder 1.1.3__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.
- python_package_folder/__init__.py +24 -0
- python_package_folder/__main__.py +13 -0
- python_package_folder/analyzer.py +313 -0
- python_package_folder/finder.py +234 -0
- python_package_folder/manager.py +539 -0
- python_package_folder/publisher.py +310 -0
- python_package_folder/py.typed +0 -0
- python_package_folder/python_package_folder.py +239 -0
- python_package_folder/subfolder_build.py +477 -0
- python_package_folder/types.py +66 -0
- python_package_folder/utils.py +106 -0
- python_package_folder/version.py +253 -0
- python_package_folder-1.1.3.dist-info/METADATA +795 -0
- python_package_folder-1.1.3.dist-info/RECORD +17 -0
- python_package_folder-1.1.3.dist-info/WHEEL +4 -0
- python_package_folder-1.1.3.dist-info/entry_points.txt +2 -0
- python_package_folder-1.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version management functionality.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for setting and managing package versions
|
|
5
|
+
in pyproject.toml files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import tomllib
|
|
15
|
+
except ImportError:
|
|
16
|
+
try:
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
except ImportError:
|
|
19
|
+
tomllib = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VersionManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages package version in pyproject.toml.
|
|
25
|
+
|
|
26
|
+
This class can set, get, and validate package versions in pyproject.toml files.
|
|
27
|
+
It supports both static version strings and dynamic versioning configurations.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, project_root: Path) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Initialize the version manager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
project_root: Root directory containing pyproject.toml
|
|
36
|
+
"""
|
|
37
|
+
self.project_root = project_root.resolve()
|
|
38
|
+
self.pyproject_path = self.project_root / "pyproject.toml"
|
|
39
|
+
|
|
40
|
+
def get_current_version(self) -> str | None:
|
|
41
|
+
"""
|
|
42
|
+
Get the current version from pyproject.toml.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Current version string, or None if not found or using dynamic versioning
|
|
46
|
+
"""
|
|
47
|
+
if not self.pyproject_path.exists():
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
if tomllib:
|
|
52
|
+
content = self.pyproject_path.read_bytes()
|
|
53
|
+
data = tomllib.loads(content)
|
|
54
|
+
project = data.get("project", {})
|
|
55
|
+
if "version" in project:
|
|
56
|
+
return project["version"]
|
|
57
|
+
else:
|
|
58
|
+
# Fallback: simple regex parsing
|
|
59
|
+
content = self.pyproject_path.read_text(encoding="utf-8")
|
|
60
|
+
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
61
|
+
if match:
|
|
62
|
+
return match.group(1)
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def set_version(self, version: str) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Set a static version in pyproject.toml.
|
|
71
|
+
|
|
72
|
+
This method:
|
|
73
|
+
1. Validates the version format (PEP 440)
|
|
74
|
+
2. Removes dynamic versioning configuration if present
|
|
75
|
+
3. Sets a static version in the [project] section
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
version: Version string to set (must be PEP 440 compliant)
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If version format is invalid
|
|
82
|
+
FileNotFoundError: If pyproject.toml doesn't exist
|
|
83
|
+
"""
|
|
84
|
+
if not self.pyproject_path.exists():
|
|
85
|
+
raise FileNotFoundError(f"pyproject.toml not found: {self.pyproject_path}")
|
|
86
|
+
|
|
87
|
+
# Validate version format (basic PEP 440 check)
|
|
88
|
+
if not self._validate_version(version):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Invalid version format: {version}. "
|
|
91
|
+
"Version must be PEP 440 compliant (e.g., '1.2.3', '1.2.3a1', '1.2.3.post1')"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
content = self.pyproject_path.read_text(encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
# Remove dynamic versioning if present
|
|
97
|
+
content = self._remove_dynamic_versioning(content)
|
|
98
|
+
|
|
99
|
+
# Set static version in [project] section
|
|
100
|
+
content = self._set_static_version(content, version)
|
|
101
|
+
|
|
102
|
+
# Write back to file
|
|
103
|
+
self.pyproject_path.write_text(content, encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
def _validate_version(self, version: str) -> bool:
|
|
106
|
+
"""
|
|
107
|
+
Validate version format (basic PEP 440 check).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
version: Version string to validate
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if version appears valid, False otherwise
|
|
114
|
+
"""
|
|
115
|
+
# Basic PEP 440 validation regex
|
|
116
|
+
# Allows: 1.2.3, 1.2.3a1, 1.2.3b2, 1.2.3rc1, 1.2.3.post1, 1.2.3.dev1
|
|
117
|
+
pep440_pattern = r"^(\d+!)?(\d+)(\.\d+)*([a-zA-Z]+\d+)?(\.post\d+)?(\.dev\d+)?$"
|
|
118
|
+
return bool(re.match(pep440_pattern, version))
|
|
119
|
+
|
|
120
|
+
def _remove_dynamic_versioning(self, content: str) -> str:
|
|
121
|
+
"""Remove dynamic versioning configuration from pyproject.toml content."""
|
|
122
|
+
lines = content.split("\n")
|
|
123
|
+
result = []
|
|
124
|
+
skip_next = False
|
|
125
|
+
in_hatch_version = False
|
|
126
|
+
in_uv_dynamic = False
|
|
127
|
+
|
|
128
|
+
for _i, line in enumerate(lines):
|
|
129
|
+
# Skip lines in sections we want to remove
|
|
130
|
+
if skip_next:
|
|
131
|
+
skip_next = False
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Track section boundaries
|
|
135
|
+
if line.strip().startswith("[tool.hatch.version]"):
|
|
136
|
+
in_hatch_version = True
|
|
137
|
+
continue
|
|
138
|
+
elif line.strip().startswith("[tool.uv-dynamic-versioning]"):
|
|
139
|
+
in_uv_dynamic = True
|
|
140
|
+
continue
|
|
141
|
+
elif line.strip().startswith("[") and in_hatch_version:
|
|
142
|
+
in_hatch_version = False
|
|
143
|
+
elif line.strip().startswith("[") and in_uv_dynamic:
|
|
144
|
+
in_uv_dynamic = False
|
|
145
|
+
|
|
146
|
+
# Skip lines in dynamic versioning sections
|
|
147
|
+
if in_hatch_version or in_uv_dynamic:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Remove 'version' from dynamic list if present
|
|
151
|
+
if re.match(r"^\s*dynamic\s*=\s*\[", line):
|
|
152
|
+
# Check if version is in the list
|
|
153
|
+
if "version" in line:
|
|
154
|
+
# Remove version from the list
|
|
155
|
+
line = re.sub(r'"version"', "", line)
|
|
156
|
+
line = re.sub(r"'version'", "", line)
|
|
157
|
+
line = re.sub(r",\s*,", ",", line) # Remove double commas
|
|
158
|
+
line = re.sub(r"\[\s*,", "[", line) # Remove leading comma
|
|
159
|
+
line = re.sub(r",\s*\]", "]", line) # Remove trailing comma
|
|
160
|
+
# If dynamic list is now empty, skip the line
|
|
161
|
+
if re.match(r"^\s*dynamic\s*=\s*\[\s*\]", line):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
result.append(line)
|
|
165
|
+
|
|
166
|
+
return "\n".join(result)
|
|
167
|
+
|
|
168
|
+
def _set_static_version(self, content: str, version: str) -> str:
|
|
169
|
+
"""Set static version in [project] section."""
|
|
170
|
+
lines = content.split("\n")
|
|
171
|
+
result = []
|
|
172
|
+
in_project = False
|
|
173
|
+
version_set = False
|
|
174
|
+
|
|
175
|
+
for line in lines:
|
|
176
|
+
if line.strip().startswith("[project]"):
|
|
177
|
+
in_project = True
|
|
178
|
+
result.append(line)
|
|
179
|
+
elif line.strip().startswith("[") and in_project:
|
|
180
|
+
# End of [project] section, add version if not set
|
|
181
|
+
if not version_set:
|
|
182
|
+
result.append(f'version = "{version}"')
|
|
183
|
+
in_project = False
|
|
184
|
+
result.append(line)
|
|
185
|
+
elif in_project and re.match(r"^\s*version\s*=", line):
|
|
186
|
+
# Replace existing version
|
|
187
|
+
result.append(f'version = "{version}"')
|
|
188
|
+
version_set = True
|
|
189
|
+
else:
|
|
190
|
+
result.append(line)
|
|
191
|
+
|
|
192
|
+
# If [project] section exists but no version was set, add it
|
|
193
|
+
if in_project and not version_set:
|
|
194
|
+
result.append(f'version = "{version}"')
|
|
195
|
+
|
|
196
|
+
return "\n".join(result)
|
|
197
|
+
|
|
198
|
+
def restore_dynamic_versioning(self) -> None:
|
|
199
|
+
"""
|
|
200
|
+
Restore dynamic versioning configuration.
|
|
201
|
+
|
|
202
|
+
This restores the original dynamic versioning setup if it was removed.
|
|
203
|
+
Note: This is a best-effort restoration and may not perfectly match
|
|
204
|
+
the original configuration.
|
|
205
|
+
"""
|
|
206
|
+
if not self.pyproject_path.exists():
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
content = self.pyproject_path.read_text(encoding="utf-8")
|
|
210
|
+
|
|
211
|
+
# Check if dynamic versioning is already present
|
|
212
|
+
if "[tool.hatch.version]" in content:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Add dynamic versioning configuration
|
|
216
|
+
lines = content.split("\n")
|
|
217
|
+
result = []
|
|
218
|
+
project_section_found = False
|
|
219
|
+
|
|
220
|
+
for i, line in enumerate(lines):
|
|
221
|
+
result.append(line)
|
|
222
|
+
|
|
223
|
+
# Add dynamic = ["version"] after [project] if not present
|
|
224
|
+
if line.strip().startswith("[project]") and not project_section_found:
|
|
225
|
+
project_section_found = True
|
|
226
|
+
# Check next few lines for dynamic or version
|
|
227
|
+
has_dynamic = False
|
|
228
|
+
has_version = False
|
|
229
|
+
for j in range(i + 1, min(i + 10, len(lines))):
|
|
230
|
+
if "dynamic" in lines[j] or "version" in lines[j]:
|
|
231
|
+
if "dynamic" in lines[j]:
|
|
232
|
+
has_dynamic = True
|
|
233
|
+
if "version" in lines[j] and not lines[j].strip().startswith("#"):
|
|
234
|
+
has_version = True
|
|
235
|
+
break
|
|
236
|
+
if lines[j].strip().startswith("["):
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
if not has_dynamic and not has_version:
|
|
240
|
+
result.append('dynamic = ["version"]')
|
|
241
|
+
|
|
242
|
+
# Add hatch versioning configuration at the end
|
|
243
|
+
if "[tool.hatch.version]" not in content:
|
|
244
|
+
result.append("")
|
|
245
|
+
result.append("[tool.hatch.version]")
|
|
246
|
+
result.append('source = "uv-dynamic-versioning"')
|
|
247
|
+
result.append("")
|
|
248
|
+
result.append("[tool.uv-dynamic-versioning]")
|
|
249
|
+
result.append('vcs = "git"')
|
|
250
|
+
result.append('style = "pep440"')
|
|
251
|
+
result.append("bump = true")
|
|
252
|
+
|
|
253
|
+
self.pyproject_path.write_text("\n".join(result), encoding="utf-8")
|