python-package-folder 1.2.2__tar.gz → 1.2.3__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.
Files changed (43) hide show
  1. python_package_folder-1.2.3/PKG-INFO +23 -0
  2. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/coverage.svg +2 -2
  3. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/manager.py +75 -2
  4. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/subfolder_build.py +59 -24
  5. python_package_folder-1.2.3/tests/folder_structure/subfolder_to_build/README.md +3 -0
  6. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_build_with_external_deps.py +30 -0
  7. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_subfolder_build.py +48 -7
  8. python_package_folder-1.2.2/PKG-INFO +0 -881
  9. python_package_folder-1.2.2/README.md +0 -861
  10. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.copier-answers.yml +0 -0
  11. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.cursor/rules/general.mdc +0 -0
  12. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.cursor/rules/python.mdc +0 -0
  13. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.github/workflows/ci.yml +0 -0
  14. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.github/workflows/publish.yml +0 -0
  15. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.gitignore +0 -0
  16. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/.vscode/settings.json +0 -0
  17. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/LICENSE +0 -0
  18. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/Makefile +0 -0
  19. {python_package_folder-1.2.2/tests/folder_structure/subfolder_to_build → python_package_folder-1.2.3}/README.md +0 -0
  20. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/development.md +0 -0
  21. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/installation.md +0 -0
  22. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/publishing.md +0 -0
  23. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/pyproject.toml +0 -0
  24. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/__init__.py +0 -0
  25. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/__main__.py +0 -0
  26. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/analyzer.py +0 -0
  27. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/finder.py +0 -0
  28. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/publisher.py +0 -0
  29. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/py.typed +0 -0
  30. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/python_package_folder.py +0 -0
  31. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/types.py +0 -0
  32. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/utils.py +0 -0
  33. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/src/python_package_folder/version.py +0 -0
  34. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/folder_structure/some_globals.py +0 -0
  35. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  36. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  37. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  38. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_linting.py +0 -0
  39. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_publisher.py +0 -0
  40. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_utils.py +0 -0
  41. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/test_version_manager.py +0 -0
  42. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/tests/tests.py +0 -0
  43. {python_package_folder-1.2.2 → python_package_folder-1.2.3}/uv.lock +0 -0
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-package-folder
3
+ Version: 1.2.3
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">67%</text>
18
- <text x="81" y="14">67%</text>
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>
@@ -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. It handles errors gracefully and clears the internal tracking lists.
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 and removes copied files
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],
@@ -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
- # Backup the original project root pyproject.toml if it exists
238
+ # Store reference to original project root pyproject.toml
232
239
  original_pyproject = self.project_root / "pyproject.toml"
233
- if original_pyproject.exists():
234
- backup_path = self.project_root / "pyproject.toml.backup"
235
- shutil.copy2(original_pyproject, backup_path)
236
- self.original_pyproject_backup = backup_path
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 project root
244
- original_pyproject.write_text(adjusted_content, encoding="utf-8")
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
- # Create a backup
274
- backup_path = self.project_root / "pyproject.toml.backup"
275
- shutil.copy2(original_pyproject, backup_path)
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
- original_pyproject.write_text(modified_content, encoding="utf-8")
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
- If a subfolder pyproject.toml was used, it restores the original project root
589
- pyproject.toml from backup. If a temporary pyproject.toml was created, it
590
- restores the original as well.
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.temp_pyproject
635
- and self.original_pyproject_backup
636
- and self.original_pyproject_backup.exists()
637
- ):
638
- original_pyproject = self.project_root / "pyproject.toml"
639
- shutil.copy2(self.original_pyproject_backup, original_pyproject)
640
- self.original_pyproject_backup.unlink()
641
- self.original_pyproject_backup = None
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:
@@ -0,0 +1,3 @@
1
+ # Example subfolder
2
+
3
+ Example subfolder built and published with https://github.com/alelom/python-package-folder.
@@ -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"
@@ -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.backup").exists()
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 backup was created
409
- assert (project_root / "pyproject.toml.backup").exists()
410
- backup_content = (project_root / "pyproject.toml.backup").read_text()
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.backup").exists()
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.backup").exists()
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.backup").exists()
640
+ assert (project_root / "pyproject.toml.original").exists()
600
641
 
601
642
  # Cleanup
602
643
  config.restore()