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,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build management functionality.
|
|
3
|
+
|
|
4
|
+
This module provides the BuildManager class which orchestrates the entire
|
|
5
|
+
build process: finding external dependencies, copying them temporarily,
|
|
6
|
+
running the build command, and cleaning up afterward.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import tomllib
|
|
18
|
+
except ImportError:
|
|
19
|
+
try:
|
|
20
|
+
import tomli as tomllib
|
|
21
|
+
except ImportError:
|
|
22
|
+
tomllib = None
|
|
23
|
+
|
|
24
|
+
from .analyzer import ImportAnalyzer
|
|
25
|
+
from .finder import ExternalDependencyFinder
|
|
26
|
+
from .subfolder_build import SubfolderBuildConfig
|
|
27
|
+
from .types import ExternalDependency, ImportInfo
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BuildManager:
|
|
31
|
+
"""
|
|
32
|
+
Manages the build process with external dependency handling.
|
|
33
|
+
|
|
34
|
+
This is the main class for using the package. It coordinates finding
|
|
35
|
+
external dependencies, copying them into the source directory, running
|
|
36
|
+
the build, and cleaning up.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
project_root: Root directory of the project
|
|
40
|
+
src_dir: Source directory containing the package code
|
|
41
|
+
copied_files: List of file paths that were copied (for cleanup)
|
|
42
|
+
copied_dirs: List of directory paths that were copied (for cleanup)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
project_root: Path,
|
|
48
|
+
src_dir: Path | None = None,
|
|
49
|
+
exclude_patterns: list[str] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Initialize the build manager.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project_root: Root directory of the project
|
|
56
|
+
src_dir: Source directory (defaults to project_root/src, or current dir if it has Python files)
|
|
57
|
+
exclude_patterns: Additional patterns to exclude from copying (e.g., ['_SS', '__sandbox'])
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If the source directory does not exist or is invalid
|
|
61
|
+
"""
|
|
62
|
+
from .utils import find_source_directory
|
|
63
|
+
|
|
64
|
+
self.project_root = project_root.resolve()
|
|
65
|
+
|
|
66
|
+
# If src_dir not provided, try to find it intelligently
|
|
67
|
+
if src_dir is None:
|
|
68
|
+
src_dir = find_source_directory(self.project_root)
|
|
69
|
+
if src_dir is None:
|
|
70
|
+
# Fallback to standard src/ directory
|
|
71
|
+
src_dir = self.project_root / "src"
|
|
72
|
+
|
|
73
|
+
self.src_dir = Path(src_dir).resolve()
|
|
74
|
+
|
|
75
|
+
# Validate source directory
|
|
76
|
+
if not self.src_dir.exists():
|
|
77
|
+
raise ValueError(f"Source directory not found: {self.src_dir}")
|
|
78
|
+
|
|
79
|
+
if not self.src_dir.is_dir():
|
|
80
|
+
raise ValueError(f"Source path is not a directory: {self.src_dir}")
|
|
81
|
+
|
|
82
|
+
self.copied_files: list[Path] = []
|
|
83
|
+
self.copied_dirs: list[Path] = []
|
|
84
|
+
self.exclude_patterns = exclude_patterns or []
|
|
85
|
+
self.finder = ExternalDependencyFinder(
|
|
86
|
+
self.project_root, self.src_dir, exclude_patterns=exclude_patterns
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Check if it's a valid Python package directory
|
|
90
|
+
if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
|
|
91
|
+
# Allow empty directories for now, but warn
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
self.copied_files: list[Path] = []
|
|
95
|
+
self.copied_dirs: list[Path] = []
|
|
96
|
+
|
|
97
|
+
def find_src_package_dir(self) -> Path | None:
|
|
98
|
+
"""
|
|
99
|
+
Find the main package directory within src/.
|
|
100
|
+
|
|
101
|
+
Looks for directories with __init__.py files. If multiple are found,
|
|
102
|
+
tries to match one with the project name from pyproject.toml.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Path to the main package directory, or src_dir if not found
|
|
106
|
+
"""
|
|
107
|
+
if not self.src_dir.exists():
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# Look for directories with __init__.py
|
|
111
|
+
package_dirs = [
|
|
112
|
+
d for d in self.src_dir.iterdir() if d.is_dir() and (d / "__init__.py").exists()
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
if len(package_dirs) == 1:
|
|
116
|
+
return package_dirs[0]
|
|
117
|
+
|
|
118
|
+
# If multiple, try to find the one matching the project name
|
|
119
|
+
project_name = self._get_project_name()
|
|
120
|
+
if project_name:
|
|
121
|
+
for pkg_dir in package_dirs:
|
|
122
|
+
if pkg_dir.name.replace("-", "_") == project_name.replace("-", "_"):
|
|
123
|
+
return pkg_dir
|
|
124
|
+
|
|
125
|
+
# Return the first one or src_dir itself
|
|
126
|
+
return package_dirs[0] if package_dirs else self.src_dir
|
|
127
|
+
|
|
128
|
+
def _get_project_name(self) -> str | None:
|
|
129
|
+
"""
|
|
130
|
+
Get the project name from pyproject.toml.
|
|
131
|
+
|
|
132
|
+
Uses tomllib (Python 3.11+) or tomli as fallback to parse the file.
|
|
133
|
+
Falls back to simple string parsing if TOML parsing is unavailable.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Project name from pyproject.toml, or None if not found
|
|
137
|
+
"""
|
|
138
|
+
pyproject_path = self.project_root / "pyproject.toml"
|
|
139
|
+
if not pyproject_path.exists():
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
if tomllib:
|
|
144
|
+
content = pyproject_path.read_bytes()
|
|
145
|
+
data = tomllib.loads(content)
|
|
146
|
+
return data.get("project", {}).get("name")
|
|
147
|
+
else:
|
|
148
|
+
# Fallback: simple parsing
|
|
149
|
+
content = pyproject_path.read_text(encoding="utf-8")
|
|
150
|
+
for line in content.split("\n"):
|
|
151
|
+
if line.strip().startswith("name ="):
|
|
152
|
+
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def prepare_build(self) -> list[ExternalDependency]:
|
|
159
|
+
"""
|
|
160
|
+
Prepare for build by finding and copying external dependencies.
|
|
161
|
+
|
|
162
|
+
This method:
|
|
163
|
+
1. Finds all Python files in the source directory
|
|
164
|
+
2. Analyzes them for external dependencies
|
|
165
|
+
3. Copies external files/directories into the source directory
|
|
166
|
+
4. Reports any ambiguous imports
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of ExternalDependency objects that were copied
|
|
170
|
+
"""
|
|
171
|
+
analyzer = ImportAnalyzer(self.project_root)
|
|
172
|
+
|
|
173
|
+
# Find all Python files in src/
|
|
174
|
+
python_files = analyzer.find_all_python_files(self.src_dir)
|
|
175
|
+
|
|
176
|
+
# Find external dependencies using the configured finder
|
|
177
|
+
external_deps = self.finder.find_external_dependencies(python_files)
|
|
178
|
+
|
|
179
|
+
# Copy external dependencies
|
|
180
|
+
for dep in external_deps:
|
|
181
|
+
self._copy_dependency(dep)
|
|
182
|
+
|
|
183
|
+
# Report ambiguous imports
|
|
184
|
+
self._report_ambiguous_imports(python_files)
|
|
185
|
+
|
|
186
|
+
return external_deps
|
|
187
|
+
|
|
188
|
+
def _copy_dependency(self, dep: ExternalDependency) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Copy an external dependency to the target location.
|
|
191
|
+
|
|
192
|
+
Handles both files and directories. Checks for idempotency to avoid
|
|
193
|
+
duplicate copies. Creates parent directories as needed.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
dep: ExternalDependency object with source and target paths
|
|
197
|
+
"""
|
|
198
|
+
source = dep.source_path
|
|
199
|
+
target = dep.target_path
|
|
200
|
+
|
|
201
|
+
if not source.exists():
|
|
202
|
+
print(f"Warning: External dependency not found: {source}", file=sys.stderr)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Create target directory if needed
|
|
206
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
|
|
208
|
+
# Check if already copied (idempotency)
|
|
209
|
+
if target.exists():
|
|
210
|
+
# Check if it's the same file
|
|
211
|
+
if source.is_file() and target.is_file():
|
|
212
|
+
try:
|
|
213
|
+
if source.samefile(target):
|
|
214
|
+
return # Already in place, skip
|
|
215
|
+
except OSError:
|
|
216
|
+
# Files are different, proceed with copy
|
|
217
|
+
pass
|
|
218
|
+
elif source.is_dir() and target.is_dir():
|
|
219
|
+
# For directories, check if they have the same structure
|
|
220
|
+
# Simple check: if target has __init__.py and source does too, assume copied
|
|
221
|
+
# Also check if target has any Python files
|
|
222
|
+
source_has_init = (source / "__init__.py").exists()
|
|
223
|
+
target_has_init = (target / "__init__.py").exists()
|
|
224
|
+
if source_has_init == target_has_init:
|
|
225
|
+
# Check if target has at least some files from source
|
|
226
|
+
source_files = {f.name for f in source.rglob("*.py")}
|
|
227
|
+
target_files = {f.name for f in target.rglob("*.py")}
|
|
228
|
+
if source_files and target_files and source_files.issubset(target_files):
|
|
229
|
+
# Assume already copied if structure matches
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if source.is_file():
|
|
234
|
+
shutil.copy2(source, target)
|
|
235
|
+
self.copied_files.append(target)
|
|
236
|
+
print(f"Copied external file: {source} -> {target}")
|
|
237
|
+
elif source.is_dir():
|
|
238
|
+
if target.exists():
|
|
239
|
+
shutil.rmtree(target)
|
|
240
|
+
# Use custom copy function that excludes certain patterns
|
|
241
|
+
self._copytree_excluding(source, target)
|
|
242
|
+
self.copied_dirs.append(target)
|
|
243
|
+
print(f"Copied external directory: {source} -> {target}")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
print(f"Error copying {source} to {target}: {e}", file=sys.stderr)
|
|
246
|
+
|
|
247
|
+
def _copytree_excluding(self, src: Path, dst: Path) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Copy a directory tree, excluding certain patterns.
|
|
250
|
+
|
|
251
|
+
Excludes directories matching patterns like _SS, __SS, _sandbox, etc.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
src: Source directory
|
|
255
|
+
dst: Destination directory
|
|
256
|
+
"""
|
|
257
|
+
default_patterns = [
|
|
258
|
+
"_SS",
|
|
259
|
+
"__SS",
|
|
260
|
+
"_sandbox",
|
|
261
|
+
"__sandbox",
|
|
262
|
+
"_skip",
|
|
263
|
+
"__skip",
|
|
264
|
+
"_test",
|
|
265
|
+
"__test__",
|
|
266
|
+
]
|
|
267
|
+
exclude_patterns = default_patterns + self.exclude_patterns
|
|
268
|
+
|
|
269
|
+
def should_exclude(path: Path) -> bool:
|
|
270
|
+
"""Check if a path should be excluded."""
|
|
271
|
+
# Check each component of the path
|
|
272
|
+
for part in path.parts:
|
|
273
|
+
# Check if any part matches an exclusion pattern
|
|
274
|
+
for pattern in exclude_patterns:
|
|
275
|
+
# Match if part equals pattern or starts with pattern
|
|
276
|
+
if part == pattern or part.startswith(pattern):
|
|
277
|
+
return True
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
# Create destination directory
|
|
281
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
|
|
283
|
+
# Copy files and subdirectories, excluding patterns
|
|
284
|
+
for item in src.iterdir():
|
|
285
|
+
if should_exclude(item):
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
src_item = src / item.name
|
|
289
|
+
dst_item = dst / item.name
|
|
290
|
+
|
|
291
|
+
if src_item.is_file():
|
|
292
|
+
shutil.copy2(src_item, dst_item)
|
|
293
|
+
elif src_item.is_dir():
|
|
294
|
+
self._copytree_excluding(src_item, dst_item)
|
|
295
|
+
|
|
296
|
+
def _report_ambiguous_imports(self, python_files: list[Path]) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Report any ambiguous imports that couldn't be resolved.
|
|
299
|
+
|
|
300
|
+
Prints warnings to stderr for imports that couldn't be classified
|
|
301
|
+
as stdlib, third-party, local, or external.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
python_files: List of Python files to check for ambiguous imports
|
|
305
|
+
"""
|
|
306
|
+
analyzer = ImportAnalyzer(self.project_root)
|
|
307
|
+
ambiguous: list[ImportInfo] = []
|
|
308
|
+
|
|
309
|
+
for file_path in python_files:
|
|
310
|
+
imports = analyzer.extract_imports(file_path)
|
|
311
|
+
for imp in imports:
|
|
312
|
+
analyzer.classify_import(imp, self.src_dir)
|
|
313
|
+
if imp.classification == "ambiguous":
|
|
314
|
+
ambiguous.append(imp)
|
|
315
|
+
|
|
316
|
+
if ambiguous:
|
|
317
|
+
print("\nWarning: Found ambiguous imports that could not be resolved:", file=sys.stderr)
|
|
318
|
+
for imp in ambiguous:
|
|
319
|
+
print(
|
|
320
|
+
f" {imp.module_name} (line {imp.line_number} in {imp.file_path})",
|
|
321
|
+
file=sys.stderr,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def cleanup(self) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Remove all copied files and directories.
|
|
327
|
+
|
|
328
|
+
This method removes all files and directories that were copied
|
|
329
|
+
during prepare_build(). It handles errors gracefully and clears
|
|
330
|
+
the internal tracking lists.
|
|
331
|
+
"""
|
|
332
|
+
# Remove copied directories first (they may contain files)
|
|
333
|
+
for dir_path in reversed(self.copied_dirs):
|
|
334
|
+
if dir_path.exists():
|
|
335
|
+
try:
|
|
336
|
+
shutil.rmtree(dir_path)
|
|
337
|
+
print(f"Removed copied directory: {dir_path}")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
print(f"Error removing {dir_path}: {e}", file=sys.stderr)
|
|
340
|
+
|
|
341
|
+
# Remove copied files
|
|
342
|
+
for file_path in self.copied_files:
|
|
343
|
+
if file_path.exists():
|
|
344
|
+
try:
|
|
345
|
+
file_path.unlink()
|
|
346
|
+
print(f"Removed copied file: {file_path}")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"Error removing {file_path}: {e}", file=sys.stderr)
|
|
349
|
+
|
|
350
|
+
self.copied_files.clear()
|
|
351
|
+
self.copied_dirs.clear()
|
|
352
|
+
|
|
353
|
+
def run_build(self, build_command: Callable[[], None]) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Run the build process with dependency management.
|
|
356
|
+
|
|
357
|
+
This is a convenience method that:
|
|
358
|
+
1. Calls prepare_build() to find and copy dependencies
|
|
359
|
+
2. Executes the provided build_command
|
|
360
|
+
3. Always calls cleanup() afterward, even if build fails
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
build_command: Callable that executes the build process
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
```python
|
|
367
|
+
def build():
|
|
368
|
+
subprocess.run(["uv", "build"], check=True)
|
|
369
|
+
|
|
370
|
+
manager.run_build(build)
|
|
371
|
+
```
|
|
372
|
+
"""
|
|
373
|
+
try:
|
|
374
|
+
print("Analyzing project for external dependencies...")
|
|
375
|
+
external_deps = self.prepare_build()
|
|
376
|
+
|
|
377
|
+
if external_deps:
|
|
378
|
+
print(f"\nFound {len(external_deps)} external dependencies")
|
|
379
|
+
print("Copied external dependencies into source directory\n")
|
|
380
|
+
else:
|
|
381
|
+
print("No external dependencies found\n")
|
|
382
|
+
|
|
383
|
+
print("Running build...")
|
|
384
|
+
# Build command should run from project root to find pyproject.toml
|
|
385
|
+
import os
|
|
386
|
+
|
|
387
|
+
original_cwd = os.getcwd()
|
|
388
|
+
try:
|
|
389
|
+
os.chdir(self.project_root)
|
|
390
|
+
build_command()
|
|
391
|
+
finally:
|
|
392
|
+
os.chdir(original_cwd)
|
|
393
|
+
|
|
394
|
+
finally:
|
|
395
|
+
print("\nCleaning up copied files...")
|
|
396
|
+
self.cleanup()
|
|
397
|
+
|
|
398
|
+
def build_and_publish(
|
|
399
|
+
self,
|
|
400
|
+
build_command: Callable[[], None],
|
|
401
|
+
repository: str | None = None,
|
|
402
|
+
repository_url: str | None = None,
|
|
403
|
+
username: str | None = None,
|
|
404
|
+
password: str | None = None,
|
|
405
|
+
skip_existing: bool = False,
|
|
406
|
+
version: str | None = None,
|
|
407
|
+
restore_versioning: bool = True,
|
|
408
|
+
package_name: str | None = None,
|
|
409
|
+
dependency_group: str | None = None,
|
|
410
|
+
) -> None:
|
|
411
|
+
"""
|
|
412
|
+
Build and publish the package in one operation.
|
|
413
|
+
|
|
414
|
+
This method combines build and publish operations:
|
|
415
|
+
1. Sets version if specified
|
|
416
|
+
2. Prepares external dependencies
|
|
417
|
+
3. Runs the build command
|
|
418
|
+
4. Publishes to the specified repository
|
|
419
|
+
5. Cleans up copied files
|
|
420
|
+
6. Restores versioning configuration if requested
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
build_command: Callable that executes the build process
|
|
424
|
+
repository: Target repository ('pypi', 'testpypi', or 'azure')
|
|
425
|
+
repository_url: Custom repository URL (required for Azure)
|
|
426
|
+
username: Username for publishing (will prompt if not provided)
|
|
427
|
+
password: Password/token for publishing (will prompt if not provided)
|
|
428
|
+
skip_existing: If True, skip files that already exist on the repository
|
|
429
|
+
version: Manual version to set before building (PEP 440 format)
|
|
430
|
+
restore_versioning: If True, restore dynamic versioning after build
|
|
431
|
+
package_name: Package name for subfolder builds (default: derived from src_dir name)
|
|
432
|
+
dependency_group: Name of dependency group to copy from parent pyproject.toml
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
```python
|
|
436
|
+
def build():
|
|
437
|
+
subprocess.run(["uv", "build"], check=True)
|
|
438
|
+
|
|
439
|
+
manager.build_and_publish(build, repository="pypi", version="1.2.3")
|
|
440
|
+
```
|
|
441
|
+
"""
|
|
442
|
+
from .publisher import Publisher
|
|
443
|
+
from .version import VersionManager
|
|
444
|
+
|
|
445
|
+
version_manager = None
|
|
446
|
+
original_version = None
|
|
447
|
+
subfolder_config = None
|
|
448
|
+
|
|
449
|
+
# Check if we're building a subfolder (not the main src/ directory)
|
|
450
|
+
is_subfolder_build = not self.src_dir.is_relative_to(self.project_root / "src") or (
|
|
451
|
+
self.src_dir != self.project_root / "src" and self.src_dir != self.project_root
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
# For subfolder builds, create a temporary pyproject.toml
|
|
456
|
+
if is_subfolder_build and version:
|
|
457
|
+
if not package_name:
|
|
458
|
+
# Derive package name from subfolder
|
|
459
|
+
package_name = (
|
|
460
|
+
self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
|
|
461
|
+
)
|
|
462
|
+
print(f"Building subfolder as package '{package_name}' version '{version}'...")
|
|
463
|
+
subfolder_config = SubfolderBuildConfig(
|
|
464
|
+
project_root=self.project_root,
|
|
465
|
+
src_dir=self.src_dir,
|
|
466
|
+
package_name=package_name,
|
|
467
|
+
version=version,
|
|
468
|
+
dependency_group=dependency_group,
|
|
469
|
+
)
|
|
470
|
+
subfolder_config.create_temp_pyproject()
|
|
471
|
+
elif version:
|
|
472
|
+
# Regular build with version override
|
|
473
|
+
version_manager = VersionManager(self.project_root)
|
|
474
|
+
original_version = version_manager.get_current_version()
|
|
475
|
+
print(f"Setting version to {version}...")
|
|
476
|
+
version_manager.set_version(version)
|
|
477
|
+
|
|
478
|
+
# Build the package
|
|
479
|
+
self.run_build(build_command)
|
|
480
|
+
|
|
481
|
+
# Publish if repository is specified
|
|
482
|
+
if repository:
|
|
483
|
+
# Determine package name and version for filtering
|
|
484
|
+
publish_package_name = None
|
|
485
|
+
publish_version = version
|
|
486
|
+
|
|
487
|
+
if is_subfolder_build and package_name:
|
|
488
|
+
publish_package_name = package_name
|
|
489
|
+
elif not is_subfolder_build:
|
|
490
|
+
# For regular builds, get package name from pyproject.toml
|
|
491
|
+
try:
|
|
492
|
+
import tomllib
|
|
493
|
+
except ImportError:
|
|
494
|
+
try:
|
|
495
|
+
import tomli as tomllib
|
|
496
|
+
except ImportError:
|
|
497
|
+
tomllib = None
|
|
498
|
+
|
|
499
|
+
if tomllib:
|
|
500
|
+
pyproject_path = self.project_root / "pyproject.toml"
|
|
501
|
+
if pyproject_path.exists():
|
|
502
|
+
with pyproject_path.open("rb") as f:
|
|
503
|
+
data = tomllib.load(f)
|
|
504
|
+
if "project" in data and "name" in data["project"]:
|
|
505
|
+
publish_package_name = data["project"]["name"]
|
|
506
|
+
|
|
507
|
+
publisher = Publisher(
|
|
508
|
+
repository=repository,
|
|
509
|
+
dist_dir=self.project_root / "dist",
|
|
510
|
+
repository_url=repository_url,
|
|
511
|
+
username=username,
|
|
512
|
+
password=password,
|
|
513
|
+
package_name=publish_package_name,
|
|
514
|
+
version=publish_version,
|
|
515
|
+
)
|
|
516
|
+
publisher.publish(skip_existing=skip_existing)
|
|
517
|
+
finally:
|
|
518
|
+
# Restore subfolder config if used
|
|
519
|
+
if subfolder_config and restore_versioning:
|
|
520
|
+
try:
|
|
521
|
+
subfolder_config.restore()
|
|
522
|
+
print("Restored original pyproject.toml")
|
|
523
|
+
except Exception as e:
|
|
524
|
+
print(f"Warning: Could not restore pyproject.toml: {e}", file=sys.stderr)
|
|
525
|
+
|
|
526
|
+
# Restore versioning if needed
|
|
527
|
+
if version_manager and restore_versioning:
|
|
528
|
+
try:
|
|
529
|
+
if original_version:
|
|
530
|
+
version_manager.set_version(original_version)
|
|
531
|
+
else:
|
|
532
|
+
version_manager.restore_dynamic_versioning()
|
|
533
|
+
print("Restored versioning configuration")
|
|
534
|
+
except Exception as e:
|
|
535
|
+
print(f"Warning: Could not restore versioning: {e}", file=sys.stderr)
|
|
536
|
+
|
|
537
|
+
# Cleanup is already handled by run_build, but ensure it's done
|
|
538
|
+
if self.copied_files or self.copied_dirs:
|
|
539
|
+
self.cleanup()
|