python-package-folder 8.3.0__tar.gz → 8.4.0__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 (60) hide show
  1. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/pyproject.toml +1 -1
  3. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/analyzer.py +64 -5
  4. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/manager.py +198 -22
  5. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_subfolder_build.py +1679 -226
  6. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.copier-answers.yml +0 -0
  7. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  8. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  9. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.cursor/rules/general.mdc +0 -0
  10. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.cursor/rules/python.mdc +0 -0
  11. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.github/workflows/ci.yml +0 -0
  12. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.github/workflows/publish.yml +0 -0
  13. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.gitignore +0 -0
  14. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/.vscode/settings.json +0 -0
  15. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/LICENSE +0 -0
  16. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/MANIFEST.in +0 -0
  17. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/Makefile +0 -0
  18. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/README.md +0 -0
  19. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/coverage.svg +0 -0
  20. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/development.md +0 -0
  21. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/USAGE.md +0 -0
  26. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/installation.md +0 -0
  28. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/publishing.md +0 -0
  29. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/finder.py +0 -0
  32. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/publisher.py +0 -0
  33. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/py.typed +0 -0
  34. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/python_package_folder.py +0 -0
  35. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/subfolder_build.py +0 -0
  36. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/src/python_package_folder/version_calculator.py +0 -0
  40. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/conftest.py +0 -0
  41. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_utils.py +0 -0
  57. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/tests/tests.py +0 -0
  60. {python_package_folder-8.3.0 → python_package_folder-8.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 8.3.0
3
+ Version: 8.4.0
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>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.3.0"
46
+ version = "8.4.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -396,7 +396,9 @@ class ImportAnalyzer:
396
396
  Check if a module is a third-party package.
397
397
 
398
398
  Uses importlib to find the module and checks if its location
399
- is in site-packages or dist-packages.
399
+ is in site-packages or dist-packages. Also tries to import the module
400
+ to check if it's available, which helps catch cases where packages
401
+ are installed but metadata lookup fails.
400
402
 
401
403
  Args:
402
404
  module_name: Name of the module to check
@@ -405,12 +407,69 @@ class ImportAnalyzer:
405
407
  True if the module is a third-party package, False otherwise
406
408
  """
407
409
  root_module = module_name.split(".")[0]
410
+
411
+ # Skip if already known as stdlib
412
+ stdlib_modules = self.get_stdlib_modules()
413
+ if root_module in stdlib_modules:
414
+ return False
415
+
408
416
  try:
409
417
  spec = importlib.util.find_spec(root_module)
410
418
  if spec and spec.origin:
411
419
  origin_path = Path(spec.origin)
412
- # Check if it's in site-packages
413
- return "site-packages" in str(origin_path) or "dist-packages" in str(origin_path)
414
- except (ImportError, ValueError, AttributeError):
415
- pass
420
+ origin_str = str(origin_path)
421
+
422
+ # Check if it's in site-packages or dist-packages
423
+ if "site-packages" in origin_str or "dist-packages" in origin_str:
424
+ return True
425
+
426
+ # Check if it's outside the standard library directory
427
+ # Standard library is typically in the Python installation directory
428
+ # and not in site-packages
429
+ stdlib_paths = [
430
+ "lib/python",
431
+ "Lib\\python", # Windows
432
+ "lib64/python",
433
+ "Lib64\\python", # Windows
434
+ ]
435
+ is_stdlib_path = any(stdlib_path in origin_str for stdlib_path in stdlib_paths)
436
+ if not is_stdlib_path and "site-packages" not in origin_str:
437
+ # If it's not in stdlib paths and not in site-packages,
438
+ # and it's not in the project directory, it might be third-party
439
+ # Check if it's outside the project directory
440
+ try:
441
+ origin_path.resolve().relative_to(self.project_root.resolve())
442
+ # It's in the project, so it's not third-party
443
+ return False
444
+ except ValueError:
445
+ # It's outside the project, might be third-party
446
+ # Try to import it to verify
447
+ try:
448
+ __import__(root_module)
449
+ # If import succeeds and it's outside project, likely third-party
450
+ return True
451
+ except ImportError:
452
+ pass
453
+
454
+ except (ImportError, ValueError, AttributeError, Exception):
455
+ # If importlib fails, try direct import as fallback
456
+ try:
457
+ imported_module = __import__(root_module)
458
+ # Check if the module's __file__ points to site-packages
459
+ if hasattr(imported_module, "__file__") and imported_module.__file__:
460
+ module_file = str(imported_module.__file__)
461
+ if "site-packages" in module_file or "dist-packages" in module_file:
462
+ return True
463
+ # Check if it's outside the project
464
+ try:
465
+ Path(imported_module.__file__).resolve().relative_to(
466
+ self.project_root.resolve()
467
+ )
468
+ return False # It's in the project
469
+ except ValueError:
470
+ # It's outside the project, likely third-party
471
+ return True
472
+ except ImportError:
473
+ pass
474
+
416
475
  return False
@@ -605,6 +605,51 @@ class BuildManager:
605
605
  Returns:
606
606
  List of unique third-party package names (e.g., ["pypdf", "requests", "pymupdf"])
607
607
  """
608
+ # Common third-party packages that are often misclassified as ambiguous
609
+ COMMON_THIRD_PARTY_PACKAGES = {
610
+ "torch",
611
+ "torchvision",
612
+ "numpy",
613
+ "pandas",
614
+ "matplotlib",
615
+ "scipy",
616
+ "sklearn",
617
+ "tensorflow",
618
+ "keras",
619
+ "pillow",
620
+ "PIL",
621
+ "cv2",
622
+ "opencv",
623
+ "requests",
624
+ "flask",
625
+ "django",
626
+ "pytest",
627
+ "pydantic",
628
+ "fastapi",
629
+ "sqlalchemy",
630
+ "pymongo",
631
+ "redis",
632
+ "celery",
633
+ "boto3",
634
+ "azure",
635
+ "google",
636
+ "openai",
637
+ "langchain",
638
+ "fiftyone",
639
+ "pycocotools",
640
+ "albumentations",
641
+ "loguru",
642
+ "tqdm",
643
+ "rich",
644
+ "pypdf",
645
+ "pymupdf",
646
+ "networkx",
647
+ "openpyxl",
648
+ "nltk",
649
+ "spacy",
650
+ "textdistance",
651
+ "IPython",
652
+ }
608
653
  third_party_packages: set[str] = set()
609
654
  # Cache package name lookups to avoid repeated expensive searches
610
655
  package_name_cache: dict[str, str | None] = {}
@@ -648,23 +693,34 @@ class BuildManager:
648
693
  # Fallback to using the import name
649
694
  third_party_packages.add(root_module)
650
695
  # If it's ambiguous or unresolved, and not stdlib/local/external,
651
- # only add as dependency if we can verify it's actually an installed package
696
+ # check if it's a common third-party package or can be verified
652
697
  elif imp.classification == "ambiguous" or imp.classification is None:
653
698
  # Check if it's not a local or external module
654
699
  if not imp.resolved_path:
655
- # Try to verify it's actually an installed package before adding
656
- # Check cache first
657
- if root_module not in package_name_cache:
658
- package_name_cache[root_module] = self._get_package_name_from_import(
659
- imp.module_name
660
- )
661
- actual_package = package_name_cache[root_module]
662
- # Only add if we can verify it's an actual installed package
663
- # Don't add ambiguous imports that we can't verify
664
- if actual_package:
665
- third_party_packages.add(actual_package)
666
- # If we can't verify it's a package, don't add it
667
- # (it's likely a local file that wasn't resolved properly)
700
+ # Check if it's a common third-party package
701
+ if root_module in COMMON_THIRD_PARTY_PACKAGES:
702
+ # Try to get package name, or use module name as fallback
703
+ if root_module not in package_name_cache:
704
+ package_name_cache[root_module] = self._get_package_name_from_import(
705
+ imp.module_name
706
+ )
707
+ actual_package = package_name_cache[root_module]
708
+ if actual_package:
709
+ third_party_packages.add(actual_package)
710
+ else:
711
+ # Fallback: use module name (common packages like torch, numpy)
712
+ third_party_packages.add(root_module)
713
+ else:
714
+ # For other ambiguous imports, only add if verified
715
+ if root_module not in package_name_cache:
716
+ package_name_cache[root_module] = self._get_package_name_from_import(
717
+ imp.module_name
718
+ )
719
+ actual_package = package_name_cache[root_module]
720
+ if actual_package:
721
+ third_party_packages.add(actual_package)
722
+ # If we can't verify it's a package, don't add it
723
+ # (it's likely a local file that wasn't resolved properly)
668
724
 
669
725
  if total_files > 50:
670
726
  print() # New line after progress indicator
@@ -765,6 +821,44 @@ class BuildManager:
765
821
  file=sys.stderr,
766
822
  )
