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.
Files changed (42) hide show
  1. python_package_folder-1.0.0/.github/workflows/ci.yml +126 -0
  2. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/PKG-INFO +175 -9
  3. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/README.md +174 -8
  4. python_package_folder-1.0.0/coverage.svg +20 -0
  5. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/pyproject.toml +2 -0
  6. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/__init__.py +5 -0
  7. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/finder.py +1 -1
  8. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/manager.py +94 -7
  9. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/publisher.py +63 -6
  10. python_package_folder-1.0.0/src/python_package_folder/python_package_folder.py +231 -0
  11. python_package_folder-1.0.0/src/python_package_folder/subfolder_build.py +466 -0
  12. python_package_folder-1.0.0/src/python_package_folder/utils.py +107 -0
  13. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/version.py +1 -2
  14. python_package_folder-1.0.0/tests/folder_structure/subfolder_to_build/README.md +3 -0
  15. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/test_build_with_external_deps.py +2 -2
  16. python_package_folder-1.0.0/tests/test_publisher.py +203 -0
  17. python_package_folder-1.0.0/tests/test_subfolder_build.py +360 -0
  18. python_package_folder-1.0.0/tests/test_utils.py +159 -0
  19. python_package_folder-1.0.0/tests/test_version_manager.py +148 -0
  20. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/tests.py +2 -2
  21. python_package_folder-1.0.0/uv.lock +840 -0
  22. python_package_folder-0.1.0/.github/workflows/ci.yml +0 -63
  23. python_package_folder-0.1.0/src/python_package_folder/python_package_folder.py +0 -151
  24. python_package_folder-0.1.0/uv.lock +0 -231
  25. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.copier-answers.yml +0 -0
  26. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.cursor/rules/general.mdc +0 -0
  27. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.cursor/rules/python.mdc +0 -0
  28. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.github/workflows/publish.yml +0 -0
  29. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.gitignore +0 -0
  30. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/.vscode/settings.json +0 -0
  31. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/LICENSE +0 -0
  32. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/Makefile +0 -0
  33. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/development.md +0 -0
  34. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/installation.md +0 -0
  35. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/publishing.md +0 -0
  36. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/__main__.py +0 -0
  37. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/analyzer.py +0 -0
  38. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/py.typed +0 -0
  39. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/src/python_package_folder/types.py +0 -0
  40. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/folder_structure/some_globals.py +0 -0
  41. {python_package_folder-0.1.0 → python_package_folder-1.0.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  42. {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: 0.1.0
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
+ [![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
+
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. **Credential Management**:
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
- 3. **Repository Configuration**: Configures the target repository (PyPI, TestPyPI, or Azure)
482
- 4. **Upload**: Uses `twine` to upload distribution files to the repository
483
- 5. **Verification**: Confirms successful upload
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
+ [![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)
4
+ [![Coverage](https://raw.githubusercontent.com/alelom/python-package-folder/main/coverage.svg)](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. **Credential Management**:
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
- 3. **Repository Configuration**: Configures the target repository (PyPI, TestPyPI, or Azure)
462
- 4. **Upload**: Uses `twine` to upload distribution files to the repository
463
- 5. **Verification**: Confirms successful upload
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>