python-package-folder 8.0.0__tar.gz → 8.2.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.0.0 → python_package_folder-8.2.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/coverage.svg +2 -2
  3. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/pyproject.toml +1 -1
  4. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/finder.py +46 -29
  5. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/manager.py +19 -3
  6. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/subfolder_build.py +209 -1
  7. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_subfolder_build.py +167 -0
  8. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.copier-answers.yml +0 -0
  9. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  10. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  11. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.cursor/rules/general.mdc +0 -0
  12. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.cursor/rules/python.mdc +0 -0
  13. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.github/workflows/ci.yml +0 -0
  14. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.github/workflows/publish.yml +0 -0
  15. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.gitignore +0 -0
  16. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/.vscode/settings.json +0 -0
  17. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/LICENSE +0 -0
  18. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/MANIFEST.in +0 -0
  19. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/Makefile +0 -0
  20. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/README.md +0 -0
  21. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/development.md +0 -0
  22. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/DEVELOPMENT.md +0 -0
  23. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/INSTALLATION.md +0 -0
  24. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/PUBLISHING.md +0 -0
  25. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/REFERENCE.md +0 -0
  26. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/USAGE.md +0 -0
  27. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/docs/VERSION_RESOLUTION.md +0 -0
  28. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/installation.md +0 -0
  29. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/publishing.md +0 -0
  30. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/__init__.py +0 -0
  31. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/__main__.py +0 -0
  32. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/analyzer.py +0 -0
  33. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/publisher.py +0 -0
  34. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/py.typed +0 -0
  35. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/python_package_folder.py +0 -0
  36. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/src/python_package_folder/version_calculator.py +0 -0
  40. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/conftest.py +0 -0
  41. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_utils.py +0 -0
  57. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/tests/tests.py +0 -0
  60. {python_package_folder-8.0.0 → python_package_folder-8.2.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 8.0.0
3
+ Version: 8.2.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">66%</text>
18
- <text x="81" y="14">66%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
+ <text x="81" y="14">67%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.0.0"
46
+ version = "8.2.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}"
@@ -1153,12 +1157,24 @@ class BuildManager:
1153
1157
  f"Temporary package directory does not exist: {temp_dir}. "
1154
1158
  "This should have been created during prepare_build()."
1155
1159
  )
1160
+ # Debug: List all files in temp directory
1161
+ all_files = list(temp_dir.rglob("*"))
1162
+ all_files = [f for f in all_files if f.is_file()]
1163
+ print(
1164
+ f"DEBUG: Temp package directory {temp_dir} contains {len(all_files)} files: "
1165
+ f"{[str(f.relative_to(temp_dir)) for f in all_files[:10]]}",
1166
+ file=sys.stderr,
1167
+ )
1156
1168
  # Verify it contains Python files
1157
1169
  py_files = list(temp_dir.glob("*.py"))
1158
1170
  if not py_files:
1159
- raise RuntimeError(
1160
- f"Temporary package directory exists but contains no Python files: {temp_dir}"
1161
- )
1171
+ # Also check recursively
1172
+ py_files = list(temp_dir.rglob("*.py"))
1173
+ if not py_files:
1174
+ raise RuntimeError(
1175
+ f"Temporary package directory exists but contains no Python files: {temp_dir}. "
1176
+ f"Total files found: {len(all_files)}"
1177
+ )
1162
1178
  # Verify __init__.py exists
1163
1179
  if not (temp_dir / "__init__.py").exists():