767
823
 
824
+ def _calculate_relative_import_depth(
825
+ self, file_path: Path, module_path: Path, src_dir: Path
826
+ ) -> str:
827
+ """
828
+ Calculate the relative import prefix (., .., ...) based on directory depth.
829
+
830
+ Args:
831
+ file_path: Path to the file containing the import
832
+ module_path: Path to the imported module (file or directory)
833
+ src_dir: Source directory (package root)
834
+
835
+ Returns:
836
+ String with appropriate number of dots (e.g., ".", "..", "...")
837
+ """
838
+ try:
839
+ # Get relative paths from src_dir
840
+ file_rel = file_path.parent.relative_to(src_dir)
841
+ module_rel = module_path.parent.relative_to(src_dir)
842
+
843
+ # Calculate depth difference
844
+ # If paths are ".", depth is 0
845
+ file_depth = len(file_rel.parts) if file_rel.parts != (".",) else 0
846
+ module_depth = len(module_rel.parts) if module_rel.parts != (".",) else 0
847
+
848
+ depth_diff = file_depth - module_depth
849
+
850
+ if depth_diff == 0:
851
+ return "." # Same level
852
+ elif depth_diff > 0:
853
+ # File is deeper than module, need to go up
854
+ return "." * (depth_diff + 1) # Go up depth_diff levels
855
+ else:
856
+ # Module is deeper than file, use single dot
857
+ return "."
858
+ except ValueError:
859
+ # If paths are not relative to src_dir, fall back to single dot
860
+ return "."
861
+
768
862
  def _convert_imports_to_relative(
769
863
  self, python_files: list[Path], external_deps: list[ExternalDependency]
770
864
  ) -> None:
