Apply customizations to any project without templating placeholders.
Customizing a project (e.g., white-label products) forces a choice between bad options:
- Templating tools (Cookiecutter, Copier, Yeoman) require
{{ placeholders }}in source code — the repo is no longer a working app - Forking leads to diverging codebases that are painful to sync with upstream
- Manual editing is error-prone, undocumented, and impossible to reproduce
engraft solves this by keeping the source repo clean and runnable while providing a declarative, reproducible customization layer on top.
engraft uses a two-file model:
- Template file — defines what can be customized and how (maintained by the repo author)
- Values file — contains the consumer's customization values
The original project stays untouched. Run engraft apply and the customizations are applied in place.
pip install engraft
Given a project with a config.json:
{
"name": "DefaultApp",
"version": "1.0.0"
}Create a template file engraft.template.yml:
variables:
app_name:
description: Application name
default: DefaultApp
customizations:
- action: json_replace
file: config.json
replace:
- selector: $.name
variable: app_nameCreate a values file engraft.values.yml:
app_name: MyAppApply:
engraft apply --template engraft.template.yml --values engraft.values.yml
Result — config.json now contains:
{
"name": "MyApp",
"version": "1.0.0"
}Replace values in JSON files using JSONPath-like selectors.
- action: json_replace
file: app.json
replace:
- selector: $.expo.name
variable: app_name
- selector: $.expo.extra.items[0].label
variable: item_labelSelectors use dot notation with optional array indices: $.path.to.key or $.array[0].field.
Replace values in HTML files using XPath selectors. Supports both element text and attribute values.
- action: html_replace
file: index.html
replace:
- selector: //title
variable: page_title
- selector: //meta[@name='description']/@content
variable: page_descriptionThe selector must match exactly one element or attribute. Matching zero or more than one is an error.
Replace values in any text file using regex with a named capture group.
- action: regex_replace
file: src/theme.ts
replace:
- selector: '(PRIMARY_COLOR\s*=\s*)"(?P<value>[^"]*)"'
variable: primary_colorThe selector must contain a (?P<value>...) named capture group. Only the captured group is replaced; the surrounding match is preserved.
Replace an entire file with a source file referenced by a variable.
- action: file_replace
file: assets/logo.png
variable: logoThe variable value is a path relative to the values file directory. Useful for binary files like images.
Versioning is automatic via hatch-vcs — the package version is derived from git tags.
To publish a new release:
- Create a GitHub Release with a tag matching
vX.Y.Z(e.g.,v0.2.0) - The release pipeline automatically runs lint, format check, and tests
- If all checks pass, the package is built and published to PyPI with Sigstore signing
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run linter
ruff check src/ tests/