Skip to main content

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.toml seeded 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 --help option rows from help output to reduce noise
  • Empty Section Removal: Removes empty Options sections when --help is the only option
  • Selective Updates: Use --filter to 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

  1. Install lfp-build in your project's dependencies
  2. Create a root pyproject.toml if you don't have one
  3. Configure workspace members to tell lfp-build which projects to manage
  4. 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 management
  • ruff: Python formatting and linting
  • cyclopts: CLI framework
  • lfp-logging: Logging facade used across the CLI
  • python-dotenv: Loads environment configuration from .env files
  • tomlkit: TOML manipulation
  • mergedeep: Deep merge support for synced config tables
  • sitecustomize-entrypoints: Automatic config initialization via lfp_build._config:load (loads .dev.env by default, override with LFP_BUILD_PYTHON_DOTENV_FILE)

Optional external tools:

  • taplo: Preferred TOML formatter when available
  • tombi: Fallback TOML formatter used when taplo is 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 maintain tool.uv.sources.<dep> = { workspace = true }.
    • true: write internal dependencies as name @ file://${PROJECT_ROOT}/.... During dist, built wheel metadata is inspected and workspace-local Requires-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:

  1. Create a new module in src/lfp_build/commands/ (one file per top-level verb).
  2. Either expose a plain function (for leaf verbs like init, sync, etc.) or a cyclopts.App (when the command owns its own subcommand group, as readme does).
  3. Mount it in cli.py via app.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lfp_build-0.1.12.tar.gz (35.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lfp_build-0.1.12-py3-none-any.whl (42.5 kB view details)

Uploaded Python 3

File details

Details for the file lfp_build-0.1.12.tar.gz.

File metadata

  • Download URL: lfp_build-0.1.12.tar.gz
  • Upload date:
  • Size: 35.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lfp_build-0.1.12.tar.gz
Algorithm Hash digest
SHA256 ac3e61d048f42d3cc919f66e26aff8543c3d2291c6d3db8b868b89c022558587
MD5 5642200eb31aa4e606200d774d3cf61d
BLAKE2b-256 b6a1cbab9b6d1f00a27e7c28057c1100e4229eada88b0d3dbc591ddf39875f95

See more details on using hashes here.

Provenance

The following attestation bundles were made for lfp_build-0.1.12.tar.gz:

Publisher: publish.yml on regbo/lfp-build-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file lfp_build-0.1.12-py3-none-any.whl.

File metadata

  • Download URL: lfp_build-0.1.12-py3-none-any.whl
  • Upload date:
  • Size: 42.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for lfp_build-0.1.12-py3-none-any.whl
Algorithm Hash digest
SHA256 66c778de119fa63f294ba05368274f300812e12a97d0b80843ea190cb762d47e
MD5 999256693fe594eeb423b8b3e5a9a3cc
BLAKE2b-256 622691ec50a7a4bb1c601b7d1befa265d265b847137a478d1c3115b124eaeb48

See more details on using hashes here.

Provenance

The following attestation bundles were made for lfp_build-0.1.12-py3-none-any.whl:

Publisher: publish.yml on regbo/lfp-build-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page