python-package-folder 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. python_package_folder-1.4.0/PKG-INFO +881 -0
  2. python_package_folder-1.4.0/README.md +861 -0
  3. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/coverage.svg +2 -2
  4. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/manager.py +17 -3
  5. python_package_folder-1.3.0/PKG-INFO +0 -23
  6. python_package_folder-1.3.0/tests/folder_structure/subfolder_to_build/README.md +0 -3
  7. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.copier-answers.yml +0 -0
  8. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.cursor/rules/general.mdc +0 -0
  9. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.cursor/rules/python.mdc +0 -0
  10. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.github/workflows/ci.yml +0 -0
  11. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.github/workflows/publish.yml +0 -0
  12. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.gitignore +0 -0
  13. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/.vscode/settings.json +0 -0
  14. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/LICENSE +0 -0
  15. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/Makefile +0 -0
  16. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/development.md +0 -0
  17. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/installation.md +0 -0
  18. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/publishing.md +0 -0
  19. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/pyproject.toml +0 -0
  20. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/__init__.py +0 -0
  21. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/__main__.py +0 -0
  22. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/analyzer.py +0 -0
  23. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/finder.py +0 -0
  24. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/publisher.py +0 -0
  25. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/py.typed +0 -0
  26. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/python_package_folder.py +0 -0
  27. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/subfolder_build.py +0 -0
  28. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/types.py +0 -0
  29. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/utils.py +0 -0
  30. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/src/python_package_folder/version.py +0 -0
  31. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/folder_structure/some_globals.py +0 -0
  32. {python_package_folder-1.3.0 → python_package_folder-1.4.0/tests/folder_structure/subfolder_to_build}/README.md +0 -0
  33. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  34. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  35. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  36. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_build_with_external_deps.py +0 -0
  37. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_linting.py +0 -0
  38. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_publisher.py +0 -0
  39. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_subfolder_build.py +0 -0
  40. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_utils.py +0 -0
  41. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/test_version_manager.py +0 -0
  42. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/tests/tests.py +0 -0
  43. {python_package_folder-1.3.0 → python_package_folder-1.4.0}/uv.lock +0 -0
