python-package-folder 1.1.3__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.
- python_package_folder/__init__.py +24 -0
- python_package_folder/__main__.py +13 -0
- python_package_folder/analyzer.py +313 -0
- python_package_folder/finder.py +234 -0
- python_package_folder/manager.py +539 -0
- python_package_folder/publisher.py +310 -0
- python_package_folder/py.typed +0 -0
- python_package_folder/python_package_folder.py +239 -0
- python_package_folder/subfolder_build.py +477 -0
- python_package_folder/types.py +66 -0
- python_package_folder/utils.py +106 -0
- python_package_folder/version.py +253 -0
- python_package_folder-1.1.3.dist-info/METADATA +795 -0
- python_package_folder-1.1.3.dist-info/RECORD +17 -0
- python_package_folder-1.1.3.dist-info/WHEEL +4 -0
- python_package_folder-1.1.3.dist-info/entry_points.txt +2 -0
- python_package_folder-1.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package publishing functionality.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to publish built packages to various
|
|
5
|
+
repositories including PyPI, PyPI Test, and Azure Artifacts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import getpass
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import keyring
|
|
18
|
+
except ImportError:
|
|
19
|
+
keyring = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Repository(Enum):
|
|
23
|
+
"""
|
|
24
|
+
Supported package repositories.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
PYPI: Official Python Package Index (https://pypi.org)
|
|
28
|
+
PYPI_TEST: Test PyPI for testing package uploads (https://test.pypi.org)
|
|
29
|
+
AZURE: Azure Artifacts feed (requires custom repository_url)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
PYPI = "pypi"
|
|
33
|
+
PYPI_TEST = "testpypi"
|
|
34
|
+
AZURE = "azure"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Publisher:
|
|
38
|
+
"""
|
|
39
|
+
Handles publishing Python packages to various repositories.
|
|
40
|
+
|
|
41
|
+
This class manages the publishing process, including credential handling
|
|
42
|
+
and repository configuration. It uses twine under the hood for actual publishing.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
repository: Target repository for publishing
|
|
46
|
+
dist_dir: Directory containing built distribution files
|
|
47
|
+
repository_url: Custom repository URL (for Azure or custom PyPI servers)
|
|
48
|
+
username: Username for authentication (optional, can be prompted)
|
|
49
|
+
password: Password/token for authentication (optional, can be prompted)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
repository: Repository | str,
|
|
55
|
+
dist_dir: Path | None = None,
|
|
56
|
+
repository_url: str | None = None,
|
|
57
|
+
username: str | None = None,
|
|
58
|
+
password: str | None = None,
|
|
59
|
+
package_name: str | None = None,
|
|
60
|
+
version: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Initialize the publisher.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
repository: Target repository (Repository enum or string)
|
|
67
|
+
dist_dir: Directory containing distribution files (default: dist/)
|
|
68
|
+
repository_url: Custom repository URL (required for Azure)
|
|
69
|
+
username: Username for authentication (will prompt if not provided)
|
|
70
|
+
password: Password/token for authentication (will prompt if not provided)
|
|
71
|
+
package_name: Package name to filter distribution files (optional)
|
|
72
|
+
version: Package version to filter distribution files (optional)
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(repository, str):
|
|
75
|
+
try:
|
|
76
|
+
self.repository = Repository(repository.lower())
|
|
77
|
+
except ValueError as err:
|
|
78
|
+
valid_repos = ", ".join(r.value for r in Repository)
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid repository: {repository}. Must be one of: {valid_repos}"
|
|
81
|
+
) from err
|
|
82
|
+
else:
|
|
83
|
+
self.repository = repository
|
|
84
|
+
|
|
85
|
+
self.dist_dir = dist_dir or Path("dist")
|
|
86
|
+
self.repository_url = repository_url
|
|
87
|
+
self.username = username
|
|
88
|
+
self.password = password
|
|
89
|
+
self.package_name = package_name
|
|
90
|
+
self.version = version
|
|
91
|
+
|
|
92
|
+
def _get_repository_url(self) -> str:
|
|
93
|
+
"""Get the repository URL based on the selected repository."""
|
|
94
|
+
if self.repository_url:
|
|
95
|
+
return self.repository_url
|
|
96
|
+
|
|
97
|
+
if self.repository == Repository.PYPI:
|
|
98
|
+
return "https://upload.pypi.org/legacy/"
|
|
99
|
+
elif self.repository == Repository.PYPI_TEST:
|
|
100
|
+
return "https://test.pypi.org/legacy/"
|
|
101
|
+
elif self.repository == Repository.AZURE:
|
|
102
|
+
if not self.repository_url:
|
|
103
|
+
raise ValueError("repository_url is required for Azure Artifacts")
|
|
104
|
+
return self.repository_url
|
|
105
|
+
|
|
106
|
+
raise ValueError(f"Unknown repository: {self.repository}")
|
|
107
|
+
|
|
108
|
+
def _get_credentials(self) -> tuple[str, str]:
|
|
109
|
+
"""
|
|
110
|
+
Get credentials for publishing.
|
|
111
|
+
|
|
112
|
+
Prompts for username and password/token if not already provided.
|
|
113
|
+
Uses keyring if available to store/retrieve credentials securely.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Tuple of (username, password/token)
|
|
117
|
+
"""
|
|
118
|
+
username = self.username
|
|
119
|
+
password = self.password
|
|
120
|
+
|
|
121
|
+
# Try to get from keyring if available
|
|
122
|
+
if keyring and not username:
|
|
123
|
+
try:
|
|
124
|
+
username = keyring.get_password(
|
|
125
|
+
f"python-package-folder-{self.repository.value}", "username"
|
|
126
|
+
)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
if keyring and not password:
|
|
131
|
+
try:
|
|
132
|
+
password = keyring.get_password(
|
|
133
|
+
f"python-package-folder-{self.repository.value}", username or "token"
|
|
134
|
+
)
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
# Prompt if still not available
|
|
139
|
+
if not username:
|
|
140
|
+
username = input(f"Enter username for {self.repository.value}: ").strip()
|
|
141
|
+
if not username:
|
|
142
|
+
raise ValueError("Username is required")
|
|
143
|
+
|
|
144
|
+
if not password:
|
|
145
|
+
if self.repository == Repository.AZURE:
|
|
146
|
+
prompt = f"Enter Azure Artifacts token for {username}: "
|
|
147
|
+
else:
|
|
148
|
+
prompt = f"Enter PyPI token for {username} (or __token__ for API token): "
|
|
149
|
+
password = getpass.getpass(prompt)
|
|
150
|
+
if not password:
|
|
151
|
+
raise ValueError("Password/token is required")
|
|
152
|
+
|
|
153
|
+
# Auto-detect if password is an API token and adjust username
|
|
154
|
+
if password.startswith("pypi-") or password.startswith("pypi_Ag"):
|
|
155
|
+
# This is an API token, username should be __token__
|
|
156
|
+
if username != "__token__":
|
|
157
|
+
print(
|
|
158
|
+
f"Note: Detected API token. Using '__token__' as username instead of '{username}'",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
username = "__token__"
|
|
162
|
+
|
|
163
|
+
# Store in keyring if available
|
|
164
|
+
if keyring:
|
|
165
|
+
try:
|
|
166
|
+
keyring.set_password(
|
|
167
|
+
f"python-package-folder-{self.repository.value}", "username", username
|
|
168
|
+
)
|
|
169
|
+
keyring.set_password(
|
|
170
|
+
f"python-package-folder-{self.repository.value}", username, password
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
# Keyring storage is optional, continue if it fails
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
return username, password
|
|
177
|
+
|
|
178
|
+
def _check_twine_installed(self) -> bool:
|
|
179
|
+
"""Check if twine is installed."""
|
|
180
|
+
try:
|
|
181
|
+
subprocess.run(["twine", "--version"], capture_output=True, check=True)
|
|
182
|
+
return True
|
|
183
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def publish(self, skip_existing: bool = False) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Publish the package to the selected repository.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
skip_existing: If True, skip files that already exist on the repository
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If twine is not installed or credentials are invalid
|
|
195
|
+
subprocess.CalledProcessError: If publishing fails
|
|
196
|
+
"""
|
|
197
|
+
if not self._check_twine_installed():
|
|
198
|
+
raise ValueError("twine is required for publishing. Install it with: pip install twine")
|
|
199
|
+
|
|
200
|
+
if not self.dist_dir.exists():
|
|
201
|
+
raise ValueError(f"Distribution directory not found: {self.dist_dir}")
|
|
202
|
+
|
|
203
|
+
all_dist_files = list(self.dist_dir.glob("*.whl")) + list(self.dist_dir.glob("*.tar.gz"))
|
|
204
|
+
|
|
205
|
+
# Filter files by package name and version if provided
|
|
206
|
+
if self.package_name and self.version:
|
|
207
|
+
# Normalize package name - try both hyphen and underscore variants
|
|
208
|
+
# Wheel names typically use hyphens, but source dists might use underscores
|
|
209
|
+
name_hyphen = self.package_name.replace("_", "-").lower()
|
|
210
|
+
name_underscore = self.package_name.replace("-", "_").lower()
|
|
211
|
+
name_original = self.package_name.lower()
|
|
212
|
+
|
|
213
|
+
# Try all name variants
|
|
214
|
+
name_variants = {name_hyphen, name_underscore, name_original}
|
|
215
|
+
version_str = self.version
|
|
216
|
+
|
|
217
|
+
dist_files = []
|
|
218
|
+
for f in all_dist_files:
|
|
219
|
+
# Get the base filename without extension
|
|
220
|
+
# For wheels: name-version-tag.whl -> name-version-tag
|
|
221
|
+
# For source: name-version.tar.gz -> name-version
|
|
222
|
+
stem = f.stem
|
|
223
|
+
if f.suffix == ".gz" and stem.endswith(".tar"):
|
|
224
|
+
# Handle .tar.gz files
|
|
225
|
+
stem = stem[:-4] # Remove .tar
|
|
226
|
+
|
|
227
|
+
# Check if filename starts with any name variant followed by version
|
|
228
|
+
matches = False
|
|
229
|
+
for name_variant in name_variants:
|
|
230
|
+
# Pattern: {name}-{version} or {name}-{version}-{tag}
|
|
231
|
+
if stem.startswith(f"{name_variant}-{version_str}"):
|
|
232
|
+
matches = True
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if matches:
|
|
236
|
+
dist_files.append(f)
|
|
237
|
+
else:
|
|
238
|
+
dist_files = all_dist_files
|
|
239
|
+
|
|
240
|
+
if not dist_files:
|
|
241
|
+
if self.package_name and self.version:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"No distribution files found matching package '{self.package_name}' "
|
|
244
|
+
f"version '{self.version}' in {self.dist_dir}"
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError(f"No distribution files found in {self.dist_dir}")
|
|
248
|
+
|
|
249
|
+
username, password = self._get_credentials()
|
|
250
|
+
repo_url = self._get_repository_url()
|
|
251
|
+
|
|
252
|
+
# Build twine command
|
|
253
|
+
cmd = ["twine", "upload"]
|
|
254
|
+
if skip_existing:
|
|
255
|
+
cmd.append("--skip-existing")
|
|
256
|
+
cmd.extend(["--repository-url", repo_url])
|
|
257
|
+
cmd.extend(["--username", username])
|
|
258
|
+
cmd.extend(["--password", password])
|
|
259
|
+
cmd.extend([str(f) for f in dist_files])
|
|
260
|
+
|
|
261
|
+
print(f"\nPublishing to {self.repository.value} at {repo_url}")
|
|
262
|
+
print(f"Files to upload: {len(dist_files)}")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
subprocess.run(cmd, check=True, text=True)
|
|
266
|
+
print(f"\n✓ Successfully published to {self.repository.value}")
|
|
267
|
+
except subprocess.CalledProcessError as e:
|
|
268
|
+
print(f"\n✗ Failed to publish to {self.repository.value}", file=sys.stderr)
|
|
269
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
270
|
+
raise
|
|
271
|
+
|
|
272
|
+
def publish_interactive(self, skip_existing: bool = False) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Publish with interactive credential prompts.
|
|
275
|
+
|
|
276
|
+
This is a convenience method that ensures credentials are prompted
|
|
277
|
+
even if they were provided during initialization.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
skip_existing: If True, skip files that already exist on the repository
|
|
281
|
+
"""
|
|
282
|
+
# Clear cached credentials to force prompt
|
|
283
|
+
self.username = None
|
|
284
|
+
self.password = None
|
|
285
|
+
self.publish(skip_existing=skip_existing)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_repository_help() -> str:
|
|
289
|
+
"""
|
|
290
|
+
Get help text for repository configuration.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Helpful text about repository options and configuration
|
|
294
|
+
"""
|
|
295
|
+
return """
|
|
296
|
+
Repository Options:
|
|
297
|
+
- pypi: Official Python Package Index (https://pypi.org)
|
|
298
|
+
- testpypi: Test PyPI for testing package uploads (https://test.pypi.org)
|
|
299
|
+
- azure: Azure Artifacts feed (requires repository_url)
|
|
300
|
+
|
|
301
|
+
For PyPI/TestPyPI:
|
|
302
|
+
- Username: Your PyPI username or '__token__' for API tokens
|
|
303
|
+
- Password: Your PyPI password or API token
|
|
304
|
+
|
|
305
|
+
For Azure Artifacts:
|
|
306
|
+
- Username: Your Azure username or feed name
|
|
307
|
+
- Password: Personal Access Token (PAT) with packaging permissions
|
|
308
|
+
- Repository URL: Your Azure Artifacts feed URL
|
|
309
|
+
Example: https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload
|
|
310
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for the python-package-folder package.
|
|
3
|
+
|
|
4
|
+
This module provides the command-line interface for the package.
|
|
5
|
+
It can be invoked via:
|
|
6
|
+
- The `python-package-folder` command (after installation)
|
|
7
|
+
- `python -m python_package_folder`
|
|
8
|
+
- Direct import and call to main()
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .manager import BuildManager
|
|
18
|
+
from .utils import find_project_root, find_source_directory
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> int:
|
|
22
|
+
"""
|
|
23
|
+
Main entry point for the build script.
|
|
24
|
+
|
|
25
|
+
Parses command-line arguments and runs the build process with
|
|
26
|
+
external dependency management.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Exit code (0 for success, non-zero for errors)
|
|
30
|
+
"""
|
|
31
|
+
import argparse
|
|
32
|
+
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description="Build Python package with external dependency management"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--project-root",
|
|
38
|
+
type=Path,
|
|
39
|
+
help="Root directory of the project (auto-detected from pyproject.toml if not specified)",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--src-dir",
|
|
43
|
+
type=Path,
|
|
44
|
+
help="Source directory to build (default: auto-detected from current directory or project_root/src)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--analyze-only",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="Only analyze imports, don't run build",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--build-command",
|
|
53
|
+
default="uv build",
|
|
54
|
+
help="Command to run for building (default: 'uv build')",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--publish",
|
|
58
|
+
choices=["pypi", "testpypi", "azure"],
|
|
59
|
+
help="Publish to repository after building (pypi, testpypi, or azure)",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--repository-url",
|
|
63
|
+
help="Custom repository URL (required for Azure Artifacts)",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--username",
|
|
67
|
+
help="Username for publishing (will prompt if not provided)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--password",
|
|
71
|
+
help="Password/token for publishing (will prompt if not provided)",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--skip-existing",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Skip files that already exist on the repository",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--version",
|
|
80
|
+
help="Set a specific version before building (PEP 440 format, e.g., '1.2.3'). Required for subfolder builds.",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--package-name",
|
|
84
|
+
help="Package name for subfolder builds (default: derived from source directory name)",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--dependency-group",
|
|
88
|
+
dest="dependency_group",
|
|
89
|
+
help="Dependency group name from parent pyproject.toml to include in subfolder build",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--no-restore-versioning",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Don't restore dynamic versioning after build (keeps static version)",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--exclude-pattern",
|
|
98
|
+
action="append",
|
|
99
|
+
dest="exclude_patterns",
|
|
100
|
+
help="Additional directory/file patterns to exclude from copying (e.g., '_SS', '__sandbox'). Can be specified multiple times.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
args = parser.parse_args()
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# Auto-detect project root if not specified
|
|
107
|
+
if args.project_root:
|
|
108
|
+
project_root = Path(args.project_root).resolve()
|
|
109
|
+
else:
|
|
110
|
+
project_root = find_project_root()
|
|
111
|
+
if project_root is None:
|
|
112
|
+
print(
|
|
113
|
+
"Error: Could not find project root (pyproject.toml not found).\n"
|
|
114
|
+
"Please run from a directory with pyproject.toml or specify --project-root",
|
|
115
|
+
file=sys.stderr,
|
|
116
|
+
)
|
|
117
|
+
return 1
|
|
118
|
+
print(f"Auto-detected project root: {project_root}")
|
|
119
|
+
|
|
120
|
+
# Determine source directory
|
|
121
|
+
if args.src_dir:
|
|
122
|
+
src_dir = Path(args.src_dir).resolve()
|
|
123
|
+
else:
|
|
124
|
+
# Auto-detect: use current directory if it has Python files, otherwise use project_root/src
|
|
125
|
+
src_dir = find_source_directory(project_root)
|
|
126
|
+
if src_dir:
|
|
127
|
+
print(f"Auto-detected source directory: {src_dir}")
|
|
128
|
+
else:
|
|
129
|
+
src_dir = project_root / "src"
|
|
130
|
+
|
|
131
|
+
manager = BuildManager(project_root, src_dir, exclude_patterns=args.exclude_patterns)
|
|
132
|
+
|
|
133
|
+
if args.analyze_only:
|
|
134
|
+
external_deps = manager.prepare_build()
|
|
135
|
+
print(f"\nFound {len(external_deps)} external dependencies:")
|
|
136
|
+
for dep in external_deps:
|
|
137
|
+
print(f" {dep.import_name}: {dep.source_path} -> {dep.target_path}")
|
|
138
|
+
manager.cleanup()
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
def build_cmd() -> None:
|
|
142
|
+
# Run build command from project root to ensure pyproject.toml is found
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
args.build_command,
|
|
145
|
+
shell=True,
|
|
146
|
+
check=False,
|
|
147
|
+
cwd=project_root,
|
|
148
|
+
)
|
|
149
|
+
if result.returncode != 0:
|
|
150
|
+
sys.exit(result.returncode)
|
|
151
|
+
|
|
152
|
+
# Check if building a subfolder (not the main src/)
|
|
153
|
+
is_subfolder = not src_dir.is_relative_to(project_root / "src") or (
|
|
154
|
+
src_dir != project_root / "src" and src_dir != project_root
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# For subfolder builds, version is required
|
|
158
|
+
if is_subfolder and not args.version and (not args.analyze_only):
|
|
159
|
+
print(
|
|
160
|
+
"Error: --version is required when building from a subfolder.\n"
|
|
161
|
+
"Subfolders must be built as separate packages with their own version.",
|
|
162
|
+
file=sys.stderr,
|
|
163
|
+
)
|
|
164
|
+
return 1
|
|
165
|
+
|
|
166
|
+
if args.publish:
|
|
167
|
+
manager.build_and_publish(
|
|
168
|
+
build_cmd,
|
|
169
|
+
repository=args.publish,
|
|
170
|
+
repository_url=args.repository_url,
|
|
171
|
+
username=args.username,
|
|
172
|
+
password=args.password,
|
|
173
|
+
skip_existing=args.skip_existing,
|
|
174
|
+
version=args.version,
|
|
175
|
+
restore_versioning=not args.no_restore_versioning,
|
|
176
|
+
package_name=args.package_name,
|
|
177
|
+
dependency_group=args.dependency_group,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
# Handle version setting even without publishing
|
|
181
|
+
if args.version:
|
|
182
|
+
# Check if subfolder build
|
|
183
|
+
if is_subfolder:
|
|
184
|
+
from .subfolder_build import SubfolderBuildConfig
|
|
185
|
+
|
|
186
|
+
package_name = args.package_name or src_dir.name.replace("_", "-").replace(
|
|
187
|
+
" ", "-"
|
|
188
|
+
).lower().strip("-")
|
|
189
|
+
subfolder_config = SubfolderBuildConfig(
|
|
190
|
+
project_root=project_root,
|
|
191
|
+
src_dir=src_dir,
|
|
192
|
+
package_name=package_name,
|
|
193
|
+
version=args.version,
|
|
194
|
+
dependency_group=args.dependency_group,
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
subfolder_config.create_temp_pyproject()
|
|
198
|
+
manager.run_build(build_cmd)
|
|
199
|
+
if not args.no_restore_versioning:
|
|
200
|
+
subfolder_config.restore()
|
|
201
|
+
print("Restored original pyproject.toml")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(f"Error managing subfolder build: {e}", file=sys.stderr)
|
|
204
|
+
if subfolder_config:
|
|
205
|
+
subfolder_config.restore()
|
|
206
|
+
raise
|
|
207
|
+
else:
|
|
208
|
+
from .version import VersionManager
|
|
209
|
+
|
|
210
|
+
version_manager = VersionManager(project_root)
|
|
211
|
+
original_version = version_manager.get_current_version()
|
|
212
|
+
try:
|
|
213
|
+
print(f"Setting version to {args.version}...")
|
|
214
|
+
version_manager.set_version(args.version)
|
|
215
|
+
manager.run_build(build_cmd)
|
|
216
|
+
if not args.no_restore_versioning:
|
|
217
|
+
if original_version:
|
|
218
|
+
version_manager.set_version(original_version)
|
|
219
|
+
else:
|
|
220
|
+
version_manager.restore_dynamic_versioning()
|
|
221
|
+
print("Restored versioning configuration")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"Error managing version: {e}", file=sys.stderr)
|
|
224
|
+
raise
|
|
225
|
+
else:
|
|
226
|
+
manager.run_build(build_cmd)
|
|
227
|
+
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
232
|
+
import traceback
|
|
233
|
+
|
|
234
|
+
traceback.print_exc()
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
sys.exit(main())
|