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 subprocess command fails. |
Classes¶
Group subcommands by category in help output. |
|
Encapsulates pyproject.toml parsing, migration, and generation. |
|
Encapsulates lockfile operations and diff logic. |
|
Encapsulates UV binary discovery, version check, and command execution. |
|
Formatter with dimmed caller info (funcName:lineno) and message. |
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.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.HelpFormatterGroup 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¶
- 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_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 = False, **kwargs: Any) 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.SubprocessError(message: str, returncode: int)¶
Bases:
ExceptionRaised when a subprocess command 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) 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.FormatterFormatter 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¶