Easily build and publish any target folder in a repository, including subfolders of a monorepo.
Together with sysappend, this library makes relative imports, flexible import management, and package publishing a breeze.
- Use Cases
- Features
- Installation and requirements
- Quick Start
- How does
python-package-folderwork? - Python API Usage
- Working with sysappend
- Publishing version Management
- Publishing Packages
- Command Line Options
- API Reference
- Development
If you have a monorepo structure with multiple packages in src/:
project/
├── src/
│ ├── core_package/
│ │ ├── __init__.py
│ │ ├── core.py
│ │ └── README.md
│ ├── api_package/
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── README.md
│ └── utils_package/
│ ├── __init__.py
│ ├── utils.py
│ └── README.md
├── shared/
│ └── common.py
└── pyproject.toml
You can build and publish any subfolder from src/ as a standalone package:
# Navigate to the subfolder you want to publish
cd src/api_package
# Build and publish to TestPyPI with version 1.2.0
python-package-folder --publish testpypi --version 1.2.0
# Or publish to PyPI with a custom package name
python-package-folder --publish pypi --version 1.2.0 --package-name "my-api-package"
# Include a specific dependency group from the parent pyproject.toml
python-package-folder --publish pypi --version 1.2.0 --dependency-group "dev"The tool will automatically:
- Detect the project root (where
pyproject.tomlis located) - Use
src/api_packageas the source directory - Copy any external dependencies (like
shared/common.py) into the package before building - Use the subfolder's README if present, or create a minimal one
- Create a temporary
pyproject.tomlwith the subfolder's package name and version - Build and publish the package
- Clean up all temporary files and restore the original
pyproject.toml
This is especially useful for monorepos where you want to publish individual packages independently while sharing common code.
If your project structure looks like this:
project/
├── src/
│ └── my_package/
│ └── main.py
├── shared/
│ ├── utils.py
│ └── helpers.py
└── pyproject.toml
And main.py imports from shared/:
from shared.utils import some_function
from shared.helpers import HelperThis package will automatically:
- Detect that
shared/is outsidesrc/ - Copy
shared/intosrc/before building - Build your package with all dependencies included
- Clean up the copied files after build
-
Subfolder Build Support: Build subfolders as separate packages with automatic detection and configuration
- Automatic subfolder detection: Detects when building a subfolder (not the main
src/directory) - 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__.pycreation for non-package subfolders; uses subfolder README if present, otherwise creates minimal README - Automatic package name derivation from subfolder name
- Automatic temporary
pyproject.tomlcreation with correct package structure - Dependency group selection: specify which dependency group from parent
pyproject.tomlto include.
- Automatic subfolder detection: Detects when building a subfolder (not the main
-
Smart Import Classification and analysis:
- Recursively parses all
.pyfiles to detectimportandfrom ... import ...statements - 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.
- Recursively parses all
-
Idempotent Operations: Safely handles repeated runs without duplicating files
-
Build Integration: Seamlessly integrates with build tools like
uv build,pip build, etc. -
Version Management:
- Set static versions for publishing (PEP 440 compliant)
- Temporarily override dynamic versioning during builds
- Automatic restoration of dynamic versioning after build
-
Package Publishing:
- Uses twine to publish the built folder/subfolder
- Handles publishing to to PyPI, TestPyPI, or Azure Artifacts, with interactive credential prompts, secure storage support
Python >= 3.11 is required.
uv add python-package-folder
# or
pip install python-package-folderNote: For publishing functionality, you'll also need twine:
pip install twine
# or
uv add twineFor secure credential storage: keyring is optional but recommended (install with pip install keyring)
The simplest way to use this package is via the command-line interface
Build/publish a specific subfolder in a repository
Useful for monorepos containing many subfolders that may need publishing as stand-alone packages for external usage.
# First cd to the specific subfolder
cd src/subfolder_to_build_and_publish
# Build and publish any subdirectory of your repo to TestPyPi (https://test.pypi.org/)
python-package-folder --publish testpypi --version 0.0.2
# Only analyse (no building)
cd src/subfolder_to_build_and_publish
python-package-folder --analyze-only
# Only build
cd src/subfolder_to_build_and_publish
python-package-folder
# Build with automatic dependency management
python-package-folder --build-command "uv build"You can also target a specific subfolder via commandline, rather than cding there:
# Specify custom project root and source directory
python-package-folder --project-root /path/to/project --src-dir /path/to/src --build-command "pip build"- Import Extraction: Uses Python's AST module to parse all
.pyfiles and extract import statements - Classification: Each import is classified as:
- stdlib: Standard library modules
- third_party: Packages installed in site-packages
- local: Modules within the source directory
- external: Modules outside source directory but in the project
- ambiguous: Cannot be resolved
- Dependency Resolution: For external imports, the tool resolves the file path by checking:
- Parent directories of the source directory
- Project root and its subdirectories
- Relative import paths
- File Copying: External dependencies are temporarily copied into the source directory
- Build Execution: Your build command runs with all dependencies in place
- Cleanup: All temporarily copied files are removed after build
- Build Verification: Ensures distribution files exist in the
dist/directory - File Filtering: Automatically filters distribution files to only include those matching the current package name and version (prevents uploading old artifacts)
- Credential Management:
- Prompts for credentials if not provided via command-line arguments
- Credentials are not stored - you'll be prompted each time (unless provided via
--usernameand--password) - Supports both username/password and API tokens
- Auto-detects API tokens and uses
__token__as username
- Repository Configuration: Configures the target repository (PyPI, TestPyPI, or Azure)
- Upload: Uses
twineto upload distribution files to the repository - Verification: Confirms successful upload
- Project Root Detection: Searches parent directories for
pyproject.toml - Source Directory Detection: Uses current directory if it contains Python files, otherwise falls back to
project_root/src - Package Initialization: Creates temporary
__init__.pyif subfolder doesn't have one (required for hatchling) - README Handling:
- Checks for README files in the subfolder (README.md, README.rst, README.txt, or README)
- If found, copies the subfolder README to project root (backing up the original parent README)
- If not found, creates a minimal README with just the folder name
- Configuration Creation: Creates temporary
pyproject.tomlwith:[build-system]section using hatchling (replaces any existing build-system configuration)- Subfolder-specific package name (derived or custom)
- Specified version
- Correct package path for hatchling
- Build Execution: Runs build command with all dependencies in place
- Cleanup: Restores original
pyproject.tomland removes temporary__init__.py
This is useful for monorepos containing many subfolders that may need publishing as stand-alone packages for external usage.
The tool automatically detects the project root by searching for pyproject.toml in parent directories.
This allows you to build subfolders of a main project as separate packages:
# From a subdirectory, the tool will:
# 1. Find pyproject.toml in parent directories (project root)
# 2. Use current directory as source if it contains Python files
# 3. Build with dependencies from the parent project
# 4. Create a temporary build config with subfolder-specific name and version
cd my_project/subfolder_to_build
python-package-folder --version "1.0.0" --publish pypiThe 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.
The tool automatically:
- Detects subfolder builds: Automatically identifies when building from a subdirectory
- Finds the project root by looking for
pyproject.tomlin parent directories - Uses the current directory as the source directory if it contains Python files
- Falls back to
project_root/srcif the current directory isn't suitable - For subfolder builds: Handles
pyproject.tomlconfiguration:- If
pyproject.tomlexists in subfolder: Uses that file (copies it to project root temporarily, adjusting package paths and ensuring[build-system]uses hatchling) - If no
pyproject.tomlin subfolder: Creates a temporarypyproject.tomlwith:[build-system]section using hatchling (always uses hatchling, even if parent uses setuptools)- Package name derived from the subfolder name (e.g.,
empty_drawing_detection→empty-drawing-detection) - Version from
--versionargument (defaults to0.0.0with a warning if not provided) - Proper package path configuration for hatchling
- Dependency groups from parent
pyproject.tomlif specified
- If
- Creates temporary
__init__.pyfiles if needed to make subfolders valid Python packages - README handling for subfolder builds:
- 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
- If no README exists in the subfolder, a minimal README with just the folder name will be created
- Restores the original
pyproject.tomlafter build (unless--no-restore-versioningis used) - Cleans up temporary
__init__.pyfiles after build
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.
Subfolder Build Example:
# Build a subfolder as a separate package
cd tests/folder_structure/subfolder_to_build
python-package-folder --version "0.1.0" --package-name "my-subfolder-package" --publish pypi
# Build with a specific dependency group from parent pyproject.toml
python-package-folder --version "0.1.0" --dependency-group "dev" --publish pypi
# If subfolder has its own pyproject.toml, it will be used automatically
# (package-name and version arguments are ignored in this case)
cd src/integration/my_package # assuming my_package/pyproject.toml exists
python-package-folder --publish pypiDependency 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:
# Use the 'dev' dependency group from parent pyproject.toml
python-package-folder --version "1.0.0" --dependency-group "dev" --publish pypiThe 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.
You can also use the package programmatically:
from pathlib import Path
from python_package_folder import BuildManager
# Initialize the build manager
manager = BuildManager(
project_root=Path("."),
src_dir=Path("src")
)
# Prepare build (finds and copies external dependencies)
external_deps = manager.prepare_build()
print(f"Found {len(external_deps)} external dependencies")
for dep in external_deps:
print(f" {dep.import_name}: {dep.source_path} -> {dep.target_path}")
# Run your build process here
# ...
# Cleanup copied files (also restores pyproject.toml if subfolder build)
manager.cleanup()from pathlib import Path
from python_package_folder import BuildManager
import subprocess
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
def build_command():
subprocess.run(["uv", "build"], check=True)
# Automatically handles prepare, build, and cleanup
manager.run_build(build_command)The tool automatically detects when you're building a subfolder and sets up the appropriate configuration:
from pathlib import Path
from python_package_folder import BuildManager
import subprocess
# Building a subfolder - automatic detection!
manager = BuildManager(
project_root=Path("."),
src_dir=Path("src/integration/empty_drawing_detection")
)
def build_command():
subprocess.run(["uv", "build"], check=True)
# prepare_build() automatically:
# - Detects this is a subfolder build
# - If pyproject.toml exists in subfolder: uses that file
# - If no pyproject.toml in subfolder: creates temporary one with package name "empty-drawing-detection"
# - Uses version "0.0.0" (or pass version="1.0.0" to override) if creating temporary pyproject.toml
external_deps = manager.prepare_build(version="1.0.0")
# Run build - uses the pyproject.toml (either from subfolder or temporary)
build_command()
# Cleanup restores original pyproject.toml and removes copied files
manager.cleanup()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.
Or use the convenience method:
manager = BuildManager(
project_root=Path("."),
src_dir=Path("src/integration/empty_drawing_detection")
)
def build_command():
subprocess.run(["uv", "build"], check=True)
# All handled automatically: subfolder detection, pyproject.toml setup, build, cleanup
manager.run_build(build_command, version="1.0.0", package_name="my-custom-name")This package works well with projects using sysappend for flexible import management. When you have imports like:
if True:
import sysappend; sysappend.all()
from some_globals import SOME_GLOBAL_VARIABLE
from folder_structure.utility_folder.some_utility import print_somethingThe package will correctly identify and copy external dependencies even when they're referenced without full package paths.
The package supports both dynamic versioning (from git tags) and manual version specification.
You can manually set a version before building and publishing:
# Build with a specific version
python-package-folder --version "1.2.3"
# Build and publish with a specific version
python-package-folder --version "1.2.3" --publish pypi
# Keep the static version (don't restore dynamic versioning)
python-package-folder --version "1.2.3" --no-restore-versioningThe --version option:
- Sets a static version in
pyproject.tomlbefore building - Temporarily removes dynamic versioning configuration
- Restores the original configuration after build (unless
--no-restore-versioningis used) - Validates version format (must be PEP 440 compliant)
Version Format: Versions must follow PEP 440 (e.g., 1.2.3, 1.2.3a1, 1.2.3.post1, 1.2.3.dev1)
When building from a subdirectory (not the main src/ directory), the tool automatically detects the subfolder and sets up the build configuration:
# Build a subfolder as a separate package (version recommended but not required)
cd my_project/subfolder_to_build
python-package-folder --version "1.0.0" --publish pypi
# With custom package name
python-package-folder --version "1.0.0" --package-name "my-custom-name" --publish pypi
# Version defaults to "0.0.0" if not specified (with a warning)
python-package-folder --publish pypiFor subfolder builds:
- Automatic detection: The tool automatically detects subfolder builds
- pyproject.toml handling:
- If
pyproject.tomlexists in subfolder: Uses that file (copied to project root temporarily) - If no
pyproject.tomlin subfolder: Creates temporary one with correct package structure
- If
- Version: Recommended but not required when creating temporary pyproject.toml. If not provided, defaults to
0.0.0with a warning. Ignored if subfolder has its ownpyproject.toml. - Package name: Automatically derived from the subfolder name (e.g.,
subfolder_to_build→subfolder-to-build). Only used when creating temporary pyproject.toml. - Restoration: Original
pyproject.tomlis restored after build - Temporary configuration: Creates a temporary
pyproject.tomlwith:- Custom package name (from
--package-nameor derived) - Specified version
- Correct package path for hatchling
- Dependency group from parent (if
--dependency-groupis specified)
- Custom package name (from
- Package initialization: Automatically creates
__init__.pyif the subfolder doesn't have one (required for hatchling) - README handling:
- If a README file 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
- Auto-restore: Original
pyproject.tomlis restored after build, and temporary__init__.pyfiles are removed
from python_package_folder import VersionManager
from pathlib import Path
# Set a version
version_manager = VersionManager(project_root=Path("."))
version_manager.set_version("1.2.3")
# Get current version
current_version = version_manager.get_current_version()
# Restore dynamic versioning
version_manager.restore_dynamic_versioning()By default, the package uses uv-dynamic-versioning which derives versions from git tags. This is configured in pyproject.toml:
[project]
dynamic = ["version"]
[tool.hatch.version]
source = "uv-dynamic-versioning"
[tool.uv-dynamic-versioning]
vcs = "git"
style = "pep440"
bump = trueWhen you use --version, the package temporarily switches to static versioning for that build, then restores the dynamic configuration.
The package includes built-in support for publishing to PyPI, TestPyPI, and Azure Artifacts.
Publish after building:
# Publish to PyPI
python-package-folder --publish pypi
# Publish to PyPI with a specific version
python-package-folder --version "1.2.3" --publish pypi
# Publish to TestPyPI (for testing)
python-package-folder --publish testpypi
# Publish to Azure Artifacts
python-package-folder --publish azure --repository-url "https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/pypi/upload"The command will prompt for credentials if not provided:
# Provide credentials via command line (less secure)
python-package-folder --publish pypi --username __token__ --password pypi-xxxxx
# Skip existing files on repository
python-package-folder --publish pypi --skip-existingFor PyPI/TestPyPI:
- Username: Your PyPI username, or
__token__for API tokens - Password: Your PyPI password or API token (recommended)
- 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
Common Authentication Issues:
- 403 Forbidden: Usually means you used your username instead of
__token__with an API token. The tool now auto-detects this. - TestPyPI vs PyPI: TestPyPI requires a separate account and token from https://test.pypi.org/manage/account/token/
When publishing, the tool automatically filters distribution files to only upload those matching the current build:
- Package name matching: Only uploads files for the package being built
- Version matching: Only uploads files for the specified version
- Automatic cleanup: Old build artifacts in
dist/are ignored, preventing accidental uploads
This ensures that when building a subfolder package, only that package's distribution files are uploaded, not files from previous builds of other packages.
To get a PyPI API token:
- Go to https://pypi.org/manage/account/token/
- Create a new API token
- Use
__token__as username and the token as password
For Azure Artifacts:
- Username: Your Azure username or feed name
- Password: Personal Access Token (PAT) with packaging permissions
- Repository URL: Your Azure Artifacts feed URL
You can also publish programmatically:
from pathlib import Path
from python_package_folder import BuildManager, Publisher, Repository
import subprocess
# Build and publish in one step
manager = BuildManager(project_root=Path("."), src_dir=Path("src"))
def build():
subprocess.run(["uv", "build"], check=True)
manager.build_and_publish(
build,
repository="pypi",
username="__token__",
password="pypi-xxxxx",
version="1.2.3" # Optional: set specific version
)
Or publish separately:
```python
from python_package_folder import Publisher, Repository
# Publish existing distribution
publisher = Publisher(
repository=Repository.PYPI,
dist_dir=Path("dist"),
username="__token__",
password="pypi-xxxxx"
)
publisher.publish()
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.
If you previously used an older version that stored credentials in keyring, you can clear them using:
from python_package_folder import Publisher, Repository
publisher = Publisher(repository=Repository.AZURE)
publisher.clear_stored_credentials()Or manually using Python:
import keyring
keyring.delete_password("python-package-folder-azure", "username")
# Also delete the password if you know the usernameusage: python-package-folder [-h] [--project-root PROJECT_ROOT]
[--src-dir SRC_DIR] [--analyze-only]
[--build-command BUILD_COMMAND]
[--publish {pypi,testpypi,azure}]
[--repository-url REPOSITORY_URL]
[--username USERNAME] [--password PASSWORD]
[--skip-existing]
Build Python package with external dependency management
options:
-h, --help show this help message and exit
--project-root PROJECT_ROOT
Root directory of the project (default: current directory)
--src-dir SRC_DIR Source directory (default: project_root/src)
--analyze-only Only analyze imports, don't run build
--build-command BUILD_COMMAND
Command to run for building (default: 'uv build')
--publish {pypi,testpypi,azure}
Publish to repository after building
--repository-url REPOSITORY_URL
Custom repository URL (required for Azure Artifacts)
--username USERNAME Username for publishing (will prompt if not provided)
--password PASSWORD Password/token for publishing (will prompt if not provided)
--skip-existing Skip files that already exist on the repository
--version VERSION Set a specific version before building (PEP 440 format).
Required for subfolder builds.
--package-name PACKAGE_NAME
Package name for subfolder builds (default: derived from
source directory name)
--dependency-group DEPENDENCY_GROUP
Dependency group name from parent pyproject.toml to include
in subfolder build
--no-restore-versioning
Don't restore dynamic versioning after build
Main class for managing the build process with external dependency handling.
from python_package_folder import BuildManager
from pathlib import Path
manager = BuildManager(
project_root: Path, # Root directory of the project
src_dir: Path | None # Source directory (default: project_root/src)
)Methods:
prepare_build() -> list[ExternalDependency]: Find and copy external dependenciescleanup() -> None: Remove all copied files and directoriesrun_build(build_command: Callable[[], None]) -> None: Run build with automatic prepare and cleanup
Analyzes Python files to extract and classify import statements.
from python_package_folder import ImportAnalyzer
from pathlib import Path
analyzer = ImportAnalyzer(project_root=Path("."))
python_files = analyzer.find_all_python_files(Path("src"))
imports = analyzer.extract_imports(python_files[0])
analyzer.classify_import(imports[0], src_dir=Path("src"))Finds external dependencies that need to be copied.
from python_package_folder import ExternalDependencyFinder
from pathlib import Path
finder = ExternalDependencyFinder(
project_root=Path("."),
src_dir=Path("src")
)
dependencies = finder.find_external_dependencies(python_files)Publishes built packages to PyPI, TestPyPI, or Azure Artifacts.
from python_package_folder import Publisher, Repository
from pathlib import Path
publisher = Publisher(
repository=Repository.PYPI,
dist_dir=Path("dist"),
username="__token__",
password="pypi-xxxxx",
package_name="my-package", # Optional: filter files by package name
version="1.2.3" # Optional: filter files by version
)
publisher.publish()Methods:
publish(skip_existing: bool = False) -> None: Publish the package (automatically filters by package_name/version if provided)publish_interactive(skip_existing: bool = False) -> None: Publish with interactive credential prompts
Note: When package_name and version are provided, only distribution files matching those parameters are uploaded. This prevents uploading old build artifacts.
Manages package version in pyproject.toml.
from python_package_folder import VersionManager
from pathlib import Path
version_manager = VersionManager(project_root=Path("."))
# Set a static version
version_manager.set_version("1.2.3")
# Get current version
version = version_manager.get_current_version()
# Restore dynamic versioning
version_manager.restore_dynamic_versioning()Methods:
set_version(version: str) -> None: Set a static version (validates PEP 440 format)get_current_version() -> str | None: Get current version from pyproject.tomlrestore_dynamic_versioning() -> None: Restore dynamic versioning configuration
Manages temporary build configuration for subfolder builds. If a pyproject.toml exists
in the subfolder, it will be used instead of creating a new one.
from python_package_folder import SubfolderBuildConfig
from pathlib import Path
config = SubfolderBuildConfig(
project_root=Path("."),
src_dir=Path("subfolder"),
package_name="my-subfolder", # Only used if subfolder has no pyproject.toml
version="1.0.0" # Only used if subfolder has no pyproject.toml
)
# Create temporary pyproject.toml (or use subfolder's if it exists)
config.create_temp_pyproject()
# ... build process ...
# Restore original configuration
config.restore()Methods:
create_temp_pyproject() -> Path: Use subfolder'spyproject.tomlif it exists (adjusting package paths and ensuring[build-system]uses hatchling), otherwise create temporarypyproject.tomlwith subfolder-specific configuration including[build-system]section using hatchlingrestore() -> None: Restore originalpyproject.tomland clean up temporary files
Note: This class automatically:
- pyproject.toml handling: If a
pyproject.tomlexists 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. - 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.
- Package initialization: Creates
__init__.pyfiles if needed to make subfolders valid Python packages.
# Clone the repository
git clone https://github.com/alelom/python-package-folder.git
cd python-package-folder
# Install dependencies
uv sync --all-extras
# Run tests
uv run pytest
# Run linting
make lintpython-package-folder/
├── src/
│ └── python_package_folder/
│ ├── __init__.py # Package exports
│ ├── types.py # Type definitions
│ ├── analyzer.py # Import analysis
│ ├── finder.py # Dependency finding
│ ├── manager.py # Build management
│ └── python_package_folder.py # CLI entry point
├── tests/
│ ├── test_build_with_external_deps.py
│ └── folder_structure/ # Test fixtures
├── devtools/
│ └── lint.py # Development tools
└── pyproject.toml
MIT License - see LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.