Skip to content

Commit 085280b

Browse files
committed
Enhance subfolder build process with version mismatch detection and pyproject.toml handling
This commit introduces a new section in the documentation detailing version mismatch detection during the publishing process. It updates the version in the subfolder's `pyproject.toml` to match the derived version, providing warnings for discrepancies. Additionally, the handling of subfolder `pyproject.toml` is improved, including automatic merging of fields, dependency management, and exclusion patterns. The BuildManager and Publisher classes are updated to support these enhancements, ensuring a more robust build process for subfolder packages.
1 parent 712a75d commit 085280b

6 files changed

Lines changed: 1248 additions & 118 deletions

File tree

docs/PUBLISHING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ When publishing, the tool automatically filters distribution files to only uploa
5151

5252
This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
5353

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+
5465
To get a PyPI API token:
5566
1. Go to https://pypi.org/manage/account/token/
5667
2. Create a new API token

docs/USAGE.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@ The tool automatically:
7575
- Uses the current directory as the source directory if it contains Python files
7676
- Falls back to `project_root/src` if the current directory isn't suitable
7777
- **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.
7985
- **If no `pyproject.toml` in subfolder**: Creates a temporary `pyproject.toml` with:
8086
- `[build-system]` section using hatchling (always uses hatchling, even if parent uses setuptools)
8187
- 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" --
101107
python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
102108

103109
# 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)
105112
cd src/integration/my_package # assuming my_package/pyproject.toml exists
106113
python-package-folder --publish pypi
107114
```
108115

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+
109136
**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:
110137

111138
```bash

src/python_package_folder/manager.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -255,28 +255,46 @@ def prepare_build(
255255
# Fallback to just subfolder name if root project name not found
256256
package_name = subfolder_name
257257

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
273274
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:
274293
# If temporary package directory was created, use it for all operations
275294
# This ensures dependencies are copied to the correct location and
276295
# imports are fixed in the files that will actually be packaged
277296
if (
278-
self.subfolder_config
279-
and self.subfolder_config._temp_package_dir
297+
self.subfolder_config._temp_package_dir
280298
and self.subfolder_config._temp_package_dir.exists()
281299
):
282300
# Update src_dir to point to temp package directory

src/python_package_folder/publisher.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import getpass
1111
import os
12+
import re
1213
import subprocess
1314
import sys
1415
from enum import Enum
@@ -284,10 +285,40 @@ def publish(self, skip_existing: bool = False) -> None:
284285

285286
if not dist_files:
286287
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 = (
288303
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"
290305
)
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)
291322
else:
292323
raise ValueError(f"No distribution files found in {self.dist_dir}")
293324

0 commit comments

Comments
 (0)