python-package-folder 3.1.1__py3-none-any.whl → 3.1.3__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.
@@ -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,13 @@ class BuildManager:
270
272
  for dep in external_deps:
271
273
  self._copy_dependency(dep)
272
274
 
275
+ # For subfolder builds, fix imports
276
+ if self._is_subfolder_build() and 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)
281
+
273
282
  # For subfolder builds, extract third-party dependencies and add to pyproject.toml
274
283
  if self._is_subfolder_build() and self.subfolder_config:
275
284
  # Re-analyze all Python files (including copied dependencies) to find third-party imports
@@ -629,6 +638,258 @@ class BuildManager:
629
638
 
630
639
  return sorted(list(third_party_packages))
631
640
 
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(
736
+ self, python_files: list[Path], external_deps: list[ExternalDependency]
737
+ ) -> None:
738
+ """
739
+ Convert absolute imports to relative imports for subfolder builds.
740
+
741
+ For subfolder builds, when external dependencies are copied into the subfolder,
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`)
746
+
747
+ Args:
748
+ python_files: List of Python files in the source directory
749
+ external_deps: List of external dependencies that were copied
750
+ """
751
+ import ast
752
+ import re
753
+
754
+ # Build a set of import names that were copied
755
+ copied_import_names: set[str] = set()
756
+ for dep in external_deps:
757
+ root_module = dep.import_name.split(".")[0]
758
+ copied_import_names.add(root_module)
759
+ copied_import_names.add(dep.import_name)
760
+
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)
775
+
776
+ # Only modify files that are in the original subfolder (not the copied dependencies)
777
+ for file_path in python_files:
778
+ # Skip files that are part of copied dependencies
779
+ is_copied_file = any(file_path.is_relative_to(dep.target_path) for dep in external_deps)
780
+ if is_copied_file:
781
+ continue
782
+
783
+ # Skip if file is not in src_dir (shouldn't happen, but safety check)
784
+ if not file_path.is_relative_to(self.src_dir):
785
+ continue
786
+
787
+ try:
788
+ content = file_path.read_text(encoding="utf-8")
789
+ original_content = content
790
+ lines = content.split("\n")
791
+ modified = False
792
+
793
+ # Parse the file with AST to find imports accurately
794
+ try:
795
+ tree = ast.parse(content, filename=str(file_path))
796
+ except SyntaxError:
797
+ # Skip files with syntax errors
798
+ continue
799
+
800
+ # Track which lines need to be modified
801
+ lines_to_modify: dict[int, str] = {}
802
+
803
+ for node in ast.walk(tree):
804
+ if isinstance(node, ast.ImportFrom):
805
+ if node.module is None:
806
+ continue
807
+
808
+ # Check if this import matches a copied dependency or a local file
809
+ root_module = node.module.split(".")[0]
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:
816
+ continue
817
+
818
+ # Get the line content
819
+ line_num = node.lineno - 1 # Convert to 0-based index
820
+ if line_num < 0 or line_num >= len(lines):
821
+ continue
822
+
823
+ original_line = lines[line_num]
824
+
825
+ # Skip if already a relative import
826
+ if original_line.strip().startswith("from ."):
827
+ continue
828
+
829
+ # Convert absolute import to relative import
830
+ # from _shared.image_utils import ... -> from ._shared.image_utils import ...
831
+ new_line = re.sub(
832
+ rf"^(\s*)from\s+{re.escape(node.module)}\s+import",
833
+ rf"\1from .{node.module} import",
834
+ original_line,
835
+ )
836
+
837
+ if new_line != original_line:
838
+ lines_to_modify[line_num] = new_line
839
+ modified = True
840
+
841
+ elif isinstance(node, ast.Import):
842
+ # Handle "import X" statements
843
+ for alias in node.names:
844
+ root_module = alias.name.split(".")[0]
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:
849
+ continue
850
+
851
+ line_num = node.lineno - 1
852
+ if line_num < 0 or line_num >= len(lines):
853
+ continue
854
+
855
+ original_line = lines[line_num]
856
+
857
+ # Skip if already a relative import
858
+ if original_line.strip().startswith("import ."):
859
+ continue
860
+
861
+ # Convert "import _shared" to "from . import _shared"
862
+ # This is more complex, so we'll use a regex replacement
863
+ new_line = re.sub(
864
+ rf"^(\s*)import\s+{re.escape(alias.name)}\b",
865
+ rf"\1from . import {alias.name}",
866
+ original_line,
867
+ )
868
+
869
+ if new_line != original_line:
870
+ lines_to_modify[line_num] = new_line
871
+ modified = True
872
+
873
+ # Apply modifications
874
+ if modified:
875
+ for line_num, new_line in lines_to_modify.items():
876
+ lines[line_num] = new_line
877
+
878
+ new_content = "\n".join(lines)
879
+ # Store original content for restoration
880
+ if file_path not in self._modified_import_files:
881
+ self._modified_import_files[file_path] = original_content
882
+
883
+ # Write modified content
884
+ file_path.write_text(new_content, encoding="utf-8")
885
+ print(f"Converted imports to relative in: {file_path}")
886
+
887
+ except Exception as e:
888
+ print(
889
+ f"Warning: Could not modify imports in {file_path}: {e}",
890
+ file=sys.stderr,
891
+ )
892
+
632
893
  def _report_ambiguous_imports(self, python_files: list[Path]) -> None:
