src.appenv

appenv - a single-file tool that pins Python packages to exact versions and

exposes their binaries via symlinks. Drop into a repository, commit, and every checkout gets the same tools at the same versions.

Important assumptions:

  • Python 3.9+

  • pyproject.toml next to the appenv file

  • system has usable uv (see UV_MIN_VERSION) or uv can be installed on-demand

  • the appenv file is placed in a repo with the name of the application

Attributes

Exceptions

NoValidUvError

Raised when no valid uv binary can be found or installed.

CommandError

Raised when a command (subprocess) fails.

Classes

GroupedHelpFormatter

Group subcommands by category in help output.

UsageArgumentParser

ArgumentParser that exits with EXIT_CODE_USAGE (64) on usage errors.

IndexEntry

A [[tool.uv.index]] entry migrated from a pip index option.

RequirementsTxtInfo

Pyproject

Encapsulates pyproject.toml parsing, migration, and generation.

LockFile

Encapsulates lockfile operations and diff logic.

UvVersion

UvBin

Encapsulates UV binary discovery, version check, and command execution.

AppEnvSettings

InitParams

AppEnv

ColoredConsoleFormatter

User-facing console formatter: dim arrow + message, no caller internals.

Functions

convert_version_preference(versions)

Convert version list to requires-python specifier.

version_satisfies_constraints(version, min_version[, ...])

remove_path(path)

Remove a path regardless of type (symlink, file, or directory).

create_pyproject(target, project_name, description, ...)

Factory function to create a new pyproject.toml file.

ensure_uv(appenv_dir)

Ensure uv is available and meets minimum version.

ensure_pyproject(base)

Ensure pyproject.toml exists with [project] section, return Pyproject.

ensure_lock_file(base)

Ensure lockfile exists, return LockFile instance.

ensure_gitignore(base, entries)

Ensure .gitignore contains the given entries.

cmd(c, *[, merge_stderr, quiet, cwd, env])

ensure_best_python(base)

Ensure best Python for pyproject.toml workflow.

find_available_pythons()

Find all available Python versions in PATH.

print_colored_diff(old_content, new_content, fromfile, ...)

Print a unified diff with ANSI colors.

appenv_settings_from_env()

Read settings from environment variables.

setup_logging(command_name, log_dir, *, verbose)

Setup command-specific logging with daily rotation.

main()

Module Contents

src.appenv.log: logging.Logger
src.appenv.EXIT_CODE_DATAERR: Final[int] = 65
src.appenv.EXIT_CODE_NOINPUT: Final[int] = 67
src.appenv.EXIT_CODE_UNAVAILABLE: Final[int] = 68
src.appenv.EXIT_CODE_USAGE: Final[int] = 64
src.appenv.MAX_HELP_TEXT_LENGTH: Final[int] = 50
src.appenv.VERSION_PARTS_COUNT: Final[int] = 3
class src.appenv.GroupedHelpFormatter(prog, indent_increment=2, max_help_position=24, width=None, color=True)

Bases: argparse.RawDescriptionHelpFormatter

Group subcommands by category in help output.

Inherits from argparse.RawDescriptionHelpFormatter so the parser description keeps its literal newlines (the \n\n between the version banner and the description). HelpFormatter would collapse all whitespace to single spaces, merging the version line with the description into one paragraph.

class src.appenv.UsageArgumentParser(prog=None, usage=None, description=None, epilog=None, parents=[], formatter_class=HelpFormatter, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='error', add_help=True, allow_abbrev=True, exit_on_error=True, *, suggest_on_error=False, color=True)

Bases: argparse.ArgumentParser

ArgumentParser that exits with EXIT_CODE_USAGE (64) on usage errors.

argparse defaults to exit code 2 for invalid choices and missing required arguments, but appenv documents 64 (EXIT_CODE_USAGE) for all usage errors (see docs/user/commands.md). Overriding error() on the main parser also covers every subparser: add_subparsers defaults parser_class to type(self), so subcommands inherit this override automatically. The --version action (which calls self.exit(0)) is unaffected.

