python-package-folder 9.0.0__tar.gz → 9.1.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.
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/PKG-INFO +1 -1
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/coverage.svg +2 -2
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/PUBLISHING.md +11 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/USAGE.md +29 -2
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/pyproject.toml +1 -1
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/manager.py +35 -17
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/publisher.py +33 -2
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/subfolder_build.py +417 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_subfolder_build.py +723 -97
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.copier-answers.yml +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.github/workflows/ci.yml +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.github/workflows/publish.yml +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.gitignore +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/.vscode/settings.json +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/LICENSE +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/MANIFEST.in +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/Makefile +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/README.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/development.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/DEVELOPMENT.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/INSTALLATION.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/REFERENCE.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/docs/VERSION_RESOLUTION.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/installation.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/publishing.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/__init__.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/finder.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/python_package_folder.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/types.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/utils.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/version.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/version_calculator.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/conftest.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_build_with_external_deps.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_exclude_patterns.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_linting.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_preserve_directory_structure.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_publisher.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_shared_subdirectory_imports.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_spreadsheet_creation_imports.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_third_party_dependencies.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_utils.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_version_calculator.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/test_version_manager.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/tests/tests.py +0 -0
- {python_package_folder-9.0.0 → python_package_folder-9.1.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-package-folder
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.1.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">
|
|
18
|
-
<text x="81" y="14">
|
|
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**:
|
|
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
|
-
#
|
|
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
|
{python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/manager.py
RENAMED
|
@@ -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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
{python_package_folder-9.0.0 → python_package_folder-9.1.0}/src/python_package_folder/publisher.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -79,6 +79,7 @@ class SubfolderBuildConfig:
|
|
|
79
79
|
self._excluded_files: list[tuple[Path, Path]] = [] # List of (original_path, temp_path) tuples
|
|
80
80
|
self._exclude_temp_dir: Path | None = None
|
|
81
81
|
self._temp_package_dir: Path | None = None
|
|
82
|
+
self._has_existing_dependencies = False # Track if subfolder toml has dependencies
|
|
82
83
|
|
|
83
84
|
def _derive_package_name(self) -> str:
|
|
84
85
|
"""
|
|
@@ -549,6 +550,361 @@ class SubfolderBuildConfig:
|
|
|
549
550
|
|
|
550
551
|
return "\n".join(result)
|
|
551
552
|
|
|
553
|
+
def _update_version_in_pyproject(self, content: str) -> str:
|
|
554
|
+
"""
|
|
555
|
+
Update the version in pyproject.toml content to match self.version.
|
|
556
|
+
|
|
557
|
+
Also checks if version differs and warns the user.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
content: Content of pyproject.toml
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Content with updated version
|
|
564
|
+
"""
|
|
565
|
+
if not self.version:
|
|
566
|
+
return content
|
|
567
|
+
|
|
568
|
+
lines = content.split("\n")
|
|
569
|
+
result = []
|
|
570
|
+
in_project_section = False
|
|
571
|
+
version_set = False
|
|
572
|
+
existing_version = None
|
|
573
|
+
|
|
574
|
+
for line in lines:
|
|
575
|
+
# Detect [project] section
|
|
576
|
+
if line.strip() == "[project]":
|
|
577
|
+
in_project_section = True
|
|
578
|
+
result.append(line)
|
|
579
|
+
continue
|
|
580
|
+
elif line.strip().startswith("[") and in_project_section:
|
|
581
|
+
# End of [project] section - add version if not set
|
|
582
|
+
if not version_set:
|
|
583
|
+
result.append(f'version = "{self.version}"')
|
|
584
|
+
version_set = True
|
|
585
|
+
in_project_section = False
|
|
586
|
+
result.append(line)
|
|
587
|
+
elif in_project_section:
|
|
588
|
+
# Check if this is a version line
|
|
589
|
+
version_match = re.match(r'^\s*version\s*=\s*["\']([^"\']+)["\']', line)
|
|
590
|
+
if version_match:
|
|
591
|
+
existing_version = version_match.group(1)
|
|
592
|
+
result.append(f'version = "{self.version}"')
|
|
593
|
+
version_set = True
|
|
594
|
+
continue # Skip the original version line
|
|
595
|
+
else:
|
|
596
|
+
result.append(line)
|
|
597
|
+
else:
|
|
598
|
+
result.append(line)
|
|
599
|
+
|
|
600
|
+
# If we never found [project] section or version wasn't set, add it
|
|
601
|
+
if not version_set:
|
|
602
|
+
# Try to find where to insert it - after [project] if it exists
|
|
603
|
+
if "[project]" in content:
|
|
604
|
+
# Insert after [project] line
|
|
605
|
+
new_lines = []
|
|
606
|
+
inserted = False
|
|
607
|
+
for i, line in enumerate(lines):
|
|
608
|
+
new_lines.append(line)
|
|
609
|
+
if line.strip() == "[project]" and not inserted:
|
|
610
|
+
# Insert version right after [project]
|
|
611
|
+
new_lines.append(f'version = "{self.version}"')
|
|
612
|
+
inserted = True
|
|
613
|
+
result = new_lines
|
|
614
|
+
else:
|
|
615
|
+
# No [project] section, add it at the beginning
|
|
616
|
+
result.insert(0, "[project]")
|
|
617
|
+
result.insert(1, f'version = "{self.version}"')
|
|
618
|
+
|
|
619
|
+
# Warn if version differs
|
|
620
|
+
if existing_version and existing_version != self.version:
|
|
621
|
+
print(
|
|
622
|
+
f"\nWarning: Version mismatch in subfolder pyproject.toml",
|
|
623
|
+
file=sys.stderr,
|
|
624
|
+
)
|
|
625
|
+
print(
|
|
626
|
+
f" - Version in file: {existing_version}",
|
|
627
|
+
file=sys.stderr,
|
|
628
|
+
)
|
|
629
|
+
print(
|
|
630
|
+
f" - Derived version: {self.version} (from conventional commits/CLI)",
|
|
631
|
+
file=sys.stderr,
|
|
632
|
+
)
|
|
633
|
+
print(
|
|
634
|
+
f" - Using derived version: {self.version}",
|
|
635
|
+
file=sys.stderr,
|
|
636
|
+
)
|
|
637
|
+
print(
|
|
638
|
+
f" - The version in the subfolder's pyproject.toml will be updated for this build.\n",
|
|
639
|
+
file=sys.stderr,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
return "\n".join(result)
|
|
643
|
+
|
|
644
|
+
def _check_and_warn_about_dependencies(self, content: str) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Check if subfolder pyproject.toml has a non-empty dependencies field.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
content: Content of pyproject.toml
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
True if dependencies field exists and is non-empty, False otherwise
|
|
653
|
+
"""
|
|
654
|
+
if not tomllib:
|
|
655
|
+
# Fallback: simple regex check
|
|
656
|
+
# Look for dependencies = [...] pattern
|
|
657
|
+
deps_match = re.search(r'^\s*dependencies\s*=\s*\[', content, re.MULTILINE)
|
|
658
|
+
if deps_match:
|
|
659
|
+
# Try to find if there are any dependencies in the list
|
|
660
|
+
# This is a simple heuristic - look for non-empty content between [ and ]
|
|
661
|
+
lines = content.split("\n")
|
|
662
|
+
in_project = False
|
|
663
|
+
in_dependencies = False
|
|
664
|
+
dependency_count = 0
|
|
665
|
+
|
|
666
|
+
for line in lines:
|
|
667
|
+
if line.strip() == "[project]":
|
|
668
|
+
in_project = True
|
|
669
|
+
elif line.strip().startswith("[") and in_project:
|
|
670
|
+
in_project = False
|
|
671
|
+
elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
|
|
672
|
+
in_dependencies = True
|
|
673
|
+
# Check if line has content after [
|
|
674
|
+
if "]" not in line:
|
|
675
|
+
# Multi-line dependencies
|
|
676
|
+
continue
|
|
677
|
+
else:
|
|
678
|
+
# Single line: dependencies = ["pkg1", "pkg2"]
|
|
679
|
+
deps_str = line.split("[", 1)[1].rsplit("]", 1)[0]
|
|
680
|
+
if deps_str.strip():
|
|
681
|
+
return True
|
|
682
|
+
return False
|
|
683
|
+
elif in_dependencies:
|
|
684
|
+
# Check if this line has a dependency (not just whitespace or closing bracket)
|
|
685
|
+
stripped = line.strip()
|
|
686
|
+
if stripped and not stripped.startswith("#") and stripped != "]":
|
|
687
|
+
# Check if it looks like a dependency string
|
|
688
|
+
if re.search(r'["\'][^"\']+["\']', stripped):
|
|
689
|
+
dependency_count += 1
|
|
690
|
+
if "]" in line:
|
|
691
|
+
return dependency_count > 0
|
|
692
|
+
|
|
693
|
+
return dependency_count > 0
|
|
694
|
+
return False
|
|
695
|
+
|
|
696
|
+
# Use tomllib for more accurate parsing
|
|
697
|
+
try:
|
|
698
|
+
data = tomllib.loads(content.encode())
|
|
699
|
+
project = data.get("project", {})
|
|
700
|
+
dependencies = project.get("dependencies", [])
|
|
701
|
+
|
|
702
|
+
# Check if dependencies list is non-empty
|
|
703
|
+
if dependencies and len(dependencies) > 0:
|
|
704
|
+
return True
|
|
705
|
+
return False
|
|
706
|
+
except Exception:
|
|
707
|
+
# If parsing fails, fall back to regex-based check
|
|
708
|
+
# Look for dependencies = [...] pattern
|
|
709
|
+
deps_match = re.search(r'^\s*dependencies\s*=\s*\[', content, re.MULTILINE)
|
|
710
|
+
if deps_match:
|
|
711
|
+
# Try to find if there are any dependencies in the list
|
|
712
|
+
lines = content.split("\n")
|
|
713
|
+
in_project = False
|
|
714
|
+
in_dependencies = False
|
|
715
|
+
dependency_count = 0
|
|
716
|
+
|
|
717
|
+
for line in lines:
|
|
718
|
+
if line.strip() == "[project]":
|
|
719
|
+
in_project = True
|
|
720
|
+
elif line.strip().startswith("[") and in_project:
|
|
721
|
+
in_project = False
|
|
722
|
+
elif in_project and re.match(r"^\s*dependencies\s*=\s*\[", line):
|
|
723
|
+
in_dependencies = True
|
|
724
|
+
# Check if line has content after [
|
|
725
|
+
if "]" not in line:
|
|
726
|
+
# Multi-line dependencies
|
|
727
|
+
continue
|
|
728
|
+
else:
|
|
729
|
+
# Single line: dependencies = ["pkg1", "pkg2"]
|
|
730
|
+
deps_str = line.split("[", 1)[1].rsplit("]", 1)[0]
|
|
731
|
+
if deps_str.strip():
|
|
732
|
+
return True
|
|
733
|
+
return False
|
|
734
|
+
elif in_dependencies:
|
|
735
|
+
# Check if this line has a dependency (not just whitespace or closing bracket)
|
|
736
|
+
stripped = line.strip()
|
|
737
|
+
if stripped and not stripped.startswith("#") and stripped != "]":
|
|
738
|
+
# Check if it looks like a dependency string
|
|
739
|
+
if re.search(r'["\'][^"\']+["\']', stripped):
|
|
740
|
+
dependency_count += 1
|
|
741
|
+
if "]" in line:
|
|
742
|
+
return dependency_count > 0
|
|
743
|
+
|
|
744
|
+
return dependency_count > 0
|
|
745
|
+
return False
|
|
746
|
+
|
|
747
|
+
def _check_and_warn_about_name(self, content: str) -> str | None:
|
|
748
|
+
"""
|
|
749
|
+
Check if subfolder pyproject.toml has a name field and warn if it differs from derived.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
content: Content of pyproject.toml
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Name from subfolder toml if found, None otherwise
|
|
756
|
+
"""
|
|
757
|
+
if not tomllib:
|
|
758
|
+
# Fallback: simple regex check
|
|
759
|
+
name_match = re.search(r'^\s*name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
760
|
+
if name_match:
|
|
761
|
+
return name_match.group(1)
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
# Use tomllib for more accurate parsing
|
|
765
|
+
try:
|
|
766
|
+
data = tomllib.loads(content.encode())
|
|
767
|
+
project = data.get("project", {})
|
|
768
|
+
name = project.get("name")
|
|
769
|
+
return name
|
|
770
|
+
except Exception:
|
|
771
|
+
# If parsing fails, fall back to regex
|
|
772
|
+
name_match = re.search(r'^\s*name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
|
|
773
|
+
if name_match:
|
|
774
|
+
return name_match.group(1)
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
def _merge_from_parent_pyproject(self, subfolder_content: str, parent_content: str) -> str:
|
|
778
|
+
"""
|
|
779
|
+
Merge missing fields from parent pyproject.toml into subfolder content.
|
|
780
|
+
|
|
781
|
+
Priority: subfolder > parent (only fill missing fields from parent)
|
|
782
|
+
|
|
783
|
+
This uses string manipulation to add missing fields since we don't have tomli-w
|
|
784
|
+
for proper TOML round-trip. Only common fields are merged.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
subfolder_content: Content of subfolder pyproject.toml
|
|
788
|
+
parent_content: Content of parent pyproject.toml
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
Merged content
|
|
792
|
+
"""
|
|
793
|
+
if not tomllib:
|
|
794
|
+
# If tomllib not available, return subfolder content as-is
|
|
795
|
+
return subfolder_content
|
|
796
|
+
|
|
797
|
+
try:
|
|
798
|
+
subfolder_data = tomllib.loads(subfolder_content.encode())
|
|
799
|
+
parent_data = tomllib.loads(parent_content.encode())
|
|
800
|
+
|
|
801
|
+
# Fields to merge from parent if missing in subfolder
|
|
802
|
+
fields_to_merge = [
|
|
803
|
+
"description", "readme", "requires-python", "authors",
|
|
804
|
+
"keywords", "classifiers", "license", "urls"
|
|
805
|
+
]
|
|
806
|
+
|
|
807
|
+
# Check what's missing and needs to be added
|
|
808
|
+
missing_fields = []
|
|
809
|
+
if "project" in parent_data and "project" in subfolder_data:
|
|
810
|
+
parent_project = parent_data["project"]
|
|
811
|
+
subfolder_project = subfolder_data["project"]
|
|
812
|
+
|
|
813
|
+
for field in fields_to_merge:
|
|
814
|
+
if field not in subfolder_project and field in parent_project:
|
|
815
|
+
missing_fields.append((field, parent_project[field]))
|
|
816
|
+
|
|
817
|
+
# If no missing fields, return as-is
|
|
818
|
+
if not missing_fields:
|
|
819
|
+
return subfolder_content
|
|
820
|
+
|
|
821
|
+
# Add missing fields using string manipulation
|
|
822
|
+
# Find [project] section and add fields after it
|
|
823
|
+
lines = subfolder_content.split("\n")
|
|
824
|
+
result = []
|
|
825
|
+
in_project = False
|
|
826
|
+
project_section_end = -1
|
|
827
|
+
|
|
828
|
+
for i, line in enumerate(lines):
|
|
829
|
+
if line.strip() == "[project]":
|
|
830
|
+
in_project = True
|
|
831
|
+
result.append(line)
|
|
832
|
+
elif line.strip().startswith("[") and in_project:
|
|
833
|
+
# End of [project] section - insert missing fields here
|
|
834
|
+
project_section_end = i
|
|
835
|
+
in_project = False
|
|
836
|
+
# Add missing fields before the next section
|
|
837
|
+
for field_name, field_value in missing_fields:
|
|
838
|
+
if field_name == "urls" and isinstance(field_value, dict):
|
|
839
|
+
# Handle [project.urls] separately
|
|
840
|
+
continue
|
|
841
|
+
# Format the field value appropriately
|
|
842
|
+
formatted = self._format_toml_value(field_name, field_value)
|
|
843
|
+
if formatted:
|
|
844
|
+
result.append(formatted)
|
|
845
|
+
result.append(line)
|
|
846
|
+
else:
|
|
847
|
+
result.append(line)
|
|
848
|
+
|
|
849
|
+
# Handle [project.urls] separately if it exists in parent
|
|
850
|
+
if "project" in parent_data and "urls" in parent_data["project"]:
|
|
851
|
+
urls = parent_data["project"]["urls"]
|
|
852
|
+
if isinstance(urls, dict) and "project.urls" not in subfolder_content:
|
|
853
|
+
# Add [project.urls] section at the end
|
|
854
|
+
result.append("")
|
|
855
|
+
result.append("[project.urls]")
|
|
856
|
+
for key, value in urls.items():
|
|
857
|
+
result.append(f'{key} = "{value}"')
|
|
858
|
+
|
|
859
|
+
return "\n".join(result)
|
|
860
|
+
|
|
861
|
+
except Exception as e:
|
|
862
|
+
print(
|
|
863
|
+
f"Warning: Could not merge from parent pyproject.toml: {e}. Using subfolder content as-is.",
|
|
864
|
+
file=sys.stderr,
|
|
865
|
+
)
|
|
866
|
+
return subfolder_content
|
|
867
|
+
|
|
868
|
+
def _format_toml_value(self, field_name: str, value: any) -> str | None:
|
|
869
|
+
"""
|
|
870
|
+
Format a TOML field value as a string.
|
|
871
|
+
|
|
872
|
+
Args:
|
|
873
|
+
field_name: Name of the field
|
|
874
|
+
value: Value to format
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Formatted string or None if cannot format
|
|
878
|
+
"""
|
|
879
|
+
if value is None:
|
|
880
|
+
return None
|
|
881
|
+
|
|
882
|
+
if isinstance(value, str):
|
|
883
|
+
return f'{field_name} = "{value}"'
|
|
884
|
+
elif isinstance(value, list):
|
|
885
|
+
if not value:
|
|
886
|
+
return None
|
|
887
|
+
# Format list items
|
|
888
|
+
if isinstance(value[0], dict):
|
|
889
|
+
# List of dicts (e.g., authors)
|
|
890
|
+
items = []
|
|
891
|
+
for item in value:
|
|
892
|
+
if isinstance(item, dict):
|
|
893
|
+
# Format as inline table: {name = "...", email = "..."}
|
|
894
|
+
parts = [f'{k} = "{v}"' for k, v in item.items() if v]
|
|
895
|
+
items.append("{" + ", ".join(parts) + "}")
|
|
896
|
+
return f"{field_name} = [\n " + ",\n ".join(items) + "\n]"
|
|
897
|
+
else:
|
|
898
|
+
# List of strings
|
|
899
|
+
items = [f'"{v}"' for v in value]
|
|
900
|
+
return f"{field_name} = [\n " + ",\n ".join(items) + "\n]"
|
|
901
|
+
elif isinstance(value, bool):
|
|
902
|
+
return f"{field_name} = {str(value).lower()}"
|
|
903
|
+
elif isinstance(value, (int, float)):
|
|
904
|
+
return f"{field_name} = {value}"
|
|
905
|
+
else:
|
|
906
|
+
return None
|
|
907
|
+
|
|
552
908
|
def create_temp_pyproject(self) -> Path | None:
|
|
553
909
|
"""
|
|
554
910
|
Create a temporary pyproject.toml for the subfolder build.
|
|
@@ -634,11 +990,64 @@ class SubfolderBuildConfig:
|
|
|
634
990
|
# Create temporary pyproject.toml file
|
|
635
991
|
temp_pyproject_path = self.project_root / "pyproject.toml.temp"
|
|
636
992
|
|
|
993
|
+
# Check for name mismatch and warn (but use subfolder name)
|
|
994
|
+
subfolder_name = self._check_and_warn_about_name(subfolder_content)
|
|
995
|
+
if subfolder_name and subfolder_name != self.package_name:
|
|
996
|
+
print(
|
|
997
|
+
f"\nWarning: Package name mismatch in subfolder pyproject.toml",
|
|
998
|
+
file=sys.stderr,
|
|
999
|
+
)
|
|
1000
|
+
print(
|
|
1001
|
+
f" - Name in file: {subfolder_name}",
|
|
1002
|
+
file=sys.stderr,
|
|
1003
|
+
)
|
|
1004
|
+
print(
|
|
1005
|
+
f" - Derived name: {self.package_name}",
|
|
1006
|
+
file=sys.stderr,
|
|
1007
|
+
)
|
|
1008
|
+
print(
|
|
1009
|
+
f" - Using name from subfolder toml: {subfolder_name}\n",
|
|
1010
|
+
file=sys.stderr,
|
|
1011
|
+
)
|
|
1012
|
+
# Update package_name to use subfolder's name
|
|
1013
|
+
self.package_name = subfolder_name
|
|
1014
|
+
|
|
1015
|
+
# Check for dependencies and warn if automatic detection will be skipped
|
|
1016
|
+
self._has_existing_dependencies = self._check_and_warn_about_dependencies(subfolder_content)
|
|
1017
|
+
if self._has_existing_dependencies:
|
|
1018
|
+
print(
|
|
1019
|
+
f"\nWarning: Subfolder pyproject.toml contains a non-empty 'dependencies' field.",
|
|
1020
|
+
file=sys.stderr,
|
|
1021
|
+
)
|
|
1022
|
+
print(
|
|
1023
|
+
f" - Automatic dependency detection will be SKIPPED.",
|
|
1024
|
+
file=sys.stderr,
|
|
1025
|
+
)
|
|
1026
|
+
print(
|
|
1027
|
+
f" - To enable automatic dependency detection, remove or empty the 'dependencies' field in the subfolder's pyproject.toml.\n",
|
|
1028
|
+
file=sys.stderr,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
# Merge missing fields from parent pyproject.toml if it exists
|
|
1032
|
+
if original_pyproject.exists():
|
|
1033
|
+
try:
|
|
1034
|
+
parent_content = original_pyproject.read_text(encoding="utf-8")
|
|
1035
|
+
subfolder_content = self._merge_from_parent_pyproject(subfolder_content, parent_content)
|
|
1036
|
+
except Exception as e:
|
|
1037
|
+
print(
|
|
1038
|
+
f"Warning: Could not merge from parent pyproject.toml: {e}",
|
|
1039
|
+
file=sys.stderr,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
637
1042
|
# Adjust packages path to be relative to project root
|
|
638
1043
|
# This must be called AFTER _create_temp_package_directory() so _get_package_structure()
|
|
639
1044
|
# can find the temporary directory
|
|
640
1045
|
adjusted_content = self._adjust_subfolder_pyproject_packages_path(subfolder_content)
|
|
641
1046
|
|
|
1047
|
+
# Update version in subfolder pyproject.toml to match calculated version
|
|
1048
|
+
# This ensures the built package version matches what we're trying to publish
|
|
1049
|
+
adjusted_content = self._update_version_in_pyproject(adjusted_content)
|
|
1050
|
+
|
|
642
1051
|
# Read exclude patterns from root pyproject.toml and inject them (if it exists)
|
|
643
1052
|
exclude_patterns = []
|
|
644
1053
|
if original_pyproject.exists():
|
|
@@ -1213,6 +1622,14 @@ class SubfolderBuildConfig:
|
|
|
1213
1622
|
Args:
|
|
1214
1623
|
dependencies: List of third-party package names to add (e.g., ["pypdf", "requests"])
|
|
1215
1624
|
"""
|
|
1625
|
+
# Skip if subfolder toml already has dependencies
|
|
1626
|
+
if self._has_existing_dependencies:
|
|
1627
|
+
print(
|
|
1628
|
+
f"Skipping automatic dependency detection - subfolder pyproject.toml already has dependencies defined.",
|
|
1629
|
+
file=sys.stderr,
|
|
1630
|
+
)
|
|
1631
|
+
return
|
|
1632
|
+
|
|
1216
1633
|
if not self.temp_pyproject or not self.temp_pyproject.exists():
|
|
1217
1634
|
print(
|
|
1218
1635
|
f"Warning: Cannot add third-party dependencies - pyproject.toml not found at {self.temp_pyproject}",
|