init Command — Internals
This page describes the internal design of scaldys-project init
(src/scaldys_project/cli/commands/cmd_init.py). It is intended for
contributors who need to maintain or extend the command.
For the user-facing guide see Project Initialisation.
Overview
init is a self-contained command — unlike the build commands it does
not instantiate WindowsBuilder or use find_project_root(), because it
operates on a directory that does not yet exist. All file-system work is done
through stdlib modules only: pathlib, shutil, tempfile,
zipfile, urllib.request, subprocess, datetime, and re.
No new package-level dependencies are introduced.
The command is registered as a single top-level command in cli/cli.py:
app.command("init")(cmd_init.init)
Module structure
The module is organised as a set of private helper functions called in sequence
by the public init() entry point.
cmd_init.py
│
├── _to_package_name(s) slug helper: "My App" → "my_app"
├── _to_project_slug(s) slug helper: "My App" → "my-app"
│
├── _collect_params() interactive wizard → dict[str, Any]
├── _print_summary(params) Rich panel before confirmation
│
├── _download_template(ref, tmp) GitHub ZIP → extracted root Path
├── _copy_filtered(src, dst) copy tree, skipping excluded items
│
├── _build_replacements(params) ordered list of (find, replace) pairs
├── _is_binary(path) detect files to skip for text replacement
├── _replace_in_file(path, repls) apply replacements to one text file
├── _replace_content(root, repls) walk tree, call _replace_in_file on each
│
├── _rename_component(name, ...) apply name substitutions to one path component
├── _rename_paths(root, ...) post-order rename of all matching paths
│
├── _post_process(root, params) targeted fixes: conf.py, pyproject.toml, sp.toml
├── _run_post_init(dir, git, sync) git init + commit, uv sync
│
└── init(...) CLI entry point — orchestrates the above
Template acquisition
When --local is not provided the command downloads a ZIP archive from
GitHub:
https://github.com/scaldys/scaldys-template/archive/refs/heads/<ref>.zip
If that URL fails (e.g. for a tag), it retries with:
https://github.com/scaldys/scaldys-template/archive/refs/tags/<ref>.zip
The archive is extracted into a tempfile.mkdtemp() directory. GitHub’s
ZIP format always places all files inside a single root folder named
<repo>-<ref>/; _download_template strips that root and returns the
inner directory as the template source path.
The caller (init()) wraps the entire processing block in a try/finally
that calls shutil.rmtree(tmp_dir, ignore_errors=True) to guarantee cleanup
even if an exception aborts the process.
When --local PATH is provided, local is used directly as
template_src and no temporary directory is created or cleaned up.
Exclusion list
_copy_filtered skips the following during the copy phase:
Pattern |
Matched by |
|---|---|
|
|
|
|
|
|
The function recurses through the source tree and creates the corresponding
subtree in dst, skipping any entry (directory or file) whose name matches
the exclusion rules.
Replacement ordering
_build_replacements() returns a fixed-order list of (find, replace)
pairs. The ordering is critical: each pattern must be substituted before
any pattern of which it is a sub-string.
# |
Find |
Replace with |
Why this position |
|---|---|---|---|
1 |
|
project name |
Contains |
2 |
|
package name |
Underscored form — distinct from the hyphenated form; order relative to position 3 is not strictly required, but kept for clarity. |
3 |
|
project slug |
Hyphenated form — does not overlap with position 2. |
4 |
|
author email |
Specific string; placed before the bare |
5 |
|
organization name |
Most general; placed last among the core replacements. |
6 |
|
|
Appended conditionally — only when the user provides a GitHub username. |
All replacements are case-sensitive plain string operations (str.replace),
applied in order to the full text of each file.
Binary file detection
_is_binary(path) returns True (skip content replacement) if:
The file’s suffix (lowercased) is in
_BINARY_EXTENSIONS:.ico,.png,.jpg,.jpeg,.gif,.bmp,.exe,.dll,.pyd,.whl,.zip,.tar.Attempting to decode the first 1 024 bytes of the file as UTF-8 raises
UnicodeDecodeError.
Binary files are still renamed (their path component may contain a placeholder string); only their contents are left unchanged.
Path renaming
_rename_paths() renames every file and directory whose name contains
scaldys_template or scaldys-template.
The rename walk uses a post-order strategy: all paths are sorted by depth
(len(path.parts)) in descending order, so the deepest entries are renamed
first. This ensures that a directory rename does not invalidate the recorded
path of a child that has not yet been renamed.
Each path is checked with path.exists() before renaming because a parent
directory may have already been renamed (moving the child to a new path).
The replacement is performed by _rename_component(name, package_name,
project_slug), which applies the two name substitutions in order:
scaldys_template → package_name, then scaldys-template →
project_slug.
Targeted post-processing
After generic content replacement and path renaming, _post_process()
applies three targeted fixes that cannot be expressed as simple string
substitutions.
pyproject.toml — description and version
Generic replacement cannot distinguish the placeholder description
("A skeleton for Python projects by Scaldys.") from any other string that
happens to contain Scaldys. _post_process performs a literal
str.replace targeting the exact placeholder text, replacing it with the
user-supplied description.
The template version "0.9.0" is replaced via re.sub with a
MULTILINE flag, targeting the version = "0.9.0" line specifically.
scaldys-project.toml — deployment mode
When the user selects a deployment mode other than the default wheel_only,
a literal str.replace updates the deployment_mode value. No action is
taken when the default is kept.
Error handling and cleanup
Partial output cleanup
If a typer.Exit exception is raised during the processing phase (e.g. a
missing --local path, or a download failure), the except typer.Exit
block removes the partially-created target directory before re-raising. This
leaves the filesystem in a clean state rather than stranding an incomplete
project.
Post-init failures are non-fatal
_run_post_init() treats both git and uv sync failures as warnings
rather than errors. The project files are fully created before these steps
run, so a failure (e.g. git not on PATH, or a network error during
uv sync) does not invalidate the project. A yellow warning message directs
the user to run the failed step manually.
Force overwrite
When --force is supplied and the target directory already exists,
shutil.rmtree(target_dir) removes it entirely before the processing
phase begins. The removal happens unconditionally once --force is
confirmed — there is no additional safety prompt.
Extending the command
Adding a new wizard prompt
Add the
typer.prompt()ortyper.confirm()call inside_collect_params()and include the result in the returneddict.If the value maps to a string substitution, add a new
(find, replace)pair to_build_replacements()— observe the Replacement ordering rules when choosing the insertion position.If the value requires a targeted fix (non-string operation), add a block to
_post_process().Update the summary table in
_print_summary()and the documentation in Project Initialisation.
Adding a new exclusion
Exact name matches (directory or file): add to
_EXCLUDE_NAMESor_EXCLUDE_EXACT.Suffix matches: add to
_EXCLUDE_SUFFIXES.Binary file extensions (skip content replacement only, not copy): add to
_BINARY_EXTENSIONS.
Changing the template source
The GitHub URL constants are defined at module level:
_GITHUB_ZIP_HEADS = "https://github.com/scaldys/scaldys-template/archive/refs/heads/{ref}.zip"
_GITHUB_ZIP_TAGS = "https://github.com/scaldys/scaldys-template/archive/refs/tags/{ref}.zip"
To point init at a different template repository, update these two
constants. No other changes are required.