python-package-folder 1.2.2__tar.gz → 1.3.0__tar.gz
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-1.3.0/PKG-INFO +23 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/coverage.svg +2 -2
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/manager.py +76 -2
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/publisher.py +12 -4
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/subfolder_build.py +59 -24
- python_package_folder-1.3.0/tests/folder_structure/subfolder_to_build/README.md +3 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_build_with_external_deps.py +30 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_publisher.py +33 -3
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_subfolder_build.py +48 -7
- python_package_folder-1.2.2/PKG-INFO +0 -881
- python_package_folder-1.2.2/README.md +0 -861
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.copier-answers.yml +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.github/workflows/ci.yml +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.github/workflows/publish.yml +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.gitignore +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/.vscode/settings.json +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/LICENSE +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/Makefile +0 -0
- {python_package_folder-1.2.2/tests/folder_structure/subfolder_to_build → python_package_folder-1.3.0}/README.md +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/development.md +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/installation.md +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/publishing.md +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/pyproject.toml +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/python_package_folder.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/types.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/utils.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/version.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_linting.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_utils.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_version_manager.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/tests.py +0 -0
- {python_package_folder-1.2.2 → python_package_folder-1.3.0}/uv.lock +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-package-folder
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
|
|
5
|
+
Project-URL: Repository, https://github.com/alelom/python-package-folder
|
|
6
|
+
Author-email: Alessio Lombardi <work@alelom.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: <4.0,>=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Example subfolder
|
|
22
|
+
|
|
23
|
+
Example subfolder built and published with https://github.com/alelom/python-package-folder.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
15
15
|
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
16
16
|
<text x="31.5" y="14">coverage</text>
|
|
17
|
-
<text x="81" y="15" fill="#010101" fill-opacity=".3">
|
|
18
|
-
<text x="81" y="14">
|
|
17
|
+
<text x="81" y="15" fill="#010101" fill-opacity=".3">68%</text>
|
|
18
|
+
<text x="81" y="14">68%</text>
|
|
19
19
|
</g>
|
|
20
20
|
</svg>
|
{python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/manager.py
RENAMED
|
@@ -415,7 +415,9 @@ class BuildManager:
|
|
|
415
415
|
|
|
416
416
|
This method removes all files and directories that were copied during prepare_build().
|
|
417
417
|
It also restores the original pyproject.toml if a temporary one was created for a
|
|
418
|
-
subfolder build.
|
|
418
|
+
subfolder build. Additionally, it removes all .egg-info directories created during
|
|
419
|
+
the build process and cleans up any empty directories that remain after removing
|
|
420
|
+
copied files. It handles errors gracefully and clears the internal tracking lists.
|
|
419
421
|
|
|
420
422
|
This is automatically called by run_build(), but you can call it manually if you
|
|
421
423
|
use prepare_build() directly.
|
|
@@ -425,7 +427,7 @@ class BuildManager:
|
|
|
425
427
|
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
|
|
426
428
|
deps = manager.prepare_build()
|
|
427
429
|
# ... do your build ...
|
|
428
|
-
manager.cleanup() # Restores pyproject.toml
|
|
430
|
+
manager.cleanup() # Restores pyproject.toml, removes copied files, .egg-info dirs, and empty dirs
|
|
429
431
|
```
|
|
430
432
|
"""
|
|
431
433
|
# Restore subfolder config if it was created
|
|
@@ -458,6 +460,77 @@ class BuildManager:
|
|
|
458
460
|
self.copied_files.clear()
|
|
459
461
|
self.copied_dirs.clear()
|
|
460
462
|
|
|
463
|
+
# Remove all .egg-info directories in src_dir and project_root
|
|
464
|
+
self._cleanup_egg_info_dirs()
|
|
465
|
+
|
|
466
|
+
# Remove empty directories that may remain after cleanup
|
|
467
|
+
self._cleanup_empty_dirs()
|
|
468
|
+
|
|
469
|
+
def _cleanup_egg_info_dirs(self) -> None:
|
|
470
|
+
"""
|
|
471
|
+
Remove all .egg-info directories in the source directory and project root.
|
|
472
|
+
|
|
473
|
+
These directories are created by setuptools during the build process and
|
|
474
|
+
should be cleaned up after the build completes.
|
|
475
|
+
"""
|
|
476
|
+
# Search in src_dir and project_root
|
|
477
|
+
search_dirs = [self.src_dir, self.project_root]
|
|
478
|
+
|
|
479
|
+
for search_dir in search_dirs:
|
|
480
|
+
if not search_dir.exists():
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
# Find all .egg-info directories
|
|
484
|
+
for egg_info_dir in search_dir.rglob("*.egg-info"):
|
|
485
|
+
if egg_info_dir.is_dir():
|
|
486
|
+
try:
|
|
487
|
+
shutil.rmtree(egg_info_dir)
|
|
488
|
+
print(f"Removed .egg-info directory: {egg_info_dir}")
|
|
489
|
+
except Exception as e:
|
|
490
|
+
print(
|
|
491
|
+
f"Warning: Could not remove .egg-info directory {egg_info_dir}: {e}",
|
|
492
|
+
file=sys.stderr,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _cleanup_empty_dirs(self) -> None:
|
|
496
|
+
"""
|
|
497
|
+
Remove empty directories in the source directory after cleanup.
|
|
498
|
+
|
|
499
|
+
After removing copied files and directories, some parent directories may
|
|
500
|
+
become empty. This method recursively removes empty directories starting
|
|
501
|
+
from the deepest level.
|
|
502
|
+
"""
|
|
503
|
+
if not self.src_dir.exists():
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Collect all directories in src_dir, sorted by depth (deepest first)
|
|
507
|
+
all_dirs: list[Path] = []
|
|
508
|
+
for item in self.src_dir.rglob("*"):
|
|
509
|
+
if item.is_dir():
|
|
510
|
+
all_dirs.append(item)
|
|
511
|
+
|
|
512
|
+
# Sort by path depth (deepest first) so we remove children before parents
|
|
513
|
+
all_dirs.sort(key=lambda p: len(p.parts), reverse=True)
|
|
514
|
+
|
|
515
|
+
# Remove empty directories (but not src_dir itself)
|
|
516
|
+
for dir_path in all_dirs:
|
|
517
|
+
if dir_path == self.src_dir:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
# Check if directory is empty
|
|
522
|
+
if dir_path.exists() and not any(dir_path.iterdir()):
|
|
523
|
+
dir_path.rmdir()
|
|
524
|
+
print(f"Removed empty directory: {dir_path}")
|
|
525
|
+
except (OSError, PermissionError):
|
|
526
|
+
# Directory not empty or permission error - skip it
|
|
527
|
+
pass
|
|
528
|
+
except Exception as e:
|
|
529
|
+
print(
|
|
530
|
+
f"Warning: Could not remove directory {dir_path}: {e}",
|
|
531
|
+
file=sys.stderr,
|
|
532
|
+
)
|
|
533
|
+
|
|
461
534
|
def run_build(
|
|
462
535
|
self,
|
|
463
536
|
build_command: Callable[[], None],
|
|
@@ -604,6 +677,7 @@ class BuildManager:
|
|
|
604
677
|
# Determine package name and version for filtering
|
|
605
678
|
publish_package_name = None
|
|
606
679
|
publish_version = version
|
|
680
|
+
publish_package_name = None
|
|
607
681
|
is_subfolder_build = self._is_subfolder_build()
|
|
608
682
|
|
|
609
683
|
if is_subfolder_build:
|
{python_package_folder-1.2.2 → python_package_folder-1.3.0}/src/python_package_folder/publisher.py
RENAMED
|
@@ -201,13 +201,21 @@ class Publisher:
|
|
|
201
201
|
# Handle .tar.gz files
|
|
202
202
|
stem = stem[:-4] # Remove .tar
|
|
203
203
|
|
|
204
|
-
# Check if filename
|
|
204
|
+
# Check if filename matches any name variant with exact version
|
|
205
205
|
matches = False
|
|
206
206
|
for name_variant in name_variants:
|
|
207
207
|
# Pattern: {name}-{version} or {name}-{version}-{tag}
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
# Use exact match: must start with name-version and next char (if any) must be - or end of string
|
|
209
|
+
expected_prefix = f"{name_variant}-{version_str}"
|
|
210
|
+
if stem.startswith(expected_prefix):
|
|
211
|
+
# Ensure exact version match (not a longer version like 1.0.10 matching 1.0.1)
|
|
212
|
+
# Check that after the version, we have either:
|
|
213
|
+
# - End of string (for source dists: name-version)
|
|
214
|
+
# - A hyphen followed by more characters (for wheels: name-version-tag)
|
|
215
|
+
remaining = stem[len(expected_prefix) :]
|
|
216
|
+
if not remaining or remaining.startswith("-"):
|
|
217
|
+
matches = True
|
|
218
|
+
break
|
|
211
219
|
|
|
212
220
|
if matches:
|
|
213
221
|
dist_files.append(f)
|
|
@@ -36,6 +36,8 @@ class SubfolderBuildConfig:
|
|
|
36
36
|
- Always ensures [build-system] section uses hatchling (replaces any existing build-system
|
|
37
37
|
configuration from parent or subfolder)
|
|
38
38
|
- Handles README files similarly (uses subfolder README if present)
|
|
39
|
+
- **Never modifies the root pyproject.toml**: The original file is temporarily moved to
|
|
40
|
+
pyproject.toml.original and restored after the build, ensuring the original is never modified
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
def __init__(
|
|
@@ -66,6 +68,7 @@ class SubfolderBuildConfig:
|
|
|
66
68
|
self.dependency_group = dependency_group
|
|
67
69
|
self.temp_pyproject: Path | None = None
|
|
68
70
|
self.original_pyproject_backup: Path | None = None
|
|
71
|
+
self.original_pyproject_path: Path | None = None
|
|
69
72
|
self._temp_init_created = False
|
|
70
73
|
self.temp_readme: Path | None = None
|
|
71
74
|
self.original_readme_backup: Path | None = None
|
|
@@ -205,6 +208,10 @@ class SubfolderBuildConfig:
|
|
|
205
208
|
The [build-system] section is always set to use hatchling, even if the parent or
|
|
206
209
|
subfolder pyproject.toml uses a different build backend (e.g., setuptools).
|
|
207
210
|
|
|
211
|
+
**Important**: The root pyproject.toml is never modified. Instead, it is temporarily
|
|
212
|
+
moved to pyproject.toml.original and restored after the build completes. This ensures
|
|
213
|
+
the original file remains unchanged.
|
|
214
|
+
|
|
208
215
|
Returns:
|
|
209
216
|
Path to the pyproject.toml file (either from subfolder or created temporary),
|
|
210
217
|
or None if no parent pyproject.toml exists (in which case subfolder config is skipped)
|
|
@@ -228,20 +235,30 @@ class SubfolderBuildConfig:
|
|
|
228
235
|
print(f"Using existing pyproject.toml from subfolder: {subfolder_pyproject}")
|
|
229
236
|
self._used_subfolder_pyproject = True
|
|
230
237
|
|
|
231
|
-
#
|
|
238
|
+
# Store reference to original project root pyproject.toml
|
|
232
239
|
original_pyproject = self.project_root / "pyproject.toml"
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
self.original_pyproject_path = original_pyproject
|
|
241
|
+
|
|
242
|
+
# Create temporary pyproject.toml file
|
|
243
|
+
temp_pyproject_path = self.project_root / "pyproject.toml.temp"
|
|
237
244
|
|
|
238
245
|
# Read and adjust the subfolder pyproject.toml
|
|
239
246
|
subfolder_content = subfolder_pyproject.read_text(encoding="utf-8")
|
|
240
247
|
# Adjust packages path to be relative to project root
|
|
241
248
|
adjusted_content = self._adjust_subfolder_pyproject_packages_path(subfolder_content)
|
|
242
249
|
|
|
243
|
-
# Write adjusted content to
|
|
244
|
-
|
|
250
|
+
# Write adjusted content to temporary file
|
|
251
|
+
temp_pyproject_path.write_text(adjusted_content, encoding="utf-8")
|
|
252
|
+
self.temp_pyproject = temp_pyproject_path
|
|
253
|
+
|
|
254
|
+
# If original pyproject.toml exists, temporarily move it
|
|
255
|
+
if original_pyproject.exists():
|
|
256
|
+
backup_path = self.project_root / "pyproject.toml.original"
|
|
257
|
+
original_pyproject.rename(backup_path)
|
|
258
|
+
self.original_pyproject_backup = backup_path
|
|
259
|
+
|
|
260
|
+
# Move temp file to pyproject.toml for the build
|
|
261
|
+
temp_pyproject_path.rename(original_pyproject)
|
|
245
262
|
self.temp_pyproject = original_pyproject
|
|
246
263
|
|
|
247
264
|
# Handle README file
|
|
@@ -270,9 +287,12 @@ class SubfolderBuildConfig:
|
|
|
270
287
|
|
|
271
288
|
original_content = original_pyproject.read_text(encoding="utf-8")
|
|
272
289
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
# Store reference to original
|
|
291
|
+
self.original_pyproject_path = original_pyproject
|
|
292
|
+
|
|
293
|
+
# Temporarily move original to backup location
|
|
294
|
+
backup_path = self.project_root / "pyproject.toml.original"
|
|
295
|
+
original_pyproject.rename(backup_path)
|
|
276
296
|
self.original_pyproject_backup = backup_path
|
|
277
297
|
|
|
278
298
|
# Parse and modify the pyproject.toml
|
|
@@ -327,8 +347,12 @@ class SubfolderBuildConfig:
|
|
|
327
347
|
original_content, parent_dependency_group
|
|
328
348
|
)
|
|
329
349
|
|
|
330
|
-
# Write the modified content
|
|
331
|
-
|
|
350
|
+
# Write the modified content to a temporary file
|
|
351
|
+
temp_pyproject_path = self.project_root / "pyproject.toml.temp"
|
|
352
|
+
temp_pyproject_path.write_text(modified_content, encoding="utf-8")
|
|
353
|
+
|
|
354
|
+
# Move temp file to pyproject.toml for the build
|
|
355
|
+
temp_pyproject_path.rename(original_pyproject)
|
|
332
356
|
self.temp_pyproject = original_pyproject
|
|
333
357
|
|
|
334
358
|
# Handle README file
|
|
@@ -585,9 +609,10 @@ class SubfolderBuildConfig:
|
|
|
585
609
|
"""
|
|
586
610
|
Restore the original pyproject.toml and remove temporary __init__.py if created.
|
|
587
611
|
|
|
588
|
-
|
|
589
|
-
pyproject.toml
|
|
590
|
-
restores the original
|
|
612
|
+
The root pyproject.toml is never modified during subfolder builds. Instead, it is
|
|
613
|
+
temporarily moved to pyproject.toml.original and then restored after the build.
|
|
614
|
+
This method removes the temporary pyproject.toml and restores the original from
|
|
615
|
+
the backup location, ensuring the original file is never modified.
|
|
591
616
|
"""
|
|
592
617
|
# Remove temporary __init__.py if we created it
|
|
593
618
|
if self._temp_init_created:
|
|
@@ -630,16 +655,26 @@ class SubfolderBuildConfig:
|
|
|
630
655
|
self.temp_readme = None
|
|
631
656
|
|
|
632
657
|
# Restore original pyproject.toml (only if we created/used one)
|
|
633
|
-
if
|
|
634
|
-
self.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
658
|
+
if self.temp_pyproject and self.original_pyproject_path:
|
|
659
|
+
original_pyproject = self.original_pyproject_path
|
|
660
|
+
|
|
661
|
+
# Remove the temporary pyproject.toml we created
|
|
662
|
+
if original_pyproject.exists():
|
|
663
|
+
try:
|
|
664
|
+
original_pyproject.unlink()
|
|
665
|
+
except Exception as e:
|
|
666
|
+
print(
|
|
667
|
+
f"Warning: Could not remove temporary pyproject.toml: {e}",
|
|
668
|
+
file=sys.stderr,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Restore the original pyproject.toml from backup if it existed
|
|
672
|
+
if self.original_pyproject_backup and self.original_pyproject_backup.exists():
|
|
673
|
+
self.original_pyproject_backup.rename(original_pyproject)
|
|
674
|
+
self.original_pyproject_backup = None
|
|
675
|
+
|
|
642
676
|
self.temp_pyproject = None
|
|
677
|
+
self.original_pyproject_path = None
|
|
643
678
|
self._used_subfolder_pyproject = False
|
|
644
679
|
|
|
645
680
|
def __enter__(self) -> Self:
|
{python_package_folder-1.2.2 → python_package_folder-1.3.0}/tests/test_build_with_external_deps.py
RENAMED
|
@@ -277,6 +277,36 @@ class TestBuildManager:
|
|
|
277
277
|
assert len(manager.copied_files) == 0
|
|
278
278
|
assert len(manager.copied_dirs) == 0
|
|
279
279
|
|
|
280
|
+
def test_cleanup_removes_egg_info_dirs(self, test_project_root: Path) -> None:
|
|
281
|
+
"""Test that cleanup removes .egg-info directories."""
|
|
282
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
283
|
+
manager = BuildManager(test_project_root, src_dir)
|
|
284
|
+
|
|
285
|
+
# Create a fake .egg-info directory
|
|
286
|
+
egg_info_dir = src_dir / "package.egg-info"
|
|
287
|
+
egg_info_dir.mkdir()
|
|
288
|
+
(egg_info_dir / "PKG-INFO").write_text("test")
|
|
289
|
+
|
|
290
|
+
manager.cleanup()
|
|
291
|
+
|
|
292
|
+
# Verify .egg-info directory was removed
|
|
293
|
+
assert not egg_info_dir.exists()
|
|
294
|
+
|
|
295
|
+
def test_cleanup_removes_empty_dirs(self, test_project_root: Path) -> None:
|
|
296
|
+
"""Test that cleanup removes empty directories."""
|
|
297
|
+
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
298
|
+
manager = BuildManager(test_project_root, src_dir)
|
|
299
|
+
|
|
300
|
+
# Create a nested empty directory structure
|
|
301
|
+
empty_dir = src_dir / "empty_parent" / "empty_child"
|
|
302
|
+
empty_dir.mkdir(parents=True)
|
|
303
|
+
|
|
304
|
+
manager.cleanup()
|
|
305
|
+
|
|
306
|
+
# Verify empty directories were removed
|
|
307
|
+
assert not empty_dir.exists()
|
|
308
|
+
assert not (src_dir / "empty_parent").exists()
|
|
309
|
+
|
|
280
310
|
def test_cleanup_handles_missing_files(self, test_project_root: Path) -> None:
|
|
281
311
|
"""Test that cleanup handles already-removed files gracefully."""
|
|
282
312
|
src_dir = test_project_root / "folder_structure" / "subfolder_to_build"
|
|
@@ -19,6 +19,9 @@ def test_dist_dir(tmp_path: Path) -> Path:
|
|
|
19
19
|
# Create some distribution files
|
|
20
20
|
(dist_dir / "package-1.0.0-py3-none-any.whl").write_text("fake wheel")
|
|
21
21
|
(dist_dir / "package-1.0.0.tar.gz").write_text("fake source")
|
|
22
|
+
(dist_dir / "package-1.0.1-py3-none-any.whl").write_text("fake wheel")
|
|
23
|
+
(dist_dir / "package-1.0.1.tar.gz").write_text("fake source")
|
|
24
|
+
(dist_dir / "package-1.0.10-py3-none-any.whl").write_text("fake wheel")
|
|
22
25
|
(dist_dir / "other-package-2.0.0-py3-none-any.whl").write_text("fake wheel")
|
|
23
26
|
|
|
24
27
|
return dist_dir
|
|
@@ -125,6 +128,30 @@ class TestPublisher:
|
|
|
125
128
|
assert all("package-1.0.0" in str(f) for f in file_args)
|
|
126
129
|
assert not any("other-package" in str(f) for f in file_args)
|
|
127
130
|
|
|
131
|
+
def test_publish_filters_exact_version(self, test_dist_dir: Path) -> None:
|
|
132
|
+
"""Test that publish filters files by exact version (not partial matches)."""
|
|
133
|
+
publisher = Publisher(
|
|
134
|
+
repository=Repository.PYPI,
|
|
135
|
+
dist_dir=test_dist_dir,
|
|
136
|
+
package_name="package",
|
|
137
|
+
version="1.0.1",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
with patch("python_package_folder.publisher.subprocess.run") as mock_run:
|
|
141
|
+
mock_run.return_value = MagicMock(returncode=0)
|
|
142
|
+
|
|
143
|
+
with patch.object(publisher, "_get_credentials", return_value=("user", "pass")):
|
|
144
|
+
publisher.publish()
|
|
145
|
+
|
|
146
|
+
call_args = mock_run.call_args[0][0]
|
|
147
|
+
file_args = [arg for arg in call_args if str(test_dist_dir) in str(arg)]
|
|
148
|
+
|
|
149
|
+
# Should only include 1.0.1 files, not 1.0.0 or 1.0.10
|
|
150
|
+
assert all("1.0.1" in str(f) for f in file_args)
|
|
151
|
+
assert not any("1.0.0" in str(f) for f in file_args)
|
|
152
|
+
assert not any("1.0.10" in str(f) for f in file_args)
|
|
153
|
+
assert len(file_args) == 2 # wheel and source dist
|
|
154
|
+
|
|
128
155
|
def test_publish_filters_by_version(self, test_dist_dir: Path) -> None:
|
|
129
156
|
"""Test that publish filters files by version."""
|
|
130
157
|
publisher = Publisher(
|
|
@@ -143,8 +170,11 @@ class TestPublisher:
|
|
|
143
170
|
call_args = mock_run.call_args[0][0]
|
|
144
171
|
file_args = [arg for arg in call_args if str(test_dist_dir) in str(arg)]
|
|
145
172
|
|
|
146
|
-
# Should only include 1.0.0 files, not 2.0.0
|
|
173
|
+
# Should only include 1.0.0 files, not 1.0.1, 1.0.10, or 2.0.0
|
|
147
174
|
assert all("1.0.0" in str(f) for f in file_args)
|
|
175
|
+
assert not any("1.0.1" in str(f) for f in file_args)
|
|
176
|
+
assert not any("1.0.10" in str(f) for f in file_args)
|
|
177
|
+
assert not any("2.0.0" in str(f) for f in file_args)
|
|
148
178
|
|
|
149
179
|
def test_publish_no_filtering(self, test_dist_dir: Path) -> None:
|
|
150
180
|
"""Test that publish includes all files when no filter specified."""
|
|
@@ -162,8 +192,8 @@ class TestPublisher:
|
|
|
162
192
|
call_args = mock_run.call_args[0][0]
|
|
163
193
|
file_args = [arg for arg in call_args if str(test_dist_dir) in str(arg)]
|
|
164
194
|
|
|
165
|
-
# Should include all distribution files
|
|
166
|
-
assert len(file_args) ==
|
|
195
|
+
# Should include all distribution files (6 files: 4 wheels + 2 source dists)
|
|
196
|
+
assert len(file_args) == 6
|
|
167
197
|
|
|
168
198
|
def test_publish_raises_when_no_files(self, tmp_path: Path) -> None:
|
|
169
199
|
"""Test that publish raises when no distribution files found."""
|
|
@@ -169,7 +169,7 @@ class TestSubfolderBuildConfig:
|
|
|
169
169
|
assert restored_content == original_content
|
|
170
170
|
|
|
171
171
|
# Check backup is removed
|
|
172
|
-
assert not (test_project_with_pyproject / "pyproject.toml.
|
|
172
|
+
assert not (test_project_with_pyproject / "pyproject.toml.original").exists()
|
|
173
173
|
|
|
174
174
|
def test_restore_removes_temp_init(self, test_project_with_pyproject: Path) -> None:
|
|
175
175
|
"""Test that restore removes temporary __init__.py."""
|
|
@@ -405,9 +405,9 @@ requests = ">=2.0.0"
|
|
|
405
405
|
assert 'name = "test-package"' not in content
|
|
406
406
|
assert 'name = "subfolder"' not in content
|
|
407
407
|
|
|
408
|
-
# Verify
|
|
409
|
-
assert (project_root / "pyproject.toml.
|
|
410
|
-
backup_content = (project_root / "pyproject.toml.
|
|
408
|
+
# Verify original was moved to backup location
|
|
409
|
+
assert (project_root / "pyproject.toml.original").exists()
|
|
410
|
+
backup_content = (project_root / "pyproject.toml.original").read_text()
|
|
411
411
|
assert 'name = "test-package"' in backup_content
|
|
412
412
|
|
|
413
413
|
# Verify flag is set
|
|
@@ -434,6 +434,11 @@ requests = ">=2.0.0"
|
|
|
434
434
|
)
|
|
435
435
|
|
|
436
436
|
config.create_temp_pyproject()
|
|
437
|
+
|
|
438
|
+
# Verify original content is preserved in backup (not modified)
|
|
439
|
+
backup_content = (project_root / "pyproject.toml.original").read_text()
|
|
440
|
+
assert backup_content == original_content
|
|
441
|
+
|
|
437
442
|
config.restore()
|
|
438
443
|
|
|
439
444
|
# Verify original is restored
|
|
@@ -441,7 +446,43 @@ requests = ">=2.0.0"
|
|
|
441
446
|
assert restored_content == original_content
|
|
442
447
|
|
|
443
448
|
# Verify backup is removed
|
|
444
|
-
assert not (project_root / "pyproject.toml.
|
|
449
|
+
assert not (project_root / "pyproject.toml.original").exists()
|
|
450
|
+
|
|
451
|
+
def test_root_pyproject_toml_never_modified(self, test_project_with_pyproject: Path) -> None:
|
|
452
|
+
"""Test that root pyproject.toml is never modified, only moved and restored."""
|
|
453
|
+
project_root = test_project_with_pyproject
|
|
454
|
+
subfolder = project_root / "subfolder"
|
|
455
|
+
original_pyproject = project_root / "pyproject.toml"
|
|
456
|
+
original_content = original_pyproject.read_text()
|
|
457
|
+
|
|
458
|
+
# Create pyproject.toml in subfolder
|
|
459
|
+
(subfolder / "pyproject.toml").write_text(
|
|
460
|
+
'[project]\nname = "subfolder-package"\nversion = "3.0.0"\n'
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
config = SubfolderBuildConfig(
|
|
464
|
+
project_root=project_root,
|
|
465
|
+
src_dir=subfolder,
|
|
466
|
+
version="1.0.0",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
config.create_temp_pyproject()
|
|
470
|
+
|
|
471
|
+
# Verify original was moved (not modified in place)
|
|
472
|
+
assert not original_pyproject.exists() or original_pyproject.read_text() != original_content
|
|
473
|
+
assert (project_root / "pyproject.toml.original").exists()
|
|
474
|
+
backup_content = (project_root / "pyproject.toml.original").read_text()
|
|
475
|
+
assert backup_content == original_content # Original content preserved exactly
|
|
476
|
+
|
|
477
|
+
config.restore()
|
|
478
|
+
|
|
479
|
+
# Verify original is restored with exact same content
|
|
480
|
+
assert original_pyproject.exists()
|
|
481
|
+
restored_content = original_pyproject.read_text()
|
|
482
|
+
assert restored_content == original_content
|
|
483
|
+
|
|
484
|
+
# Verify backup is removed
|
|
485
|
+
assert not (project_root / "pyproject.toml.original").exists()
|
|
445
486
|
|
|
446
487
|
def test_subfolder_pyproject_toml_without_parent_backup(
|
|
447
488
|
self, test_project_with_pyproject: Path
|
|
@@ -477,7 +518,7 @@ version = "3.0.0"
|
|
|
477
518
|
assert 'name = "subfolder-package"' in content
|
|
478
519
|
|
|
479
520
|
# No backup should be created since parent didn't exist
|
|
480
|
-
assert not (project_root / "pyproject.toml.
|
|
521
|
+
assert not (project_root / "pyproject.toml.original").exists()
|
|
481
522
|
|
|
482
523
|
# Restore original for cleanup
|
|
483
524
|
parent_pyproject.write_text(original_content)
|
|
@@ -596,7 +637,7 @@ class TestSubfolderBuildTemporaryPyprojectCreation:
|
|
|
596
637
|
assert 'packages = ["subfolder"]' in content or '"subfolder"' in content
|
|
597
638
|
|
|
598
639
|
# Verify backup was created
|
|
599
|
-
assert (project_root / "pyproject.toml.
|
|
640
|
+
assert (project_root / "pyproject.toml.original").exists()
|
|
600
641
|
|
|
601
642
|
# Cleanup
|
|
602
643
|
config.restore()
|