@@ -844,7 +938,24 @@ class BuildManager:
844
938
  if node.module is None:
845
939
  continue
846
940
 
847
- # Classify the import to determine if it should be converted
941
+ # Extract root module (first part before the first dot)
942
+ root_module = node.module.split(".")[0]
943
+
944
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
945
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
946
+ root_import_info = ImportInfo(
947
+ module_name=root_module,
948
+ import_type="from",
949
+ line_number=node.lineno,
950
+ file_path=file_path,
951
+ )
952
+ analyzer.classify_import(root_import_info, self.src_dir)
953
+
954
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
955
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
956
+ continue
957
+
958
+ # Now classify the full import to determine if it should be converted
848
959
  import_info = ImportInfo(
849
960
  module_name=node.module,
850
961
  import_type="from",
@@ -860,7 +971,6 @@ class BuildManager:
860
971
 
861
972
  # Check if this import matches a copied dependency or a local file
862
973
  # (additional safety check, but classification takes precedence)
863
- root_module = node.module.split(".")[0]
864
974
  is_copied_dependency = (
865
975
  root_module in copied_import_names or node.module in copied_import_names
866
976
  )
@@ -880,11 +990,36 @@ class BuildManager:
880
990
  if original_line.strip().startswith("from ."):
881
991
  continue
882
992
 
993
+ # Find the target path for the imported module
994
+ module_target_path = None
995
+ for dep in external_deps:
996
+ if dep.import_name == node.module or node.module.startswith(dep.import_name + "."):
997
+ module_target_path = dep.target_path
998
+ break
999
+
1000
+ # If not found in external deps, check if it's a local file
1001
+ if module_target_path is None:
1002
+ # Try to find it as a local file
1003
+ module_parts = node.module.split(".")
1004
+ potential_path = self.src_dir / "/".join(module_parts)
1005
+ if (potential_path / "__init__.py").exists():
1006
+ module_target_path = potential_path / "__init__.py"
1007
+ elif potential_path.with_suffix(".py").exists():
1008
+ module_target_path = potential_path.with_suffix(".py")
1009
+ else:
1010
+ # Fallback: assume same level
1011
+ module_target_path = file_path.parent
1012
+
1013
+ # Calculate the correct relative import depth
1014
+ dots = self._calculate_relative_import_depth(
1015
+ file_path, module_target_path, self.src_dir
1016
+ )
1017
+
883
1018
  # Convert absolute import to relative import
884
- # from _shared.image_utils import ... -> from ._shared.image_utils import ...
1019
+ # from _shared.image_utils import ... -> from .._shared.image_utils import ...
885
1020
  new_line = re.sub(
886
1021
  rf"^(\s*)from\s+{re.escape(node.module)}\s+import",
887
- rf"\1from .{node.module} import",
1022
+ rf"\1from {dots}{node.module} import",
888
1023
  original_line,
889
1024
  )
890
1025
 
@@ -895,7 +1030,24 @@ class BuildManager:
895
1030
  elif isinstance(node, ast.Import):
896
1031
  # Handle "import X" statements
897
1032
  for alias in node.names:
898
- # Classify the import to determine if it should be converted
1033
+ # Extract root module (first part before the first dot)
1034
+ root_module = alias.name.split(".")[0]
1035
+
1036
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
1037
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
1038
+ root_import_info = ImportInfo(
1039
+ module_name=root_module,
1040
+ import_type="import",
1041
+ line_number=node.lineno,
1042
+ file_path=file_path,
1043
+ )
1044
+ analyzer.classify_import(root_import_info, self.src_dir)
1045
+
1046
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
1047
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
1048
+ continue
1049
+
1050
+ # Now classify the full import to determine if it should be converted
899
1051
  import_info = ImportInfo(
900
1052
  module_name=alias.name,
901
1053
  import_type="import",
@@ -911,7 +1063,6 @@ class BuildManager:
911
1063
 
912
1064
  # Check if this import matches a copied dependency or a local file
913
1065
  # (additional safety check, but classification takes precedence)
914
- root_module = alias.name.split(".")[0]
915
1066
  is_copied_dependency = root_module in copied_import_names
916
1067
  is_local_file = root_module in local_file_names
917
1068
 
@@ -928,11 +1079,36 @@ class BuildManager:
928
1079
  if original_line.strip().startswith("import ."):
929
1080
  continue
930
1081
 
931
- # Convert "import _shared" to "from . import _shared"
1082
+ # Find the target path for the imported module
1083
+ module_target_path = None
1084
+ for dep in external_deps:
1085
+ if dep.import_name == alias.name or alias.name.startswith(dep.import_name + "."):
1086
+ module_target_path = dep.target_path
1087
+ break
1088
+
1089
+ # If not found in external deps, check if it's a local file
1090
+ if module_target_path is None:
1091
+ # Try to find it as a local file
1092
+ module_parts = alias.name.split(".")
1093
+ potential_path = self.src_dir / "/".join(module_parts)
1094
+ if (potential_path / "__init__.py").exists():
1095
+ module_target_path = potential_path / "__init__.py"
1096
+ elif potential_path.with_suffix(".py").exists():
1097
+ module_target_path = potential_path.with_suffix(".py")
1098
+ else:
1099
+ # Fallback: assume same level
1100
+ module_target_path = file_path.parent
1101
+
1102
+ # Calculate the correct relative import depth
1103
+ dots = self._calculate_relative_import_depth(
1104
+ file_path, module_target_path, self.src_dir
1105
+ )
1106
+
1107
+ # Convert "import _shared" to "from .. import _shared" (with correct depth)
932
1108
  # This is more complex, so we'll use a regex replacement
933
1109
  new_line = re.sub(
934
1110
  rf"^(\s*)import\s+{re.escape(alias.name)}\b",
935
- rf"\1from . import {alias.name}",
1111
+ rf"\1from {dots} import {alias.name}",
936
1112
  original_line,
937
1113
  )
938
1114