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.
@@ -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")