633
894
  """
634
895
  Report any ambiguous imports that couldn't be resolved.
@@ -708,6 +969,19 @@ class BuildManager:
708
969
  self.copied_files.clear()
709
970
  self.copied_dirs.clear()
710
971
 
972
+ # Restore files with modified imports
973
+ for file_path, original_content in self._modified_import_files.items():
974
+ if file_path.exists():
975
+ try:
976
+ file_path.write_text(original_content, encoding="utf-8")
977
+ print(f"Restored original imports in: {file_path}")
978
+ except Exception as e:
979
+ print(
980
+ f"Warning: Could not restore imports in {file_path}: {e}",
981
+ file=sys.stderr,
982
+ )
983
+ self._modified_import_files.clear()
984
+
711
985
  # Remove all .egg-info directories in src_dir and project_root
712
986
  self._cleanup_egg_info_dirs()
713
987
 
@@ -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.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>
@@ -2,7 +2,7 @@ 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=cmTNUDCWBIh3XZ_mShlQVG1P9NN_oe3FUBTirVtYfTQ,16709
4
4
  python_package_folder/finder.py,sha256=RPidZ7LKCFuQ_KgCFIZdHWPXsZIDor3M4C0hKeYW7EI,11799
5
- python_package_folder/manager.py,sha256=kyULmS948uW3yWUcabc2EVK_Nic3wrWMj8qCs18jj2Q,43526
5
+ python_package_folder/manager.py,sha256=Z9RPg0ZQ7jZhmEXfCzX9OrD_oiA5p2Pnm5Y9tgW3ObQ,55970
6
6
  python_package_folder/publisher.py,sha256=TSjdOvxvnWLbJCnduTK_xZBRfvsrq9kpEH-sfebeWkU,13507
7
7
  python_package_folder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  python_package_folder/python_package_folder.py,sha256=RPsqRcIy_LjzzTHdp4qdtFJ4-4xhtR_0YLIC0RlUxFo,8841
@@ -10,8 +10,8 @@ python_package_folder/subfolder_build.py,sha256=oH_KKLJIMByUZCl8y3AyohUO6Om0OvsI
10
10
  python_package_folder/types.py,sha256=3yeSRR5p_3PDKEAaehW_RJ7NwJHexOIeA08bGaT1iSY,2368
11
11
  python_package_folder/utils.py,sha256=lIkWsFKeAYAJ9TDUM99T4pUBHJVbUvCdUgkWQN-LUho,3111
12
12
  python_package_folder/version.py,sha256=kIDP6S9trEfs9gj7lBYGxrWm4RPssRla24UtlO9Jkh4,9111
13
- python_package_folder-3.1.1.dist-info/METADATA,sha256=ccaosiXpsDgxQyy6UYHLYj34BKnrosnLbvFBjh18kNY,33282
14
- python_package_folder-3.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- python_package_folder-3.1.1.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
- python_package_folder-3.1.1.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
- python_package_folder-3.1.1.dist-info/RECORD,,
13
+ python_package_folder-3.1.3.dist-info/METADATA,sha256=dkCtq6nLmfiygTwYXzgVomMejX4AfQTM1U3LYdev8zQ,33282
14
+ python_package_folder-3.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ python_package_folder-3.1.3.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
+ python_package_folder-3.1.3.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
+ python_package_folder-3.1.3.dist-info/RECORD,,