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.

SubprocessError

Raised when a subprocess command fails.

Classes

GroupedHelpFormatter

Group subcommands by category in help output.

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

ColoredCallerFormatter

Formatter with dimmed caller info (funcName:lineno) and message.

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])

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.tomllib: Any = None
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.HelpFormatter

Group subcommands by category in help output.

class src.appenv.RequirementsTxtInfo

Bases: NamedTuple

dependencies: list[str]
editable_dependencies: list[str]
python_versions: 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.

read_existing_project() dict[str, Any] | None

Parse existing [project] section using tomllib.

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 = '>=3.10') 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 = False, **kwargs: Any) 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.SubprocessError(message: str, returncode: int)

Bases: Exception

Raised when a subprocess command 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) 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.ColoredCallerFormatter(fmt=None, datefmt=None, style='%', validate=True, *, defaults=None)

Bases: logging.Formatter

Formatter with dimmed caller info (funcName:lineno) and message.

dim: str = '\x1b[2m'
reset: str = '\x1b[0m'
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.

src.appenv.main() None