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.
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/PKG-INFO +1 -1
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/manager.py +160 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_build_with_external_deps.py +88 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.copier-answers.yml +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.github/workflows/ci.yml +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.github/workflows/publish.yml +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.gitignore +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/.vscode/settings.json +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/LICENSE +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/Makefile +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/README.md +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/coverage.svg +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/development.md +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/installation.md +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/publishing.md +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/pyproject.toml +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/publisher.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/python_package_folder.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/subfolder_build.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/types.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/utils.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/version.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/conftest.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_linting.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_preserve_directory_structure.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_publisher.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_shared_subdirectory_imports.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_spreadsheet_creation_imports.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_subfolder_build.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_third_party_dependencies.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_utils.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_version_manager.py +0 -0
- {python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/tests.py +0 -0
- {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.
|
|
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>
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/manager.py
RENAMED
|
@@ -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
|
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_build_with_external_deps.py
RENAMED
|
@@ -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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__init__.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/__main__.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/analyzer.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/finder.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/publisher.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/types.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/utils.py
RENAMED
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/src/python_package_folder/version.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/folder_structure/some_globals.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_package_folder-3.1.1 → python_package_folder-3.1.2}/tests/test_third_party_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|