python-package-folder 3.1.3__py3-none-any.whl → 4.1.0__py3-none-any.whl

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.
@@ -0,0 +1,39 @@
1
+ """
2
+ Hatch build hook to automatically include all files from the scripts directory.
3
+
4
+ This hook ensures all non-Python files in the scripts directory are included
5
+ in the wheel without creating duplicates, and automatically includes any new
6
+ files added to the directory without requiring manual configuration updates.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
13
+
14
+
15
+ class CustomBuildHook(BuildHookInterface):
16
+ """Build hook to include all files from the scripts directory."""
17
+
18
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
19
+ """Initialize the build hook and add scripts directory files."""
20
+ # Get the source directory for the package
21
+ source_dir = Path(self.root) / "src" / "python_package_folder"
22
+ scripts_dir = source_dir / "scripts"
23
+
24
+ # If scripts directory exists, include all files from it
25
+ if scripts_dir.exists() and scripts_dir.is_dir():
26
+ # Add all files from scripts directory to force-include
27
+ # This ensures they're included in the wheel at the correct location
28
+ for script_file in scripts_dir.iterdir():
29
+ if script_file.is_file():
30
+ # Calculate relative paths
31
+ source_path = script_file.relative_to(self.root)
32
+ # Target path inside the wheel package
33
+ target_path = f"python_package_folder/scripts/{script_file.name}"
34
+
35
+ # Add to force-include (hatchling will handle this)
36
+ # We need to add it to build_data['force_include']
37
+ if "force_include" not in build_data:
38
+ build_data["force_include"] = {}
39
+ build_data["force_include"][str(source_path)] = target_path
@@ -14,10 +14,157 @@ import subprocess
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
+ try:
18
+ from importlib import resources
19
+ except ImportError:
20
+ import importlib_resources as resources # type: ignore[no-redef]
21
+
17
22
  from .manager import BuildManager
18
23
  from .utils import find_project_root, find_source_directory
19
24
 
20
25
 
26
+ def resolve_version_via_semantic_release(
27
+ project_root: Path,
28
+ subfolder_path: Path | None = None,
29
+ package_name: str | None = None,
30
+ repository: str | None = None,
31
+ repository_url: str | None = None,
32
+ ) -> str | None:
33
+ """
34
+ Resolve the next version using semantic-release via Node.js script.
35
+
36
+ Args:
37
+ project_root: Root directory of the project
38
+ subfolder_path: Optional path to subfolder (relative to project_root) for Workflow 1
39
+ package_name: Optional package name for subfolder builds
40
+ repository: Optional target repository ('pypi', 'testpypi', or 'azure')
41
+ repository_url: Optional repository URL (required for Azure Artifacts)
42
+
43
+ Returns:
44
+ Version string if a release is determined, None if no release or error
45
+ """
46
+ # Try to find the script in multiple locations:
47
+ # 1. Project root / scripts (for development or when script is in repo)
48
+ # 2. Package installation directory / scripts (for installed package)
49
+ # - For normal installs: direct file path
50
+ # - For zip/pex installs: extract to temporary file using as_file()
51
+
52
+ # Track temporary file context for cleanup
53
+ temp_script_context = None
54
+
55
+ try:
56
+ # First, try project root (development)
57
+ dev_script = project_root / "scripts" / "get-next-version.cjs"
58
+ if dev_script.exists():
59
+ script_path = dev_script
60
+ else:
61
+ # Try to locate script in installed package using importlib.resources
62
+ script_path = None
63
+ try:
64
+ package = resources.files("python_package_folder")
65
+ script_resource = package / "scripts" / "get-next-version.cjs"
66
+ if script_resource.is_file():
67
+ # Try direct path conversion first (normal file system install)
68
+ try:
69
+ script_path_candidate = Path(str(script_resource))
70
+ if script_path_candidate.exists():
71
+ script_path = script_path_candidate
72
+ except (TypeError, ValueError):
73
+ pass
74
+
75
+ # If direct path didn't work, try as_file() for zip/pex installs
76
+ if script_path is None:
77
+ try:
78
+ temp_script_context = resources.as_file(script_resource)
79
+ script_path = temp_script_context.__enter__()
80
+ except (TypeError, ValueError, OSError):
81
+ pass
82
+ except (ImportError, ModuleNotFoundError, TypeError, AttributeError, OSError):
83
+ pass
84
+
85
+ # Fallback: try relative to package directory
86
+ if script_path is None:
87
+ package_dir = Path(__file__).parent
88
+ fallback_script = package_dir / "scripts" / "get-next-version.cjs"
89
+ if fallback_script.exists():
90
+ script_path = fallback_script
91
+
92
+ if not script_path:
93
+ return None
94
+
95
+ # Build command arguments
96
+ cmd = ["node", str(script_path), str(project_root)]
97
+ if subfolder_path and package_name:
98
+ # Workflow 1: subfolder build
99
+ rel_path = (
100
+ subfolder_path.relative_to(project_root)
101
+ if subfolder_path.is_absolute()
102
+ else subfolder_path
103
+ )
104
+ cmd.extend([str(rel_path), package_name])
105
+ elif package_name:
106
+ # Main package build with package_name (for registry queries)
107
+ # Pass null for subfolder_path, then package_name
108
+ cmd.extend(["", package_name])
109
+ # Workflow 2: main package without package_name (no additional args needed)
110
+
111
+ # Add repository information if provided
112
+ if repository:
113
+ cmd.append(repository)
114
+ if repository_url:
115
+ cmd.append(repository_url)
116
+
117
+ result = subprocess.run(
118
+ cmd,
119
+ capture_output=True,
120
+ text=True,
121
+ cwd=project_root,
122
+ check=False,
123
+ )
124
+
125
+ if result.returncode != 0:
126
+ # Log error details for debugging
127
+ if result.stderr:
128
+ print(
129
+ f"Warning: semantic-release version resolution failed: {result.stderr}",
130
+ file=sys.stderr,
131
+ )
132
+ elif result.stdout:
133
+ print(
134
+ f"Warning: semantic-release version resolution failed: {result.stdout}",
135
+ file=sys.stderr,
136
+ )
137
+ return None
138
+
139
+ version = result.stdout.strip()
140
+ if version and version != "none":
141
+ return version
142
+
143
+ return None
144
+ except FileNotFoundError:
145
+ # Node.js not found
146
+ print(
147
+ "Warning: Node.js not found. Cannot resolve version via semantic-release.",
148
+ file=sys.stderr,
149
+ )
150
+ return None
151
+ except Exception as e:
152
+ # Other errors (e.g., permission issues, script not found)
153
+ print(
154
+ f"Warning: Error resolving version via semantic-release: {e}",
155
+ file=sys.stderr,
156
+ )
157
+ return None
158
+ finally:
159
+ # Clean up temporary file if we extracted from zip/pex
160
+ # This must be at function level to ensure cleanup even on early return
161
+ if temp_script_context is not None:
162
+ try:
163
+ temp_script_context.__exit__(None, None, None)
164
+ except Exception:
165
+ pass
166
+
167
+
21
168
  def main() -> int:
22
169
  """
