python-package-folder 9.0.0__tar.gz → 9.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 (66) hide show
  1. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/PKG-INFO +1 -1
  2. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/coverage.svg +2 -2
  3. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/PUBLISHING.md +11 -0
  4. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/USAGE.md +29 -2
  5. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/pyproject.toml +1 -1
  6. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/analyzer.py +16 -0
  7. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/manager.py +35 -17
  8. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/publisher.py +33 -2
  9. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/subfolder_build.py +417 -0
  10. python_package_folder-9.2.0/tests/test_subfolder_build.py +62 -0
  11. python_package_folder-9.2.0/tests/test_subfolder_build_config.py +399 -0
  12. python_package_folder-9.2.0/tests/test_subfolder_imports.py +600 -0
  13. python_package_folder-9.2.0/tests/test_subfolder_pyproject_toml.py +1145 -0
  14. python_package_folder-9.2.0/tests/test_subfolder_temp_directory.py +413 -0
  15. python_package_folder-9.2.0/tests/test_subfolder_wheel_packaging.py +124 -0
  16. python_package_folder-9.0.0/tests/test_subfolder_build.py +0 -3848
  17. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.copier-answers.yml +0 -0
  18. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  19. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  20. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.cursor/rules/general.mdc +0 -0
  21. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.cursor/rules/python.mdc +0 -0
  22. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.github/workflows/ci.yml +0 -0
  23. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.github/workflows/publish.yml +0 -0
  24. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.gitignore +0 -0
  25. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/.vscode/settings.json +0 -0
  26. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/LICENSE +0 -0
  27. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/MANIFEST.in +0 -0
  28. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/Makefile +0 -0
  29. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/README.md +0 -0
  30. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/development.md +0 -0
  31. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/DEVELOPMENT.md +0 -0
  32. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/INSTALLATION.md +0 -0
  33. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/REFERENCE.md +0 -0
  34. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/docs/VERSION_RESOLUTION.md +0 -0
  35. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/installation.md +0 -0
  36. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/publishing.md +0 -0
  37. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/__init__.py +0 -0
  38. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/__main__.py +0 -0
  39. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/finder.py +0 -0
  40. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/py.typed +0 -0
  41. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/python_package_folder.py +0 -0
  42. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/types.py +0 -0
  43. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/utils.py +0 -0
  44. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/version.py +0 -0
  45. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/src/python_package_folder/version_calculator.py +0 -0
  46. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/conftest.py +0 -0
  47. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/some_globals.py +0 -0
  48. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  49. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  50. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  51. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  52. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  53. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  54. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_build_with_external_deps.py +0 -0
  55. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_exclude_patterns.py +0 -0
  56. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_linting.py +0 -0
  57. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_preserve_directory_structure.py +0 -0
  58. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_publisher.py +0 -0
  59. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_shared_subdirectory_imports.py +0 -0
  60. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  61. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_third_party_dependencies.py +0 -0
  62. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_utils.py +0 -0
  63. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_version_calculator.py +0 -0
  64. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/test_version_manager.py +0 -0
  65. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/tests/tests.py +0 -0
  66. {python_package_folder-9.0.0 → python_package_folder-9.2.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 9.0.0
3
+ Version: 9.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">67%</text>
18
- <text x="81" y="14">67%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">65%</text>
18
+ <text x="81" y="14">65%</text>
19
19
  </g>
20
20
  </svg>
@@ -51,6 +51,17 @@ When publishing, the tool automatically filters distribution files to only uploa
51
51
 
52
52
  This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
53
53
 
54
+ ## Version Mismatch Detection
55
+
56
+ If there's a version mismatch between the built package and the expected version (e.g., when a subfolder's `pyproject.toml` has a different version than the derived version), the error message will show:
57
+
58
+ - What version was actually built
59
+ - What version is expected for publishing
60
+ - An explanation of the mismatch
61
+ - A solution suggestion
62
+
63
+ The tool automatically updates the version in the subfolder's `pyproject.toml` to match the derived version, so this error should only occur if the build process fails before the version update takes effect.
64
+
54
65
  To get a PyPI API token:
55
66
  1. Go to https://pypi.org/manage/account/token/
56
67
  2. Create a new API token
@@ -75,7 +75,13 @@ The tool automatically:
75
75
  - Uses the current directory as the source directory if it contains Python files
76
76
  - Falls back to `project_root/src` if the current directory isn't suitable
77
77
  - **For subfolder builds**: Handles `pyproject.toml` configuration:
78
- - **If `pyproject.toml` exists in subfolder**: Uses that file (copies it to project root temporarily, adjusting package paths and ensuring `[build-system]` uses hatchling)
78
+ - **If `pyproject.toml` exists in subfolder**:
79
+ - Uses that file (copies it to project root temporarily, adjusting package paths and ensuring `[build-system]` uses hatchling)
80
+ - **Version handling**: The version in the subfolder's `pyproject.toml` is automatically updated to match the derived version (from conventional commits or `--version` argument). A warning is shown if versions differ.
81
+ - **Name handling**: If the subfolder's `pyproject.toml` has a `name` field that differs from the derived name, a warning is shown but the subfolder's name is used (not the derived one).
82
+ - **Dependencies handling**: If the subfolder's `pyproject.toml` has a non-empty `dependencies` field, automatic dependency detection is skipped with a warning. To enable automatic detection, remove or empty the `dependencies` field.
83
+ - **Field merging**: Missing fields (like `description`, `authors`, `keywords`, `classifiers`, `license`, `urls`, etc.) are automatically filled from the parent `pyproject.toml` if available.
84
+ - **Exclude patterns**: Exclude patterns from the parent `pyproject.toml` are merged into the subfolder's configuration.
79
85
  - **If no `pyproject.toml` in subfolder**: Creates a temporary `pyproject.toml` with:
80
86
  - `[build-system]` section using hatchling (always uses hatchling, even if parent uses setuptools)
81
87
  - Package name derived from the subfolder name (e.g., `empty_drawing_detection` → `empty-drawing-detection`)
@@ -101,11 +107,32 @@ python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --
101
107
  python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
102
108
 
103
109
  # If subfolder has its own pyproject.toml, it will be used automatically
104
- # (package-name and version arguments are ignored in this case)
110
+ # The version will be updated to match the derived version (from conventional commits)
111
+ # The package name from the subfolder toml will be used (even if it differs from derived)
105
112
  cd src/integration/my_package # assuming my_package/pyproject.toml exists
106
113
  python-package-folder --publish pypi
107
114
  ```
108
115
 
116
+ **Subfolder `pyproject.toml` Behavior:**
117
+
118
+ When a subfolder has its own `pyproject.toml`, the tool intelligently merges it with derived information:
119
+
120
+ 1. **Version**: Always updated to match the derived version (from conventional commits or `--version`). If the subfolder's version differs, a warning is shown but the derived version is used.
121
+
122
+ 2. **Name**: If the subfolder's `pyproject.toml` has a `name` field, it takes precedence over the derived name. A warning is shown if they differ.
123
+
124
+ 3. **Dependencies**:
125
+ - If the subfolder's `pyproject.toml` has a non-empty `dependencies` field, automatic dependency detection is **skipped** (with a warning).
126
+ - To enable automatic dependency detection, remove or empty the `dependencies` field in the subfolder's `pyproject.toml`.
127
+ - If the `dependencies` field is empty or missing, automatic detection proceeds normally.
128
+
129
+ 4. **Field Merging**: Missing fields are automatically filled from the parent `pyproject.toml`:
130
+ - `description`, `readme`, `requires-python`, `authors`, `keywords`, `classifiers`, `license`
131
+ - `[project.urls]` section
132
+ - Other `[tool.*]` sections (except `tool.hatch.build.*` and `tool.python-package-folder.*`)
133
+
134
+ 5. **Exclude Patterns**: Automatically merged from parent `pyproject.toml`.
135
+
109
136
  **Dependency Groups**: When building a subfolder, you can specify a dependency group from the parent `pyproject.toml` to include in the subfolder's build configuration. This allows subfolders to inherit specific dependencies from the parent project:
110
137
 
111
138
  ```bash
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "9.0.0"
46
+ version = "9.2.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -310,6 +310,15 @@ class ImportAnalyzer:
310
310
  self.project_root / module_path_str / "__init__.py",
311
311
  (self.project_root / module_path_str).with_suffix(".py"),
312
312
  ]
313
+
314
+ # Also check project_root/src/ for files at src/ root (like _globals.py)
315
+ # This is important when building subfolders where src_dir points to temp directory
316
+ src_base = self.project_root / "src"
317
+ if src_base.exists():
318
+ potential_paths.extend([
319
+ src_base / module_path_str / "__init__.py",
320
+ (src_base / module_path_str).with_suffix(".py"),
321
+ ])
313
322
 
314
323
  for path in potential_paths:
315
324
  if path.exists():
@@ -335,6 +344,13 @@ class ImportAnalyzer:
335
344
  potential_file = parent / f"{module_name.split('.')[-1]}.py"
336
345
  if potential_file.exists():
337
346
  return potential_file
347
+
348
+ # Also check project_root/src/ for files at src/ root
349
+ # This handles cases like _globals.py at src/_globals.py when building subfolders
350
+ if parent == self.project_root and src_base.exists():
351
+ src_file = src_base / f"{module_name.split('.')[-1]}.py"
352
+ if src_file.exists():
353
+ return src_file
338
354
 
339
355
  # Check all subdirectories in parent (not just common ones)
340
356
  # This handles cases like src/data/spreadsheet_creation/spreadsheet_formatting_dataclasses.py
@@ -255,28 +255,46 @@ class BuildManager:
255
255
  # Fallback to just subfolder name if root project name not found
