python-package-folder 3.1.2__tar.gz → 3.1.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 (48) hide show
  1. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/PKG-INFO +1 -1
  2. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/coverage.svg +2 -2
  3. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/manager.py +131 -17
  4. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_build_with_external_deps.py +30 -0
  5. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.copier-answers.yml +0 -0
  6. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.cursor/rules/general.mdc +0 -0
  7. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.cursor/rules/python.mdc +0 -0
  8. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.github/workflows/ci.yml +0 -0
  9. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.github/workflows/publish.yml +0 -0
  10. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.gitignore +0 -0
  11. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/.vscode/settings.json +0 -0
  12. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/LICENSE +0 -0
  13. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/Makefile +0 -0
  14. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/README.md +0 -0
  15. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/development.md +0 -0
  16. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/installation.md +0 -0
  17. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/publishing.md +0 -0
  18. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/pyproject.toml +0 -0
  19. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/__init__.py +0 -0
  20. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/__main__.py +0 -0
  21. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/analyzer.py +0 -0
  22. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/finder.py +0 -0
  23. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/publisher.py +0 -0
  24. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/py.typed +0 -0
  25. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/python_package_folder.py +0 -0
  26. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/subfolder_build.py +0 -0
  27. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/types.py +0 -0
  28. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/utils.py +0 -0
  29. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/src/python_package_folder/version.py +0 -0
  30. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/conftest.py +0 -0
  31. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/some_globals.py +0 -0
  32. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  33. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  34. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  35. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  36. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  37. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  38. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_linting.py +0 -0
  39. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_preserve_directory_structure.py +0 -0
  40. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_publisher.py +0 -0
  41. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_shared_subdirectory_imports.py +0 -0
  42. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_spreadsheet_creation_imports.py +0 -0
  43. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_subfolder_build.py +0 -0
  44. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_third_party_dependencies.py +0 -0
  45. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_utils.py +0 -0
  46. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/test_version_manager.py +0 -0
  47. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/tests/tests.py +0 -0
  48. {python_package_folder-3.1.2 → python_package_folder-3.1.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 3.1.2
3
+ Version: 3.1.3
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>
@@ -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">68%</text>
18
- <text x="81" y="14">68%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">69%</text>
18
+ <text x="81" y="14">69%</text>
19
19
  </g>
20
20
  </svg>
@@ -272,9 +272,12 @@ class BuildManager:
272
272
  for dep in external_deps:
273
273
  self._copy_dependency(dep)
274
274
 
275
- # For subfolder builds, convert absolute imports of copied dependencies to relative imports
275
+ # For subfolder builds, fix imports
276
276
  if self._is_subfolder_build() and external_deps:
277
- self._convert_copied_dependency_imports_to_relative(python_files, external_deps)
277
+ # Fix relative imports in copied dependency files (convert to absolute)
278
+ self._fix_relative_imports_in_copied_files(external_deps)
279
+ # Convert absolute imports of copied dependencies and local files to relative imports
280
+ self._convert_imports_to_relative(python_files, external_deps)
278
281
 
279
282
  # For subfolder builds, extract third-party dependencies and add to pyproject.toml
280
283
  if self._is_subfolder_build() and self.subfolder_config:
@@ -635,15 +638,111 @@ class BuildManager:
635
638
 
636
639
  return sorted(list(third_party_packages))
637
640
 
638
- def _convert_copied_dependency_imports_to_relative(
641
+ def _fix_relative_imports_in_copied_files(
642
+ self, external_deps: list[ExternalDependency]
643
+ ) -> None:
644
+ """
645
+ Fix relative imports in copied dependency files.
646
+
647
+ When files are copied into the subfolder, their relative imports (like
648
+ `from ._shared.shared_dataclasses import ...`) break because the file
649
+ structure has changed. Convert these to absolute imports based on the
650
+ target location.
651
+
652
+ Args:
653
+ external_deps: List of external dependencies that were copied
654
+ """
655
+ import ast
656
+ import re
657
+
658
+ # Find all Python files in copied dependencies
659
+ copied_files: list[Path] = []
660
+ for dep in external_deps:
661
+ if dep.target_path.is_file() and dep.target_path.suffix == ".py":
662
+ copied_files.append(dep.target_path)
663
+ elif dep.target_path.is_dir():
664
+ copied_files.extend(dep.target_path.rglob("*.py"))
665
+
666
+ for file_path in copied_files:
667
+ try:
668
+ content = file_path.read_text(encoding="utf-8")
669
+ original_content = content
670
+ lines = content.split("\n")
671
+ modified = False
672
+
673
+ try:
674
+ tree = ast.parse(content, filename=str(file_path))
675
+ except SyntaxError:
676
+ continue
677
+
678
+ lines_to_modify: dict[int, str] = {}
679
+
680
+ for node in ast.walk(tree):
681
+ if isinstance(node, ast.ImportFrom):
682
+ if node.module is None:
683
+ continue
684
+
685
+ # Check if this is a relative import (level > 0)
686
+ if node.level == 0:
687
+ continue
688
+
689
+ line_num = node.lineno - 1
690
+ if line_num < 0 or line_num >= len(lines):
691
+ continue
692
+
693
+ original_line = lines[line_num]
694
+
695
+ # Convert relative import to absolute based on target location
696
+ # When a file is copied to the package root, relative imports need to be absolute
697
+ # For example: from ._shared.shared_dataclasses -> from _shared.shared_dataclasses
698
+ if node.module:
699
+ # Remove the leading dots and convert to absolute
700
+ # If it was `from ._shared.shared_dataclasses`, it becomes `from _shared.shared_dataclasses`
701
+ absolute_module = node.module
702
+ new_line = re.sub(
703
+ rf"^(\s*)from\s+\.+{re.escape(node.module)}\s+import",
704
+ rf"\1from {absolute_module} import",
705
+ original_line,
706
+ )
707
+ else:
708
+ # from . import X -> from . import X (keep as relative, but at package root level)
709
+ # Actually, if we're at package root, this should work as-is
710
+ # But if the file was in a subdirectory, we need to adjust
711
+ # For now, keep it as relative import
712
+ continue
713
+
714
+ if new_line != original_line:
715
+ lines_to_modify[line_num] = new_line
716
+ modified = True
717
+
718
+ if modified:
719
+ for line_num, new_line in lines_to_modify.items():
720
+ lines[line_num] = new_line
721
+
722
+ new_content = "\n".join(lines)
723
+ if file_path not in self._modified_import_files:
724
+ self._modified_import_files[file_path] = original_content
725
+
726
+ file_path.write_text(new_content, encoding="utf-8")
727
+ print(f"Fixed relative imports in copied file: {file_path}")
728
+
729
+ except Exception as e:
730
+ print(
731
+ f"Warning: Could not fix imports in copied file {file_path}: {e}",
732
+ file=sys.stderr,
733
+ )
734
+
735
+ def _convert_imports_to_relative(
639
736
  self, python_files: list[Path], external_deps: list[ExternalDependency]
640
737
  ) -> None:
641
738
  """
642
- Convert absolute imports of copied dependencies to relative imports.
739
+ Convert absolute imports to relative imports for subfolder builds.
643
740
 
644
741
  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.
742
+ imports need to be converted from absolute to relative so they work correctly
743
+ when the package is installed. This includes:
744
+ 1. Imports of copied dependencies (e.g., `from _shared.image_utils` -> `from ._shared.image_utils`)
745
+ 2. Imports of local files within the subfolder (e.g., `from detect_empty_drawings_utils` -> `from .detect_empty_drawings_utils`)
647
746
 
648
747
  Args:
649
748
  python_files: List of Python files in the source directory
@@ -652,17 +751,27 @@ class BuildManager:
652
751
  import ast
653
752
  import re
654
753
 
655
- # Build a set of import names that were copied (e.g., "_shared", "_globals", "empty_drawing_detection_config")
754
+ # Build a set of import names that were copied
656
755
  copied_import_names: set[str] = set()
657
756
  for dep in external_deps:
658
- # Get the root module name (first part of the import)
659
757
  root_module = dep.import_name.split(".")[0]
660
758
  copied_import_names.add(root_module)
661
- # Also add the full module name for nested imports
662
759
  copied_import_names.add(dep.import_name)
663
760
 
664
- if not copied_import_names:
665
- return
761
+ # Build a set of local file names in the subfolder (excluding copied dependencies)
762
+ local_file_names: set[str] = set()
763
+ for file_path in python_files:
764
+ # Skip files that are part of copied dependencies
765
+ is_copied_file = any(file_path.is_relative_to(dep.target_path) for dep in external_deps)
766
+ if is_copied_file:
767
+ continue
768
+ if not file_path.is_relative_to(self.src_dir):
769
+ continue
770
+ # Get the module name (filename without .py extension)
771
+ if file_path.suffix == ".py":
772
+ module_name = file_path.stem
773
+ if module_name != "__init__":
774
+ local_file_names.add(module_name)
666
775
 
667
776
  # Only modify files that are in the original subfolder (not the copied dependencies)
668
777
  for file_path in python_files:
@@ -696,12 +805,14 @@ class BuildManager:
696
805
  if node.module is None:
697
806
  continue
698
807
 
699
- # Check if this import matches a copied dependency
808
+ # Check if this import matches a copied dependency or a local file
700
809
  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
- ):
810
+ is_copied_dependency = (
811
+ root_module in copied_import_names or node.module in copied_import_names
812
+ )
813
+ is_local_file = root_module in local_file_names
814
+
815
+ if not is_copied_dependency and not is_local_file:
705
816
  continue
706
817
 
707
818
  # Get the line content
@@ -731,7 +842,10 @@ class BuildManager:
731
842
  # Handle "import X" statements
732
843
  for alias in node.names:
733
844
  root_module = alias.name.split(".")[0]
734
- if root_module not in copied_import_names:
845
+ is_copied_dependency = root_module in copied_import_names
846
+ is_local_file = root_module in local_file_names
847
+
848
+ if not is_copied_dependency and not is_local_file:
735
849
  continue
736
850
 
737
851
  line_num = node.lineno - 1
@@ -391,12 +391,18 @@ version = "0.1.0"
391
391
  subfolder.mkdir(parents=True)
392
392
  (subfolder / "__init__.py").write_text("")
393
393
 
394
+ # Create a local file that will be imported
395
+ local_file = subfolder / "local_utils.py"
396
+ local_file.write_text("def local_function(): return 42")
397
+
394
398
  # Create a file in subfolder that imports copied dependencies with absolute imports
395
399
  test_file = subfolder / "detect_empty_drawings.py"
396
400
  original_content = """from pathlib import Path
397
401
  from _shared.image_utils import save_PIL_image
398
402
  from _shared.file_utils import get_filepaths_config
399
403
  from _globals import is_testing
404
+ from local_utils import local_function
405
+ from config import get_config
400
406
 
401
407
  def analyze_folder():
402
408
  save_PIL_image()
@@ -414,6 +420,18 @@ def get_config():
414
420
  """
415
421
  test_file2.write_text(original_content2)
416
422
 
423
+ # Create a copied dependency file with relative imports (simulating detect_empty_drawings_utils.py)
424
+ # This will be created when _shared is copied
425
+ # First, let's create a file in _shared that will be copied
426
+ shared_utils = shared_dir / "shared_utils.py"
427
+ shared_utils.write_text(
428
+ """from .file_utils import get_filepaths_config
429
+
430
+ def shared_function():
431
+ return get_filepaths_config()
432
+ """
433
+ )
434
+
417
435
  manager = BuildManager(project_root, subfolder)
418
436
 
419
437
  try:
@@ -430,6 +448,9 @@ def get_config():
430
448
  assert "from ._shared.image_utils import save_PIL_image" in modified_content
431
449
  assert "from ._shared.file_utils import get_filepaths_config" in modified_content
432
450
  assert "from ._globals import is_testing" in modified_content
451
+ # Verify local file imports were converted to relative
452
+ assert "from .local_utils import local_function" in modified_content
453
+ assert "from .config import get_config" in modified_content
433
454
  # Verify stdlib import was not changed
434
455
  assert "from pathlib import Path" in modified_content
435
456
 
@@ -437,6 +458,15 @@ def get_config():
437
458
  modified_content2 = test_file2.read_text()
438
459
  assert "from . import _globals" in modified_content2
439
460
 
461
+ # Verify relative imports in copied files were fixed
462
+ if (subfolder / "_shared" / "shared_utils.py").exists():
463
+ shared_utils_content = (subfolder / "_shared" / "shared_utils.py").read_text()
464
+ # The relative import should be converted to absolute
465
+ assert (
466
+ "from .file_utils import" not in shared_utils_content
467
+ or "from file_utils import" in shared_utils_content
468
+ )
469
+
440
470
  # Cleanup should restore original imports
441
471
  manager.cleanup()
442
472