23
170
  Main entry point for the build script.
@@ -77,7 +224,7 @@ def main() -> int:
77
224
  )
78
225
  parser.add_argument(
79
226
  "--version",
80
- help="Set a specific version before building (PEP 440 format, e.g., '1.2.3'). Required for subfolder builds.",
227
+ help="Set a specific version before building (PEP 440 format, e.g., '1.2.3'). Optional: if omitted, version will be resolved via semantic-release when needed.",
81
228
  )
82
229
  parser.add_argument(
83
230
  "--package-name",
@@ -151,18 +298,81 @@ def main() -> int:
151
298
  sys.exit(result.returncode)
152
299
 
153
300
  # Check if building a subfolder (not the main src/)
154
- is_subfolder = not src_dir.is_relative_to(project_root / "src") or (
155
- src_dir != project_root / "src" and src_dir != project_root
301
+ # A subfolder must be within the project root but not the main src/ directory
302
+ is_subfolder = (
303
+ src_dir.is_relative_to(project_root)
304
+ and src_dir != project_root / "src"
305
+ and src_dir != project_root
156
306
  )
157
307
 
158
- # For subfolder builds, version is required
159
- if is_subfolder and not args.version and (not args.analyze_only):
160
- print(
161
- "Error: --version is required when building from a subfolder.\n"
162
- "Subfolders must be built as separate packages with their own version.",
163
- file=sys.stderr,
164
- )
165
- return 1
308
+ # Resolve version via semantic-release if not provided and needed
309
+ resolved_version = args.version
310
+ if not resolved_version and not args.analyze_only:
311
+ # Version is needed for subfolder builds or when publishing main package
312
+ if is_subfolder or args.publish:
313
+ print("No --version provided, attempting to resolve via semantic-release...")
314
+ # Get repository info if publishing
315
+ repository = args.publish if args.publish else None
316
+ repository_url = args.repository_url if args.publish else None
317
+
318
+ if is_subfolder:
319
+ # Workflow 1: subfolder build
320
+ # src_dir is guaranteed to be relative to project_root due to is_subfolder check
321
+ package_name = args.package_name or src_dir.name.replace("_", "-").replace(
322
+ " ", "-"
323
+ ).lower().strip("-")
324
+ subfolder_rel_path = src_dir.relative_to(project_root)
325
+ resolved_version = resolve_version_via_semantic_release(
326
+ project_root,
327
+ subfolder_rel_path,
328
+ package_name,
329
+ repository=repository,
330
+ repository_url=repository_url,
331
+ )
332
+ else:
333
+ # Workflow 2: main package
334
+ # For main package, we need package_name from pyproject.toml for registry queries
335
+ package_name_for_registry = None
336
+ if repository:
337
+ try:
338
+ import tomllib
339
+ pyproject_path = project_root / "pyproject.toml"
340
+ if pyproject_path.exists():
341
+ with open(pyproject_path, "rb") as f:
342
+ data = tomllib.load(f)
343
+ package_name_for_registry = data.get("project", {}).get("name")
344
+ except Exception:
345
+ pass
346
+
347
+ resolved_version = resolve_version_via_semantic_release(
348
+ project_root,
349
+ subfolder_path=None,
350
+ package_name=package_name_for_registry,
351
+ repository=repository,
352
+ repository_url=repository_url,
353
+ )
354
+
355
+ if resolved_version:
356
+ print(f"Resolved version via semantic-release: {resolved_version}")
357
+ else:
358
+ error_msg = (
359
+ "Could not resolve version via semantic-release.\n"
360
+ "This could mean:\n"
361
+ " - No release is needed (no relevant commits)\n"
362
+ " - semantic-release is not installed or configured\n"
363
+ " - Node.js is not available\n\n"
364
+ "Please either:\n"
365
+ " - Install semantic-release: npm install -g semantic-release"
366
+ )
367
+ if is_subfolder:
368
+ error_msg += "\n - Install semantic-release-commit-filter: npm install -g semantic-release-commit-filter"
369
+ error_msg += "\n - Or provide --version explicitly"
370
+ print(f"Error: {error_msg}", file=sys.stderr)
371
+ return 1
372
+
373
+ # Use resolved version for the rest of the flow
374
+ if resolved_version:
375
+ args.version = resolved_version
166
376
 
167
377
  if args.publish:
168
378
  manager.build_and_publish(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 3.1.3
3
+ Version: 4.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>
@@ -75,6 +75,9 @@ cd src/api_package
75
75
  # Build and publish to TestPyPI with version 1.2.0
76
76
  python-package-folder --publish testpypi --version 1.2.0
77
77
 
78
+ # Or publish to PyPI with automatic version resolution via semantic-release
79
+ python-package-folder --publish pypi
80
+
78
81
  # Or publish to PyPI with a custom package name
79
82
  python-package-folder --publish pypi --version 1.2.0 --package-name "my-api-package"
80
83
 
@@ -169,6 +172,13 @@ uv add twine
169
172
 
170
173
  **For secure credential storage**: `keyring` is optional but recommended (install with `pip install keyring`)
171
174
 
175
+ **For automatic version resolution**: When using `--version` optional mode (automatic version resolution via semantic-release), you'll need:
176
+ - Node.js and npm (or npx)
177
+ - semantic-release: `npm install -g semantic-release`
178
+ - For subfolder builds: semantic-release-commit-filter: `npm install -g semantic-release-commit-filter`
179
+
180
+ Alternatively, install these as devDependencies in your project's `package.json`.
181
+
172
182
 
173
183
  ## Quick Start
174
184
 
@@ -182,9 +192,13 @@ Useful for monorepos containing many subfolders that may need publishing as stan
182
192
  # First cd to the specific subfolder
183
193
  cd src/subfolder_to_build_and_publish
184
194
 
185
- # Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
195
+ # Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
196
+ # Version can be provided explicitly or resolved automatically via semantic-release
186
197
  python-package-folder --publish testpypi --version 0.0.2
187
198
 
199
+ # Or let semantic-release determine the next version automatically (requires semantic-release setup)
200
+ python-package-folder --publish testpypi
201
+
188
202
  # Only analyse (no building)
189
203
  cd src/subfolder_to_build_and_publish
190
204
  python-package-folder --analyze-only
@@ -457,33 +471,89 @@ The `--version` option:
457
471
  **Version Format**: Versions must follow PEP 440 (e.g., `1.2.3`, `1.2.3a1`, `1.2.3.post1`, `1.2.3.dev1`)
458
472
 
459
473
 
474
+ ### Automatic Version Resolution (semantic-release)
475
+
476
+ When `--version` is not provided, the tool can automatically determine the next version using semantic-release. This requires Node.js, npm, and semantic-release to be installed.
477
+
478
+ **Version Detection:**
479
+ - **Baseline version**:
480
+ - **Registry Query (Preferred)**: When publishing to a repository (PyPI, TestPyPI, or Azure Artifacts), the tool queries the target registry for the latest published version and uses it as the baseline for version calculation. This ensures version calculations are based on what's actually published, not just git tags.
481
+ - **Git Tags (Fallback)**: If the package doesn't exist on the registry yet (first release) or if registry query fails, the tool falls back to using git tags to determine the starting version.
482
+ - **New version to publish**: After determining the baseline version, [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) analyzes commits since that version to calculate the next version bump (major, minor, or patch) based on [_conventional commit_](https://www.conventionalcommits.org/en/v1.0.0/) messages.
483
+
484
+ **For subfolder builds (Workflow 1):**
485
+ - Uses per-package tags: `{package-name}-v{version}` (e.g., `my-package-v1.2.3`)
486
+ - Queries the target registry for the latest published version of the subfolder package
487
+ - Filters commits to only those affecting the subfolder path
488
+ - **Commit filtering behavior**: Only commits that modify files within the subfolder path are considered for version calculation. Commits that only target files outside the subfolder are excluded. For example:
489
+ - `fix: update my_subfolder/foo.py` → **Included** (affects subfolder)
490
+ - `feat: add feature to other_package/bar.py` → **Excluded** (doesn't affect subfolder)
491
+ - `fix: update my_subfolder/baz.py and shared/utils.py` → **Included** (affects subfolder, even if it also touches files outside)
492
+ - Requires `semantic-release-commit-filter` plugin
493
+
494
+ **For main package builds (Workflow 2):**
495
+ - Uses repo-level tags: `v{version}` (e.g., `v1.2.3`)
496
+ - Queries the target registry for the latest published version when publishing
497
+ - Analyzes all commits in the repository
498
+
499
+ **Registry Support:**
500
+ - **PyPI**: Fully supported via JSON API (`https://pypi.org/pypi/{package-name}/json`)
501
+ - **TestPyPI**: Fully supported via JSON API (`https://test.pypi.org/pypi/{package-name}/json`)
502
+ - **Azure Artifacts**: Basic support with fallback to git tags. Azure Artifacts uses a different API format and may require authentication, so if the query fails, the tool automatically falls back to git tags.
503
+
504
+ **Setup:**
505
+ ```bash
506
+ # Install semantic-release globally
507
+ npm install -g semantic-release
508
+
509
+ # For subfolder builds, also install semantic-release-commit-filter
510
+ npm install -g semantic-release-commit-filter
511
+ ```
512
+
513
+ **Usage:**
514
+ ```bash
515
+ # Subfolder build - version resolved automatically
516
+ cd src/my_subfolder
517
+ python-package-folder --publish pypi
518
+
519
+ # Main package - version resolved automatically
520
+ python-package-folder --publish pypi
521
+ ```
522
+
523
+ **Requirements:**
524
+ - Conventional commits (e.g., `fix:`, `feat:`, `BREAKING CHANGE:`) are required for semantic-release to determine version bumps
525
+ - The tool will fall back to requiring `--version` explicitly if semantic-release is not available or determines no release is needed
526
+
460
527
  ### Subfolder Versioning
461
528
 
462
529
  When building from a subdirectory (not the main `src/` directory), the tool automatically detects the subfolder and sets up the build configuration:
463
530
 
464
531
  ```bash
465
- # Build a subfolder as a separate package (version recommended but not required)
532
+ # Build a subfolder as a separate package with explicit version
466
533
  cd my_project/subfolder_to_build
467
534
  python-package-folder --version "1.0.0" --publish pypi
468
535
 
536
+ # Or let semantic-release determine the version automatically
537
+ python-package-folder --publish pypi
538
+
469
539
  # With custom package name
470
540
  python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
471
-
472
- # Version defaults to "0.0.0" if not specified (with a warning)
473
- python-package-folder --publish pypi
474
541
  ```
475
542
 
476
543
  For subfolder builds:
477
544
  - **Automatic detection**: The tool automatically detects subfolder builds
545
+ - **Version resolution**:
546
+ - If `--version` is provided: Uses the explicit version
547
+ - If `--version` is omitted: Attempts to resolve via semantic-release (requires setup)
548
+ - If semantic-release is unavailable or determines no release: Requires `--version` explicitly
478
549
  - **pyproject.toml handling**:
479
550
  - If `pyproject.toml` exists in subfolder: Uses that file (copied to project root temporarily)
480
551
  - If no `pyproject.toml` in subfolder: Creates temporary one with correct package structure
481
- - **Version**: Recommended but not required when creating temporary pyproject.toml. If not provided, defaults to `0.0.0` with a warning. Ignored if subfolder has its own `pyproject.toml`.
482
552
  - **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`). Only used when creating temporary pyproject.toml.
483
553
  - **Restoration**: Original `pyproject.toml` is restored after build
484
554
  - **Temporary configuration**: Creates a temporary `pyproject.toml` with:
485
555
  - Custom package name (from `--package-name` or derived)
486
- - Specified version
556
+ - Specified or resolved version
487
557
  - Correct package path for hatchling
488
558
  - Dependency group from parent (if `--dependency-group` is specified)
489
559
  - **Package initialization**: Automatically creates `__init__.py` if the subfolder doesn't have one (required for hatchling)
@@ -686,7 +756,8 @@ options:
686
756
  --password PASSWORD Password/token for publishing (will prompt if not provided)
687
757
  --skip-existing Skip files that already exist on the repository
688
758
  --version VERSION Set a specific version before building (PEP 440 format).
689
- Required for subfolder builds.
759
+ Optional: if omitted, version will be resolved via
760
+ semantic-release (requires Node.js and semantic-release setup).
690
761
  --package-name PACKAGE_NAME
691
762
  Package name for subfolder builds (default: derived from
692
763
  source directory name)
@@ -1,17 +1,18 @@
1
1
  python_package_folder/__init__.py,sha256=DQt-uldOEKfh0MUqCvKdeNKOnpuOvpb7blYvXMyO9Wc,719
2
2
  python_package_folder/__main__.py,sha256=a-__-VLhYw-J7S7CsHdhtEvQr3RiAZxiYDvKhKTgMX4,291
3
+ python_package_folder/_hatch_build.py,sha256=HNuhBz5e6uoDNVPKHFcX-8H23u6qyXNM8KO9DgRGXMM,1844
3
4
  python_package_folder/analyzer.py,sha256=cmTNUDCWBIh3XZ_mShlQVG1P9NN_oe3FUBTirVtYfTQ,16709
4
5
  python_package_folder/finder.py,sha256=RPidZ7LKCFuQ_KgCFIZdHWPXsZIDor3M4C0hKeYW7EI,11799
5
6
  python_package_folder/manager.py,sha256=Z9RPg0ZQ7jZhmEXfCzX9OrD_oiA5p2Pnm5Y9tgW3ObQ,55970
6
7
  python_package_folder/publisher.py,sha256=TSjdOvxvnWLbJCnduTK_xZBRfvsrq9kpEH-sfebeWkU,13507
7
8
  python_package_folder/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- python_package_folder/python_package_folder.py,sha256=RPsqRcIy_LjzzTHdp4qdtFJ4-4xhtR_0YLIC0RlUxFo,8841
9
+ python_package_folder/python_package_folder.py,sha256=xSMUD_uiCOVHDDT4WNso9AxAYNyqhKI103G2rJztaXw,17907
9
10
  python_package_folder/subfolder_build.py,sha256=oH_KKLJIMByUZCl8y3AyohUO6Om0OvsIQ7Xg1fkd3jE,38782
10
11
  python_package_folder/types.py,sha256=3yeSRR5p_3PDKEAaehW_RJ7NwJHexOIeA08bGaT1iSY,2368
11
12
  python_package_folder/utils.py,sha256=lIkWsFKeAYAJ9TDUM99T4pUBHJVbUvCdUgkWQN-LUho,3111
12
13
  python_package_folder/version.py,sha256=kIDP6S9trEfs9gj7lBYGxrWm4RPssRla24UtlO9Jkh4,9111
13
- python_package_folder-3.1.3.dist-info/METADATA,sha256=dkCtq6nLmfiygTwYXzgVomMejX4AfQTM1U3LYdev8zQ,33282
14
- python_package_folder-3.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- python_package_folder-3.1.3.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
16
- python_package_folder-3.1.3.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
17
- python_package_folder-3.1.3.dist-info/RECORD,,
14
+ python_package_folder-4.1.0.dist-info/METADATA,sha256=yk49H6KBuadlyqKiHeC589X6pBvUTBVd4YXs6idsnYE,37517
15
+ python_package_folder-4.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ python_package_folder-4.1.0.dist-info/entry_points.txt,sha256=ttu4wAhoYSHGhWQNercLz9IVTTpXxhVlRA9vSTvaLe0,91
17
+ python_package_folder-4.1.0.dist-info/licenses/LICENSE,sha256=vNgRJh8YiecqZoZld7TtwPI5I72HIymKD9g32fiJjCE,1073
18
+ python_package_folder-4.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any