python-package-folder 2.0.7__tar.gz → 3.0.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 (48) hide show
  1. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/PKG-INFO +1 -1
  2. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/finder.py +61 -23
  3. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_build_with_external_deps.py +15 -9
  4. python_package_folder-3.0.0/tests/test_preserve_directory_structure.py +177 -0
  5. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_spreadsheet_creation_imports.py +6 -3
  6. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.copier-answers.yml +0 -0
  7. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.cursor/rules/general.mdc +0 -0
  8. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.cursor/rules/python.mdc +0 -0
  9. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.github/workflows/ci.yml +0 -0
  10. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.github/workflows/publish.yml +0 -0
  11. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.gitignore +0 -0
  12. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/.vscode/settings.json +0 -0
  13. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/LICENSE +0 -0
  14. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/Makefile +0 -0
  15. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/README.md +0 -0
  16. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/coverage.svg +0 -0
  17. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/development.md +0 -0
  18. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/installation.md +0 -0
  19. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/publishing.md +0 -0
  20. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/pyproject.toml +0 -0
  21. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/__init__.py +0 -0
  22. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/__main__.py +0 -0
  23. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/analyzer.py +0 -0
  24. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/manager.py +0 -0
  25. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/publisher.py +0 -0
  26. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/py.typed +0 -0
  27. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/python_package_folder.py +0 -0
  28. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/subfolder_build.py +0 -0
  29. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/types.py +0 -0
  30. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/utils.py +0 -0
  31. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/src/python_package_folder/version.py +0 -0
  32. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/conftest.py +0 -0
  33. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/some_globals.py +0 -0
  34. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  35. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  36. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  37. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  38. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  39. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  40. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_linting.py +0 -0
  41. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_publisher.py +0 -0
  42. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_shared_subdirectory_imports.py +0 -0
  43. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_subfolder_build.py +0 -0
  44. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_third_party_dependencies.py +0 -0
  45. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_utils.py +0 -0
  46. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/test_version_manager.py +0 -0
  47. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/tests/tests.py +0 -0
  48. {python_package_folder-2.0.7 → python_package_folder-3.0.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 2.0.7
3
+ Version: 3.0.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>
@@ -151,8 +151,8 @@ class ExternalDependencyFinder:
151
151
  """
152
152
  Determine where an external file should be copied within src_dir.
153
153
 
154
- For files, attempts to maintain the module structure. For directories,
155
- places them directly in src_dir with their original name.
154
+ Preserves the original directory structure relative to project_root or src
155
+ to maintain import paths like `from models. ...` or `from data. ...`.
156
156
 
157
157
  Args:
158
158
  source_path: Path to the source file or directory
@@ -164,31 +164,69 @@ class ExternalDependencyFinder:
164
164
  if not source_path.exists():
165
165
  return None
166
166
 
167
- # Always create target within src_dir
168
167
  module_parts = module_name.split(".")
169
168
 
170
- if source_path.is_file():
171
- # For a file, create the directory structure based on module name
172
- if len(module_parts) > 1:
173
- # It's a submodule, create the directory structure
174
- target = self.src_dir / "/".join(module_parts[:-1]) / source_path.name
169
+ # Determine the base directory for calculating relative paths
170
+ # If source is under src/, use src/ as base to preserve structure
171
+ src_base = self.project_root / "src"
172
+ if src_base.exists() and source_path.is_relative_to(src_base):
173
+ # Source is under src/, preserve the path structure relative to src/
174
+ # This ensures imports like `from models. ...` work correctly
175
+ try:
176
+ relative_path = source_path.relative_to(src_base)
177
+ target = self.src_dir / relative_path
178
+ return target
179
+ except ValueError:
180
+ pass
181
+
182
+ # For sources not under src/, try to preserve structure based on module name
183
+ # Check if the module name structure matches the source path structure
184
+ if len(module_parts) > 1:
185
+ # For directories, check if the module path (excluding the last part for files) matches
186
+ if source_path.is_dir():
187
+ # For directories, check if module parts match the directory structure
188
+ # e.g., module "folder_structure.utility_folder.some_utility" with dir "folder_structure/utility_folder"
189
+ # should preserve "folder_structure/utility_folder" structure
190
+ # Calculate relative path from project_root
191
+ try:
192
+ relative_path = source_path.relative_to(self.project_root)
193
+ # If the module starts with the same parts as the relative path, preserve structure
194
+ relative_parts = list(relative_path.parts)
195
+ # Check if module_parts[:-1] (excluding the file/module name) matches relative_parts
196
+ if len(module_parts) > len(relative_parts):
197
+ # Module has more parts (includes the file name), check if the directory parts match
198
+ module_dir_parts = module_parts[: len(relative_parts)]
199
+ if module_dir_parts == relative_parts:
200
+ # Preserve the full structure
201
+ target = self.src_dir / relative_path
202
+ return target
203
+ # Try matching from the end
204
+ if len(relative_parts) <= len(module_parts):
205
+ # Check if the last parts of module match the relative path
206
+ for i in range(1, min(len(relative_parts), len(module_parts)) + 1):
207
+ if module_parts[-i:] == relative_parts[-i:]:
208
+ # Match found, preserve structure based on module name
209
+ target = self.src_dir / "/".join(
210
+ module_parts[: -i + 1 if i > 1 else len(module_parts)]
211
+ )
212
+ return target
213
+ except ValueError:
214
+ pass
215
+ # Fallback: preserve structure based on module name (excluding last part for files)
216
+ target = self.src_dir / "/".join(module_parts[:-1])
217
+ return target
175
218
  else:
176
- # Top-level module - try to find the main package directory
177
- # or create a matching structure
178
- main_pkg = self._find_main_package()
179
- if main_pkg:
180
- target = main_pkg / source_path.name
181
- else:
182
- target = self.src_dir / source_path.name
183
- return target
184
-
185
- # If it's a directory, copy the whole directory
186
- if source_path.is_dir():
187
- # Use the directory name directly in src_dir
188
- target = self.src_dir / source_path.name
189
- return target
219
+ # For files, preserve structure based on module name (excluding filename)
220
+ target = self.src_dir / "/".join(module_parts[:-1]) / source_path.name
221
+ return target
190
222
 
191
- return None
223
+ # Simple top-level import - copy directly to src_dir
224
+ # This handles cases like `from some_globals import ...`
225
+ if source_path.is_file():
226
+ target = self.src_dir / source_path.name
227
+ else:
228
+ target = self.src_dir / source_path.name
229
+ return target
192
230
 
193
231
  def _should_exclude_path(self, path: Path) -> bool:
194
232
  """
@@ -219,11 +219,15 @@ class TestBuildManager:
219
219
  assert len(external_deps) >= 2
220
220
 
221
221
  # Check that files were copied
222
+ # some_globals is a simple import, so it should be copied directly
222
223
  copied_some_globals = src_dir / "some_globals.py"
223
224
  assert copied_some_globals.exists()
224
225
 
225
- copied_utility = src_dir / "utility_folder"
226
- assert copied_utility.exists()
226
+ # utility_folder is imported as folder_structure.utility_folder, so structure should be preserved
227
+ copied_utility = src_dir / "folder_structure" / "utility_folder"
228
+ assert copied_utility.exists(), (
229
+ f"utility_folder should be copied with structure at {copied_utility}"
230
+ )
227
231
  assert (copied_utility / "some_utility.py").exists()
228
232
 
229
233
  def test_prepare_build_idempotent(self, test_project_root: Path) -> None:
@@ -265,7 +269,8 @@ class TestBuildManager:
265
269
 
266
270
  # Verify files were copied
267
271
  copied_file = src_dir / "some_globals.py"
268
- copied_dir = src_dir / "utility_folder"
272
+ # utility_folder is imported as folder_structure.utility_folder, so structure should be preserved
273
+ copied_dir = src_dir / "folder_structure" / "utility_folder"
269
274
  assert copied_file.exists()
270
275
  assert copied_dir.exists()
271
276
 
@@ -481,9 +486,9 @@ class TestExclusionPatterns:
481
486
 
482
487
  manager.prepare_build()
483
488
 
484
- # Verify utility_folder was copied
485
- copied_utility = src_dir / "utility_folder"
486
- assert copied_utility.exists(), "utility_folder should be copied"
489
+ # Verify utility_folder was copied (with structure preserved)
490
+ copied_utility = src_dir / "folder_structure" / "utility_folder"
491
+ assert copied_utility.exists(), "utility_folder should be copied with structure"
487
492
  assert (copied_utility / "some_utility.py").exists(), "some_utility.py should be copied"
488
493
 
489
494
  # Verify _SS directory was NOT copied
@@ -631,9 +636,10 @@ from some_globals import SOME_GLOBAL_VARIABLE
631
636
 
632
637
  manager.prepare_build()
633
638
 
634
- # Verify nested_package was copied
635
- copied_nested = src_dir / "nested_package"
636
- assert copied_nested.exists(), "nested_package should be copied"
639
+ # Verify nested_package was copied (with structure preserved)
640
+ # Import is "folder_structure.nested_package.module", so structure should be preserved
641
+ copied_nested = src_dir / "folder_structure" / "nested_package"
642
+ assert copied_nested.exists(), "nested_package should be copied with structure"
637
643
  assert (copied_nested / "module.py").exists(), "module.py should be copied"
638
644
 
639
645
  # Verify nested _SS directory was NOT copied
@@ -0,0 +1,177 @@
1
+ """Tests for preserving directory structure when copying external dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from python_package_folder import BuildManager, ExternalDependencyFinder
10
+
11
+
12
+ @pytest.fixture
13
+ def test_project_with_models_structure(tmp_path: Path) -> Path:
14
+ """Create a test project with models/ structure under src/."""
15
+ project_root = tmp_path / "test_project"
16
+ project_root.mkdir()
17
+
18
+ # Create src directory
19
+ src_dir = project_root / "src"
20
+ src_dir.mkdir()
21
+
22
+ # Create models/Information_extraction/_shared_ie/ structure
23
+ models_dir = src_dir / "models"
24
+ models_dir.mkdir()
25
+ info_extraction_dir = models_dir / "Information_extraction"
26
+ info_extraction_dir.mkdir()
27
+ shared_ie_dir = info_extraction_dir / "_shared_ie"
28
+ shared_ie_dir.mkdir()
29
+ (shared_ie_dir / "__init__.py").write_text("")
30
+ (shared_ie_dir / "ie_enums.py").write_text(
31
+ """from enum import Enum
32
+
33
+ class TitleblockPlacement(Enum):
34
+ TOP = "top"
35
+ BOTTOM = "bottom"
36
+
37
+ class EmptyDrawingLikelihood(Enum):
38
+ HIGH = "high"
39
+ LOW = "low"
40
+ """
41
+ )
42
+
43
+ # Create data/spreadsheet_creation/ structure
44
+ data_dir = src_dir / "data"
45
+ data_dir.mkdir()
46
+ spreadsheet_creation_dir = data_dir / "spreadsheet_creation"
47
+ spreadsheet_creation_dir.mkdir()
48
+ (spreadsheet_creation_dir / "spreadsheet_formatting_dataclasses.py").write_text(
49
+ """class DataClass:
50
+ pass
51
+ """
52
+ )
53
+
54
+ # Create subfolder_to_build
55
+ subfolder = src_dir / "integration" / "empty_drawing_detection"
56
+ subfolder.mkdir(parents=True)
57
+ (subfolder / "detect_empty_drawings.py").write_text(
58
+ """from models.Information_extraction._shared_ie.ie_enums import (
59
+ TitleblockPlacement,
60
+ EmptyDrawingLikelihood,
61
+ )
62
+ from data.spreadsheet_creation.spreadsheet_formatting_dataclasses import DataClass
63
+
64
+ def analyze_folder():
65
+ return TitleblockPlacement.TOP, EmptyDrawingLikelihood.HIGH, DataClass()
66
+ """
67
+ )
68
+
69
+ return project_root
70
+
71
+
72
+ class TestPreserveDirectoryStructure:
73
+ """Tests for preserving directory structure when copying external dependencies."""
74
+
75
+ def test_models_structure_preserved(self, test_project_with_models_structure: Path) -> None:
76
+ """Test that models/ structure is preserved when copying."""
77
+ project_root = test_project_with_models_structure
78
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
79
+
80
+ finder = ExternalDependencyFinder(project_root, src_dir)
81
+
82
+ # Test target path for models file
83
+ source_file = (
84
+ project_root
85
+ / "src"
86
+ / "models"
87
+ / "Information_extraction"
88
+ / "_shared_ie"
89
+ / "ie_enums.py"
90
+ )
91
+ target = finder._determine_target_path(
92
+ source_file, "models.Information_extraction._shared_ie.ie_enums"
93
+ )
94
+
95
+ assert target is not None
96
+ assert target.is_relative_to(src_dir)
97
+ # Should preserve the structure: models/Information_extraction/_shared_ie/ie_enums.py
98
+ assert "models" in str(target)
99
+ assert "Information_extraction" in str(target)
100
+ assert "_shared_ie" in str(target)
101
+ assert target.name == "ie_enums.py"
102
+ # Full path should be: src/integration/empty_drawing_detection/models/Information_extraction/_shared_ie/ie_enums.py
103
+ expected_parts = ["models", "Information_extraction", "_shared_ie", "ie_enums.py"]
104
+ for part in expected_parts:
105
+ assert part in str(target)
106
+
107
+ def test_data_structure_preserved(self, test_project_with_models_structure: Path) -> None:
108
+ """Test that data/ structure is preserved when copying."""
109
+ project_root = test_project_with_models_structure
110
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
111
+
112
+ finder = ExternalDependencyFinder(project_root, src_dir)
113
+
114
+ # Test target path for data file
115
+ source_file = (
116
+ project_root
117
+ / "src"
118
+ / "data"
119
+ / "spreadsheet_creation"
120
+ / "spreadsheet_formatting_dataclasses.py"
121
+ )
122
+ target = finder._determine_target_path(
123
+ source_file, "data.spreadsheet_creation.spreadsheet_formatting_dataclasses"
124
+ )
125
+
126
+ assert target is not None
127
+ assert target.is_relative_to(src_dir)
128
+ # Should preserve the structure: data/spreadsheet_creation/spreadsheet_formatting_dataclasses.py
129
+ assert "data" in str(target)
130
+ assert "spreadsheet_creation" in str(target)
131
+ assert target.name == "spreadsheet_formatting_dataclasses.py"
132
+
133
+ def test_build_manager_preserves_structure(
134
+ self, test_project_with_models_structure: Path
135
+ ) -> None:
136
+ """Test that BuildManager preserves directory structure when copying."""
137
+ project_root = test_project_with_models_structure
138
+ src_dir = project_root / "src" / "integration" / "empty_drawing_detection"
139
+
140
+ # Create pyproject.toml for the test
141
+ (project_root / "pyproject.toml").write_text(
142
+ """[project]
143
+ name = "test-package"
144
+ version = "0.1.0"
145
+
146
+ [tool.hatch.build.targets.wheel]
147
+ packages = ["src/test_package"]
148
+ """
149
+ )
150
+
151
+ manager = BuildManager(project_root, src_dir)
152
+
153
+ # Prepare build
154
+ external_deps = manager.prepare_build(version="1.0.0", package_name="test-package")
155
+
156
+ # Should find models and data as external dependencies
157
+ models_deps = [dep for dep in external_deps if "models" in dep.import_name.lower()]
158
+ data_deps = [dep for dep in external_deps if "data" in dep.import_name.lower()]
159
+
160
+ assert len(models_deps) > 0, "models dependencies should be found"
161
+ assert len(data_deps) > 0, "data dependencies should be found"
162
+
163
+ # Verify models structure was copied with full path
164
+ models_path = src_dir / "models" / "Information_extraction" / "_shared_ie" / "ie_enums.py"
165
+ assert models_path.exists(), (
166
+ "models/Information_extraction/_shared_ie/ie_enums.py should be copied with full structure"
167
+ )
168
+
169
+ # Verify data structure was copied with full path
170
+ data_path = (
171
+ src_dir / "data" / "spreadsheet_creation" / "spreadsheet_formatting_dataclasses.py"
172
+ )
173
+ assert data_path.exists(), (
174
+ "data/spreadsheet_creation/spreadsheet_formatting_dataclasses.py should be copied with full structure"
175
+ )
176
+
177
+ manager.cleanup()
@@ -137,9 +137,12 @@ packages = ["src/test_package"]
137
137
  "spreadsheet modules should be found as external dependencies"
138
138
  )
139
139
 
140
- # Verify spreadsheet_creation directory was copied
141
- copied_dir = src_dir / "spreadsheet_creation"
142
- assert copied_dir.exists(), "spreadsheet_creation directory should be copied"
140
+ # Verify spreadsheet_creation directory was copied with full structure preserved
141
+ # Import is "data.spreadsheet_creation.spreadsheet_formatting_dataclasses", so structure should be preserved
142
+ copied_dir = src_dir / "data" / "spreadsheet_creation"
143
+ assert copied_dir.exists(), (
144
+ f"spreadsheet_creation directory should be copied with structure at {copied_dir}"
145
+ )
143
146
  assert (copied_dir / "spreadsheet_formatting_dataclasses.py").exists(), (
144
147
  "spreadsheet_formatting_dataclasses.py should be copied"
145
148
  )