python-package-folder 3.1.1__tar.gz → 3.1.2__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 (48) hide show
  1. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/PKG-INFO +1 -1
  2. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/manager.py +160 -0
  3. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_build_with_external_deps.py +88 -0
  4. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.copier-answers.yml +0 -0
  5. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.cursor/rules/general.mdc +0 -0
  6. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.cursor/rules/python.mdc +0 -0
  7. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.github/workflows/ci.yml +0 -0
  8. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.github/workflows/publish.yml +0 -0
  9. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.gitignore +0 -0
  10. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.vscode/settings.json +0 -0
  11. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/LICENSE +0 -0
  12. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/Makefile +0 -0
  13. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/README.md +0 -0
  14. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/coverage.svg +0 -0
  15. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/development.md +0 -0
  16. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/installation.md +0 -0
  17. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/publishing.md +0 -0
  18. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/pyproject.toml +0 -0
  19. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__init__.py +0 -0
  20. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__main__.py +0 -0
  21. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/analyzer.py +0 -0
  22. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/finder.py +0 -0
  23. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/publisher.py +0 -0
  24. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/py.typed +0 -0
  25. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/python_package_folder.py +0 -0
  26. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/subfolder_build.py +0 -0
  27. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/types.py +0 -0
  28. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/utils.py +0 -0
  29. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/version.py +0 -0
  30. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/conftest.py +0 -0
  31. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/some_globals.py +0 -0
  32. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  33. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  34. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  35. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  36. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  37. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  38. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_linting.py +0 -0
  39. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_preserve_directory_structure.py +0 -0
  40. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_publisher.py +0 -0
  41. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_shared_subdirectory_imports.py +0 -0
  42. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_spreadsheet_creation_imports.py +0 -0
  43. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_subfolder_build.py +0 -0
  44. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_third_party_dependencies.py +0 -0
  45. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_utils.py +0 -0
  46. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_version_manager.py +0 -0
  47. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/tests.py +0 -0
  48. {python_package_folder-3.1.1 → python_package_folder-3.1.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 3.1.1
3
+ Version: 3.1.2
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>
@@ -88,6 +88,8 @@ class BuildManager:
88
88
  self.subfolder_config: SubfolderBuildConfig | None = None
89
89
  # Cache for package name lookups (expensive operation)
90
90
  self._packages_distributions_cache: dict[str, list[str]] | None = None
91
+ # Track files with modified imports and their original content
92
+ self._modified_import_files: dict[Path, str] = {}
91
93
 
92
94
  # Check if it's a valid Python package directory
93
95
  if not any(self.src_dir.glob("*.py")) and not (self.src_dir / "__init__.py").exists():
@@ -270,6 +272,10 @@ class BuildManager:
270
272
  for dep in external_deps:
271
273
  self._copy_dependency(dep)
272
274
 
275
+ # For subfolder builds, convert absolute imports of copied dependencies to relative imports
276
+ if self._is_subfolder_build() and external_deps:
277
+ self._convert_copied_dependency_imports_to_relative(python_files, external_deps)
278
+
273
279
  # For subfolder builds, extract third-party dependencies and add to pyproject.toml
274
280
  if self._is_subfolder_build() and self.subfolder_config:
275
281
  # Re-analyze all Python files (including copied dependencies) to find third-party imports
@@ -629,6 +635,147 @@ class BuildManager:
629
635
 
630
636
  return sorted(list(third_party_packages))
631
637
 
638
+ def _convert_copied_dependency_imports_to_relative(
639
+ self, python_files: list[Path], external_deps: list[ExternalDependency]
640
+ ) -> None:
641
+ """
642
+ Convert absolute imports of copied dependencies to relative imports.
643
+
644
+ For subfolder builds, when external dependencies are copied into the subfolder,
645
+ imports in the subfolder's files need to be converted from absolute to relative
646
+ so they work correctly when the package is installed.
647
+
648
+ Args:
649
+ python_files: List of Python files in the source directory
650
+ external_deps: List of external dependencies that were copied
651
+ """
652
+ import ast
653
+ import re
654
+
655
+ # Build a set of import names that were copied (e.g., "_shared", "_globals", "empty_drawing_detection_config")
656
+ copied_import_names: set[str] = set()
657
+ for dep in external_deps:
658
+ # Get the root module name (first part of the import)
659
+ root_module = dep.import_name.split(".")[0]
660
+ copied_import_names.add(root_module)
661
+ # Also add the full module name for nested imports
662
+ copied_import_names.add(dep.import_name)
663
+
664
+ if not copied_import_names:
665
+ return
666
+
667
+ # Only modify files that are in the original subfolder (not the copied dependencies)
668
+ for file_path in python_files:
669
+ # Skip files that are part of copied dependencies
670
+ is_copied_file = any(file_path.is_relative_to(dep.target_path) for dep in external_deps)
671
+ if is_copied_file:
672
+ continue
673
+
674
+ # Skip if file is not in src_dir (shouldn't happen, but safety check)
675
+ if not file_path.is_relative_to(self.src_dir):
676
+ continue
677
+
678
+ try:
679
+ content = file_path.read_text(encoding="utf-8")
680
+ original_content = content
681
+ lines = content.split("\n")
682
+ modified = False
683
+
684
+ # Parse the file with AST to find imports accurately
685
+ try:
686
+ tree = ast.parse(content, filename=str(file_path))
687
+ except SyntaxError:
688
+ # Skip files with syntax errors
689
+ continue
690
+
691
+ # Track which lines need to be modified
692
+ lines_to_modify: dict[int, str] = {}
693
+
694
+ for node in ast.walk(tree):
695
+ if isinstance(node, ast.ImportFrom):
696
+ if node.module is None:
697
+ continue
698
+
699
+ # Check if this import matches a copied dependency
700
+ root_module = node.module.split(".")[0]
701
+ if (
702
+ root_module not in copied_import_names
703
+ and node.module not in copied_import_names
704
+ ):
705
+ continue
706
+
707
+ # Get the line content
708
+ line_num = node.lineno - 1 # Convert to 0-based index
709
+ if line_num < 0 or line_num >= len(lines):
710
+ continue
711
+
712
+ original_line = lines[line_num]
713
+
714
+ # Skip if already a relative import
715
+ if original_line.strip().startswith("from ."):
716
+ continue
717
+
718
+ # Convert absolute import to relative import
719
+ # from _shared.image_utils import ... -> from ._shared.image_utils import ...
720
+ new_line = re.sub(
721
+ rf"^(\s*)from\s+{re.escape(node.module)}\s+import",
722
+ rf"\1from .{node.module} import",
723
+ original_line,
724
+ )
725
+
726
+ if new_line != original_line:
727
+ lines_to_modify[line_num] = new_line
728
+ modified = True
729
+
730
+ elif isinstance(node, ast.Import):
731
+ # Handle "import X" statements
732
+ for alias in node.names:
733
+ root_module = alias.name.split(".")[0]
734
+ if root_module not in copied_import_names:
735
+ continue
736
+
737
+ line_num = node.lineno - 1
738
+ if line_num < 0 or line_num >= len(lines):
739
+ continue
740
+
741
+ original_line = lines[line_num]
742
+
743
+ # Skip if already a relative import
744
+ if original_line.strip().startswith("import ."):
745
+ continue
746
+
747
+ # Convert "import _shared" to "from . import _shared"
748
+ # This is more complex, so we'll use a regex replacement
749
+ new_line = re.sub(
750
+ rf"^(\s*)import\s+{re.escape(alias.name)}\b",
751
+ rf"\1from . import {alias.name}",
752
+ original_line,
753
+ )
754
+
755
+ if new_line != original_line:
756
+ lines_to_modify[line_num] = new_line
757
+ modified = True
758
+
759
+ # Apply modifications
760
+ if modified:
761
+ for line_num, new_line in lines_to_modify.items():
762
+ lines[line_num] = new_line
763
+
764
+ new_content = "\n".join(lines)
765
+ # Store original content for restoration
766
+ if file_path not in self._modified_import_files:
767
+ self._modified_import_files[file_path] = original_content
768
+
769
+ # Write modified content
770
+ file_path.write_text(new_content, encoding="utf-8")
771
+ print(f"Converted imports to relative in: {file_path}")
772
+
773
+ except Exception as e:
774
+ print(
775
+ f"Warning: Could not modify imports in {file_path}: {e}",
776
+ file=sys.stderr,
777
+ )
778
+
632
779
  def _report_ambiguous_imports(self, python_files: list[Path]) -> None:
633
780
  """
634
781
  Report any ambiguous imports that couldn't be resolved.
@@ -708,6 +855,19 @@ class BuildManager:
708
855
  self.copied_files.clear()
709
856
  self.copied_dirs.clear()
710
857
 
858
+ # Restore files with modified imports
859
+ for file_path, original_content in self._modified_import_files.items():
860
+ if file_path.exists():
861
+ try:
862
+ file_path.write_text(original_content, encoding="utf-8")
863
+ print(f"Restored original imports in: {file_path}")
864
+ except Exception as e:
865
+ print(
866
+ f"Warning: Could not restore imports in {file_path}: {e}",
867
+ file=sys.stderr,
868
+ )
869
+ self._modified_import_files.clear()
870
+
711
871
  # Remove all .egg-info directories in src_dir and project_root
712
872
  self._cleanup_egg_info_dirs()
713
873
 
@@ -363,6 +363,94 @@ class TestBuildManager:
363
363
  assert len(manager.copied_files) == 0
364
364
  assert len(manager.copied_dirs) == 0
365
365
 
366
+ def test_convert_copied_dependency_imports_to_relative(self, tmp_path: Path) -> None:
367
+ """Test that absolute imports of copied dependencies are converted to relative imports for subfolder builds."""
368
+ project_root = tmp_path / "test_project"
369
+ project_root.mkdir()
370
+
371
+ # Create pyproject.toml
372
+ (project_root / "pyproject.toml").write_text(
373
+ """[project]
374
+ name = "test-package"
375
+ version = "0.1.0"
376
+ """
377
+ )
378
+
379
+ # Create external dependency _shared
380
+ shared_dir = project_root / "_shared"
381
+ shared_dir.mkdir()
382
+ (shared_dir / "__init__.py").write_text("")
383
+ (shared_dir / "image_utils.py").write_text("def save_PIL_image(): pass")
384
+ (shared_dir / "file_utils.py").write_text("def get_filepaths_config(): pass")
385
+
386
+ # Create external dependency _globals
387
+ (project_root / "_globals.py").write_text("is_testing = False")
388
+
389
+ # Create subfolder to build
390
+ subfolder = project_root / "src" / "integration" / "empty_drawing_detection"
391
+ subfolder.mkdir(parents=True)
392
+ (subfolder / "__init__.py").write_text("")
393
+
394
+ # Create a file in subfolder that imports copied dependencies with absolute imports
395
+ test_file = subfolder / "detect_empty_drawings.py"
396
+ original_content = """from pathlib import Path
397
+ from _shared.image_utils import save_PIL_image
398
+ from _shared.file_utils import get_filepaths_config
399
+ from _globals import is_testing
400
+
401
+ def analyze_folder():
402
+ save_PIL_image()
403
+ get_filepaths_config()
404
+ return is_testing
405
+ """
406
+ test_file.write_text(original_content)
407
+
408
+ # Create another file with different import style
409
+ test_file2 = subfolder / "config.py"
410
+ original_content2 = """import _globals
411
+
412
+ def get_config():
413
+ return _globals.is_testing
414
+ """
415
+ test_file2.write_text(original_content2)
416
+
417
+ manager = BuildManager(project_root, subfolder)
418
+
419
+ try:
420
+ # Prepare build - this should copy dependencies and convert imports
421
+ external_deps = manager.prepare_build(version="1.0.0", package_name="test-package")
422
+
423
+ # Verify dependencies were copied
424
+ assert len(external_deps) >= 2
425
+ assert (subfolder / "_shared").exists()
426
+ assert (subfolder / "_globals.py").exists()
427
+
428
+ # Verify imports were converted to relative
429
+ modified_content = test_file.read_text()
430
+ assert "from ._shared.image_utils import save_PIL_image" in modified_content
431
+ assert "from ._shared.file_utils import get_filepaths_config" in modified_content
432
+ assert "from ._globals import is_testing" in modified_content
433
+ # Verify stdlib import was not changed
434
+ assert "from pathlib import Path" in modified_content
435
+
436
+ # Verify import statement conversion
437
+ modified_content2 = test_file2.read_text()
438
+ assert "from . import _globals" in modified_content2
439
+
440
+ # Cleanup should restore original imports
441
+ manager.cleanup()
442
+
443
+ restored_content = test_file.read_text()
444
+ assert restored_content == original_content
445
+
446
+ restored_content2 = test_file2.read_text()
447
+ assert restored_content2 == original_content2
448
+
449
+ finally:
450
+ # Ensure cleanup even if test fails
451
+ if manager._modified_import_files:
452
+ manager.cleanup()
453
+
366
454
 
367
455
  class TestRealFolderStructure:
368
456
  """Tests using the real folder_structure from tests directory."""