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.
- python_package_folder/manager.py +274 -0
- {python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/METADATA +1 -1
- {python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/RECORD +6 -6
- {python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/WHEEL +0 -0
- {python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/entry_points.txt +0 -0
- {python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/licenses/LICENSE +0 -0
python_package_folder/manager.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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.
|
|
14
|
-
python_package_folder-3.1.
|
|
15
|
-
python_package_folder-3.1.
|
|
16
|
-
python_package_folder-3.1.
|
|
17
|
-
python_package_folder-3.1.
|
|
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,,
|
|
File without changes
|
{python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_package_folder-3.1.1.dist-info → python_package_folder-3.1.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|