1164
1180
  raise RuntimeError(
@@ -134,6 +134,7 @@ class SubfolderBuildConfig:
134
134
  This way, hatchling will install it with the correct name without needing force-include.
135
135
  """
136
136
  if not self.package_name:
137
+ print("DEBUG: No package_name provided, skipping temp package directory creation", file=sys.stderr)
137
138
  return
138
139
 
139
140
  # Convert package name (with hyphens) to import name (with underscores)
@@ -144,13 +145,21 @@ class SubfolderBuildConfig:
144
145
  # This way, hatchling will install it with the correct name
145
146
  import_name_dir = self.project_root / import_name
146
147
 
148
+ print(
149
+ f"DEBUG: Creating temporary package directory: {import_name_dir} "
150
+ f"(from src_dir: {self.src_dir}, import name: {import_name})",
151
+ file=sys.stderr,
152
+ )
153
+
147
154
  # Check if the directory already exists and is the correct one
148
155
  if import_name_dir.exists() and import_name_dir == self._temp_package_dir:
149
156
  # Directory already exists and is the correct one, no need to recreate
157
+ print(f"DEBUG: Temporary package directory already exists: {import_name_dir}", file=sys.stderr)
150
158
  return
151
159
 
152
160
  # Remove if it already exists (from a previous build)
153
161
  if import_name_dir.exists():
162
+ print(f"DEBUG: Removing existing temporary package directory: {import_name_dir}", file=sys.stderr)
154
163
  shutil.rmtree(import_name_dir)
155
164
 
156
165
  # Copy the entire source directory contents directly to the import name directory
@@ -171,9 +180,38 @@ class SubfolderBuildConfig:
171
180
  self._temp_package_dir = None
172
181
  return
173
182
 
183
+ # Get exclude patterns from parent pyproject.toml
184
+ exclude_patterns = []
185
+ original_pyproject = self.project_root / "pyproject.toml"
186
+ if original_pyproject.exists():
187
+ exclude_patterns = read_exclude_patterns(original_pyproject)
188
+ print(
189
+ f"DEBUG: Using exclude patterns for temp directory copy: {exclude_patterns}",
190
+ file=sys.stderr,
191
+ )
192
+
193
+ # Check if src_dir has any files before copying
194
+ src_files = list(self.src_dir.rglob("*"))
195
+ src_files = [f for f in src_files if f.is_file()]
196
+ print(
197
+ f"DEBUG: Source directory {self.src_dir} contains {len(src_files)} files before copy",
198
+ file=sys.stderr,
199
+ )
200
+
201
+ # Use a copy method that respects exclude patterns and handles missing directories
174
202
  try:
175
- shutil.copytree(self.src_dir, import_name_dir)
203
+ print(f"DEBUG: Starting copy from {self.src_dir} to {import_name_dir}", file=sys.stderr)
204
+ self._copytree_excluding_patterns(self.src_dir, import_name_dir, exclude_patterns)
176
205
  self._temp_package_dir = import_name_dir
206
+
207
+ # Verify files were copied
208
+ copied_files = list(import_name_dir.rglob("*"))
209
+ copied_files = [f for f in copied_files if f.is_file()]
210
+ print(
211
+ f"DEBUG: After copy, temp directory {import_name_dir} contains {len(copied_files)} files",
212
+ file=sys.stderr,
213
+ )
214
+
177
215
  print(
178
216
  f"Created temporary package directory: {import_name_dir} "
179
217
  f"(import name: {import_name})"
@@ -183,8 +221,141 @@ class SubfolderBuildConfig:
183
221
  f"Warning: Could not create temporary package directory: {e}",
184
222
  file=sys.stderr,
185
223
  )
224
+ import traceback
225
+ print(f"DEBUG: Traceback: {traceback.format_exc()}", file=sys.stderr)
186
226
  # Fall back to using src_dir directly
187
227
  self._temp_package_dir = None
228
+
229
+ def _copytree_excluding_patterns(self, src: Path, dst: Path, exclude_patterns: list[str]) -> None:
230
+ """
231
+ Copy a directory tree, excluding certain patterns and handling missing directories gracefully.
232
+
233
+ This is similar to BuildManager._copytree_excluding but works without needing
234
+ the BuildManager instance. It respects exclude patterns and skips missing directories
235
+ (e.g., broken symlinks or already-excluded directories).
236
+
237
+ Args:
238
+ src: Source directory
239
+ dst: Destination directory
240
+ exclude_patterns: List of patterns to exclude (e.g., ['_SS', '__SS', '.*_test.*'])
241
+ """
242
+ default_patterns = [
243
+ "_SS",
244
+ "__SS",
245
+ "_sandbox",
246
+ "__sandbox",
247
+ "_skip",
248
+ "__skip",
249
+ "_test",
250
+ "__test__",
251
+ ]
252
+ all_exclude_patterns = default_patterns + exclude_patterns
253
+
254
+ def should_exclude(path: Path) -> bool:
255
+ """Check if a path should be excluded."""
256
+ import re
257
+ # Only check parts of the path relative to src_dir, not the entire absolute path
258
+ # This prevents matching test directory names or other parts outside the source
259
+ try:
260
+ rel_path = path.relative_to(src)
261
+ # Check each component of the relative path
262
+ for part in rel_path.parts:
263
+ # Check if any part matches an exclusion pattern
264
+ for pattern in all_exclude_patterns:
265
+ # Determine if pattern is a regex (contains regex special characters)
266
+ is_regex = any(c in pattern for c in ['.', '*', '+', '?', '^', '$', '[', ']', '(', ')', '{', '}', '|', '\\'])
267
+
268
+ if is_regex:
269
+ # Use regex matching for patterns like '.*_test.*'
270
+ try:
271
+ if re.search(pattern, part):
272
+ print(f"DEBUG: Excluding {path} (part '{part}' matches regex pattern '{pattern}')", file=sys.stderr)
273
+ return True
274
+ except re.error:
275
+ # Invalid regex, fall back to simple string matching
276
+ if part == pattern or part.startswith(pattern):
277
+ print(f"DEBUG: Excluding {path} (part '{part}' matches pattern '{pattern}')", file=sys.stderr)
278
+ return True
279
+ else:
280
+ # Simple string matching for patterns like '_SS'
281
+ if part == pattern or part.startswith(pattern):
282
+ print(f"DEBUG: Excluding {path} (part '{part}' matches pattern '{pattern}')", file=sys.stderr)
283
+ return True
284
+ except ValueError:
285
+ # Path is not relative to src, check the name only
286
+ for pattern in all_exclude_patterns:
287
+ is_regex = any(c in pattern for c in ['.', '*', '+', '?', '^', '$', '[', ']', '(', ')', '{', '}', '|', '\\'])
288
+ if is_regex:
289
+ try:
290
+ if re.search(pattern, path.name):
291
+ return True
292
+ except re.error:
293
+ if path.name == pattern or path.name.startswith(pattern):
294
+ return True
295
+ else:
296
+ if path.name == pattern or path.name.startswith(pattern):
297
+ return True
298
+ return False
299
+
300
+ # Create destination directory
301
+ dst.mkdir(parents=True, exist_ok=True)
302
+
303
+ # Copy files and subdirectories, excluding patterns
304
+ copied_count = 0
305
+ excluded_count = 0
306
+ skipped_count = 0
307
+ try:
308
+ items = list(src.iterdir())
309
+ print(f"DEBUG: Copying from {src} to {dst}, found {len(items)} items", file=sys.stderr)
310
+ for item in items:
311
+ if should_exclude(item):
312
+ print(f"DEBUG: Excluding {item} from temp package directory copy", file=sys.stderr)
313
+ excluded_count += 1
314
+ continue
315
+
316
+ src_item = src / item.name
317
+ dst_item = dst / item.name
318
+
319
+ # Skip if source doesn't exist (broken symlink, already deleted, etc.)
320
+ if not src_item.exists():
321
+ print(
322
+ f"DEBUG: Skipping non-existent item: {src_item}",
323
+ file=sys.stderr,
324
+ )
325
+ skipped_count += 1
326
+ continue
327
+
328
+ if src_item.is_file():
329
+ try:
330
+ shutil.copy2(src_item, dst_item)
331
+ copied_count += 1
332
+ print(f"DEBUG: Copied file {src_item} -> {dst_item}", file=sys.stderr)
333
+ except (OSError, IOError) as e:
334
+ print(
335
+ f"DEBUG: Could not copy file {src_item}: {e}, skipping",
336
+ file=sys.stderr,
337
+ )
338
+ skipped_count += 1
339
+ continue
340
+ elif src_item.is_dir():
341
+ try:
342
+ self._copytree_excluding_patterns(src_item, dst_item, exclude_patterns)
343
+ copied_count += 1
344
+ print(f"DEBUG: Copied directory {src_item} -> {dst_item}", file=sys.stderr)
345
+ except (OSError, IOError) as e:
346
+ print(
347
+ f"DEBUG: Could not copy directory {src_item}: {e}, skipping",
348
+ file=sys.stderr,
349
+ )
350
+ skipped_count += 1
351
+ continue
352
+ print(
353
+ f"DEBUG: Copy summary: {copied_count} items copied, {excluded_count} excluded, {skipped_count} skipped",
354
+ file=sys.stderr,
355
+ )
356
+ except (OSError, IOError) as e:
357
+ # If we can't even iterate the source directory, that's a problem
358
+ raise RuntimeError(f"Cannot iterate source directory {src}: {e}") from e
188
359
 
189
360
  def _get_package_structure(self) -> tuple[str, list[str]]:
190
361
  """
@@ -198,6 +369,13 @@ class SubfolderBuildConfig:
198
369
  # Use temporary package directory if it exists, otherwise use src_dir
199
370
  package_dir = self._temp_package_dir if self._temp_package_dir and self._temp_package_dir.exists() else self.src_dir
200
371
 
372
+ print(
373
+ f"DEBUG: _get_package_structure: temp_package_dir={self._temp_package_dir}, "
374
+ f"exists={self._temp_package_dir.exists() if self._temp_package_dir else False}, "
375
+ f"using package_dir={package_dir}",
376
+ file=sys.stderr,
377
+ )
378
+
201
379
  # Check if package_dir itself is a package (has __init__.py)
202
380
  has_init = (package_dir / "__init__.py").exists()
203
381
 
@@ -211,6 +389,16 @@ class SubfolderBuildConfig:
211
389
  packages_path = str(rel_path).replace("\\", "/")
212
390
  except ValueError:
213
391
  packages_path = None
392
+ print(
393
+ f"DEBUG: Could not calculate relative path from {self.project_root} to {package_dir}",
394
+ file=sys.stderr,
395
+ )
396
+
397
+ print(
398
+ f"DEBUG: _get_package_structure returning: packages_path={packages_path}, "
399
+ f"has_init={has_init}, has_py_files={has_py_files}",
400
+ file=sys.stderr,
401
+ )
214
402
 
215
403
  # If package_dir has Python files but no __init__.py, we need to make it a package
216
404
  # or include it as a module directory
@@ -518,8 +706,28 @@ class SubfolderBuildConfig:
518
706
  # This will copy the __init__.py we just created (if any)
519
707
  self._create_temp_package_directory()
520
708
 
709
+ # Log the result of temp directory creation
710
+ if self._temp_package_dir and self._temp_package_dir.exists():
711
+ py_files = list(self._temp_package_dir.glob("*.py"))
712
+ print(
713
+ f"DEBUG: Temp package directory created successfully: {self._temp_package_dir}, "
714
+ f"contains {len(py_files)} Python files",
715
+ file=sys.stderr,
716
+ )
717
+ else:
718
+ print(
719
+ f"WARNING: Temp package directory was NOT created. "
720
+ f"Will fall back to using src_dir: {self.src_dir}",
721
+ file=sys.stderr,
722
+ )
723
+
521
724
  # Determine which directory to use (temp package dir or src_dir)
522
725
  package_dir = self._temp_package_dir if self._temp_package_dir and self._temp_package_dir.exists() else self.src_dir
726
+ print(
727
+ f"DEBUG: Using package_dir for build: {package_dir} "
728
+ f"(temp_dir={self._temp_package_dir}, src_dir={self.src_dir})",
729
+ file=sys.stderr,
730
+ )
523
731
 
524
732
  # Read the original pyproject.toml
525
733
  original_pyproject = self.project_root / "pyproject.toml"
@@ -1130,6 +1130,173 @@ class TestTemporaryPackageDirectory:
1130
1130
 
1131
1131
  config.restore()
1132
1132
 
1133
+ def test_temp_package_directory_respects_exclude_patterns_without_matching_test_dirs(
1134
+ self, test_project_with_pyproject: Path
1135
+ ) -> None:
1136
+ """
1137
+ Test that exclude patterns don't incorrectly match pytest temp directory names.
1138
+
1139
+ This test ensures that exclude patterns like '.*test_.*' only match files/directories
1140
+ within the source directory, not the pytest temp directory name (e.g.,
1141
+ 'test_real_world_ml_drawing_assistant_data_scenario').
1142
+
1143
+ This is a regression test for the bug where all files were excluded because the
1144
+ exclude pattern matching checked the entire absolute path instead of just the
1145
+ relative path within src_dir.
1146
+ """
1147
+ project_root = test_project_with_pyproject
1148
+ subfolder = project_root / "subfolder"
1149
+
1150
+ # Create files that should NOT be excluded
1151
+ (subfolder / "__init__.py").write_text("# Package init")
1152
+ (subfolder / "module.py").write_text("def func(): pass")
1153
+ (subfolder / "utils.py").write_text("def util(): pass")
1154
+
1155
+ # Create a file that SHOULD be excluded (matches pattern)
1156
+ (subfolder / "test_helper.py").write_text("def test(): pass")
1157
+ (subfolder / "_SS").mkdir()
1158
+ (subfolder / "_SS" / "excluded.py").write_text("# Should be excluded")
1159
+
1160
+ # Update pyproject.toml with exclude patterns that could match test directory names
1161
+ pyproject_path = project_root / "pyproject.toml"
1162
+ pyproject_content = pyproject_path.read_text()
1163
+ # Add exclude patterns including ones that could match pytest temp dirs
1164
+ if "[tool.python-package-folder]" not in pyproject_content:
1165
+ pyproject_content += "\n[tool.python-package-folder]\n"
1166
+ pyproject_content += 'exclude-patterns = ["_SS", "__SS", ".*_test.*", ".*test_.*", "sandbox"]\n'
1167
+ pyproject_path.write_text(pyproject_content)
1168
+
1169
+ config = SubfolderBuildConfig(
1170
+ project_root=project_root,
1171
+ src_dir=subfolder,
1172
+ version="1.0.0",
1173
+ package_name="my-package",
1174
+ )
1175
+
1176
+ # Create temp pyproject (which creates temp package directory with exclude patterns)
1177
+ config.create_temp_pyproject()
1178
+
1179
+ temp_package_dir = config._temp_package_dir
1180
+ assert temp_package_dir is not None
1181
+ assert temp_package_dir.exists()
1182
+
1183
+ # Files that should NOT be excluded should be copied
1184
+ assert (temp_package_dir / "__init__.py").exists(), (
1185
+ "__init__.py should be copied (doesn't match exclude patterns)"
1186
+ )
1187
+ assert (temp_package_dir / "module.py").exists(), (
1188
+ "module.py should be copied (doesn't match exclude patterns)"
1189
+ )
1190
+ assert (temp_package_dir / "utils.py").exists(), (
1191
+ "utils.py should be copied (doesn't match exclude patterns)"
1192
+ )
1193
+
1194
+ # Files that SHOULD be excluded should NOT be copied
1195
+ assert not (temp_package_dir / "test_helper.py").exists(), (
1196
+ "test_helper.py should be excluded (matches '.*test_.*' pattern)"
1197
+ )
1198
+ assert not (temp_package_dir / "_SS").exists(), (
1199
+ "_SS directory should be excluded (matches '_SS' pattern)"
1200
+ )
1201
+
1202
+ # Verify at least some files were copied (this would fail if all files were excluded)
1203
+ all_files = list(temp_package_dir.rglob("*"))
1204
+ all_files = [f for f in all_files if f.is_file()]
1205
+ assert len(all_files) >= 3, (
1206
+ f"Expected at least 3 files to be copied, but only found {len(all_files)}. "
1207
+ f"This suggests exclude patterns are incorrectly matching test directory names."
1208
+ )
1209
+
1210
+ config.restore()
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
+
1133
1300
 
1134
1301
  class TestWheelPackaging:
1135
1302
  """Tests to verify that wheels are correctly packaged with the right directory structure."""