python-package-folder 0.1.0__tar.gz → 1.0.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-1.0.0/.github/workflows/ci.yml +126 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/PKG-INFO +175 -9
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/README.md +174 -8
- python_package_folder-1.0.0/coverage.svg +20 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/pyproject.toml +2 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/__init__.py +5 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/finder.py +1 -1
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/manager.py +94 -7
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/publisher.py +63 -6
- python_package_folder-1.0.0/src/python_package_folder/python_package_folder.py +231 -0
- python_package_folder-1.0.0/src/python_package_folder/subfolder_build.py +466 -0
- python_package_folder-1.0.0/src/python_package_folder/utils.py +107 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/version.py +1 -2
- python_package_folder-1.0.0/tests/folder_structure/subfolder_to_build/README.md +3 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/test_build_with_external_deps.py +2 -2
- python_package_folder-1.0.0/tests/test_publisher.py +203 -0
- python_package_folder-1.0.0/tests/test_subfolder_build.py +360 -0
- python_package_folder-1.0.0/tests/test_utils.py +159 -0
- python_package_folder-1.0.0/tests/test_version_manager.py +148 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/tests.py +2 -2
- python_package_folder-1.0.0/uv.lock +840 -0
- python_package_folder-0.1.0/.github/workflows/ci.yml +0 -63
- python_package_folder-0.1.0/src/python_package_folder/python_package_folder.py +0 -151
- python_package_folder-0.1.0/uv.lock +0 -231
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.copier-answers.yml +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.cursor/rules/general.mdc +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.cursor/rules/python.mdc +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.github/workflows/publish.yml +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.gitignore +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.vscode/settings.json +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/LICENSE +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/Makefile +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/development.md +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/installation.md +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/publishing.md +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/__main__.py +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/analyzer.py +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/py.typed +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/types.py +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/folder_structure/some_globals.py +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
- {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
|
3
|
+
|
|
4
|
+
name: CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
# Use ["main", "master"] for CI only on the default branch.
|
|
9
|
+
# Use ["**"] for CI on all branches.
|
|
10
|
+
branches: ["main", "master"]
|
|
11
|
+
pull_request:
|
|
12
|
+
branches: ["main", "master"]
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
# Update this as needed:
|
|
22
|
+
# Common platforms: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
|
23
|
+
os: ["ubuntu-latest"]
|
|
24
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
25
|
+
|
|
26
|
+
# Linux only by default. Use ${{ matrix.os }} for other OSes.
|
|
27
|
+
runs-on: ${{ matrix.os }}
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
# Generally following uv docs:
|
|
31
|
+
# https://docs.astral.sh/uv/guides/integration/github/
|
|
32
|
+
|
|
33
|
+
- name: Checkout (official GitHub action)
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
with:
|
|
36
|
+
# Important for versioning plugins:
|
|
37
|
+
fetch-depth: 0
|
|
38
|
+
|
|
39
|
+
- name: Install uv (official Astral action)
|
|
40
|
+
uses: astral-sh/setup-uv@v5
|
|
41
|
+
with:
|
|
42
|
+
# Update this as needed:
|
|
43
|
+
version: "0.9.5"
|
|
44
|
+
enable-cache: true
|
|
45
|
+
python-version: ${{ matrix.python-version }}
|
|
46
|
+
|
|
47
|
+
- name: Set up Python (using uv)
|
|
48
|
+
run: uv python install
|
|
49
|
+
|
|
50
|
+
# Alternately can use the official Python action:
|
|
51
|
+
# - name: Set up Python (using actions/setup-python)
|
|
52
|
+
# uses: actions/setup-python@v5
|
|
53
|
+
# with:
|
|
54
|
+
# python-version: ${{ matrix.python-version }}
|
|
55
|
+
|
|
56
|
+
- name: Install all dependencies
|
|
57
|
+
run: uv sync --all-extras
|
|
58
|
+
|
|
59
|
+
- name: Run linting
|
|
60
|
+
run: uv run ruff check .
|
|
61
|
+
|
|
62
|
+
- name: Run type checking
|
|
63
|
+
run: uv run basedpyright src/ || true
|
|
64
|
+
|
|
65
|
+
- name: Run tests with coverage
|
|
66
|
+
run: uv run pytest --cov=src/python_package_folder --cov-report=xml --cov-report=term tests/
|
|
67
|
+
|
|
68
|
+
- name: Generate coverage badge
|
|
69
|
+
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
|
70
|
+
run: |
|
|
71
|
+
python -c "
|
|
72
|
+
import xml.etree.ElementTree as ET
|
|
73
|
+
import re
|
|
74
|
+
|
|
75
|
+
# Parse coverage.xml
|
|
76
|
+
tree = ET.parse('coverage.xml')
|
|
77
|
+
root = tree.getroot()
|
|
78
|
+
|
|
79
|
+
# Get coverage percentage
|
|
80
|
+
line_rate = float(root.get('line-rate', 0))
|
|
81
|
+
coverage_percent = int(line_rate * 100)
|
|
82
|
+
|
|
83
|
+
# Determine color based on coverage
|
|
84
|
+
if coverage_percent >= 80:
|
|
85
|
+
color = '44cc11'
|
|
86
|
+
elif coverage_percent >= 60:
|
|
87
|
+
color = 'dfb317'
|
|
88
|
+
elif coverage_percent >= 40:
|
|
89
|
+
color = 'fe7d37'
|
|
90
|
+
else:
|
|
91
|
+
color = 'e05d44'
|
|
92
|
+
|
|
93
|
+
# Generate SVG badge
|
|
94
|
+
svg = f'''<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"99\" height=\"20\">
|
|
95
|
+
<linearGradient id=\"b\" x2=\"0\" y2=\"100%\">
|
|
96
|
+
<stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>
|
|
97
|
+
<stop offset=\"1\" stop-opacity=\".1\"/>
|
|
98
|
+
</linearGradient>
|
|
99
|
+
<mask id=\"a\">
|
|
100
|
+
<rect width=\"99\" height=\"20\" rx=\"3\" fill=\"#fff\"/>
|
|
101
|
+
</mask>
|
|
102
|
+
<g mask=\"url(#a)\">
|
|
103
|
+
<path fill=\"#555\" d=\"M0 0h63v20H0z\"/>
|
|
104
|
+
<path fill=\"#{color}\" d=\"M63 0h36v20H63z\"/>
|
|
105
|
+
<path fill=\"url(#b)\" d=\"M0 0h99v20H0z\"/>
|
|
106
|
+
</g>
|
|
107
|
+
<g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"11\">
|
|
108
|
+
<text x=\"31.5\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">coverage</text>
|
|
109
|
+
<text x=\"31.5\" y=\"14\">coverage</text>
|
|
110
|
+
<text x=\"81\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{coverage_percent}%</text>
|
|
111
|
+
<text x=\"81\" y=\"14\">{coverage_percent}%</text>
|
|
112
|
+
</g>
|
|
113
|
+
</svg>'''
|
|
114
|
+
|
|
115
|
+
with open('coverage.svg', 'w') as f:
|
|
116
|
+
f.write(svg)
|
|
117
|
+
"
|
|
118
|
+
|
|
119
|
+
- name: Commit coverage badge
|
|
120
|
+
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
|
121
|
+
run: |
|
|
122
|
+
git config --local user.email "action@github.com"
|
|
123
|
+
git config --local user.name "GitHub Action"
|
|
124
|
+
git add coverage.svg || true
|
|
125
|
+
git diff --staged --quiet || git commit -m "Update coverage badge [skip ci]"
|
|
126
|
+
git push || true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-package-folder
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.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>
|
|
@@ -20,6 +20,9 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
# python-package-folder
|
|
22
22
|
|
|
23
|
+
[](https://github.com/alelom/python-package-folder/actions/workflows/ci.yml)
|
|
24
|
+
[](https://github.com/alelom/python-package-folder)
|
|
25
|
+
|
|
23
26
|
Python package to automatically analyze, detect, and manage external dependencies when building Python packages. This tool recursively parses all Python files in your project, identifies imports from outside the main package directory, and temporarily copies them into the source directory during the build process.
|
|
24
27
|
|
|
25
28
|
## Features
|
|
@@ -36,6 +39,10 @@ Python package to automatically analyze, detect, and manage external dependencie
|
|
|
36
39
|
- **Idempotent Operations**: Safely handles repeated runs without duplicating files
|
|
37
40
|
- **Build Integration**: Seamlessly integrates with build tools like `uv build`, `pip build`, etc.
|
|
38
41
|
- **Warning System**: Reports ambiguous imports that couldn't be resolved
|
|
42
|
+
- **Subfolder Build Support**: Build subfolders as separate packages with automatic project root detection
|
|
43
|
+
- **Smart Publishing**: Only uploads distribution files from the current build, filtering out old artifacts
|
|
44
|
+
- **Auto-Detection**: Automatically finds project root and source directory when run from any subdirectory
|
|
45
|
+
- **Authentication Helpers**: Auto-detects API tokens and uses correct username format
|
|
39
46
|
|
|
40
47
|
## Installation
|
|
41
48
|
|
|
@@ -64,16 +71,71 @@ uv add twine
|
|
|
64
71
|
The simplest way to use this package is via the command-line interface:
|
|
65
72
|
|
|
66
73
|
```bash
|
|
67
|
-
# Build with automatic dependency management
|
|
74
|
+
# Build with automatic dependency management (from project root)
|
|
68
75
|
python-package-folder --build-command "uv build"
|
|
69
76
|
|
|
70
77
|
# Analyze dependencies without building
|
|
71
78
|
python-package-folder --analyze-only
|
|
72
79
|
|
|
80
|
+
# Build from any subdirectory - auto-detects project root and source directory
|
|
81
|
+
cd tests/folder_structure/subfolder_to_build
|
|
82
|
+
python-package-folder --analyze-only
|
|
83
|
+
|
|
73
84
|
# Specify custom project root and source directory
|
|
74
85
|
python-package-folder --project-root /path/to/project --src-dir /path/to/src --build-command "pip build"
|
|
75
86
|
```
|
|
76
87
|
|
|
88
|
+
### Building from Subdirectories
|
|
89
|
+
|
|
90
|
+
The tool can automatically detect the project root by searching for `pyproject.toml` in parent directories. This allows you to build subfolders of a main project as separate packages:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# From a subdirectory, the tool will:
|
|
94
|
+
# 1. Find pyproject.toml in parent directories (project root)
|
|
95
|
+
# 2. Use current directory as source if it contains Python files
|
|
96
|
+
# 3. Build with dependencies from the parent project
|
|
97
|
+
# 4. Create a temporary build config with subfolder-specific name and version
|
|
98
|
+
|
|
99
|
+
cd my_project/subfolder_to_build
|
|
100
|
+
python-package-folder --version "1.0.0" --publish pypi
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Important**: When building from a subdirectory, you **must** specify `--version` because subfolders are built as separate packages with their own version.
|
|
104
|
+
|
|
105
|
+
The tool automatically:
|
|
106
|
+
- Finds the project root by looking for `pyproject.toml` in parent directories
|
|
107
|
+
- Uses the current directory as the source directory if it contains Python files
|
|
108
|
+
- Falls back to `project_root/src` if the current directory isn't suitable
|
|
109
|
+
- For subfolder builds: creates a temporary `pyproject.toml` with:
|
|
110
|
+
- Package name derived from the subfolder name (or use `--package-name` to override)
|
|
111
|
+
- Version from `--version` argument
|
|
112
|
+
- Proper package path configuration for hatchling
|
|
113
|
+
- Creates temporary `__init__.py` files if needed to make subfolders valid Python packages
|
|
114
|
+
- **README handling for subfolder builds**:
|
|
115
|
+
- 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
|
|
116
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
117
|
+
- Restores the original `pyproject.toml` after build (unless `--no-restore-versioning` is used)
|
|
118
|
+
- Cleans up temporary `__init__.py` files after build
|
|
119
|
+
|
|
120
|
+
**Subfolder Build Example:**
|
|
121
|
+
```bash
|
|
122
|
+
# Build a subfolder as a separate package
|
|
123
|
+
cd tests/folder_structure/subfolder_to_build
|
|
124
|
+
python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --publish pypi
|
|
125
|
+
|
|
126
|
+
# Build with a specific dependency group from parent pyproject.toml
|
|
127
|
+
python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**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:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Use the 'dev' dependency group from parent pyproject.toml
|
|
134
|
+
python-package-folder --version "1.0.0" --dependency-group "dev" --publish pypi
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
77
139
|
### Python API Usage
|
|
78
140
|
|
|
79
141
|
You can also use the package programmatically:
|
|
@@ -189,6 +251,33 @@ The `--version` option:
|
|
|
189
251
|
|
|
190
252
|
**Version Format**: Versions must follow PEP 440 (e.g., `1.2.3`, `1.2.3a1`, `1.2.3.post1`, `1.2.3.dev1`)
|
|
191
253
|
|
|
254
|
+
### Subfolder Versioning
|
|
255
|
+
|
|
256
|
+
When building from a subdirectory (not the main `src/` directory), you **must** specify `--version`:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# Build a subfolder as a separate package
|
|
260
|
+
cd my_project/subfolder_to_build
|
|
261
|
+
python-package-folder --version "1.0.0" --publish pypi
|
|
262
|
+
|
|
263
|
+
# With custom package name
|
|
264
|
+
python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For subfolder builds:
|
|
268
|
+
- **Version is required**: The tool will error if `--version` is not provided
|
|
269
|
+
- **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`)
|
|
270
|
+
- **Temporary configuration**: Creates a temporary `pyproject.toml` with:
|
|
271
|
+
- Custom package name (from `--package-name` or derived)
|
|
272
|
+
- Specified version
|
|
273
|
+
- Correct package path for hatchling
|
|
274
|
+
- Dependency group from parent (if `--dependency-group` is specified)
|
|
275
|
+
- **Package initialization**: Automatically creates `__init__.py` if the subfolder doesn't have one (required for hatchling)
|
|
276
|
+
- **README handling**:
|
|
277
|
+
- If a README file exists in the subfolder, it will be used instead of the parent README
|
|
278
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
279
|
+
- **Auto-restore**: Original `pyproject.toml` is restored after build, and temporary `__init__.py` files are removed
|
|
280
|
+
|
|
192
281
|
### Python API for Version Management
|
|
193
282
|
|
|
194
283
|
```python
|
|
@@ -262,6 +351,21 @@ python-package-folder --publish pypi --skip-existing
|
|
|
262
351
|
**For PyPI/TestPyPI:**
|
|
263
352
|
- **Username**: Your PyPI username, or `__token__` for API tokens
|
|
264
353
|
- **Password**: Your PyPI password or API token (recommended)
|
|
354
|
+
- **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
|
|
355
|
+
|
|
356
|
+
**Common Authentication Issues:**
|
|
357
|
+
- **403 Forbidden**: Usually means you used your username instead of `__token__` with an API token. The tool now auto-detects this.
|
|
358
|
+
- **TestPyPI vs PyPI**: TestPyPI requires a separate account and token from https://test.pypi.org/manage/account/token/
|
|
359
|
+
|
|
360
|
+
### Smart File Filtering
|
|
361
|
+
|
|
362
|
+
When publishing, the tool automatically filters distribution files to only upload those matching the current build:
|
|
363
|
+
|
|
364
|
+
- **Package name matching**: Only uploads files for the package being built
|
|
365
|
+
- **Version matching**: Only uploads files for the specified version
|
|
366
|
+
- **Automatic cleanup**: Old build artifacts in `dist/` are ignored, preventing accidental uploads
|
|
367
|
+
|
|
368
|
+
This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
|
|
265
369
|
|
|
266
370
|
To get a PyPI API token:
|
|
267
371
|
1. Go to https://pypi.org/manage/account/token/
|
|
@@ -350,7 +454,14 @@ options:
|
|
|
350
454
|
--username USERNAME Username for publishing (will prompt if not provided)
|
|
351
455
|
--password PASSWORD Password/token for publishing (will prompt if not provided)
|
|
352
456
|
--skip-existing Skip files that already exist on the repository
|
|
353
|
-
--version VERSION Set a specific version before building (PEP 440 format)
|
|
457
|
+
--version VERSION Set a specific version before building (PEP 440 format).
|
|
458
|
+
Required for subfolder builds.
|
|
459
|
+
--package-name PACKAGE_NAME
|
|
460
|
+
Package name for subfolder builds (default: derived from
|
|
461
|
+
source directory name)
|
|
462
|
+
--dependency-group DEPENDENCY_GROUP
|
|
463
|
+
Dependency group name from parent pyproject.toml to include
|
|
464
|
+
in subfolder build
|
|
354
465
|
--no-restore-versioning
|
|
355
466
|
Don't restore dynamic versioning after build
|
|
356
467
|
```
|
|
@@ -418,15 +529,19 @@ publisher = Publisher(
|
|
|
418
529
|
repository=Repository.PYPI,
|
|
419
530
|
dist_dir=Path("dist"),
|
|
420
531
|
username="__token__",
|
|
421
|
-
password="pypi-xxxxx"
|
|
532
|
+
password="pypi-xxxxx",
|
|
533
|
+
package_name="my-package", # Optional: filter files by package name
|
|
534
|
+
version="1.2.3" # Optional: filter files by version
|
|
422
535
|
)
|
|
423
536
|
publisher.publish()
|
|
424
537
|
```
|
|
425
538
|
|
|
426
539
|
**Methods:**
|
|
427
|
-
- `publish(skip_existing: bool = False) -> None`: Publish the package
|
|
540
|
+
- `publish(skip_existing: bool = False) -> None`: Publish the package (automatically filters by package_name/version if provided)
|
|
428
541
|
- `publish_interactive(skip_existing: bool = False) -> None`: Publish with interactive credential prompts
|
|
429
542
|
|
|
543
|
+
**Note**: When `package_name` and `version` are provided, only distribution files matching those parameters are uploaded. This prevents uploading old build artifacts.
|
|
544
|
+
|
|
430
545
|
### VersionManager
|
|
431
546
|
|
|
432
547
|
Manages package version in pyproject.toml.
|
|
@@ -452,6 +567,39 @@ version_manager.restore_dynamic_versioning()
|
|
|
452
567
|
- `get_current_version() -> str | None`: Get current version from pyproject.toml
|
|
453
568
|
- `restore_dynamic_versioning() -> None`: Restore dynamic versioning configuration
|
|
454
569
|
|
|
570
|
+
### SubfolderBuildConfig
|
|
571
|
+
|
|
572
|
+
Manages temporary build configuration for subfolder builds.
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
from python_package_folder import SubfolderBuildConfig
|
|
576
|
+
from pathlib import Path
|
|
577
|
+
|
|
578
|
+
config = SubfolderBuildConfig(
|
|
579
|
+
project_root=Path("."),
|
|
580
|
+
src_dir=Path("subfolder"),
|
|
581
|
+
package_name="my-subfolder",
|
|
582
|
+
version="1.0.0"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Create temporary pyproject.toml
|
|
586
|
+
config.create_temp_pyproject()
|
|
587
|
+
|
|
588
|
+
# ... build process ...
|
|
589
|
+
|
|
590
|
+
# Restore original configuration
|
|
591
|
+
config.restore()
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Methods:**
|
|
595
|
+
- `create_temp_pyproject() -> Path`: Create temporary `pyproject.toml` with subfolder-specific configuration
|
|
596
|
+
- `restore() -> None`: Restore original `pyproject.toml` and clean up temporary files
|
|
597
|
+
|
|
598
|
+
**Note**: This class automatically creates `__init__.py` files if needed to make subfolders valid Python packages. It also handles README files:
|
|
599
|
+
- If a README exists in the subfolder, it will be used instead of the parent README
|
|
600
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
601
|
+
- The original parent README is backed up and restored after the build completes
|
|
602
|
+
|
|
455
603
|
## How It Works
|
|
456
604
|
|
|
457
605
|
### Build Process
|
|
@@ -474,13 +622,31 @@ version_manager.restore_dynamic_versioning()
|
|
|
474
622
|
### Publishing Process
|
|
475
623
|
|
|
476
624
|
1. **Build Verification**: Ensures distribution files exist in the `dist/` directory
|
|
477
|
-
2. **
|
|
625
|
+
2. **File Filtering**: Automatically filters distribution files to only include those matching the current package name and version (prevents uploading old artifacts)
|
|
626
|
+
3. **Credential Management**:
|
|
478
627
|
- Prompts for credentials if not provided
|
|
479
628
|
- Uses `keyring` for secure storage (if available)
|
|
480
629
|
- Supports both username/password and API tokens
|
|
481
|
-
|
|
482
|
-
4. **
|
|
483
|
-
5. **
|
|
630
|
+
- Auto-detects API tokens and uses `__token__` as username
|
|
631
|
+
4. **Repository Configuration**: Configures the target repository (PyPI, TestPyPI, or Azure)
|
|
632
|
+
5. **Upload**: Uses `twine` to upload distribution files to the repository
|
|
633
|
+
6. **Verification**: Confirms successful upload
|
|
634
|
+
|
|
635
|
+
### Subfolder Build Process
|
|
636
|
+
|
|
637
|
+
1. **Project Root Detection**: Searches parent directories for `pyproject.toml`
|
|
638
|
+
2. **Source Directory Detection**: Uses current directory if it contains Python files, otherwise falls back to `project_root/src`
|
|
639
|
+
3. **Package Initialization**: Creates temporary `__init__.py` if subfolder doesn't have one (required for hatchling)
|
|
640
|
+
4. **README Handling**:
|
|
641
|
+
- Checks for README files in the subfolder (README.md, README.rst, README.txt, or README)
|
|
642
|
+
- If found, copies the subfolder README to project root (backing up the original parent README)
|
|
643
|
+
- If not found, creates a minimal README with just the folder name
|
|
644
|
+
5. **Configuration Creation**: Creates temporary `pyproject.toml` with:
|
|
645
|
+
- Subfolder-specific package name (derived or custom)
|
|
646
|
+
- Specified version
|
|
647
|
+
- Correct package path for hatchling
|
|
648
|
+
6. **Build Execution**: Runs build command with all dependencies in place
|
|
649
|
+
7. **Cleanup**: Restores original `pyproject.toml` and removes temporary `__init__.py`
|
|
484
650
|
|
|
485
651
|
## Requirements
|
|
486
652
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# python-package-folder
|
|
2
2
|
|
|
3
|
+
[](https://github.com/alelom/python-package-folder/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/alelom/python-package-folder)
|
|
5
|
+
|
|
3
6
|
Python package to automatically analyze, detect, and manage external dependencies when building Python packages. This tool recursively parses all Python files in your project, identifies imports from outside the main package directory, and temporarily copies them into the source directory during the build process.
|
|
4
7
|
|
|
5
8
|
## Features
|
|
@@ -16,6 +19,10 @@ Python package to automatically analyze, detect, and manage external dependencie
|
|
|
16
19
|
- **Idempotent Operations**: Safely handles repeated runs without duplicating files
|
|
17
20
|
- **Build Integration**: Seamlessly integrates with build tools like `uv build`, `pip build`, etc.
|
|
18
21
|
- **Warning System**: Reports ambiguous imports that couldn't be resolved
|
|
22
|
+
- **Subfolder Build Support**: Build subfolders as separate packages with automatic project root detection
|
|
23
|
+
- **Smart Publishing**: Only uploads distribution files from the current build, filtering out old artifacts
|
|
24
|
+
- **Auto-Detection**: Automatically finds project root and source directory when run from any subdirectory
|
|
25
|
+
- **Authentication Helpers**: Auto-detects API tokens and uses correct username format
|
|
19
26
|
|
|
20
27
|
## Installation
|
|
21
28
|
|
|
@@ -44,16 +51,71 @@ uv add twine
|
|
|
44
51
|
The simplest way to use this package is via the command-line interface:
|
|
45
52
|
|
|
46
53
|
```bash
|
|
47
|
-
# Build with automatic dependency management
|
|
54
|
+
# Build with automatic dependency management (from project root)
|
|
48
55
|
python-package-folder --build-command "uv build"
|
|
49
56
|
|
|
50
57
|
# Analyze dependencies without building
|
|
51
58
|
python-package-folder --analyze-only
|
|
52
59
|
|
|
60
|
+
# Build from any subdirectory - auto-detects project root and source directory
|
|
61
|
+
cd tests/folder_structure/subfolder_to_build
|
|
62
|
+
python-package-folder --analyze-only
|
|
63
|
+
|
|
53
64
|
# Specify custom project root and source directory
|
|
54
65
|
python-package-folder --project-root /path/to/project --src-dir /path/to/src --build-command "pip build"
|
|
55
66
|
```
|
|
56
67
|
|
|
68
|
+
### Building from Subdirectories
|
|
69
|
+
|
|
70
|
+
The tool can automatically detect the project root by searching for `pyproject.toml` in parent directories. This allows you to build subfolders of a main project as separate packages:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# From a subdirectory, the tool will:
|
|
74
|
+
# 1. Find pyproject.toml in parent directories (project root)
|
|
75
|
+
# 2. Use current directory as source if it contains Python files
|
|
76
|
+
# 3. Build with dependencies from the parent project
|
|
77
|
+
# 4. Create a temporary build config with subfolder-specific name and version
|
|
78
|
+
|
|
79
|
+
cd my_project/subfolder_to_build
|
|
80
|
+
python-package-folder --version "1.0.0" --publish pypi
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Important**: When building from a subdirectory, you **must** specify `--version` because subfolders are built as separate packages with their own version.
|
|
84
|
+
|
|
85
|
+
The tool automatically:
|
|
86
|
+
- Finds the project root by looking for `pyproject.toml` in parent directories
|
|
87
|
+
- Uses the current directory as the source directory if it contains Python files
|
|
88
|
+
- Falls back to `project_root/src` if the current directory isn't suitable
|
|
89
|
+
- For subfolder builds: creates a temporary `pyproject.toml` with:
|
|
90
|
+
- Package name derived from the subfolder name (or use `--package-name` to override)
|
|
91
|
+
- Version from `--version` argument
|
|
92
|
+
- Proper package path configuration for hatchling
|
|
93
|
+
- Creates temporary `__init__.py` files if needed to make subfolders valid Python packages
|
|
94
|
+
- **README handling for subfolder builds**:
|
|
95
|
+
- 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
|
|
96
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
97
|
+
- Restores the original `pyproject.toml` after build (unless `--no-restore-versioning` is used)
|
|
98
|
+
- Cleans up temporary `__init__.py` files after build
|
|
99
|
+
|
|
100
|
+
**Subfolder Build Example:**
|
|
101
|
+
```bash
|
|
102
|
+
# Build a subfolder as a separate package
|
|
103
|
+
cd tests/folder_structure/subfolder_to_build
|
|
104
|
+
python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --publish pypi
|
|
105
|
+
|
|
106
|
+
# Build with a specific dependency group from parent pyproject.toml
|
|
107
|
+
python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**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:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Use the 'dev' dependency group from parent pyproject.toml
|
|
114
|
+
python-package-folder --version "1.0.0" --dependency-group "dev" --publish pypi
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
57
119
|
### Python API Usage
|
|
58
120
|
|
|
59
121
|
You can also use the package programmatically:
|
|
@@ -169,6 +231,33 @@ The `--version` option:
|
|
|
169
231
|
|
|
170
232
|
**Version Format**: Versions must follow PEP 440 (e.g., `1.2.3`, `1.2.3a1`, `1.2.3.post1`, `1.2.3.dev1`)
|
|
171
233
|
|
|
234
|
+
### Subfolder Versioning
|
|
235
|
+
|
|
236
|
+
When building from a subdirectory (not the main `src/` directory), you **must** specify `--version`:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Build a subfolder as a separate package
|
|
240
|
+
cd my_project/subfolder_to_build
|
|
241
|
+
python-package-folder --version "1.0.0" --publish pypi
|
|
242
|
+
|
|
243
|
+
# With custom package name
|
|
244
|
+
python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
For subfolder builds:
|
|
248
|
+
- **Version is required**: The tool will error if `--version` is not provided
|
|
249
|
+
- **Package name**: Automatically derived from the subfolder name (e.g., `subfolder_to_build` → `subfolder-to-build`)
|
|
250
|
+
- **Temporary configuration**: Creates a temporary `pyproject.toml` with:
|
|
251
|
+
- Custom package name (from `--package-name` or derived)
|
|
252
|
+
- Specified version
|
|
253
|
+
- Correct package path for hatchling
|
|
254
|
+
- Dependency group from parent (if `--dependency-group` is specified)
|
|
255
|
+
- **Package initialization**: Automatically creates `__init__.py` if the subfolder doesn't have one (required for hatchling)
|
|
256
|
+
- **README handling**:
|
|
257
|
+
- If a README file exists in the subfolder, it will be used instead of the parent README
|
|
258
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
259
|
+
- **Auto-restore**: Original `pyproject.toml` is restored after build, and temporary `__init__.py` files are removed
|
|
260
|
+
|
|
172
261
|
### Python API for Version Management
|
|
173
262
|
|
|
174
263
|
```python
|
|
@@ -242,6 +331,21 @@ python-package-folder --publish pypi --skip-existing
|
|
|
242
331
|
**For PyPI/TestPyPI:**
|
|
243
332
|
- **Username**: Your PyPI username, or `__token__` for API tokens
|
|
244
333
|
- **Password**: Your PyPI password or API token (recommended)
|
|
334
|
+
- **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
|
|
335
|
+
|
|
336
|
+
**Common Authentication Issues:**
|
|
337
|
+
- **403 Forbidden**: Usually means you used your username instead of `__token__` with an API token. The tool now auto-detects this.
|
|
338
|
+
- **TestPyPI vs PyPI**: TestPyPI requires a separate account and token from https://test.pypi.org/manage/account/token/
|
|
339
|
+
|
|
340
|
+
### Smart File Filtering
|
|
341
|
+
|
|
342
|
+
When publishing, the tool automatically filters distribution files to only upload those matching the current build:
|
|
343
|
+
|
|
344
|
+
- **Package name matching**: Only uploads files for the package being built
|
|
345
|
+
- **Version matching**: Only uploads files for the specified version
|
|
346
|
+
- **Automatic cleanup**: Old build artifacts in `dist/` are ignored, preventing accidental uploads
|
|
347
|
+
|
|
348
|
+
This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
|
|
245
349
|
|
|
246
350
|
To get a PyPI API token:
|
|
247
351
|
1. Go to https://pypi.org/manage/account/token/
|
|
@@ -330,7 +434,14 @@ options:
|
|
|
330
434
|
--username USERNAME Username for publishing (will prompt if not provided)
|
|
331
435
|
--password PASSWORD Password/token for publishing (will prompt if not provided)
|
|
332
436
|
--skip-existing Skip files that already exist on the repository
|
|
333
|
-
--version VERSION Set a specific version before building (PEP 440 format)
|
|
437
|
+
--version VERSION Set a specific version before building (PEP 440 format).
|
|
438
|
+
Required for subfolder builds.
|
|
439
|
+
--package-name PACKAGE_NAME
|
|
440
|
+
Package name for subfolder builds (default: derived from
|
|
441
|
+
source directory name)
|
|
442
|
+
--dependency-group DEPENDENCY_GROUP
|
|
443
|
+
Dependency group name from parent pyproject.toml to include
|
|
444
|
+
in subfolder build
|
|
334
445
|
--no-restore-versioning
|
|
335
446
|
Don't restore dynamic versioning after build
|
|
336
447
|
```
|
|
@@ -398,15 +509,19 @@ publisher = Publisher(
|
|
|
398
509
|
repository=Repository.PYPI,
|
|
399
510
|
dist_dir=Path("dist"),
|
|
400
511
|
username="__token__",
|
|
401
|
-
password="pypi-xxxxx"
|
|
512
|
+
password="pypi-xxxxx",
|
|
513
|
+
package_name="my-package", # Optional: filter files by package name
|
|
514
|
+
version="1.2.3" # Optional: filter files by version
|
|
402
515
|
)
|
|
403
516
|
publisher.publish()
|
|
404
517
|
```
|
|
405
518
|
|
|
406
519
|
**Methods:**
|
|
407
|
-
- `publish(skip_existing: bool = False) -> None`: Publish the package
|
|
520
|
+
- `publish(skip_existing: bool = False) -> None`: Publish the package (automatically filters by package_name/version if provided)
|
|
408
521
|
- `publish_interactive(skip_existing: bool = False) -> None`: Publish with interactive credential prompts
|
|
409
522
|
|
|
523
|
+
**Note**: When `package_name` and `version` are provided, only distribution files matching those parameters are uploaded. This prevents uploading old build artifacts.
|
|
524
|
+
|
|
410
525
|
### VersionManager
|
|
411
526
|
|
|
412
527
|
Manages package version in pyproject.toml.
|
|
@@ -432,6 +547,39 @@ version_manager.restore_dynamic_versioning()
|
|
|
432
547
|
- `get_current_version() -> str | None`: Get current version from pyproject.toml
|
|
433
548
|
- `restore_dynamic_versioning() -> None`: Restore dynamic versioning configuration
|
|
434
549
|
|
|
550
|
+
### SubfolderBuildConfig
|
|
551
|
+
|
|
552
|
+
Manages temporary build configuration for subfolder builds.
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
from python_package_folder import SubfolderBuildConfig
|
|
556
|
+
from pathlib import Path
|
|
557
|
+
|
|
558
|
+
config = SubfolderBuildConfig(
|
|
559
|
+
project_root=Path("."),
|
|
560
|
+
src_dir=Path("subfolder"),
|
|
561
|
+
package_name="my-subfolder",
|
|
562
|
+
version="1.0.0"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Create temporary pyproject.toml
|
|
566
|
+
config.create_temp_pyproject()
|
|
567
|
+
|
|
568
|
+
# ... build process ...
|
|
569
|
+
|
|
570
|
+
# Restore original configuration
|
|
571
|
+
config.restore()
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Methods:**
|
|
575
|
+
- `create_temp_pyproject() -> Path`: Create temporary `pyproject.toml` with subfolder-specific configuration
|
|
576
|
+
- `restore() -> None`: Restore original `pyproject.toml` and clean up temporary files
|
|
577
|
+
|
|
578
|
+
**Note**: This class automatically creates `__init__.py` files if needed to make subfolders valid Python packages. It also handles README files:
|
|
579
|
+
- If a README exists in the subfolder, it will be used instead of the parent README
|
|
580
|
+
- If no README exists in the subfolder, a minimal README with just the folder name will be created
|
|
581
|
+
- The original parent README is backed up and restored after the build completes
|
|
582
|
+
|
|
435
583
|
## How It Works
|
|
436
584
|
|
|
437
585
|
### Build Process
|
|
@@ -454,13 +602,31 @@ version_manager.restore_dynamic_versioning()
|
|
|
454
602
|
### Publishing Process
|
|
455
603
|
|
|
456
604
|
1. **Build Verification**: Ensures distribution files exist in the `dist/` directory
|
|
457
|
-
2. **
|
|
605
|
+
2. **File Filtering**: Automatically filters distribution files to only include those matching the current package name and version (prevents uploading old artifacts)
|
|
606
|
+
3. **Credential Management**:
|
|
458
607
|
- Prompts for credentials if not provided
|
|
459
608
|
- Uses `keyring` for secure storage (if available)
|
|
460
609
|
- Supports both username/password and API tokens
|
|
461
|
-
|
|
462
|
-
4. **
|
|
463
|
-
5. **
|
|
610
|
+
- Auto-detects API tokens and uses `__token__` as username
|
|
611
|
+
4. **Repository Configuration**: Configures the target repository (PyPI, TestPyPI, or Azure)
|
|
612
|
+
5. **Upload**: Uses `twine` to upload distribution files to the repository
|
|
613
|
+
6. **Verification**: Confirms successful upload
|
|
614
|
+
|
|
615
|
+
### Subfolder Build Process
|
|
616
|
+
|
|
617
|
+
1. **Project Root Detection**: Searches parent directories for `pyproject.toml`
|
|
618
|
+
2. **Source Directory Detection**: Uses current directory if it contains Python files, otherwise falls back to `project_root/src`
|
|
619
|
+
3. **Package Initialization**: Creates temporary `__init__.py` if subfolder doesn't have one (required for hatchling)
|
|
620
|
+
4. **README Handling**:
|
|
621
|
+
- Checks for README files in the subfolder (README.md, README.rst, README.txt, or README)
|
|
622
|
+
- If found, copies the subfolder README to project root (backing up the original parent README)
|
|
623
|
+
- If not found, creates a minimal README with just the folder name
|
|
624
|
+
5. **Configuration Creation**: Creates temporary `pyproject.toml` with:
|
|
625
|
+
- Subfolder-specific package name (derived or custom)
|
|
626
|
+
- Specified version
|
|
627
|
+
- Correct package path for hatchling
|
|
628
|
+
6. **Build Execution**: Runs build command with all dependencies in place
|
|
629
|
+
7. **Cleanup**: Restores original `pyproject.toml` and removes temporary `__init__.py`
|
|
464
630
|
|
|
465
631
|
## Requirements
|
|
466
632
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
|
|
2
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
3
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
4
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
5
|
+
</linearGradient>
|
|
6
|
+
<mask id="a">
|
|
7
|
+
<rect width="99" height="20" rx="3" fill="#fff"/>
|
|
8
|
+
</mask>
|
|
9
|
+
<g mask="url(#a)">
|
|
10
|
+
<path fill="#555" d="M0 0h63v20H0z"/>
|
|
11
|
+
<path fill="#dfb317" d="M63 0h36v20H63z"/>
|
|
12
|
+
<path fill="url(#b)" d="M0 0h99v20H0z"/>
|
|
13
|
+
</g>
|
|
14
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
15
|
+
<text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
16
|
+
<text x="31.5" y="14">coverage</text>
|
|
17
|
+
<text x="81" y="15" fill="#010101" fill-opacity=".3">64%</text>
|
|
18
|
+
<text x="81" y="14">64%</text>
|
|
19
|
+
</g>
|
|
20
|
+
</svg>
|