256
256
  package_name = subfolder_name
257
257
 
258
- print(
259
- f"Detected subfolder build. Setting up package '{package_name}' version '{version}'..."
260
- )
261
- self.subfolder_config = SubfolderBuildConfig(
262
- project_root=self.project_root,
263
- src_dir=self.src_dir,
264
- package_name=package_name,
265
- version=version,
266
- dependency_group=dependency_group,
267
- )
268
- temp_pyproject = self.subfolder_config.create_temp_pyproject()
269
- # If temp_pyproject is None, it means no parent pyproject.toml exists
270
- # This is acceptable for tests or dependency-only operations
271
- if temp_pyproject is None:
272
- self.subfolder_config = None
258
+ # Check if subfolder_config already exists with the same parameters
259
+ # This makes prepare_build() idempotent - safe to call multiple times
260
+ if (
261
+ self.subfolder_config
262
+ and self.subfolder_config.package_name == package_name
263
+ and self.subfolder_config.version == version
264
+ and self.subfolder_config.dependency_group == dependency_group
265
+ and self.subfolder_config._temp_package_dir
266
+ and self.subfolder_config._temp_package_dir.exists()
267
+ ):
268
+ print(
269
+ f"Subfolder config already exists for '{package_name}' version '{version}', reusing it..."
270
+ )
271
+ # Still need to find external dependencies, so continue with that
272
+ # temp_pyproject should already exist from previous call
273
+ temp_pyproject = self.subfolder_config.temp_pyproject
273
274
  else:
275
+ print(
276
+ f"Detected subfolder build. Setting up package '{package_name}' version '{version}'..."
277
+ )
278
+ self.subfolder_config = SubfolderBuildConfig(
279
+ project_root=self.project_root,
280
+ src_dir=self.src_dir,
281
+ package_name=package_name,
282
+ version=version,
283
+ dependency_group=dependency_group,
284
+ )
285
+ temp_pyproject = self.subfolder_config.create_temp_pyproject()
286
+ # If temp_pyproject is None, it means no parent pyproject.toml exists
287
+ # This is acceptable for tests or dependency-only operations
288
+ if temp_pyproject is None:
289
+ self.subfolder_config = None
290
+
291
+ # If we have a subfolder_config (either newly created or reused), use temp package dir
292
+ if self.subfolder_config:
274
293
  # If temporary package directory was created, use it for all operations
275
294
  # This ensures dependencies are copied to the correct location and
276
295
  # imports are fixed in the files that will actually be packaged
277
296
  if (
278
- self.subfolder_config
279
- and self.subfolder_config._temp_package_dir
297
+ self.subfolder_config._temp_package_dir
280
298
  and self.subfolder_config._temp_package_dir.exists()
281
299
  ):
282
300
  # Update src_dir to point to temp package directory
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import getpass
11
11
  import os
12
+ import re
12
13
  import subprocess
13
14
  import sys
14
15
  from enum import Enum
@@ -284,10 +285,40 @@ class Publisher:
284
285
 
285
286
  if not dist_files:
286
287
  if self.package_name and self.version:
287
- raise ValueError(
288
+ # Try to find what version was actually built
289
+ built_versions = set()
290
+ for dist_file in all_dist_files:
291
+ # Extract version from filename: package-name-version-*.whl
292
+ # Pattern: {package_name}-{version}-{...}
293
+ if self.package_name.replace("-", "_") in dist_file.name or self.package_name in dist_file.name:
294
+ # Try to extract version from filename
295
+ parts = dist_file.stem.split("-")
296
+ # Look for version-like pattern (e.g., 1.2.3)
297
+ for i, part in enumerate(parts):
298
+ if re.match(r"^\d+\.\d+\.\d+", part):
299
+ built_versions.add(part)
300
+ break
301
+
302
+ error_msg = (
288
303
  f"No distribution files found matching package '{self.package_name}' "
289
- f"version '{self.version}' in {self.dist_dir}"
304
+ f"version '{self.version}' in {self.dist_dir}\n"
290
305
  )
306
+
307
+ if built_versions:
308
+ error_msg += (
309
+ f" - Built package version(s): {', '.join(sorted(built_versions))}\n"
310
+ f" - Expected version for publishing: {self.version}\n"
311
+ f" - This usually indicates a version mismatch between the built package and the expected version.\n"
312
+ f" - Solution: The version in the subfolder's pyproject.toml will be automatically updated to match the derived version."
313
+ )
314
+ else:
315
+ error_msg += (
316
+ f" - No distribution files found for package '{self.package_name}' in {self.dist_dir}\n"
317
+ f" - Available files: {[f.name for f in all_dist_files[:5]]}"
318
+ + (f" (and {len(all_dist_files) - 5} more)" if len(all_dist_files) > 5 else "")
319
+ )
320
+
321
+ raise ValueError(error_msg)
291
322
  else:
292
323
  raise ValueError(f"No distribution files found in {self.dist_dir}")
293
324