error(message: str) NoReturn

error(message: string)

Prints a usage message incorporating the message to stderr and exits.

If you override this in a subclass, it should not return – it should either exit or raise an exception.

class src.appenv.IndexEntry

Bases: NamedTuple

A [[tool.uv.index]] entry migrated from a pip index option.

url is always credential-free: the literal secret is stripped before this object is constructed and never reaches pyproject.toml or stdout.

name: str
url: str
has_credentials: bool
class src.appenv.RequirementsTxtInfo

Bases: NamedTuple

dependencies: list[str]
editable_dependencies: list[str]
python_versions: list[str]
indexes: list[IndexEntry]
skipped_options: list[str]
src.appenv.convert_version_preference(versions: list[str]) tuple[str, list[str]]

Convert version list to requires-python specifier.

Returns (specifier, missing_versions) where missing_versions are versions in the range that weren’t explicitly listed.

Example: [“3.11”, “3.13”, “3.10”] -> (“>=3.10,<3.14”, [“3.12”])

src.appenv.version_satisfies_constraints(version: str, min_version: str, max_version: str | None = None) bool
src.appenv.remove_path(path: Path) None

Remove a path regardless of type (symlink, file, or directory).

Logs the type before removal for debugging purposes.

class src.appenv.Pyproject(base: Path)

Encapsulates pyproject.toml parsing, migration, and generation.

path: Path
requirements_path: Path
property content: str
migrate_from_requirements_txt() Pyproject
property requires_python: tuple[str | None, str | None]

Parse requires-python from pyproject.toml.

Returns tuple of (min_version, max_version) where max may be None.

property exists: bool
property has_project_section: bool

Check if TOML content has a [project] section.

property can_be_created_from_requirements_txt: bool
property requirements_txt_info: RequirementsTxtInfo
print_migration_info() None
with_project_section(project_name: str, description: str, dependencies: list[str], requires_python: str) Pyproject

Add [project] section, write file, return fresh instance.

This method writes the updated content to disk and returns a new Pyproject instance (immutable pattern - cached content stays valid).

src.appenv.create_pyproject(target: Path, project_name: str, description: str, dependencies: list[str], python_version: str) Pyproject

Factory function to create a new pyproject.toml file.

class src.appenv.LockFile(base: Path)

Encapsulates lockfile operations and diff logic.

path: Path
property exists: bool
property content: str
diff(uv_bin: UvBin, base: Path, *, verbose: bool) str

Run uv lock in temp dir, return diff string.

diff_summary(old_lines: set[str]) str

Create summary like ‘✓ Created (+42 lines)’.

read_lockfile_lines() set[str]

Read lockfile lines as a set of non-comment lines.

class src.appenv.UvVersion
major: int
minor: int
patch: int
property valid: bool
static unknown() UvVersion
static minimum() UvVersion
static from_string(version_str: str) UvVersion
class src.appenv.UvBin(appenv_dir: Path)

Encapsulates UV binary discovery, version check, and command execution.

appenv_dir: Path
uv_dir: Path
managed_uv: Path
bin: Path
cmd(args: list[str], *, verbose: bool, merge_stderr: bool, quiet: bool, cwd: str | None) str

Execute uv command and return stdout.

property version: UvVersion
static get_uv_version(uv_path: Path) UvVersion

Get uv version by calling uv –version.

exception src.appenv.NoValidUvError

Bases: RuntimeError

Raised when no valid uv binary can be found or installed.

exception src.appenv.CommandError(message: str, returncode: int)

Bases: Exception

Raised when a command (subprocess) fails.

