Local build utilities
Project description
lfp-build
A workspace management CLI for multi-project Python repositories. It helps bootstrap member packages, keep workspace
pyproject.toml files aligned, build wheels, automate README command docs, and perform bulk rename operations.
Features
- Workspace Sync: Keep shared build settings, member dependency wiring, and workspace paths consistent.
- Project Creation: Create new member packages or bootstrap a new workspace root with a starter package.
- Wheel Builds: Build wheel artifacts for all workspace members or a selected subset.
- README Automation: Refresh command output blocks embedded in documentation files.
- Bulk Renames: Rewrite names across files and directories, with optional dash-to-underscore handling.
Installation
This package requires Python >= 3.10.12 and is published to PyPI: pypi.org/project/lfp-build.
Install the latest released version from PyPI:
pip install lfp-build
To install the latest unreleased version directly from GitHub:
pip install git+https://github.com/regbo/lfp-build-py.git
As a Dev Dependency (Recommended)
lfp-build is designed to be used as a development dependency so it stays out of your project's runtime
dependencies while remaining available to every contributor and CI job. Add it to your pyproject.toml:
[dependency-groups]
dev = [
"lfp-build",
]
Or, to track the latest unreleased version from GitHub:
[dependency-groups]
dev = [
"lfp-build @ git+https://github.com/regbo/lfp-build-py.git",
]
Then invoke it through uv run:
# Sync configurations
uv run lfp-build sync
# Initialize a new workspace
uv run lfp-build init new-service
# Add a member to the current workspace
uv run lfp-build add my-package
For lfp-build Development
# Clone and install in editable mode
git clone https://github.com/regbo/lfp-build-py.git
cd lfp-build-py
pip install -e .
Commands
The CLI follows a uv-style flat verb shape. Top-level verbs cover the common
workflow (init, sync, build, add, hooks, rename), with a single
subcommand group for readme.
Init
Usage: lfp-build init [OPTIONS] NAME
Initialize a new workspace root project.
Writes a minimal root pyproject.toml from the bundled template and creates a
common member package under packages/.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching │
│ to a subcommand. │
│ * NAME --name Name of the new workspace. Used as the project │
│ directory name. [required] │
│ --path -p Parent directory to create the workspace in. │
│ [default: .] │
│ --dependency -d Additional dependency strings to add to the common │
│ member package. │
│ --force -f If True, remove an existing target directory before │
│ creating the project. [default: False] │
╰──────────────────────────────────────────────────────────────────────────────╯
Bootstraps a new uv workspace: writes a minimal root pyproject.toml
from a bundled template, copies a local .gitignore template, and seeds
a packages/common member project. Also installs the lfp-build managed
.githooks/pre-commit block (the same logic exposed by lfp-build hooks)
so that lfp-build sync runs before each commit.
# Bootstrap a new workspace at ./agent-demo
uv run lfp-build init agent-demo
# Bootstrap with extra dependencies pre-wired into packages/common
uv run lfp-build init agent-demo --dependency requests --dependency pydantic
# Overwrite an existing target directory
uv run lfp-build init agent-demo --force
Add
Usage: lfp-build add [OPTIONS] NAME
Add a new member project to the workspace.
Sets up a pyproject.toml and a standard src/<package>/__init__.py layout.
Internal workspace dependencies are automatically synchronized after creation.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before │
│ dispatching to a subcommand. │
│ * NAME --name The name of the new project (used for directory │
│ and package name). [required] │
│ --path -p Optional parent directory within the workspace │
│ root. Defaults to packages/. [default: packages] │
│ --project-dependency -c List of existing workspace projects to depend │
│ on. │
│ --dependency -d Additional dependency strings to add to the new │
│ project's project.dependencies array. │
╰──────────────────────────────────────────────────────────────────────────────╯
Adds a new member project to the current workspace. Sets up a
pyproject.toml and a standard src/<package_name>/__init__.py layout, then
runs a workspace sync so the member is wired into [tool.uv.workspace]
immediately.
# Add a member package under packages/
uv run lfp-build add my-project
# Add a member with dependencies on other workspace projects
uv run lfp-build add my-api \
--project-dependency my-core \
--project-dependency my-models
# Add in a specific subdirectory within the workspace
uv run lfp-build add my-project --path services
Created members include:
- Standard Python src layout (
src/<package_name>/) - A
pyproject.tomlseeded with optional dependencies - An empty
__init__.py - Automatic workspace dependency wiring via
lfp-build sync
Sync
Usage: lfp-build sync [OPTIONS]
Synchronize project configurations across the workspace.
This command performs several synchronization tasks to keep member projects
aligned with the root project settings and ensure consistent dependencies.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching to │
│ a subcommand. │
│ --name Specific member project names to sync. │
│ --version Sync version from git history to all member projects. │
│ [default: True] │
│ --build-system Sync [build-system] from root project to all member │
│ projects. [default: True] │
│ --member-project Sync [tool.lfp-build.member-project] from root project │
│ to all member projects. [default: True] │
│ --sources Sync [tool.uv.sources] on both the root project and │
│ every member project, and normalize internal member │
│ dependency entries. Set │
│ LFP_BUILD_MEMBER_PROJECT_DIRECT_REFERENCE=true to write │
│ workspace deps as name @ file://${PROJECT_ROOT}/... │
│ references; otherwise plain member names are used. │
│ [default: True] │
│ --member-paths Sync member path patterns. [default: True] │
│ --reorder-pyproject Order pyproject entries where applicable. [default: │
│ True] │
│ --format-pyproject Format pyproject.toml files using taplo. [default: │
│ True] │
│ --format-python Run ruff format and check on all projects. [default: │
│ True] │
╰──────────────────────────────────────────────────────────────────────────────╯
# Sync all configuration
uv run lfp-build sync
# Sync specific projects only
uv run lfp-build sync --name project1 --name project2
# Disable specific sync tasks
uv run lfp-build sync --no-version --no-format-python
Build
Usage: lfp-build build [OPTIONS]
Build wheel artifacts for workspace projects.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching to │
│ a subcommand. │
│ --name Optional member project names to build. If omitted, all │
│ workspace projects from metadata are built in metadata │
│ order. │
│ --out-dir Destination directory for built artifacts. Builds are │
│ performed in a temporary directory first, then copied │
│ into this directory with overwrite semantics. [default: │
│ dist] │
╰──────────────────────────────────────────────────────────────────────────────╯
# Build wheel artifacts for every workspace project
uv run lfp-build build
# Build wheel artifacts for selected projects
uv run lfp-build build --name common --name api
Hooks
Usage: lfp-build hooks
Install or refresh the lfp-build managed pre-commit hook.
Discovers the workspace root via uv metadata, initializes a git repository there
if one does not already exist, configures git to use .githooks as
core.hooksPath, and writes (or refreshes) the lfp-build managed block in
.githooks/pre-commit. Hook content outside the managed markers is left
untouched.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching to │
│ a subcommand. │
╰──────────────────────────────────────────────────────────────────────────────╯
Install or refresh the lfp-build managed git pre-commit hook. The hook
runs lfp-build sync before each commit and stages any pyproject.toml
updates the sync produces. The managed portion of the hook is delimited by
# >>> lfp-build managed pre-commit >>> / # <<< lfp-build managed pre-commit <<< markers, so re-running the command updates that block in
place without overwriting any user-added hook content.
# Install or refresh the managed pre-commit hook
uv run lfp-build hooks
lfp-build init already runs this on workspace bootstrap; use lfp-build hooks directly to (re)install the hook into an existing workspace, refresh
a stale managed block, or after manually editing .githooks/pre-commit.
Rename
Usage: lfp-build rename [ARGS]
Bulk rename strings inside files and directory names.
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching │
│ to a subcommand. │
│ TRANSFORM --transform One or more old:new substitution pairs. [default: │
│ []] │
│ DRY-RUN --dry-run Preview changes without writing or renaming. │
│ [default: False] │
│ DASH-TO-UNDERSCORE Also rewrite underscore variants (old_name -> │
│ --dash-to-underscore new_name). [default: False] │
╰──────────────────────────────────────────────────────────────────────────────╯
# Rename strings in files and folder names recursively
uv run lfp-build rename old-name:new-name
# Preview rename changes without writing
uv run lfp-build rename old-name:new-name --dry-run
# Also rewrite underscore variants (old_name -> new_name)
uv run lfp-build rename old-name:new-name --dash-to-underscore
Readme
Usage: lfp-build readme COMMAND
README documentation automation.
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ update Update README command sentinel blocks. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ --working-directory Set the current working directory before dispatching to │
│ a subcommand. │
╰──────────────────────────────────────────────────────────────────────────────╯
Automatically update README.md files by executing commands embedded in sentinel blocks and replacing the body with the captured output.
# Update all command blocks in README
uv run lfp-build readme update
# Specify a different README file
uv run lfp-build readme update --readme docs/CLI.md
# Only update specific commands (filter by regex)
uv run lfp-build readme update --filter "sync"
# Preview changes without writing
uv run lfp-build readme update --write false
# Control parallelism
uv run lfp-build readme update --jobs 4
Note: This README's command documentation is automatically generated by
lfp-build readme update, which executes commands and embeds their help output into sentinel blocks.
How It Works
The readme update command looks for sentinel blocks in your README:
<!-- BEGIN:cmd lfp-build sync --help -->
<!-- END:cmd -->
It executes the command between BEGIN:cmd and --help, captures the output, and replaces the content between the
BEGIN and END markers with a formatted code block containing the command's output.
Features:
- Parallel Execution: Runs multiple commands in parallel for faster updates
- Smart Help Filtering: Automatically removes
--helpoption rows from help output to reduce noise - Empty Section Removal: Removes empty Options sections when
--helpis the only option - Selective Updates: Use
--filterto update only specific commands - Safe by Default: Preview mode available with
--write false
This approach ensures your documentation stays in sync with actual command behavior, preventing documentation drift.
Adopting lfp-build in Your Project
Initial Setup
- Install lfp-build in your project's dependencies
- Create a root pyproject.toml if you don't have one
- Configure workspace members to tell lfp-build which projects to manage
- Run your first sync to align configurations
Project Structure
The tool works with any workspace layout that has a root pyproject.toml and member projects:
your-workspace/
├── pyproject.toml # Root configuration
├── core/ # Your projects
│ ├── pyproject.toml
│ └── src/
│ └── core/
├── api/
│ ├── pyproject.toml
│ └── src/
│ └── api/
├── services/
│ ├── pyproject.toml
│ └── src/
│ └── services/
└── packages/ # Optional subdirectory for projects
Workspace Configuration
Add workspace configuration to your root pyproject.toml:
[tool.uv.workspace]
members = ["core", "api", "services"]
# Or use globs for flexibility
members = ["*/"]
exclude = ["legacy", "archived"]
[build-system]
requires = ["uv_build>=0.9.6,<0.10.0"]
build-backend = "uv_build"
# This section will be synced to all member projects
[tool.lfp-build.member-project]
# Shared configuration for all projects
Common Workflows
Keeping Projects in Sync:
# Sync everything (run after changing root config)
uv run lfp-build sync
# Or sync specific aspects
uv run lfp-build sync --format-python
Before Committing:
# Sync and format
uv run lfp-build sync
Module Reference
The package layout mirrors the CLI tree: each top-level command lives in its
own module under commands/, with shared logic at the package root.
src/lfp_build/
├── cli.py # Cyclopts entry point; mounts each command
├── _config.py # dotenv loader + runtime flag config
├── workspace.py # uv workspace metadata + path consolidation
├── pyproject.py # pyproject.toml read/write/order/format
├── version.py # git-derived semver helpers
├── util.py # subprocess helpers
├── commands/
│ ├── init.py # `lfp-build init`
│ ├── add.py # `lfp-build add`
│ ├── sync.py # `lfp-build sync`
│ ├── build.py # `lfp-build build`
│ ├── hooks.py # `lfp-build hooks`
│ ├── rename.py # `lfp-build rename`
│ └── readme.py # `lfp-build readme update`
└── templates/
└── init_pyproject.toml # bundled root pyproject.toml template
cli.py
Top-level Cyclopts entry point. Imports each command from commands/ and
mounts it as a flat verb. No business logic.
_config.py
Environment configuration loader. Auto-runs at interpreter startup via
sitecustomize-entrypoints to load a dotenv file and resolve runtime flags.
workspace.py
Retrieve uv workspace metadata with a filesystem-scan fallback and a best-effort source repair pass when uv reports a misconfigured workspace.
pyproject.py
Read, update, reorder, and format pyproject.toml files via tomlkit, with
optional taplo/tombi formatting.
version.py
Derive a normalized major.minor.patch semver string with optional
+devN / +revN suffix from git describe output and the working-tree
state. Used by commands.sync to refresh project.version on every
member during sync.
util.py
Subprocess helpers used across the CLI for logging child stdout/stderr.
commands/init.py
Bootstrap a new uv workspace: write the root pyproject.toml from the
bundled template at lfp_build/templates/init_pyproject.toml, copy a
local .gitignore template, install the managed pre-commit hook via
commands.hooks.install, and seed packages/common.
commands/add.py
Add a new member project to the current workspace. Sets up the directory,
seeds pyproject.toml, creates the src/<package>/__init__.py, then runs
commands.sync.sync so the new member is wired in. Hook bootstrap is
intentionally not part of add - use init (for new workspaces) or
hooks (to install/refresh the hook on an existing workspace).
commands/hooks.py
Install or refresh the lfp-build managed git pre-commit hook. Manages a
marker-delimited block in .githooks/pre-commit so re-running the install
logic is idempotent and preserves any user-added hook content outside the
markers. The CLI verb (lfp-build hooks) calls install, and
commands.init reuses the same install function during workspace
bootstrap.
commands/sync.py
Core synchronization driver and step implementations: versions, build system, member tool config, member dependencies, uv workspace member path patterns, ruff format, and pyproject reorder/format.
commands/build.py
Build wheel distribution artifacts for workspace projects, with optional
Requires-Dist rewriting for workspace-local file URIs.
commands/rename.py
Bulk file-content and directory rename utilities, workspace-aware so the enclosing tooling workspace itself is never modified.
commands/readme.py
README documentation automation. Currently exposes a single update verb
that re-runs BEGIN:cmd ... END:cmd sentinel commands and embeds their
output back into the markdown.
Development
Dependencies
Core dependencies:
uv: Workspace and dependency managementruff: Python formatting and lintingcyclopts: CLI frameworklfp-logging: Logging facade used across the CLIpython-dotenv: Loads environment configuration from.envfilestomlkit: TOML manipulationmergedeep: Deep merge support for synced config tablessitecustomize-entrypoints: Automatic config initialization vialfp_build._config:load(loads.dev.envby default, override withLFP_BUILD_PYTHON_DOTENV_FILE)
Optional external tools:
taplo: Preferred TOML formatter when availabletombi: Fallback TOML formatter used whentaplois unavailable on supported platforms
Environment Variables
LOG_LEVEL: Control logging verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL).LFP_BUILD_PYTHON_DOTENV_FILE: Override the dotenv file loaded at startup. Defaults to.dev.env(searched in the current working directory and the detected workspace root).LFP_BUILD_MEMBER_PROJECT_DIRECT_REFERENCE: Controls how internal workspace dependencies are written during sync and metadata repair.false(default): keep internal dependencies as plain names (for example,common) and maintaintool.uv.sources.<dep> = { workspace = true }.true: write internal dependencies asname @ file://${PROJECT_ROOT}/.... Duringdist, built wheel metadata is inspected and workspace-localRequires-Dist: ... @ file://...entries are rewritten to plain dependency names before copy.
Extending lfp-build
The tool is designed to be extended for your specific needs:
Adding New Commands
The modular architecture makes it easy to add new commands for your specific needs:
- Create a new module in
src/lfp_build/commands/(one file per top-level verb). - Either expose a plain function (for leaf verbs like
init,sync, etc.) or acyclopts.App(when the command owns its own subcommand group, asreadmedoes). - Mount it in
cli.pyviaapp.command(my_module.handler, name="...").
Real-World Examples
Use lfp-build to manage a Python monorepo where packages share workspace metadata, internal dependencies, formatting, and release artifacts.
License
See LICENSE file for details.
Contributing
Contributions that enhance workspace management capabilities are welcome. Maintain these principles:
- Preserve existing variable names and logic unless explicitly refactoring
- Add comprehensive documentation to all new or significantly modified code
- Follow Python standard ordering for globals, functions, and classes
- Use
_prefix for private functions with limited scope
Support
For issues, questions, or feature requests related to using lfp-build in your project, please open an issue on GitHub.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lfp_build-0.1.14.tar.gz.
File metadata
- Download URL: lfp_build-0.1.14.tar.gz
- Upload date:
- Size: 35.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3098277ebfe74d2c5d81423a26b701b82e3fd2789f20ed7288e20d19cf24587
|
|
| MD5 |
f0657e8bffa0c33748f1a3f8092cc5e0
|
|
| BLAKE2b-256 |
39b5e35a76023cba53ba417f2ec94ce217d7bc792fa8dff7daefe0db66e0af17
|
Provenance
The following attestation bundles were made for lfp_build-0.1.14.tar.gz:
Publisher:
publish.yml on regbo/lfp-build-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lfp_build-0.1.14.tar.gz -
Subject digest:
d3098277ebfe74d2c5d81423a26b701b82e3fd2789f20ed7288e20d19cf24587 - Sigstore transparency entry: 1509852751
- Sigstore integration time:
-
Permalink:
regbo/lfp-build-py@894b0fc795bb8b4aaa40475e5b5029697f8427a9 -
Branch / Tag:
refs/tags/v0.1.14 - Owner: https://github.com/regbo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@894b0fc795bb8b4aaa40475e5b5029697f8427a9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file lfp_build-0.1.14-py3-none-any.whl.
File metadata
- Download URL: lfp_build-0.1.14-py3-none-any.whl
- Upload date:
- Size: 42.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efcf2add1d4125d0de13ae2ecee5d374ab3f0f4d945267ea083bd12dad348ca4
|
|
| MD5 |
5c8c5764f6ec9f3511ba58a81224b192
|
|
| BLAKE2b-256 |
f933e496e1f48e37ff0b70a0b94af046943742b15bc34fba9d0c1a68d8fe79a1
|
Provenance
The following attestation bundles were made for lfp_build-0.1.14-py3-none-any.whl:
Publisher:
publish.yml on regbo/lfp-build-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lfp_build-0.1.14-py3-none-any.whl -
Subject digest:
efcf2add1d4125d0de13ae2ecee5d374ab3f0f4d945267ea083bd12dad348ca4 - Sigstore transparency entry: 1509852919
- Sigstore integration time:
-
Permalink:
regbo/lfp-build-py@894b0fc795bb8b4aaa40475e5b5029697f8427a9 -
Branch / Tag:
refs/tags/v0.1.14 - Owner: https://github.com/regbo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@894b0fc795bb8b4aaa40475e5b5029697f8427a9 -
Trigger Event:
push
-
Statement type: