python-package-folder 1.1.3__py3-none-any.whl → 1.2.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.
- python_package_folder/manager.py +175 -57
- python_package_folder/subfolder_build.py +69 -13
- {python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/METADATA +95 -22
- {python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/RECORD +7 -7
- {python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/WHEEL +0 -0
- {python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/entry_points.txt +0 -0
- {python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/licenses/LICENSE +0 -0
python_package_folder/manager.py
CHANGED
|
@@ -85,15 +85,13 @@ class BuildManager:
|
|
|
85
85
|
self.finder = ExternalDependencyFinder(
|
|
86
86
|
self.project_root, self.src_dir, exclude_patterns=exclude_patterns
|
|
87
87
|
)
|
|
88
|
+
self.subfolder_config: SubfolderBuildConfig | None = None
|
|
88
89
|
|
|
89
90
|
# Check if it's a valid Python package directory
|
|
90
91
|
if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
|
|
91
92
|
# Allow empty directories for now, but warn
|
|
92
93
|
pass
|
|
93
94
|
|
|
94
|
-
self.copied_files: list[Path] = []
|
|
95
|
-
self.copied_dirs: list[Path] = []
|
|
96
|
-
|
|
97
95
|
def find_src_package_dir(self) -> Path | None:
|
|
98
96
|
"""
|
|
99
97
|
Find the main package directory within src/.
|
|
@@ -125,6 +123,21 @@ class BuildManager:
|
|
|
125
123
|
# Return the first one or src_dir itself
|
|
126
124
|
return package_dirs[0] if package_dirs else self.src_dir
|
|
127
125
|
|
|
126
|
+
def _is_subfolder_build(self) -> bool:
|
|
127
|
+
"""
|
|
128
|
+
Check if we're building a subfolder (not the main src/ directory).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if this is a subfolder build, False otherwise
|
|
132
|
+
"""
|
|
133
|
+
# Check if src_dir is not the main src/ directory
|
|
134
|
+
main_src = self.project_root / "src"
|
|
135
|
+
return (
|
|
136
|
+
self.src_dir != main_src
|
|
137
|
+
and self.src_dir != self.project_root
|
|
138
|
+
and self.src_dir.is_relative_to(self.project_root)
|
|
139
|
+
)
|
|
140
|
+
|
|
128
141
|
def _get_project_name(self) -> str | None:
|
|
129
142
|
"""
|
|
130
143
|
Get the project name from pyproject.toml.
|
|
@@ -155,19 +168,94 @@ class BuildManager:
|
|
|
155
168
|
|
|
156
169
|
return None
|
|
157
170
|
|
|
158
|
-
def prepare_build(
|
|
171
|
+
def prepare_build(
|
|
172
|
+
self,
|
|
173
|
+
version: str | None = None,
|
|
174
|
+
package_name: str | None = None,
|
|
175
|
+
dependency_group: str | None = None,
|
|
176
|
+
) -> list[ExternalDependency]:
|
|
159
177
|
"""
|
|
160
178
|
Prepare for build by finding and copying external dependencies.
|
|
161
179
|
|
|
162
|
-
This method
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
This method automatically detects if you're building a subfolder (not the main src/
|
|
181
|
+
directory) and sets up pyproject.toml appropriately:
|
|
182
|
+
- If pyproject.toml exists in the subfolder, it will be used
|
|
183
|
+
- Otherwise, creates a temporary pyproject.toml with the correct package configuration
|
|
184
|
+
For subfolder builds without their own pyproject.toml, if no version is provided,
|
|
185
|
+
it defaults to "0.0.0" with a warning.
|
|
186
|
+
|
|
187
|
+
Process:
|
|
188
|
+
1. Detects if this is a subfolder build and sets up pyproject.toml if needed:
|
|
189
|
+
- If pyproject.toml exists in subfolder: uses that file
|
|
190
|
+
- If no pyproject.toml in subfolder: creates temporary one from parent
|
|
191
|
+
2. Finds all Python files in the source directory
|
|
192
|
+
3. Analyzes them for external dependencies
|
|
193
|
+
4. Copies external files/directories into the source directory
|
|
194
|
+
5. Reports any ambiguous imports
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
version: Version for subfolder builds. If building a subfolder and version is None,
|
|
198
|
+
defaults to "0.0.0" with a warning. Only used when creating temporary pyproject.toml
|
|
199
|
+
(ignored if subfolder has its own pyproject.toml). For regular builds, this parameter
|
|
200
|
+
is ignored.
|
|
201
|
+
package_name: Package name for subfolder builds. If None, derived from src_dir name
|
|
202
|
+
(e.g., "empty_drawing_detection" -> "empty-drawing-detection"). Only used when
|
|
203
|
+
creating temporary pyproject.toml (ignored if subfolder has its own pyproject.toml).
|
|
204
|
+
Ignored for regular builds.
|
|
205
|
+
dependency_group: Name of dependency group to copy from parent pyproject.toml.
|
|
206
|
+
Only used for subfolder builds when creating temporary pyproject.toml.
|
|
167
207
|
|
|
168
208
|
Returns:
|
|
169
209
|
List of ExternalDependency objects that were copied
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
```python
|
|
213
|
+
# Regular build (main src/ directory)
|
|
214
|
+
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
|
|
215
|
+
deps = manager.prepare_build() # No version needed
|
|
216
|
+
|
|
217
|
+
# Subfolder build (automatic detection)
|
|
218
|
+
manager = BuildManager(
|
|
219
|
+
project_root=Path("."),
|
|
220
|
+
src_dir=Path("src/integration/empty_drawing_detection")
|
|
221
|
+
)
|
|
222
|
+
# Version defaults to "0.0.0" if not provided
|
|
223
|
+
deps = manager.prepare_build(version="1.0.0", package_name="my-package")
|
|
224
|
+
```
|
|
170
225
|
"""
|
|
226
|
+
# Check if this is a subfolder build and set up config if needed
|
|
227
|
+
if self._is_subfolder_build():
|
|
228
|
+
# For subfolder builds, we need a version
|
|
229
|
+
# If not provided, use a default version
|
|
230
|
+
if not version:
|
|
231
|
+
version = "0.0.0"
|
|
232
|
+
print(
|
|
233
|
+
f"Warning: No version specified for subfolder build. Using default version '{version}'",
|
|
234
|
+
file=sys.stderr,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if not package_name:
|
|
238
|
+
# Derive package name from subfolder
|
|
239
|
+
package_name = (
|
|
240
|
+
self.src_dir.name.replace("_", "-").replace(" ", "-").lower().strip("-")
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
print(
|
|
244
|
+
f"Detected subfolder build. Setting up package '{package_name}' version '{version}'..."
|
|
245
|
+
)
|
|
246
|
+
self.subfolder_config = SubfolderBuildConfig(
|
|
247
|
+
project_root=self.project_root,
|
|
248
|
+
src_dir=self.src_dir,
|
|
249
|
+
package_name=package_name,
|
|
250
|
+
version=version,
|
|
251
|
+
dependency_group=dependency_group,
|
|
252
|
+
)
|
|
253
|
+
temp_pyproject = self.subfolder_config.create_temp_pyproject()
|
|
254
|
+
# If temp_pyproject is None, it means no parent pyproject.toml exists
|
|
255
|
+
# This is acceptable for tests or dependency-only operations
|
|
256
|
+
if temp_pyproject is None:
|
|
257
|
+
self.subfolder_config = None
|
|
258
|
+
|
|
171
259
|
analyzer = ImportAnalyzer(self.project_root)
|
|
172
260
|
|
|
173
261
|
# Find all Python files in src/
|
|
@@ -325,10 +413,30 @@ class BuildManager:
|
|
|
325
413
|
"""
|
|
326
414
|
Remove all copied files and directories.
|
|
327
415
|
|
|
328
|
-
This method removes all files and directories that were copied
|
|
329
|
-
|
|
330
|
-
the internal tracking lists.
|
|
416
|
+
This method removes all files and directories that were copied during prepare_build().
|
|
417
|
+
It also restores the original pyproject.toml if a temporary one was created for a
|
|
418
|
+
subfolder build. It handles errors gracefully and clears the internal tracking lists.
|
|
419
|
+
|
|
420
|
+
This is automatically called by run_build(), but you can call it manually if you
|
|
421
|
+
use prepare_build() directly.
|
|
422
|
+
|
|
423
|
+
Example:
|
|
424
|
+
```python
|
|
425
|
+
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
|
|
426
|
+
deps = manager.prepare_build()
|
|
427
|
+
# ... do your build ...
|
|
428
|
+
manager.cleanup() # Restores pyproject.toml and removes copied files
|
|
429
|
+
```
|
|
331
430
|
"""
|
|
431
|
+
# Restore subfolder config if it was created
|
|
432
|
+
if self.subfolder_config:
|
|
433
|
+
try:
|
|
434
|
+
self.subfolder_config.restore()
|
|
435
|
+
print("Restored original pyproject.toml")
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print(f"Warning: Could not restore pyproject.toml: {e}", file=sys.stderr)
|
|
438
|
+
self.subfolder_config = None
|
|
439
|
+
|
|
332
440
|
# Remove copied directories first (they may contain files)
|
|
333
441
|
for dir_path in reversed(self.copied_dirs):
|
|
334
442
|
if dir_path.exists():
|
|
@@ -350,29 +458,59 @@ class BuildManager:
|
|
|
350
458
|
self.copied_files.clear()
|
|
351
459
|
self.copied_dirs.clear()
|
|
352
460
|
|
|
353
|
-
def run_build(
|
|
461
|
+
def run_build(
|
|
462
|
+
self,
|
|
463
|
+
build_command: Callable[[], None],
|
|
464
|
+
version: str | None = None,
|
|
465
|
+
package_name: str | None = None,
|
|
466
|
+
dependency_group: str | None = None,
|
|
467
|
+
) -> None:
|
|
354
468
|
"""
|
|
355
469
|
Run the build process with dependency management.
|
|
356
470
|
|
|
357
|
-
This is a convenience method that:
|
|
358
|
-
1. Calls prepare_build() to find and copy dependencies
|
|
471
|
+
This is a convenience method that automatically handles the full build lifecycle:
|
|
472
|
+
1. Calls prepare_build() to find and copy dependencies (with automatic subfolder detection)
|
|
359
473
|
2. Executes the provided build_command
|
|
360
474
|
3. Always calls cleanup() afterward, even if build fails
|
|
361
475
|
|
|
476
|
+
For subfolder builds, this method automatically detects the subfolder and creates a
|
|
477
|
+
temporary pyproject.toml with the correct package configuration. The build command
|
|
478
|
+
should be runnable from the project root (e.g., "uv build", "python -m build").
|
|
479
|
+
|
|
362
480
|
Args:
|
|
363
|
-
build_command: Callable that executes the build process
|
|
481
|
+
build_command: Callable that executes the build process. Should run from project root.
|
|
482
|
+
version: Version for subfolder builds. If building a subfolder and version is None,
|
|
483
|
+
defaults to "0.0.0" with a warning. Ignored for regular builds.
|
|
484
|
+
package_name: Package name for subfolder builds. If None, derived from src_dir name.
|
|
485
|
+
Ignored for regular builds.
|
|
486
|
+
dependency_group: Name of dependency group to copy from parent pyproject.toml.
|
|
487
|
+
Only used for subfolder builds.
|
|
364
488
|
|
|
365
489
|
Example:
|
|
366
490
|
```python
|
|
491
|
+
from pathlib import Path
|
|
492
|
+
from python_package_folder import BuildManager
|
|
493
|
+
import subprocess
|
|
494
|
+
|
|
495
|
+
# Regular build
|
|
496
|
+
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
|
|
367
497
|
def build():
|
|
368
498
|
subprocess.run(["uv", "build"], check=True)
|
|
369
|
-
|
|
370
499
|
manager.run_build(build)
|
|
500
|
+
|
|
501
|
+
# Subfolder build (automatic detection)
|
|
502
|
+
manager = BuildManager(
|
|
503
|
+
project_root=Path("."),
|
|
504
|
+
src_dir=Path("src/integration/empty_drawing_detection")
|
|
505
|
+
)
|
|
506
|
+
manager.run_build(build, version="1.0.0")
|
|
371
507
|
```
|
|
372
508
|
"""
|
|
373
509
|
try:
|
|
374
510
|
print("Analyzing project for external dependencies...")
|
|
375
|
-
external_deps = self.prepare_build(
|
|
511
|
+
external_deps = self.prepare_build(
|
|
512
|
+
version=version, package_name=package_name, dependency_group=dependency_group
|
|
513
|
+
)
|
|
376
514
|
|
|
377
515
|
if external_deps:
|
|
378
516
|
print(f"\nFound {len(external_deps)} external dependencies")
|
|
@@ -444,49 +582,37 @@ class BuildManager:
|
|
|
444
582
|
|
|
445
583
|
version_manager = None
|
|
446
584
|
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
585
|
|
|
454
586
|
try:
|
|
455
|
-
# For subfolder builds
|
|
456
|
-
if
|
|
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
|
|
587
|
+
# For non-subfolder builds with version, use VersionManager
|
|
588
|
+
if version and not self._is_subfolder_build():
|
|
473
589
|
version_manager = VersionManager(self.project_root)
|
|
474
590
|
original_version = version_manager.get_current_version()
|
|
475
591
|
print(f"Setting version to {version}...")
|
|
476
592
|
version_manager.set_version(version)
|
|
477
593
|
|
|
478
|
-
# Build the package
|
|
479
|
-
self.run_build(
|
|
594
|
+
# Build the package (prepare_build will handle subfolder config if needed)
|
|
595
|
+
self.run_build(
|
|
596
|
+
build_command,
|
|
597
|
+
version=version,
|
|
598
|
+
package_name=package_name,
|
|
599
|
+
dependency_group=dependency_group,
|
|
600
|
+
)
|
|
480
601
|
|
|
481
602
|
# Publish if repository is specified
|
|
482
603
|
if repository:
|
|
483
604
|
# Determine package name and version for filtering
|
|
484
605
|
publish_package_name = None
|
|
485
606
|
publish_version = version
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
607
|
+
is_subfolder_build = self._is_subfolder_build()
|
|
608
|
+
|
|
609
|
+
if is_subfolder_build:
|
|
610
|
+
# Get package name from subfolder_config if it was created, otherwise use provided
|
|
611
|
+
if self.subfolder_config:
|
|
612
|
+
publish_package_name = self.subfolder_config.package_name
|
|
613
|
+
elif package_name:
|
|
614
|
+
publish_package_name = package_name
|
|
615
|
+
else:
|
|
490
616
|
# For regular builds, get package name from pyproject.toml
|
|
491
617
|
try:
|
|
492
618
|
import tomllib
|
|
@@ -515,15 +641,7 @@ class BuildManager:
|
|
|
515
641
|
)
|
|
516
642
|
publisher.publish(skip_existing=skip_existing)
|
|
517
643
|
finally:
|
|
518
|
-
# Restore subfolder config
|
|
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
|
|
644
|
+
# Restore versioning if needed (subfolder config is handled by cleanup)
|
|
527
645
|
if version_manager and restore_versioning:
|
|
528
646
|
try:
|
|
529
647
|
if original_version:
|
|
@@ -29,8 +29,10 @@ class SubfolderBuildConfig:
|
|
|
29
29
|
"""
|
|
30
30
|
Manages temporary build configuration for subfolder builds.
|
|
31
31
|
|
|
32
|
-
When building a subfolder as a separate package, this class
|
|
33
|
-
|
|
32
|
+
When building a subfolder as a separate package, this class:
|
|
33
|
+
- Uses the subfolder's pyproject.toml if it exists
|
|
34
|
+
- Otherwise creates a temporary pyproject.toml with the appropriate package name and version
|
|
35
|
+
- Handles README files similarly (uses subfolder README if present)
|
|
34
36
|
"""
|
|
35
37
|
|
|
36
38
|
def __init__(
|
|
@@ -47,9 +49,12 @@ class SubfolderBuildConfig:
|
|
|
47
49
|
Args:
|
|
48
50
|
project_root: Root directory containing the main pyproject.toml
|
|
49
51
|
src_dir: Source directory being built (subfolder)
|
|
50
|
-
package_name: Name for the subfolder package (default: derived from src_dir name)
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
package_name: Name for the subfolder package (default: derived from src_dir name).
|
|
53
|
+
Only used if subfolder doesn't have its own pyproject.toml.
|
|
54
|
+
version: Version for the subfolder package (required if building subfolder).
|
|
55
|
+
Only used if subfolder doesn't have its own pyproject.toml.
|
|
56
|
+
dependency_group: Name of dependency group to copy from parent pyproject.toml.
|
|
57
|
+
Only used if subfolder doesn't have its own pyproject.toml.
|
|
53
58
|
"""
|
|
54
59
|
self.project_root = project_root.resolve()
|
|
55
60
|
self.src_dir = src_dir.resolve()
|
|
@@ -61,6 +66,7 @@ class SubfolderBuildConfig:
|
|
|
61
66
|
self._temp_init_created = False
|
|
62
67
|
self.temp_readme: Path | None = None
|
|
63
68
|
self.original_readme_backup: Path | None = None
|
|
69
|
+
self._used_subfolder_pyproject = False
|
|
64
70
|
|
|
65
71
|
def _derive_package_name(self) -> str:
|
|
66
72
|
"""Derive package name from source directory name."""
|
|
@@ -105,15 +111,17 @@ class SubfolderBuildConfig:
|
|
|
105
111
|
# If it's a package or has subpackages, return the path
|
|
106
112
|
return packages_path, [packages_path] if packages_path else []
|
|
107
113
|
|
|
108
|
-
def create_temp_pyproject(self) -> Path:
|
|
114
|
+
def create_temp_pyproject(self) -> Path | None:
|
|
109
115
|
"""
|
|
110
116
|
Create a temporary pyproject.toml for the subfolder build.
|
|
111
117
|
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
If a pyproject.toml exists in the subfolder, it will be used instead of creating
|
|
119
|
+
a new one. Otherwise, creates a pyproject.toml in the project root based on the
|
|
120
|
+
parent pyproject.toml with the appropriate package name and version.
|
|
114
121
|
|
|
115
122
|
Returns:
|
|
116
|
-
Path to the
|
|
123
|
+
Path to the pyproject.toml file (either from subfolder or created temporary),
|
|
124
|
+
or None if no parent pyproject.toml exists (in which case subfolder config is skipped)
|
|
117
125
|
"""
|
|
118
126
|
if not self.version:
|
|
119
127
|
raise ValueError("Version is required for subfolder builds")
|
|
@@ -127,10 +135,47 @@ class SubfolderBuildConfig:
|
|
|
127
135
|
else:
|
|
128
136
|
self._temp_init_created = False
|
|
129
137
|
|
|
138
|
+
# Check if pyproject.toml exists in subfolder
|
|
139
|
+
subfolder_pyproject = self.src_dir / "pyproject.toml"
|
|
140
|
+
if subfolder_pyproject.exists():
|
|
141
|
+
# Use the subfolder's pyproject.toml
|
|
142
|
+
print(f"Using existing pyproject.toml from subfolder: {subfolder_pyproject}")
|
|
143
|
+
self._used_subfolder_pyproject = True
|
|
144
|
+
|
|
145
|
+
# Backup the original project root pyproject.toml if it exists
|
|
146
|
+
original_pyproject = self.project_root / "pyproject.toml"
|
|
147
|
+
if original_pyproject.exists():
|
|
148
|
+
backup_path = self.project_root / "pyproject.toml.backup"
|
|
149
|
+
shutil.copy2(original_pyproject, backup_path)
|
|
150
|
+
self.original_pyproject_backup = backup_path
|
|
151
|
+
|
|
152
|
+
# Copy subfolder pyproject.toml to project root
|
|
153
|
+
shutil.copy2(subfolder_pyproject, original_pyproject)
|
|
154
|
+
self.temp_pyproject = original_pyproject
|
|
155
|
+
|
|
156
|
+
# Handle README file
|
|
157
|
+
self._handle_readme()
|
|
158
|
+
|
|
159
|
+
return original_pyproject
|
|
160
|
+
|
|
161
|
+
# No pyproject.toml in subfolder, create one from parent
|
|
162
|
+
self._used_subfolder_pyproject = False
|
|
163
|
+
print("No pyproject.toml found in subfolder, creating temporary one from parent")
|
|
164
|
+
|
|
130
165
|
# Read the original pyproject.toml
|
|
131
166
|
original_pyproject = self.project_root / "pyproject.toml"
|
|
132
167
|
if not original_pyproject.exists():
|
|
133
|
-
|
|
168
|
+
# If no parent pyproject.toml exists, we can't create a temporary one
|
|
169
|
+
# This is acceptable for tests or cases where only dependency copying is needed
|
|
170
|
+
print(
|
|
171
|
+
f"Warning: No pyproject.toml found in project root ({original_pyproject}). "
|
|
172
|
+
"Skipping subfolder build configuration. Only dependency copying will be performed.",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
# Still handle README file
|
|
176
|
+
self._handle_readme()
|
|
177
|
+
# Return None to indicate no pyproject.toml was created
|
|
178
|
+
return None
|
|
134
179
|
|
|
135
180
|
original_content = original_pyproject.read_text(encoding="utf-8")
|
|
136
181
|
|
|
@@ -419,7 +464,13 @@ class SubfolderBuildConfig:
|
|
|
419
464
|
self.temp_readme = target_readme
|
|
420
465
|
|
|
421
466
|
def restore(self) -> None:
|
|
422
|
-
"""
|
|
467
|
+
"""
|
|
468
|
+
Restore the original pyproject.toml and remove temporary __init__.py if created.
|
|
469
|
+
|
|
470
|
+
If a subfolder pyproject.toml was used, it restores the original project root
|
|
471
|
+
pyproject.toml from backup. If a temporary pyproject.toml was created, it
|
|
472
|
+
restores the original as well.
|
|
473
|
+
"""
|
|
423
474
|
# Remove temporary __init__.py if we created it
|
|
424
475
|
if self._temp_init_created:
|
|
425
476
|
init_file = self.src_dir / "__init__.py"
|
|
@@ -460,13 +511,18 @@ class SubfolderBuildConfig:
|
|
|
460
511
|
pass # Ignore errors during cleanup
|
|
461
512
|
self.temp_readme = None
|
|
462
513
|
|
|
463
|
-
# Restore original pyproject.toml
|
|
464
|
-
if
|
|
514
|
+
# Restore original pyproject.toml (only if we created/used one)
|
|
515
|
+
if (
|
|
516
|
+
self.temp_pyproject
|
|
517
|
+
and self.original_pyproject_backup
|
|
518
|
+
and self.original_pyproject_backup.exists()
|
|
519
|
+
):
|
|
465
520
|
original_pyproject = self.project_root / "pyproject.toml"
|
|
466
521
|
shutil.copy2(self.original_pyproject_backup, original_pyproject)
|
|
467
522
|
self.original_pyproject_backup.unlink()
|
|
468
523
|
self.original_pyproject_backup = None
|
|
469
524
|
self.temp_pyproject = None
|
|
525
|
+
self._used_subfolder_pyproject = False
|
|
470
526
|
|
|
471
527
|
def __enter__(self) -> Self:
|
|
472
528
|
"""Context manager entry."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-package-folder
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
|
|
5
5
|
Project-URL: Repository, https://github.com/alelom/python-package-folder
|
|
6
6
|
Author-email: Alessio Lombardi <work@alelom.com>
|
|
@@ -125,9 +125,11 @@ This package will automatically:
|
|
|
125
125
|
|
|
126
126
|
## Features
|
|
127
127
|
|
|
128
|
-
- **Subfolder Build Support**: Build subfolders as separate packages with automatic
|
|
128
|
+
- **Subfolder Build Support**: Build subfolders as separate packages with automatic detection and configuration
|
|
129
|
+
- **Automatic subfolder detection**: Detects when building a subfolder (not the main `src/` directory)
|
|
129
130
|
- Creates any needed file for publishing automatically, cleaning up if not originally in the subfolder after the build/publish process. E.g. copies external dependencies into the source directory before build and cleans them up afterward; temporary `__init__.py` creation for non-package subfolders; uses subfolder README if present, otherwise creates minimal README
|
|
130
131
|
- Automatic package name derivation from subfolder name
|
|
132
|
+
- Automatic temporary `pyproject.toml` creation with correct package structure
|
|
131
133
|
- Dependency group selection: specify which dependency group from parent `pyproject.toml` to include.
|
|
132
134
|
|
|
133
135
|
- **Smart Import Classification and analysis**:
|
|
@@ -268,16 +270,20 @@ cd my_project/subfolder_to_build
|
|
|
268
270
|
python-package-folder --version "1.0.0" --publish pypi
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
The tool **automatically detects** when you're building a subfolder (any directory that's not the main `src/` directory) and sets up the appropriate build configuration.
|
|
272
274
|
|
|
273
275
|
The tool automatically:
|
|
276
|
+
- **Detects subfolder builds**: Automatically identifies when building from a subdirectory
|
|
274
277
|
- Finds the project root by looking for `pyproject.toml` in parent directories
|
|
275
278
|
- Uses the current directory as the source directory if it contains Python files
|
|
276
279
|
- Falls back to `project_root/src` if the current directory isn't suitable
|
|
277
|
-
- For subfolder builds
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
|
|
280
|
+
- **For subfolder builds**: Handles `pyproject.toml` configuration:
|
|
281
|
+
- **If `pyproject.toml` exists in subfolder**: Uses that file (copies it to project root temporarily)
|
|
282
|
+
- **If no `pyproject.toml` in subfolder**: Creates a temporary `pyproject.toml` with:
|
|
283
|
+
- Package name derived from the subfolder name (e.g., `empty_drawing_detection` → `empty-drawing-detection`)
|
|
284
|
+
- Version from `--version` argument (defaults to `0.0.0` with a warning if not provided)
|
|
285
|
+
- Proper package path configuration for hatchling
|
|
286
|
+
- Dependency groups from parent `pyproject.toml` if specified
|
|
281
287
|
- Creates temporary `__init__.py` files if needed to make subfolders valid Python packages
|
|
282
288
|
- **README handling for subfolder builds**:
|
|
283
289
|
- If a README file (README.md, README.rst, README.txt, or README) exists in the subfolder, it will be used instead of the parent README
|
|
@@ -285,6 +291,8 @@ The tool automatically:
|
|
|
285
291
|
- Restores the original `pyproject.toml` after build (unless `--no-restore-versioning` is used)
|
|
286
292
|
- Cleans up temporary `__init__.py` files after build
|
|
287
293
|
|
|
294
|
+
**Note**: While version is not strictly required (defaults to `0.0.0`), it's recommended to specify `--version` for subfolder builds to ensure proper versioning.
|
|
295
|
+
|
|
288
296
|
**Subfolder Build Example:**
|
|
289
297
|
```bash
|
|
290
298
|
# Build a subfolder as a separate package
|
|
@@ -293,6 +301,11 @@ python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --
|
|
|
293
301
|
|
|
294
302
|
# Build with a specific dependency group from parent pyproject.toml
|
|
295
303
|
python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
|
|
304
|
+
|
|
305
|
+
# If subfolder has its own pyproject.toml, it will be used automatically
|
|
306
|
+
# (package-name and version arguments are ignored in this case)
|
|
307
|
+
cd src/integration/my_package # assuming my_package/pyproject.toml exists
|
|
308
|
+
python-package-folder --publish pypi
|
|
296
309
|
```
|
|
297
310
|
|
|
298
311
|
**Dependency Groups**: When building a subfolder, you can specify a dependency group from the parent `pyproject.toml` to include in the subfolder's build configuration. This allows subfolders to inherit specific dependencies from the parent project:
|
|
@@ -308,6 +321,8 @@ The specified dependency group will be copied from the parent `pyproject.toml`'s
|
|
|
308
321
|
|
|
309
322
|
You can also use the package programmatically:
|
|
310
323
|
|
|
324
|
+
### Basic Usage
|
|
325
|
+
|
|
311
326
|
```python
|
|
312
327
|
from pathlib import Path
|
|
313
328
|
from python_package_folder import BuildManager
|
|
@@ -328,11 +343,11 @@ for dep in external_deps:
|
|
|
328
343
|
# Run your build process here
|
|
329
344
|
# ...
|
|
330
345
|
|
|
331
|
-
# Cleanup copied files
|
|
346
|
+
# Cleanup copied files (also restores pyproject.toml if subfolder build)
|
|
332
347
|
manager.cleanup()
|
|
333
348
|
```
|
|
334
349
|
|
|
335
|
-
|
|
350
|
+
### Using the Convenience Method
|
|
336
351
|
|
|
337
352
|
```python
|
|
338
353
|
from pathlib import Path
|
|
@@ -348,6 +363,55 @@ def build_command():
|
|
|
348
363
|
manager.run_build(build_command)
|
|
349
364
|
```
|
|
350
365
|
|
|
366
|
+
### Subfolder Builds (Automatic Detection)
|
|
367
|
+
|
|
368
|
+
The tool automatically detects when you're building a subfolder and sets up the appropriate configuration:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from pathlib import Path
|
|
372
|
+
from python_package_folder import BuildManager
|
|
373
|
+
import subprocess
|
|
374
|
+
|
|
375
|
+
# Building a subfolder - automatic detection!
|
|
376
|
+
manager = BuildManager(
|
|
377
|
+
project_root=Path("."),
|
|
378
|
+
src_dir=Path("src/integration/empty_drawing_detection")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def build_command():
|
|
382
|
+
subprocess.run(["uv", "build"], check=True)
|
|
383
|
+
|
|
384
|
+
# prepare_build() automatically:
|
|
385
|
+
# - Detects this is a subfolder build
|
|
386
|
+
# - If pyproject.toml exists in subfolder: uses that file
|
|
387
|
+
# - If no pyproject.toml in subfolder: creates temporary one with package name "empty-drawing-detection"
|
|
388
|
+
# - Uses version "0.0.0" (or pass version="1.0.0" to override) if creating temporary pyproject.toml
|
|
389
|
+
external_deps = manager.prepare_build(version="1.0.0")
|
|
390
|
+
|
|
391
|
+
# Run build - uses the pyproject.toml (either from subfolder or temporary)
|
|
392
|
+
build_command()
|
|
393
|
+
|
|
394
|
+
# Cleanup restores original pyproject.toml and removes copied files
|
|
395
|
+
manager.cleanup()
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Note**: If the subfolder has its own `pyproject.toml`, it will be used automatically. The `version` and `package_name` parameters are only used when creating a temporary `pyproject.toml` from the parent configuration.
|
|
399
|
+
|
|
400
|
+
Or use the convenience method:
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
manager = BuildManager(
|
|
404
|
+
project_root=Path("."),
|
|
405
|
+
src_dir=Path("src/integration/empty_drawing_detection")
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def build_command():
|
|
409
|
+
subprocess.run(["uv", "build"], check=True)
|
|
410
|
+
|
|
411
|
+
# All handled automatically: subfolder detection, pyproject.toml setup, build, cleanup
|
|
412
|
+
manager.run_build(build_command, version="1.0.0", package_name="my-custom-name")
|
|
413
|
+
```
|
|
414
|
+
|
|
351
415
|
## Working with sysappend
|
|
352
416
|
|
|
353
417
|
This package works well with projects using [sysappend](https://pypi.org/project/sysappend/) for flexible import management. When you have imports like:
|
|
@@ -393,20 +457,28 @@ The `--version` option:
|
|
|
393
457
|
|
|
394
458
|
### Subfolder Versioning
|
|
395
459
|
|
|
396
|
-
When building from a subdirectory (not the main `src/` directory),
|
|
460
|
+
When building from a subdirectory (not the main `src/` directory), the tool automatically detects the subfolder and sets up the build configuration:
|
|
397
461
|
|
|
398
462
|
```bash
|
|
399
|
-
# Build a subfolder as a separate package
|
|
463
|
+
# Build a subfolder as a separate package (version recommended but not required)
|
|
400
464
|
cd my_project/subfolder_to_build
|
|
401
465
|
python-package-folder --version "1.0.0" --publish pypi
|
|
402
466
|
|
|
403
467
|
# With custom package name
|
|
404
468
|
python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
|
|
469
|
+
|
|
470
|
+
# Version defaults to "0.0.0" if not specified (with a warning)
|
|
471
|
+
python-package-folder --publish pypi
|
|
405
472
|
```
|
|
406
473
|
|
|
407
474
|
For subfolder builds:
|
|
408
|
-
- **
|
|
409
|
-
- **
|
|
475
|
+
- **Automatic detection**: The tool automatically detects subfolder builds
|
|
476
|
+
- **pyproject.toml handling**:
|
|
477
|
+
- If `pyproject.toml` exists in subfolder: Uses that file (copied to project root temporarily)
|
|
478
|
+
- If no `pyproject.toml` in subfolder: Creates temporary one with correct package structure
|
|
479
|
+
- **Version**: Recommended but not required when creating temporary pyproject.toml. If not provided, defaults to `0.0.0` with a warning. Ignored if subfolder has its own `pyproject.toml`.
|
|
480
|
+
- **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`). Only used when creating temporary pyproject.toml.
|
|
481
|
+
- **Restoration**: Original `pyproject.toml` is restored after build
|
|
410
482
|
- **Temporary configuration**: Creates a temporary `pyproject.toml` with:
|
|
411
483
|
- Custom package name (from `--package-name` or derived)
|
|
412
484
|
- Specified version
|
|
@@ -715,7 +787,8 @@ version_manager.restore_dynamic_versioning()
|
|
|
715
787
|
|
|
716
788
|
### SubfolderBuildConfig
|
|
717
789
|
|
|
718
|
-
Manages temporary build configuration for subfolder builds.
|
|
790
|
+
Manages temporary build configuration for subfolder builds. If a `pyproject.toml` exists
|
|
791
|
+
in the subfolder, it will be used instead of creating a new one.
|
|
719
792
|
|
|
720
793
|
```python
|
|
721
794
|
from python_package_folder import SubfolderBuildConfig
|
|
@@ -724,11 +797,11 @@ from pathlib import Path
|
|
|
724
797
|
config = SubfolderBuildConfig(
|
|
725
798
|
project_root=Path("."),
|
|
726
799
|
src_dir=Path("subfolder"),
|
|
727
|
-
package_name="my-subfolder",
|
|
728
|
-
version="1.0.0"
|
|
800
|
+
package_name="my-subfolder", # Only used if subfolder has no pyproject.toml
|
|
801
|
+
version="1.0.0" # Only used if subfolder has no pyproject.toml
|
|
729
802
|
)
|
|
730
803
|
|
|
731
|
-
# Create temporary pyproject.toml
|
|
804
|
+
# Create temporary pyproject.toml (or use subfolder's if it exists)
|
|
732
805
|
config.create_temp_pyproject()
|
|
733
806
|
|
|
734
807
|
# ... build process ...
|
|
@@ -738,13 +811,13 @@ config.restore()
|
|
|
738
811
|
```
|
|
739
812
|
|
|
740
813
|
**Methods:**
|
|
741
|
-
- `create_temp_pyproject() -> Path`:
|
|
814
|
+
- `create_temp_pyproject() -> Path`: Use subfolder's `pyproject.toml` if it exists, otherwise create temporary `pyproject.toml` with subfolder-specific configuration
|
|
742
815
|
- `restore() -> None`: Restore original `pyproject.toml` and clean up temporary files
|
|
743
816
|
|
|
744
|
-
**Note**: This class automatically
|
|
745
|
-
- If a
|
|
746
|
-
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
747
|
-
-
|
|
817
|
+
**Note**: This class automatically:
|
|
818
|
+
- **pyproject.toml handling**: If a `pyproject.toml` exists in the subfolder, it will be used (copied to project root temporarily). Otherwise, creates a temporary one from the parent configuration.
|
|
819
|
+
- **README handling**: If a README exists in the subfolder, it will be used instead of the parent README. If no README exists in the subfolder, a minimal README with just the folder name will be created. The original parent README is backed up and restored after the build completes.
|
|
820
|
+
- **Package initialization**: Creates `__init__.py` files if needed to make subfolders valid Python packages.
|
|
748
821
|
|
|
749
822
|
|
|
750
823
|
## Development
|
|
@@ -2,16 +2,16 @@ python_package_folder/__init__.py,sha256=DQt-uldOEKfh0MUqCvKdeNKOnpuOvpb7blYvXMy
|
|
|
2
2
|
python_package_folder/__main__.py,sha256=a-__-VLhYw-J7S7CsHdhtEvQr3RiAZxiYDvKhKTgMX4,291
|
|
3
3
|
python_package_folder/analyzer.py,sha256=Iw5bdg9NahO57L3CZgGYbhU-m2mh0DpQQ-xqIINUfic,10976
|
|
4
4
|
python_package_folder/finder.py,sha256=_LvJ9xBVKv41UK5sbwbNyKmuYjAOqUbzvZhK7NCYQF8,9130
|
|
5
|
-
python_package_folder/manager.py,sha256=
|
|
5
|
+
python_package_folder/manager.py,sha256=AlzEqI7q0Q2mVmc_HjIEBmomlT053qqLBMZZ52X7IDQ,26360
|
|
6
6
|
python_package_folder/publisher.py,sha256=1xa6PuduOXNVTTp4IrJcx4qOskugX2fTeJ9QsLDLuUM,11535
|
|
7
7
|
python_package_folder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
python_package_folder/python_package_folder.py,sha256=3ECayVwjXQAnujr2e21xvsuwetcqgRF5Zawt3qgtI9M,8779
|
|
9
|
-
python_package_folder/subfolder_build.py,sha256=
|
|
9
|
+
python_package_folder/subfolder_build.py,sha256=BBiuszOVYUHX3aC4bbvg5_39Wh286e006lhTALU1fqQ,22358
|
|
10
10
|
python_package_folder/types.py,sha256=3yeSRR5p_3PDKEAaehW_RJ7NwJHexOIeA08bGaT1iSY,2368
|
|
11
11
|
python_package_folder/utils.py,sha256=hoCNRiTHe_-zjAFztxyqjNEMKiEl7XCkSDl36iaMcjM,2966
|
|
12
12
|
python_package_folder/version.py,sha256=kIDP6S9trEfs9gj7lBYGxrWm4RPssRla24UtlO9Jkh4,9111
|
|
13
|
-
python_package_folder-1.
|
|
14
|
-
python_package_folder-1.
|
|
15
|
-
python_package_folder-1.
|
|
16
|
-
python_package_folder-1.
|
|
17
|
-
python_package_folder-1.
|
|
13
|
+
python_package_folder-1.2.0.dist-info/METADATA,sha256=n6EfjNT-NT8RowwHjbdGUhHQm5v5pDS1oFSKnEfJJNU,32164
|
|
14
|
+
python_package_folder-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
python_package_folder-1.2.0.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
|
|
16
|
+
python_package_folder-1.2.0.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
|
|
17
|
+
python_package_folder-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
{python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_package_folder-1.1.3.dist-info → python_package_folder-1.2.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|