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¶
Raised when no valid uv binary can be found or installed. |
|
Raised when a command (subprocess) fails. |
Classes¶
Group subcommands by category in help output. |
|
ArgumentParser that exits with EXIT_CODE_USAGE (64) on usage errors. |
|
A |
|
Encapsulates pyproject.toml parsing, migration, and generation. |
|
Encapsulates lockfile operations and diff logic. |
|
Encapsulates UV binary discovery, version check, and command execution. |
|
User-facing console formatter: dim arrow + message, no caller internals. |
Functions¶
|
Convert version list to requires-python specifier. |
|
|
|
Remove a path regardless of type (symlink, file, or directory). |
|
Factory function to create a new pyproject.toml file. |
|
Ensure uv is available and meets minimum version. |
|
Ensure pyproject.toml exists with [project] section, return Pyproject. |
|
Ensure lockfile exists, return LockFile instance. |
|
Ensure .gitignore contains the given entries. |
|
|
|
Ensure best Python for pyproject.toml workflow. |
Find all available Python versions in PATH. |
|
|
Print a unified diff with ANSI colors. |
Read settings from environment variables. |
|
|
Setup command-specific logging with daily rotation. |
|
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.RawDescriptionHelpFormatterGroup subcommands by category in help output.
Inherits from
argparse.RawDescriptionHelpFormatterso the parser description keeps its literal newlines (the\n\nbetween the version banner and the description).HelpFormatterwould 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.ArgumentParserArgumentParser 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_subparsersdefaultsparser_classtotype(self), so subcommands inherit this override automatically. The--versionaction (which callsself.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:
NamedTupleA
[[tool.uv.index]]entry migrated from a pip index option.urlis 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¶
- 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_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.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.
- exception src.appenv.NoValidUvError¶
Bases:
RuntimeErrorRaised when no valid uv binary can be found or installed.
- exception src.appenv.CommandError(message: str, returncode: int)¶
Bases:
ExceptionRaised when a command (subprocess) fails.
- returncode: int¶
- 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_link: 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_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.FormatterUser-facing console formatter: dim arrow + message, no caller internals.
Unlike the file handler, verbose console output omits
funcName:linenoso 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¶