python-package-folder 8.1.0__tar.gz → 8.3.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.1.0 → python_package_folder-8.3.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/coverage.svg +2 -2
  3. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/pyproject.toml +1 -1
  4. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/finder.py +46 -29
  5. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/manager.py +41 -0
  6. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_subfolder_build.py +278 -1
  7. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.copier-answers.yml +0 -0
  8. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  9. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  10. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.cursor/rules/general.mdc +0 -0
  11. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.cursor/rules/python.mdc +0 -0
  12. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.github/workflows/ci.yml +0 -0
  13. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.github/workflows/publish.yml +0 -0
  14. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.gitignore +0 -0
  15. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/.vscode/settings.json +0 -0
  16. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/LICENSE +0 -0
  17. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/MANIFEST.in +0 -0
  18. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/Makefile +0 -0
  19. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/README.md +0 -0
  20. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/development.md +0 -0
  21. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/USAGE.md +0 -0
  26. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/installation.md +0 -0
  28. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/publishing.md +0 -0
  29. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/analyzer.py +0 -0
  32. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/publisher.py +0 -0
  33. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/py.typed +0 -0
  34. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/python_package_folder.py +0 -0
  35. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/subfolder_build.py +0 -0
  36. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/src/python_package_folder/version_calculator.py +0 -0
  40. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/conftest.py +0 -0
  41. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_utils.py +0 -0
  57. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/tests/tests.py +0 -0
  60. {python_package_folder-8.1.0 → python_package_folder-8.3.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 8.1.0
3
+ Version: 8.3.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>
@@ -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">67%</text>
18
- <text x="81" y="14">67%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">66%</text>
18
+ <text x="81" y="14">66%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.1.0"
46
+ version = "8.3.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -29,18 +29,26 @@ class ExternalDependencyFinder:
29
29
  """
30
30
 
31
31
  def __init__(
32
- self, project_root: Path, src_dir: Path, exclude_patterns: list[str] | None = None
32
+ self,
33
+ project_root: Path,
34
+ src_dir: Path,
35
+ exclude_patterns: list[str] | None = None,
36
+ original_src_dir: Path | None = None,
33
37
  ) -> None:
34
38
  """
35
39
  Initialize the dependency finder.
36
40
 
37
41
  Args:
38
42
  project_root: Root directory of the project
39
- src_dir: Source directory to analyze
43
+ src_dir: Source directory to analyze (may be temp directory for subfolder builds)
40
44
  exclude_patterns: Additional patterns to exclude (default: common sandbox patterns)
45
+ original_src_dir: Original source directory before any changes (e.g., before temp directory creation).
46
+ Used for relative path checks. If not provided, uses src_dir.
41
47
  """
42
48
  self.project_root = project_root.resolve()
43
49
  self.src_dir = src_dir.resolve()
50
+ # Store original src_dir for relative path checks (important for subfolder builds)
51
+ self.original_src_dir = (original_src_dir or src_dir).resolve()
44
52
  self.analyzer = ImportAnalyzer(project_root)
45
53
  # Patterns for directories/files to exclude (sandbox, skip, etc.)
46
54
  default_patterns = [
@@ -90,34 +98,43 @@ class ExternalDependencyFinder:
90
98
  if source_path.is_file():
91
99
  parent_dir = source_path.parent
92
100
 
93
- # Only copy parent directory if:
94
- # 1. It's a package (has __init__.py), OR
95
- # 2. Files from it are actually imported (which is the case here)
96
- # But only copy the immediate parent, not entire directory trees
97
- parent_is_package = (parent_dir / "__init__.py").exists()
98
- files_are_imported = True # Always true when processing an import
99
-
100
- # Only copy immediate parent directory, not grandparent directories
101
- # This prevents copying entire trees like models/Information_extraction
102
- # when we only need models/Information_extraction/_shared_ie
103
- should_copy_dir = (
104
- not self._should_exclude_path(parent_dir)
105
- and (
106
- parent_is_package or files_are_imported
107
- ) # Package OR files imported
108
- and not parent_dir.is_relative_to(self.src_dir)
109
- and not self.src_dir.is_relative_to(parent_dir)
110
- and parent_dir != self.project_root
111
- and parent_dir != self.project_root.parent
112
- )
113
-
114
- if should_copy_dir:
115
- # Copy the directory instead of just the file
116
- track_path = parent_dir
117
- source_path = parent_dir
118
- else:
119
- # Copy just the file
101
+ # Never copy the src/ directory itself - only copy individual files at its root
102
+ # This prevents copying the entire src/ directory (with all subdirectories)
103
+ # when only a file like _globals.py is needed
104
+ if parent_dir == self.project_root / "src":
105
+ # For files at root of src/, only copy the file, not the directory
120
106
  track_path = source_path
107
+ else:
108
+ # Only copy parent directory if:
109
+ # 1. It's a package (has __init__.py), OR
110
+ # 2. Files from it are actually imported (which is the case here)
111
+ # But only copy the immediate parent, not entire directory trees
112
+ parent_is_package = (parent_dir / "__init__.py").exists()
113
+ files_are_imported = True # Always true when processing an import
114
+
115
+ # Only copy immediate parent directory, not grandparent directories
116
+ # This prevents copying entire trees like models/Information_extraction
117
+ # when we only need models/Information_extraction/_shared_ie
118
+ # Use original_src_dir for relative path checks to correctly handle
119
+ # subfolder builds where src_dir may point to temp directory
120
+ should_copy_dir = (
121
+ not self._should_exclude_path(parent_dir)
122
+ and (
123
+ parent_is_package or files_are_imported
124
+ ) # Package OR files imported
125
+ and not parent_dir.is_relative_to(self.src_dir)
126
+ and not self.original_src_dir.is_relative_to(parent_dir)
127
+ and parent_dir != self.project_root
128
+ and parent_dir != self.project_root.parent
129
+ )
130
+
131
+ if should_copy_dir:
132
+ # Copy the directory instead of just the file
133
+ track_path = parent_dir
134
+ source_path = parent_dir
135
+ else:
136
+ # Copy just the file
137
+ track_path = source_path
121
138
  elif source_path.is_dir():
122
139
  # Don't copy directories that contain src_dir
123
140
  if self.src_dir.is_relative_to(source_path):
@@ -71,6 +71,8 @@ class BuildManager:
71
71
  src_dir = self.project_root / "src"
72
72
 
73
73
  self.src_dir = Path(src_dir).resolve()
74
+ # Store original src_dir before any changes (e.g., when temp directory is created)
75
+ self.original_src_dir = self.src_dir
74
76
 
75
77
  # Validate source directory
76
78
  if not self.src_dir.exists():
@@ -280,10 +282,12 @@ class BuildManager:
280
282
  # Update src_dir to point to temp package directory
281
283
  self.src_dir = self.subfolder_config._temp_package_dir
282
284
  # Recreate finder with updated src_dir so it calculates target paths correctly
285
+ # Pass original_src_dir for relative path checks to prevent copying entire src/ directory
283
286
  self.finder = ExternalDependencyFinder(
284
287
  self.project_root,
285
288
  self.src_dir,
286
289
  exclude_patterns=self.exclude_patterns,
290
+ original_src_dir=self.original_src_dir,
287
291
  )
288
292
  print(
289
293
  f"Using temporary package directory for build: {self.src_dir}"
@@ -773,6 +777,9 @@ class BuildManager:
773
777
  1. Imports of copied dependencies (e.g., `from _shared.image_utils` -> `from ._shared.image_utils`)
774
778
  2. Imports of local files within the subfolder (e.g., `from detect_empty_drawings_utils` -> `from .detect_empty_drawings_utils`)
775
779
 
780
+ Only imports classified as "external" or "local" are converted. Imports classified
781
+ as "ambiguous", "third_party", or "stdlib" are left unchanged.
782
+
776
783
  Args:
777
784
  python_files: List of Python files in the source directory
778
785
  external_deps: List of external dependencies that were copied
@@ -780,6 +787,9 @@ class BuildManager:
780
787
  import ast
781
788
  import re
782
789
 
790
+ # Create analyzer for classifying imports
791
+ analyzer = ImportAnalyzer(self.project_root)
792
+
783
793
  # Build a set of import names that were copied
784
794
  copied_import_names: set[str] = set()
785
795
  for dep in external_deps:
@@ -834,7 +844,22 @@ class BuildManager:
834
844
  if node.module is None:
835
845
  continue
836
846
 
847
+ # Classify the import to determine if it should be converted
848
+ import_info = ImportInfo(
849
+ module_name=node.module,
850
+ import_type="from",
851
+ line_number=node.lineno,
852
+ file_path=file_path,
853
+ )
854
+ analyzer.classify_import(import_info, self.src_dir)
855
+
856
+ # Only convert imports classified as "external" or "local"
857
+ # Skip ambiguous, third_party, and stdlib imports
858
+ if import_info.classification not in ("external", "local"):
859
+ continue
860
+
837
861
  # Check if this import matches a copied dependency or a local file
862
+ # (additional safety check, but classification takes precedence)
838
863
  root_module = node.module.split(".")[0]
839
864
  is_copied_dependency = (
840
865
  root_module in copied_import_names or node.module in copied_import_names
@@ -870,6 +895,22 @@ class BuildManager:
870
895
  elif isinstance(node, ast.Import):
871
896
  # Handle "import X" statements
872
897
  for alias in node.names:
898
+ # Classify the import to determine if it should be converted
899
+ import_info = ImportInfo(
900
+ module_name=alias.name,
901
+ import_type="import",
902
+ line_number=node.lineno,
903
+ file_path=file_path,
904
+ )
905
+ analyzer.classify_import(import_info, self.src_dir)
906
+
907
+ # Only convert imports classified as "external" or "local"
908
+ # Skip ambiguous, third_party, and stdlib imports
909
+ if import_info.classification not in ("external", "local"):
910
+ continue
911
+
912
+ # Check if this import matches a copied dependency or a local file
913
+ # (additional safety check, but classification takes precedence)
873
914
  root_module = alias.name.split(".")[0]
874
915
  is_copied_dependency = root_module in copied_import_names
875
916
  is_local_file = root_module in local_file_names
@@ -1209,6 +1209,94 @@ class TestTemporaryPackageDirectory:
1209
1209
 
1210
1210
  config.restore()
1211
1211
 
1212
+ def test_only_globals_file_copied_not_entire_src_directory(
1213
+ self, test_project_with_pyproject: Path
1214
+ ) -> None:
1215
+ """
1216
+ Test that when a subfolder imports a file from src/ root (like _globals.py),
1217
+ only that file is copied, not the entire src/ directory.
1218
+
1219
+ This is a regression test for the bug where the entire src/ directory
1220
+ (including features/, integration/, docs/, infrastructure/) was being
1221
+ copied when only _globals.py was needed.
1222
+ """
1223
+ project_root = test_project_with_pyproject
1224
+ subfolder = project_root / "subfolder"
1225
+
1226
+ # Create a file in subfolder that imports _globals
1227
+ (subfolder / "__init__.py").write_text("# Package init")
1228
+ (subfolder / "module.py").write_text(
1229
+ "from _globals import IS_TESTING\n\ndef func(): return IS_TESTING"
1230
+ )
1231
+
1232
+ # Create _globals.py at root of src/ (outside subfolder)
1233
+ src_dir = project_root / "src"
1234
+ src_dir.mkdir(exist_ok=True)
1235
+ (src_dir / "_globals.py").write_text("IS_TESTING = False")
1236
+
1237
+ # Create other directories in src/ that should NOT be copied
1238
+ (src_dir / "features").mkdir()
1239
+ (src_dir / "features" / "__init__.py").write_text("# Features")
1240
+ (src_dir / "features" / "feature.py").write_text("def feature(): pass")
1241
+
1242
+ (src_dir / "integration").mkdir()
1243
+ (src_dir / "integration" / "__init__.py").write_text("# Integration")
1244
+ (src_dir / "integration" / "integration.py").write_text("def integration(): pass")
1245
+
1246
+ (src_dir / "docs").mkdir()
1247
+ (src_dir / "docs" / "readme.md").write_text("# Docs")
1248
+
1249
+ (src_dir / "infrastructure").mkdir()
1250
+ (src_dir / "infrastructure" / "__init__.py").write_text("# Infrastructure")
1251
+
1252
+ # Build the subfolder
1253
+ manager = BuildManager(project_root=project_root, src_dir=subfolder)
1254
+
1255
+ try:
1256
+ external_deps = manager.prepare_build(version="1.0.0", package_name="my-package")
1257
+
1258
+ # Verify _globals.py was found as an external dependency
1259
+ globals_deps = [d for d in external_deps if d.source_path.name == "_globals.py"]
1260
+ assert len(globals_deps) > 0, "_globals.py should be found as an external dependency"
1261
+
1262
+ # Verify the temp package directory exists
1263
+ assert manager.subfolder_config is not None
1264
+ temp_dir = manager.subfolder_config._temp_package_dir
1265
+ assert temp_dir is not None and temp_dir.exists()
1266
+
1267
+ # Verify _globals.py was copied to temp directory
1268
+ assert (temp_dir / "_globals.py").exists(), "_globals.py should be copied to temp directory"
1269
+
1270
+ # Verify other directories from src/ were NOT copied
1271
+ assert not (temp_dir / "features").exists(), (
1272
+ "features/ directory should NOT be copied (not imported)"
1273
+ )
1274
+ assert not (temp_dir / "integration").exists(), (
1275
+ "integration/ directory should NOT be copied (not imported)"
1276
+ )
1277
+ assert not (temp_dir / "docs").exists(), (
1278
+ "docs/ directory should NOT be copied (not imported)"
1279
+ )
1280
+ assert not (temp_dir / "infrastructure").exists(), (
1281
+ "infrastructure/ directory should NOT be copied (not imported)"
1282
+ )
1283
+
1284
+ # Verify only _globals.py and subfolder contents are in temp directory
1285
+ all_items = list(temp_dir.iterdir())
1286
+ item_names = [item.name for item in all_items]
1287
+
1288
+ # Should have _globals.py, __init__.py, module.py, and possibly pyproject.toml
1289
+ # But NOT features/, integration/, docs/, infrastructure/
1290
+ unexpected_dirs = {"features", "integration", "docs", "infrastructure"}
1291
+ found_unexpected = unexpected_dirs.intersection(set(item_names))
1292
+ assert len(found_unexpected) == 0, (
1293
+ f"Found unexpected directories in temp package: {found_unexpected}. "
1294
+ f"Only _globals.py should be copied, not the entire src/ directory."
1295
+ )
1296
+
1297
+ finally:
1298
+ manager.cleanup()
1299
+
1212
1300
 
1213
1301
  class TestWheelPackaging:
1214
1302
  """Tests to verify that wheels are correctly packaged with the right directory structure."""
@@ -1821,4 +1909,193 @@ only-include = ["src/data", "pyproject.toml", "README.md"]
1821
1909
  # Installation or import failed - this is acceptable if dependencies are missing
1822
1910
  # The main verification (wheel contents) has already passed
1823
1911
  print(f"Note: Installation/import test skipped due to: {e}")
1824
- # The wheel packaging verification above is the main test
1912
+ # The wheel packaging verification above is the main test
1913
+
1914
+
1915
+ class TestImportConversion:
1916
+ """Tests to verify that import conversion respects classification."""
1917
+
1918
+ def test_third_party_imports_not_converted_to_relative(
1919
+ self, test_project_with_pyproject: Path
1920
+ ) -> None:
1921
+ """
1922
+ Test that third-party imports (like torch, torchvision) are NOT converted
1923
+ to relative imports, even if they match local file names.
1924
+ """
1925
+ project_root = test_project_with_pyproject
1926
+ subfolder = project_root / "subfolder"
1927
+
1928
+ # Create a module that imports third-party packages
1929
+ (subfolder / "__init__.py").write_text("# Package init")
1930
+ (subfolder / "module.py").write_text(
1931
+ """import torch
1932
+ import torch.utils.data
1933
+ from torchvision import datasets
1934
+ import numpy as np
1935
+ from PIL import Image
1936
+ """
1937
+ )
1938
+
1939
+ # Build the subfolder
1940
+ manager = BuildManager(project_root=project_root, src_dir=subfolder)
1941
+
1942
+ try:
1943
+ manager.prepare_build(version="1.0.0", package_name="my-package")
1944
+
1945
+ # Verify the temp package directory exists
1946
+ assert manager.subfolder_config is not None
1947
+ temp_dir = manager.subfolder_config._temp_package_dir
1948
+ assert temp_dir is not None and temp_dir.exists()
1949
+
1950
+ # Read the modified file
1951
+ modified_content = (temp_dir / "module.py").read_text(encoding="utf-8")
1952
+
1953
+ # Verify third-party imports were NOT converted to relative imports
1954
+ assert "import torch" in modified_content, (
1955
+ "torch import should remain absolute, not converted to relative"
1956
+ )
1957
+ assert "import torch.utils.data" in modified_content or "from torch.utils import data" in modified_content, (
1958
+ "torch.utils.data import should remain absolute"
1959
+ )
1960
+ assert "from torchvision import datasets" in modified_content, (
1961
+ "torchvision import should remain absolute, not converted to relative"
1962
+ )
1963
+ assert "import numpy as np" in modified_content, (
1964
+ "numpy import should remain absolute"
1965
+ )
1966
+ assert "from PIL import Image" in modified_content, (
1967
+ "PIL import should remain absolute"
1968
+ )
1969
+
1970
+ # Verify NO relative imports were added for these third-party packages
1971
+ assert "from . import torch" not in modified_content, (
1972
+ "torch should NOT be converted to relative import"
1973
+ )
1974
+ assert "from .torchvision import" not in modified_content, (
1975
+ "torchvision should NOT be converted to relative import"
1976
+ )
1977
+ assert "from . import numpy" not in modified_content, (
1978
+ "numpy should NOT be converted to relative import"
1979
+ )
1980
+
1981
+ finally:
1982
+ manager.cleanup()
1983
+
1984
+ def test_ambiguous_imports_not_converted_to_relative(
1985
+ self, test_project_with_pyproject: Path
1986
+ ) -> None:
1987
+ """
1988
+ Test that ambiguous imports (like time, math) are NOT converted
1989
+ to relative imports, even if they match local file names.
1990
+ """
1991
+ project_root = test_project_with_pyproject
1992
+ subfolder = project_root / "subfolder"
1993
+
1994
+ # Create a module that imports standard library modules
1995
+ # (which may be classified as ambiguous if not in stdlib list)
1996
+ (subfolder / "__init__.py").write_text("# Package init")
1997
+ (subfolder / "module.py").write_text(
1998
+ """import time
1999
+ import math
2000
+ from datetime import datetime
2001
+ import os
2002
+ import sys
2003
+ """
2004
+ )
2005
+
2006
+ # Build the subfolder
2007
+ manager = BuildManager(project_root=project_root, src_dir=subfolder)
2008
+
2009
+ try:
2010
+ manager.prepare_build(version="1.0.0", package_name="my-package")
2011
+
2012
+ # Verify the temp package directory exists
2013
+ assert manager.subfolder_config is not None
2014
+ temp_dir = manager.subfolder_config._temp_package_dir
2015
+ assert temp_dir is not None and temp_dir.exists()
2016
+
2017
+ # Read the modified file
2018
+ modified_content = (temp_dir / "module.py").read_text(encoding="utf-8")
2019
+
2020
+ # Verify stdlib/ambiguous imports were NOT converted to relative imports
2021
+ assert "import time" in modified_content, (
2022
+ "time import should remain absolute, not converted to relative"
2023
+ )
2024
+ assert "import math" in modified_content, (
2025
+ "math import should remain absolute, not converted to relative"
2026
+ )
2027
+ assert "from datetime import datetime" in modified_content, (
2028
+ "datetime import should remain absolute"
2029
+ )
2030
+ assert "import os" in modified_content, (
2031
+ "os import should remain absolute"
2032
+ )
2033
+ assert "import sys" in modified_content, (
2034
+ "sys import should remain absolute"
2035
+ )
2036
+
2037
+ # Verify NO relative imports were added for these stdlib modules
2038
+ assert "from . import time" not in modified_content, (
2039
+ "time should NOT be converted to relative import"
2040
+ )
2041
+ assert "from . import math" not in modified_content, (
2042
+ "math should NOT be converted to relative import"
2043
+ )
2044
+ assert "from .datetime import" not in modified_content, (
2045
+ "datetime should NOT be converted to relative import"
2046
+ )
2047
+
2048
+ finally:
2049
+ manager.cleanup()
2050
+
2051
+ def test_external_imports_are_converted_to_relative(
2052
+ self, test_project_with_pyproject: Path
2053
+ ) -> None:
2054
+ """
2055
+ Test that external imports (from copied dependencies) ARE converted
2056
+ to relative imports.
2057
+ """
2058
+ project_root = test_project_with_pyproject
2059
+ subfolder = project_root / "subfolder"
2060
+
2061
+ # Create an external dependency
2062
+ external_dir = project_root / "src" / "_shared"
2063
+ external_dir.mkdir(parents=True)
2064
+ (external_dir / "__init__.py").write_text("# External shared module")
2065
+ (external_dir / "utils.py").write_text("def helper(): return 'help'")
2066
+
2067
+ # Create a module that imports the external dependency
2068
+ (subfolder / "__init__.py").write_text("# Package init")
2069
+ (subfolder / "module.py").write_text(
2070
+ "from _shared.utils import helper\n\ndef func(): return helper()"
2071
+ )
2072
+
2073
+ # Build the subfolder
2074
+ manager = BuildManager(project_root=project_root, src_dir=subfolder)
2075
+
2076
+ try:
2077
+ external_deps = manager.prepare_build(version="1.0.0", package_name="my-package")
2078
+
2079
+ # Verify external dependency was found and copied
2080
+ assert len(external_deps) > 0, "External dependency should be found"
2081
+
2082
+ # Verify the temp package directory exists
2083
+ assert manager.subfolder_config is not None
2084
+ temp_dir = manager.subfolder_config._temp_package_dir
2085
+ assert temp_dir is not None and temp_dir.exists()
2086
+
2087
+ # Read the modified file
2088
+ modified_content = (temp_dir / "module.py").read_text(encoding="utf-8")
2089
+
2090
+ # Verify external import WAS converted to relative import
2091
+ assert "from ._shared.utils import helper" in modified_content, (
2092
+ "External import should be converted to relative import"
2093
+ )
2094
+ assert "from _shared.utils import helper" not in modified_content or (
2095
+ "from ._shared.utils import helper" in modified_content
2096
+ ), (
2097
+ "Original absolute import should be replaced with relative import"
2098
+ )
2099
+
2100
+ finally:
2101
+ manager.cleanup()