@@ -0,0 +1,881 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-package-folder
3
+ Version: 1.4.0
4
+ Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
5
+ Project-URL: Repository, https://github.com/alelom/python-package-folder
6
+ Author-email: Alessio Lombardi <work@alelom.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: <4.0,>=3.11
19
+ Description-Content-Type: text/markdown
20
+
21
+ # python-package-folder <!-- omit from toc -->
22
+
23
+ [![Tests](https://github.com/alelom/python-package-folder/actions/workflows/ci.yml/badge.svg)](https://github.com/alelom/python-package-folder/actions/workflows/ci.yml)
24
+ [![Coverage](https://raw.githubusercontent.com/alelom/python-package-folder/main/coverage.svg)](https://github.com/alelom/python-package-folder)
25
+
26
+ Easily build and publish any target folder in a repository, including subfolders of a monorepo.
27
+ Together with [sysappend](https://pypi.org/project/sysappend/), this library makes relative imports, flexible import management, and package publishing a breeze.
28
+
29
+ - [Use Cases](#use-cases)
30
+ - [Features](#features)
31
+ - [Installation and requirements](#installation-and-requirements)
32
+ - [Quick Start](#quick-start)
33
+ - [How does `python-package-folder` work?](#how-does-python-package-folder-work)
34
+ - [Python API Usage](#python-api-usage)
35
+ - [Working with sysappend](#working-with-sysappend)
36
+ - [Publishing version Management](#publishing-version-management)
37
+ - [Publishing Packages](#publishing-packages)
38
+ - [Command Line Options](#command-line-options)
39
+ - [API Reference](#api-reference)
40
+ - [Development](#development)
41
+
42
+
43
+ ## Use Cases
44
+
45
+ ### 1) Publishing a Subfolder from src/ in a Monorepo
46
+
47
+ If you have a monorepo structure with multiple packages in `src/`:
48
+
49
+ ```
50
+ project/
51
+ ├── src/
52
+ │ ├── core_package/
53
+ │ │ ├── __init__.py
54
+ │ │ ├── core.py
55
+ │ │ └── README.md
56
+ │ ├── api_package/
57
+ │ │ ├── __init__.py
58
+ │ │ ├── api.py
59
+ │ │ └── README.md
60
+ │ └── utils_package/
61
+ │ ├── __init__.py
62
+ │ ├── utils.py
63
+ │ └── README.md
64
+ ├── shared/
65
+ │ └── common.py
66
+ └── pyproject.toml
67
+ ```
68
+
69
+ You can build and publish any subfolder from `src/` as a standalone package:
70
+
71
+ ```bash
72
+ # Navigate to the subfolder you want to publish
73
+ cd src/api_package
74
+
75
+ # Build and publish to TestPyPI with version 1.2.0
76
+ python-package-folder --publish testpypi --version 1.2.0
77
+
78
+ # Or publish to PyPI with a custom package name
79
+ python-package-folder --publish pypi --version 1.2.0 --package-name "my-api-package"
80
+
81
+ # Include a specific dependency group from the parent pyproject.toml
82
+ python-package-folder --publish pypi --version 1.2.0 --dependency-group "dev"
83
+ ```
84
+
85
+ The tool will automatically:
86
+ 1. Detect the project root (where `pyproject.toml` is located)
87
+ 2. Use `src/api_package` as the source directory
88
+ 3. Copy any external dependencies (like `shared/common.py`) into the package before building
89
+ 4. Use the subfolder's README if present, or create a minimal one
90
+ 5. Create a temporary `pyproject.toml` with the subfolder's package name and version
91
+ 6. Build and publish the package
92
+ 7. Clean up all temporary files and restore the original `pyproject.toml`
93
+
94
+ This is especially useful for monorepos where you want to publish individual packages independently while sharing common code.
95
+
96
+
97
+ ### 2) Building Packages with Shared Code
98
+
99
+ If your project structure looks like this:
100
+
101
+ ```
102
+ project/
103
+ ├── src/
104
+ │ └── my_package/
105
+ │ └── main.py
106
+ ├── shared/
107
+ │ ├── utils.py
108
+ │ └── helpers.py
109
+ └── pyproject.toml
110
+ ```
111
+
112
+ And `main.py` imports from `shared/`:
113
+
114
+ ```python
115
+ from shared.utils import some_function
116
+ from shared.helpers import Helper
117
+ ```
118
+
119
+ This package will automatically:
120
+ 1. Detect that `shared/` is outside `src/`
121
+ 2. Copy `shared/` into `src/` before building
122
+ 3. Build your package with all dependencies included
123
+ 4. Clean up the copied files after build
124
+
125
+
126
+ ## Features
127
+
128
+ - **Subfolder Build Support**: Build subfolders as separate packages with automatic detection and configuration
129
+ - **Automatic subfolder detection**: Detects when building a subfolder (not the main `src/` directory)
130
+ - Creates any needed file for publishing automatically, cleaning up if not originally in the subfolder after the build/publish process. E.g. copies external dependencies into the source directory before build and cleans them up afterward; temporary `__init__.py` creation for non-package subfolders; uses subfolder README if present, otherwise creates minimal README
131
+ - Automatic package name derivation from subfolder name
132
+ - Automatic temporary `pyproject.toml` creation with correct package structure
133
+ - Dependency group selection: specify which dependency group from parent `pyproject.toml` to include.
134
+
135
+ - **Smart Import Classification and analysis**:
136
+ - Recursively parses all `.py` files to detect `import` and `from ... import ...` statements
137
+ - Handles external dependencies (modules and files that originate from outside the main package directory), and distinguishes standard library imports, 3rd-party packages (from site-packages), local/external/relative/ambiguous imports.
138
+
139
+ - **Idempotent Operations**: Safely handles repeated runs without duplicating files
140
+ - **Build Integration**: Seamlessly integrates with build tools like `uv build`, `pip build`, etc.
141
+ - **Version Management**:
142
+ - Set static versions for publishing (PEP 440 compliant)
143
+ - Temporarily override dynamic versioning during builds
144
+ - Automatic restoration of dynamic versioning after build
145
+ - **Package Publishing**:
146
+ - Uses twine to publish the built folder/subfolder
147
+ - Handles publishing to to PyPI, TestPyPI, or Azure Artifacts, with interactive credential prompts, secure storage support
148
+
149
+
150
+ ## Installation and requirements
151
+
152
+ Python >= 3.11 is required.
153
+
154
+ ```bash
155
+ uv add python-package-folder
156
+
157
+ # or
158
+
159
+ pip install python-package-folder
160
+ ```
161
+
162
+ **Note**: For publishing functionality, you'll also need `twine`:
163
+
164
+ ```bash
165
+ pip install twine
166
+ # or
167
+ uv add twine
168
+ ```
169
+
170
+ **For secure credential storage**: `keyring` is optional but recommended (install with `pip install keyring`)
171
+
172
+
173
+ ## Quick Start
174
+
175
+ The simplest way to use this package is via the command-line interface
176
+
177
+ **Build/publish a specific subfolder in a repository**
178
+
179
+ Useful for monorepos containing many subfolders that may need publishing as stand-alone packages for external usage.
180
+
181
+ ```bash
182
+ # First cd to the specific subfolder
183
+ cd src/subfolder_to_build_and_publish
184
+
185
+ # Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
186
+ python-package-folder --publish testpypi --version 0.0.2
187
+
188
+ # Only analyse (no building)
189
+ cd src/subfolder_to_build_and_publish
190
+ python-package-folder --analyze-only
191
+
192
+ # Only build
193
+ cd src/subfolder_to_build_and_publish
194
+ python-package-folder
195
+
196
+ # Build with automatic dependency management
197
+ python-package-folder --build-command "uv build"
198
+ ```
199
+
200
+ You can also target a specific subfolder via commandline, rather than `cd`ing there:
201
+
202
+ ```python
203
+ # Specify custom project root and source directory
204
+ python-package-folder --project-root /path/to/project --src-dir /path/to/src --build-command "pip build"
205
+ ```
206
+
207
+ ## How does `python-package-folder` work?
208
+
209
+
210
+ ### Build Process
211
+
212
+ 1. **Import Extraction**: Uses Python's AST module to parse all `.py` files and extract import statements
213
+ 2. **Classification**: Each import is classified as:
214
+ - **stdlib**: Standard library modules
215
+ - **third_party**: Packages installed in site-packages
216
+ - **local**: Modules within the source directory
217
+ - **external**: Modules outside source directory but in the project
218
+ - **ambiguous**: Cannot be resolved
219
+ 3. **Dependency Resolution**: For external imports, the tool resolves the file path by checking:
220
+ - Parent directories of the source directory
221
+ - Project root and its subdirectories
222
+ - Relative import paths
223
+ 4. **File Copying**: External dependencies are temporarily copied into the source directory
224
+ 5. **Build Execution**: Your build command runs with all dependencies in place
225
+ 6. **Cleanup**: All temporarily copied files are removed after build
226
+
227
+ ### Publishing Process
228
+
229
+ 1. **Build Verification**: Ensures distribution files exist in the `dist/` directory
230
+ 2. **File Filtering**: Automatically filters distribution files to only include those matching the current package name and version (prevents uploading old artifacts)
231
+ 3. **Credential Management**:
232
+ - Prompts for credentials if not provided via command-line arguments
233
+ - Credentials are not stored - you'll be prompted each time (unless provided via `--username` and `--password`)
234
+ - Supports both username/password and API tokens
235
+ - Auto-detects API tokens and uses `__token__` as username
236
+ 4. **Repository Configuration**: Configures the target repository (PyPI, TestPyPI, or Azure)
237
+ 5. **Upload**: Uses `twine` to upload distribution files to the repository
238
+ 6. **Verification**: Confirms successful upload
239
+
240
+ ### Subfolder Build Process
241
+
242
+ 1. **Project Root Detection**: Searches parent directories for `pyproject.toml`
243
+ 2. **Source Directory Detection**: Uses current directory if it contains Python files, otherwise falls back to `project_root/src`
244
+ 3. **Package Initialization**: Creates temporary `__init__.py` if subfolder doesn't have one (required for hatchling)
245
+ 4. **README Handling**:
246
+ - Checks for README files in the subfolder (README.md, README.rst, README.txt, or README)
247
+ - If found, copies the subfolder README to project root (backing up the original parent README)
248
+ - If not found, creates a minimal README with just the folder name
249
+ 5. **Configuration Creation**: Creates temporary `pyproject.toml` with:
250
+ - `[build-system]` section using hatchling (replaces any existing build-system configuration)
251
+ - Subfolder-specific package name (derived or custom)
252
+ - Specified version
253
+ - Correct package path for hatchling
254
+ 6. **Build Execution**: Runs build command with all dependencies in place
255
+ 7. **Cleanup**: Restores original `pyproject.toml` and removes temporary `__init__.py`
256
+
257
+ ### How does building from Subdirectories work?
258
+
259
+ This is useful for monorepos containing many subfolders that may need publishing as stand-alone packages for external usage.
260
+ The tool automatically detects the project root by searching for `pyproject.toml` in parent directories.
261
+ This allows you to build subfolders of a main project as separate packages:
262
+
263
+ ```bash
264
+ # From a subdirectory, the tool will:
265
+ # 1. Find pyproject.toml in parent directories (project root)
266
+ # 2. Use current directory as source if it contains Python files
267
+ # 3. Build with dependencies from the parent project
268
+ # 4. Create a temporary build config with subfolder-specific name and version
269
+
270
+ cd my_project/subfolder_to_build
271
+ python-package-folder --version "1.0.0" --publish pypi
272
+ ```
273
+
274
+ The tool **automatically detects** when you're building a subfolder (any directory that's not the main `src/` directory) and sets up the appropriate build configuration.
275
+
276
+ The tool automatically:
277
+ - **Detects subfolder builds**: Automatically identifies when building from a subdirectory
278
+ - Finds the project root by looking for `pyproject.toml` in parent directories
279
+ - Uses the current directory as the source directory if it contains Python files
280
+ - Falls back to `project_root/src` if the current directory isn't suitable
281
+ - **For subfolder builds**: Handles `pyproject.toml` configuration:
282
+ - **If `pyproject.toml` exists in subfolder**: Uses that file (copies it to project root temporarily, adjusting package paths and ensuring `[build-system]` uses hatchling)
283
+ - **If no `pyproject.toml` in subfolder**: Creates a temporary `pyproject.toml` with:
284
+ - `[build-system]` section using hatchling (always uses hatchling, even if parent uses setuptools)
285
+ - Package name derived from the subfolder name (e.g., `empty_drawing_detection` → `empty-drawing-detection`)
286
+ - Version from `--version` argument (defaults to `0.0.0` with a warning if not provided)
287
+ - Proper package path configuration for hatchling
288
+ - Dependency groups from parent `pyproject.toml` if specified
289
+ - Creates temporary `__init__.py` files if needed to make subfolders valid Python packages
290
+ - **README handling for subfolder builds**:
291
+ - If a README file (README.md, README.rst, README.txt, or README) exists in the subfolder, it will be used instead of the parent README
292
+ - If no README exists in the subfolder, a minimal README with just the folder name will be created
293
+ - Restores the original `pyproject.toml` after build (unless `--no-restore-versioning` is used)
294
+ - Cleans up temporary `__init__.py` files after build
295
+
296
+ **Note**: While version is not strictly required (defaults to `0.0.0`), it's recommended to specify `--version` for subfolder builds to ensure proper versioning.
297
+
298
+ **Subfolder Build Example:**
299
+ ```bash
300
+ # Build a subfolder as a separate package
301
+ cd tests/folder_structure/subfolder_to_build
302
+ python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --publish pypi
303
+
304
+ # Build with a specific dependency group from parent pyproject.toml
305
+ python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
306
+
307
+ # If subfolder has its own pyproject.toml, it will be used automatically
308
+ # (package-name and version arguments are ignored in this case)
309
+ cd src/integration/my_package # assuming my_package/pyproject.toml exists
310
+ python-package-folder --publish pypi
311
+ ```
312
+
313
+ **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:
314
+
315
+ ```bash
316
+ # Use the 'dev' dependency group from parent pyproject.toml
317
+ python-package-folder --version "1.0.0" --dependency-group "dev" --publish pypi
318
+ ```
319
+
320
+ The specified dependency group will be copied from the parent `pyproject.toml`'s `[dependency-groups]` section into the temporary `pyproject.toml` used for the subfolder build.
321
+
322
+ ## Python API Usage
323
+
324
+ You can also use the package programmatically:
325
+
326
+ ### Basic Usage
327
+
328
+ ```python
329
+ from pathlib import Path
330
+ from python_package_folder import BuildManager
331
+
332
+ # Initialize the build manager
333
+ manager = BuildManager(
334
+ project_root=Path("."),
335
+ src_dir=Path("src")
336
+ )
337
+
338
+ # Prepare build (finds and copies external dependencies)
339
+ external_deps = manager.prepare_build()
340
+
341
+ print(f"Found {len(external_deps)} external dependencies")
342
+ for dep in external_deps:
343
+ print(f" {dep.import_name}: {dep.source_path} -> {dep.target_path}")
344
+
345
+ # Run your build process here
346
+ # ...
347
+
348
+ # Cleanup copied files (also restores pyproject.toml if subfolder build)
349
+ manager.cleanup()
350
+ ```
351
+
352
+ ### Using the Convenience Method
353
+
354
+ ```python
355
+ from pathlib import Path
356
+ from python_package_folder import BuildManager
357
+ import subprocess
358
+
359
+ manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
360
+
361
+ def build_command():
362
+ subprocess.run(["uv", "build"], check=True)
363
+
364
+ # Automatically handles prepare, build, and cleanup
365
+ manager.run_build(build_command)
366
+ ```
367
+
368
+ ### Subfolder Builds (Automatic Detection)
369
+
370
+ The tool automatically detects when you're building a subfolder and sets up the appropriate configuration:
371
+
372
+ ```python
373
+ from pathlib import Path
374
+ from python_package_folder import BuildManager
375
+ import subprocess
376
+
377
+ # Building a subfolder - automatic detection!
378
+ manager = BuildManager(
379
+ project_root=Path("."),
380
+ src_dir=Path("src/integration/empty_drawing_detection")
381
+ )
382
+
383
+ def build_command():
384
+ subprocess.run(["uv", "build"], check=True)
385
+
386
+ # prepare_build() automatically:
387
+ # - Detects this is a subfolder build
388
+ # - If pyproject.toml exists in subfolder: uses that file
389
+ # - If no pyproject.toml in subfolder: creates temporary one with package name "empty-drawing-detection"
390
+ # - Uses version "0.0.0" (or pass version="1.0.0" to override) if creating temporary pyproject.toml
391
+ external_deps = manager.prepare_build(version="1.0.0")
392
+
393
+ # Run build - uses the pyproject.toml (either from subfolder or temporary)
394
+ build_command()
395
+
396
+ # Cleanup restores original pyproject.toml and removes copied files
397
+ manager.cleanup()
398
+ ```
399
+
400
+ **Note**: If the subfolder has its own `pyproject.toml`, it will be used automatically. The `version` and `package_name` parameters are only used when creating a temporary `pyproject.toml` from the parent configuration.
401
+
402
+ Or use the convenience method:
403
+
404
+ ```python
405
+ manager = BuildManager(
406
+ project_root=Path("."),
407
+ src_dir=Path("src/integration/empty_drawing_detection")
408
+ )
409
+
410
+ def build_command():
411
+ subprocess.run(["uv", "build"], check=True)
412
+
413
+ # All handled automatically: subfolder detection, pyproject.toml setup, build, cleanup
414
+ manager.run_build(build_command, version="1.0.0", package_name="my-custom-name")
415
+ ```
416
+
417
+ ## Working with sysappend
418
+
419
+ This package works well with projects using [sysappend](https://pypi.org/project/sysappend/) for flexible import management. When you have imports like:
420
+
421
+ ```python
422
+ if True:
423
+ import sysappend; sysappend.all()
424
+
425
+ from some_globals import SOME_GLOBAL_VARIABLE
426
+ from folder_structure.utility_folder.some_utility import print_something
427
+ ```
428
+
429
+ The package will correctly identify and copy external dependencies even when they're referenced without full package paths.
430
+
431
+ ## Publishing version Management
432
+
433
+ The package supports both dynamic versioning (from git tags) and manual version specification.
434
+
435
+
436
+ ### Manual Version Setting
437
+
438
+ You can manually set a version before building and publishing:
439
+
440
+ ```bash
441
+ # Build with a specific version
442
+ python-package-folder --version "1.2.3"
443
+
444
+ # Build and publish with a specific version
445
+ python-package-folder --version "1.2.3" --publish pypi
446
+
447
+ # Keep the static version (don't restore dynamic versioning)
448
+ python-package-folder --version "1.2.3" --no-restore-versioning
449
+ ```
450
+
451
+ The `--version` option:
452
+ - Sets a static version in `pyproject.toml` before building
453
+ - Temporarily removes dynamic versioning configuration
454
+ - Restores the original configuration after build (unless `--no-restore-versioning` is used)
455
+ - Validates version format (must be PEP 440 compliant)
456
+
457
+ **Version Format**: Versions must follow PEP 440 (e.g., `1.2.3`, `1.2.3a1`, `1.2.3.post1`, `1.2.3.dev1`)
458
+
459
+
460
+ ### Subfolder Versioning
461
+
462
+ When building from a subdirectory (not the main `src/` directory), the tool automatically detects the subfolder and sets up the build configuration:
463
+
464
+ ```bash
465
+ # Build a subfolder as a separate package (version recommended but not required)
466
+ cd my_project/subfolder_to_build
467
+ python-package-folder --version "1.0.0" --publish pypi
468
+
469
+ # With custom package name
470
+ 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
+ ```
475
+
476
+ For subfolder builds:
477
+ - **Automatic detection**: The tool automatically detects subfolder builds
478
+ - **pyproject.toml handling**:
479
+ - If `pyproject.toml` exists in subfolder: Uses that file (copied to project root temporarily)
480
+ - 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
+ - **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`). Only used when creating temporary pyproject.toml.
483
+ - **Restoration**: Original `pyproject.toml` is restored after build
484
+ - **Temporary configuration**: Creates a temporary `pyproject.toml` with:
485
+ - Custom package name (from `--package-name` or derived)
486
+ - Specified version
487
+ - Correct package path for hatchling
488
+ - Dependency group from parent (if `--dependency-group` is specified)
489
+ - **Package initialization**: Automatically creates `__init__.py` if the subfolder doesn't have one (required for hatchling)
490
+ - **README handling**:
491
+ - If a README file exists in the subfolder, it will be used instead of the parent README
492
+ - If no README exists in the subfolder, a minimal README with just the folder name will be created
493
+ - **Auto-restore**: Original `pyproject.toml` is restored after build, and temporary `__init__.py` files are removed
494
+
495
+
496
+ ### Python API for Version Management
497
+
498
+ ```python
499
+ from python_package_folder import VersionManager
500
+ from pathlib import Path
501
+
502
+ # Set a version
503
+ version_manager = VersionManager(project_root=Path("."))
504
+ version_manager.set_version("1.2.3")
505
+
506
+ # Get current version
507
+ current_version = version_manager.get_current_version()
508
+
509
+ # Restore dynamic versioning
510
+ version_manager.restore_dynamic_versioning()
511
+ ```
512
+
513
+ ### Dynamic Versioning
514
+
515
+ By default, the package uses `uv-dynamic-versioning` which derives versions from git tags. This is configured in `pyproject.toml`:
516
+
517
+ ```toml
518
+ [project]
519
+ dynamic = ["version"]
520
+
521
+ [tool.hatch.version]
522
+ source = "uv-dynamic-versioning"
523
+
524
+ [tool.uv-dynamic-versioning]
525
+ vcs = "git"
526
+ style = "pep440"
527
+ bump = true
528
+ ```
529
+
530
+ When you use `--version`, the package temporarily switches to static versioning for that build, then restores the dynamic configuration.
531
+
532
+ ## Publishing Packages
533
+
534
+ The package includes built-in support for publishing to PyPI, TestPyPI, and Azure Artifacts.
535
+
536
+
537
+ ### Command Line Publishing
538
+
539
+ Publish after building:
540
+
541
+ ```bash
542
+ # Publish to PyPI
543
+ python-package-folder --publish pypi
544
+
545
+ # Publish to PyPI with a specific version
546
+ python-package-folder --version "1.2.3" --publish pypi
547
+
548
+ # Publish to TestPyPI (for testing)
549
+ python-package-folder --publish testpypi
550
+
551
+ # Publish to Azure Artifacts
552
+ python-package-folder --publish azure --repository-url "https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload"
553
+ ```
554
+
555
+ The command will prompt for credentials if not provided:
556
+
557
+ ```bash
558
+ # Provide credentials via command line (less secure)
559
+ python-package-folder --publish pypi --username __token__ --password pypi-xxxxx
560
+
561
+ # Skip existing files on repository
562
+ python-package-folder --publish pypi --skip-existing
563
+ ```
564
+
565
+
566
+ ### Credentials
567
+
568
+ **For PyPI/TestPyPI:**
569
+ - **Username**: Your PyPI username, or `__token__` for API tokens
570
+ - **Password**: Your PyPI password or API token (recommended)
571
+ - **Auto-detection**: If you provide an API token (starts with `pypi-`), the tool will automatically use `__token__` as the username, even if you entered a different username
572
+
573
+ **Common Authentication Issues:**
574
+ - **403 Forbidden**: Usually means you used your username instead of `__token__` with an API token. The tool now auto-detects this.
575
+ - **TestPyPI vs PyPI**: TestPyPI requires a separate account and token from https://test.pypi.org/manage/account/token/
576
+
577
+
578
+ ### Smart File Filtering
579
+
580
+ When publishing, the tool automatically filters distribution files to only upload those matching the current build:
581
+
582
+ - **Package name matching**: Only uploads files for the package being built
583
+ - **Version matching**: Only uploads files for the specified version
584
+ - **Automatic cleanup**: Old build artifacts in `dist/` are ignored, preventing accidental uploads
585
+
586
+ This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
587
+
588
+ To get a PyPI API token:
589
+ 1. Go to https://pypi.org/manage/account/token/
590
+ 2. Create a new API token
591
+ 3. Use `__token__` as username and the token as password
592
+
593
+ **For Azure Artifacts:**
594
+ - **Username**: Your Azure username or feed name
595
+ - **Password**: Personal Access Token (PAT) with packaging permissions
596
+ - **Repository URL**: Your Azure Artifacts feed URL
597
+
598
+
599
+ ### Python API Publishing
600
+
601
+ You can also publish programmatically:
602
+
603
+ ```python
604
+ from pathlib import Path
605
+ from python_package_folder import BuildManager, Publisher, Repository
606
+ import subprocess
607
+
608
+ # Build and publish in one step
609
+ manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
610
+
611
+ def build():
612
+ subprocess.run(["uv", "build"], check=True)
613
+
614
+ manager.build_and_publish(
615
+ build,
616
+ repository="pypi",
617
+ username="__token__",
618
+ password="pypi-xxxxx",
619
+ version="1.2.3" # Optional: set specific version
620
+ )
621
+ ```
622
+ ```
623
+
624
+ Or publish separately:
625
+
626
+ ```python
627
+ from python_package_folder import Publisher, Repository
628
+
629
+ # Publish existing distribution
630
+ publisher = Publisher(
631
+ repository=Repository.PYPI,
632
+ dist_dir=Path("dist"),
633
+ username="__token__",
634
+ password="pypi-xxxxx"
635
+ )
636
+ publisher.publish()
637
+ ```
638
+
639
+
640
+ ### Credential Storage
641
+
642
+ **Note**: The package does not store credentials by default. Credentials must be provided via command-line arguments (`--username` and `--password`) or will be prompted each time you run the publish command. This ensures credentials are not persisted and must be entered fresh each time.
643
+
644
+ If you previously used an older version that stored credentials in keyring, you can clear them using:
645
+
646
+ ```python
647
+ from python_package_folder import Publisher, Repository
648
+
649
+ publisher = Publisher(repository=Repository.AZURE)
650
+ publisher.clear_stored_credentials()
651
+ ```
652
+
653
+ Or manually using Python:
654
+ ```python
655
+ import keyring
656
+ keyring.delete_password("python-package-folder-azure", "username")
657
+ # Also delete the password if you know the username
658
+ ```
659
+
660
+ ## Command Line Options
661
+
662
+ ```
663
+ usage: python-package-folder [-h] [--project-root PROJECT_ROOT]
664
+ [--src-dir SRC_DIR] [--analyze-only]
665
+ [--build-command BUILD_COMMAND]
666
+ [--publish {pypi,testpypi,azure}]
667
+ [--repository-url REPOSITORY_URL]
668
+ [--username USERNAME] [--password PASSWORD]
669
+ [--skip-existing]
670
+
671
+ Build Python package with external dependency management
672
+
673
+ options:
674
+ -h, --help show this help message and exit
675
+ --project-root PROJECT_ROOT
676
+ Root directory of the project (default: current directory)
677
+ --src-dir SRC_DIR Source directory (default: project_root/src)
678
+ --analyze-only Only analyze imports, don't run build
679
+ --build-command BUILD_COMMAND
680
+ Command to run for building (default: 'uv build')
681
+ --publish {pypi,testpypi,azure}
682
+ Publish to repository after building
683
+ --repository-url REPOSITORY_URL
684
+ Custom repository URL (required for Azure Artifacts)
685
+ --username USERNAME Username for publishing (will prompt if not provided)
686
+ --password PASSWORD Password/token for publishing (will prompt if not provided)
687
+ --skip-existing Skip files that already exist on the repository
688
+ --version VERSION Set a specific version before building (PEP 440 format).
689
+ Required for subfolder builds.
690
+ --package-name PACKAGE_NAME
691
+ Package name for subfolder builds (default: derived from
692
+ source directory name)
693
+ --dependency-group DEPENDENCY_GROUP
694
+ Dependency group name from parent pyproject.toml to include
695
+ in subfolder build
696
+ --no-restore-versioning
697
+ Don't restore dynamic versioning after build
698
+ ```
699
+
700
+ ## API Reference
701
+
702
+ ### BuildManager
703
+
704
+ Main class for managing the build process with external dependency handling.
705
+
706
+ ```python
707
+ from python_package_folder import BuildManager
708
+ from pathlib import Path
709
+
710
+ manager = BuildManager(
711
+ project_root: Path, # Root directory of the project
712
+ src_dir: Path | None # Source directory (default: project_root/src)
713
+ )
714
+ ```
715
+
716
+ **Methods:**
717
+
718
+ - `prepare_build() -> list[ExternalDependency]`: Find and copy external dependencies
719
+ - `cleanup() -> None`: Remove all copied files and directories
720
+ - `run_build(build_command: Callable[[], None]) -> None`: Run build with automatic prepare and cleanup
721
+
722
+ ### ImportAnalyzer
723
+
724
+ Analyzes Python files to extract and classify import statements.
725
+
726
+ ```python
727
+ from python_package_folder import ImportAnalyzer
728
+ from pathlib import Path
729
+
730
+ analyzer = ImportAnalyzer(project_root=Path("."))
731
+ python_files = analyzer.find_all_python_files(Path("src"))
732
+ imports = analyzer.extract_imports(python_files[0])
733
+ analyzer.classify_import(imports[0], src_dir=Path("src"))
734
+ ```
735
+
736
+ ### ExternalDependencyFinder
737
+
738
+ Finds external dependencies that need to be copied.
739
+
740
+ ```python
741
+ from python_package_folder import ExternalDependencyFinder
742
+ from pathlib import Path
743
+
744
+ finder = ExternalDependencyFinder(
745
+ project_root=Path("."),
746
+ src_dir=Path("src")
747
+ )
748
+ dependencies = finder.find_external_dependencies(python_files)
749
+ ```
750
+
751
+ ### Publisher
752
+
753
+ Publishes built packages to PyPI, TestPyPI, or Azure Artifacts.
754
+
755
+ ```python
756
+ from python_package_folder import Publisher, Repository
757
+ from pathlib import Path
758
+
759
+ publisher = Publisher(
760
+ repository=Repository.PYPI,
761
+ dist_dir=Path("dist"),
762
+ username="__token__",
763
+ password="pypi-xxxxx",
764
+ package_name="my-package", # Optional: filter files by package name
765
+ version="1.2.3" # Optional: filter files by version
766
+ )
767
+ publisher.publish()
768
+ ```
769
+
770
+ **Methods:**
771
+ - `publish(skip_existing: bool = False) -> None`: Publish the package (automatically filters by package_name/version if provided)
772
+ - `publish_interactive(skip_existing: bool = False) -> None`: Publish with interactive credential prompts
773
+
774
+ **Note**: When `package_name` and `version` are provided, only distribution files matching those parameters are uploaded. This prevents uploading old build artifacts.
775
+
776
+ ### VersionManager
777
+
778
+ Manages package version in pyproject.toml.
779
+
780
+ ```python
781
+ from python_package_folder import VersionManager
782
+ from pathlib import Path
783
+
784
+ version_manager = VersionManager(project_root=Path("."))
785
+
786
+ # Set a static version
787
+ version_manager.set_version("1.2.3")
788
+
789
+ # Get current version
790
+ version = version_manager.get_current_version()
791
+
792
+ # Restore dynamic versioning
793
+ version_manager.restore_dynamic_versioning()
794
+ ```
795
+
796
+ **Methods:**
797
+ - `set_version(version: str) -> None`: Set a static version (validates PEP 440 format)
798
+ - `get_current_version() -> str | None`: Get current version from pyproject.toml
799
+ - `restore_dynamic_versioning() -> None`: Restore dynamic versioning configuration
800
+
801
+ ### SubfolderBuildConfig
802
+
803
+ Manages temporary build configuration for subfolder builds. If a `pyproject.toml` exists
804
+ in the subfolder, it will be used instead of creating a new one.
805
+
806
+ ```python
807
+ from python_package_folder import SubfolderBuildConfig
808
+ from pathlib import Path
809
+
810
+ config = SubfolderBuildConfig(
811
+ project_root=Path("."),
812
+ src_dir=Path("subfolder"),
813
+ package_name="my-subfolder", # Only used if subfolder has no pyproject.toml
814
+ version="1.0.0" # Only used if subfolder has no pyproject.toml
815
+ )
816
+
817
+ # Create temporary pyproject.toml (or use subfolder's if it exists)
818
+ config.create_temp_pyproject()
819
+
820
+ # ... build process ...
821
+
822
+ # Restore original configuration
823
+ config.restore()
824
+ ```
825
+
826
+ **Methods:**
827
+ - `create_temp_pyproject() -> Path`: Use subfolder's `pyproject.toml` if it exists (adjusting package paths and ensuring `[build-system]` uses hatchling), otherwise create temporary `pyproject.toml` with subfolder-specific configuration including `[build-system]` section using hatchling
828
+ - `restore() -> None`: Restore original `pyproject.toml` and clean up temporary files
829
+
830
+ **Note**: This class automatically:
831
+ - **pyproject.toml handling**: If a `pyproject.toml` exists in the subfolder, it will be used (copied to project root temporarily with adjusted package paths). Otherwise, creates a temporary one from the parent configuration. In both cases, the `[build-system]` section is always set to use hatchling, replacing any existing build-system configuration.
832
+ - **README handling**: If a README exists in the subfolder, it will be used instead of the parent README. If no README exists in the subfolder, a minimal README with just the folder name will be created. The original parent README is backed up and restored after the build completes.
833
+ - **Package initialization**: Creates `__init__.py` files if needed to make subfolders valid Python packages.
834
+
835
+
836
+ ## Development
837
+
838
+ ### Setup
839
+
840
+ ```bash
841
+ # Clone the repository
842
+ git clone https://github.com/alelom/python-package-folder.git
843
+ cd python-package-folder
844
+
845
+ # Install dependencies
846
+ uv sync --all-extras
847
+
848
+ # Run tests
849
+ uv run pytest
850
+
851
+ # Run linting
852
+ make lint
853
+ ```
854
+
855
+ ### Project Structure
856
+
857
+ ```
858
+ python-package-folder/
859
+ ├── src/
860
+ │ └── python_package_folder/
861
+ │ ├── __init__.py # Package exports
862
+ │ ├── types.py # Type definitions
863
+ │ ├── analyzer.py # Import analysis
864
+ │ ├── finder.py # Dependency finding
865
+ │ ├── manager.py # Build management
866
+ │ └── python_package_folder.py # CLI entry point
867
+ ├── tests/
868
+ │ ├── test_build_with_external_deps.py
869
+ │ └── folder_structure/ # Test fixtures
870
+ ├── devtools/
871
+ │ └── lint.py # Development tools
872
+ └── pyproject.toml
873
+ ```
874
+
875
+ ## License <!-- omit from toc -->
876
+
877
+ MIT License - see LICENSE file for details
878
+
879
+ ## Contributing <!-- omit from toc -->
880
+
881
+ Contributions are welcome! Please feel free to submit a Pull Request.