returncode: int
class src.appenv.AppEnvSettings
verbose: bool
extras: list[str]
basedir: pathlib.Path
class src.appenv.InitParams
command_name: str
dependencies: list[str]
project_name: str
description: str
python_version: str
class src.appenv.AppEnv(original_cwd: Path, settings: AppEnvSettings)
base: Path
original_cwd: Path
settings: AppEnvSettings
appenv_dir: Path
appenv_script: Path
log_dir: Path
venv_real: Path
venv_python: Path
run(command: str, argv: list[str]) None
meta(remaining_args: list[str] | None = None, prog: str = 'appenv') None
prepare(args: Namespace | None = None, remaining: list[str] | None = None) Path

Prepare venv with production dependencies only.

init(args: Namespace | None = None, remaining: list[str] | None = None) None

Create a new pyproject.toml project.

migrate(args: Namespace | None = None, remaining: list[str] | None = None) None

Migrate from requirements.txt to pyproject.toml.

self_update(args: Namespace | None = None, remaining: list[str] | None = None) None

Update the local ./appenv script to match the running version.

python(args: Namespace, remaining: list[str]) None
run_script(args: Namespace, remaining: list[str]) None

Run a command in the project venv via uv run.

run_uv(args: Namespace, remaining: list[str]) None

Run uv with the appenv-configured uv binary.

show_version(args: Namespace | None = None, remaining: list[str] | None = None) None

Show appenv version.

reset(args: Namespace | None = None, remaining: list[str] | None = None) None

Remove appenv-managed files/directories

update_lockfile(args: Namespace | None = None, remaining: list[str] | None = None) None
src.appenv.ensure_uv(appenv_dir: Path) UvBin

Ensure uv is available and meets minimum version.

Exits with error if uv is not available or too old. Returns UvBin instance.

src.appenv.ensure_pyproject(base: Path) Pyproject

Ensure pyproject.toml exists with [project] section, return Pyproject.

src.appenv.ensure_lock_file(base: Path) LockFile

Ensure lockfile exists, return LockFile instance.

src.appenv.ensure_gitignore(base: Path, entries: list[str]) None

Ensure .gitignore contains the given entries.

Idempotent: appends only missing entries, never modifies existing content.

src.appenv.cmd(c: str | list[str], *, merge_stderr: bool, quiet: bool, cwd: str | None, env: dict[str, str] | None) bytes
src.appenv.ensure_best_python(base: Path) None

Ensure best Python for pyproject.toml workflow.

Reads requires-python from pyproject.toml and selects the newest available Python that satisfies the constraint.

When running inside a virtual environment (sys.prefix != sys.base_prefix), skips re-exec only if the current Python already satisfies the constraint. This handles both legitimate venvs (uvx appenv init) and NixOS Python environments where sys.prefix != sys.base_prefix is always true.

src.appenv.find_available_pythons() list[tuple[str, str]]

Find all available Python versions in PATH.

Returns list of (version_str, path) tuples, sorted by version (newest first).

src.appenv.print_colored_diff(old_content: str, new_content: str, fromfile: str, tofile: str) bool

Print a unified diff with ANSI colors.

Returns True if there were changes, False otherwise.

src.appenv.appenv_settings_from_env() AppEnvSettings

Read settings from environment variables.

class src.appenv.ColoredConsoleFormatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)

Bases: logging.Formatter

User-facing console formatter: dim arrow + message, no caller internals.

Unlike the file handler, verbose console output omits funcName:lineno so users see operational steps (uv commands, venv creation, Python selection) instead of developer debug detail.

dim: str = '\x1b[2m'
reset: str = '\x1b[0m'
arrow: str = '→'
format(record: logging.LogRecord) str

Format the specified record as text.

The record’s attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message.

src.appenv.setup_logging(command_name: str, log_dir: Path, *, verbose: bool) None

Setup command-specific logging with daily rotation.

The file handler always records full caller detail (funcName:lineno) for developer post-mortem. The verbose console handler renders only user-relevant operational messages, hiding internal diagnostics and Python object reprs.

src.appenv.main() None