python-package-folder 8.1.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.1.0 → python_package_folder-8.2.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/pyproject.toml +1 -1
  3. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/finder.py +46 -29
  4. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/manager.py +4 -0
  5. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_subfolder_build.py +88 -0
  6. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.copier-answers.yml +0 -0
  7. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  8. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  9. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.cursor/rules/general.mdc +0 -0
  10. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.cursor/rules/python.mdc +0 -0
  11. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.github/workflows/ci.yml +0 -0
  12. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.github/workflows/publish.yml +0 -0
  13. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.gitignore +0 -0
  14. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/.vscode/settings.json +0 -0
  15. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/LICENSE +0 -0
  16. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/MANIFEST.in +0 -0
  17. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/Makefile +0 -0
  18. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/README.md +0 -0
  19. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/coverage.svg +0 -0
  20. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/development.md +0 -0
  21. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/DEVELOPMENT.md +0 -0
  22. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/INSTALLATION.md +0 -0
  23. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/PUBLISHING.md +0 -0
  24. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/REFERENCE.md +0 -0
  25. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/USAGE.md +0 -0
  26. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/docs/VERSION_RESOLUTION.md +0 -0
  27. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/installation.md +0 -0
  28. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/publishing.md +0 -0
  29. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/__init__.py +0 -0
  30. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/__main__.py +0 -0
  31. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/analyzer.py +0 -0
  32. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/publisher.py +0 -0
  33. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/py.typed +0 -0
  34. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/python_package_folder.py +0 -0
  35. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/subfolder_build.py +0 -0
  36. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/types.py +0 -0
  37. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/utils.py +0 -0
  38. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/version.py +0 -0
  39. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/src/python_package_folder/version_calculator.py +0 -0
  40. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/conftest.py +0 -0
  41. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/some_globals.py +0 -0
  42. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  43. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  44. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  45. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  46. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  47. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  48. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_build_with_external_deps.py +0 -0
  49. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_exclude_patterns.py +0 -0
  50. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_linting.py +0 -0
  51. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_preserve_directory_structure.py +0 -0
  52. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_publisher.py +0 -0
  53. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_shared_subdirectory_imports.py +0 -0
  54. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  55. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_third_party_dependencies.py +0 -0
  56. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_utils.py +0 -0
  57. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_version_calculator.py +0 -0
  58. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/test_version_manager.py +0 -0
  59. {python_package_folder-8.1.0 → python_package_folder-8.2.0}/tests/tests.py +0 -0
  60. {python_package_folder-8.1.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.1.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>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.1.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}"
@@ -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."""