twat-ez is a Python library that provides a collection of easy-to-use utilities designed to enhance the twat ecosystem and simplify common Python scripting tasks. Its core component, the py_needs module, offers powerful features for managing script dependencies, finding tools and executables, and downloading files from the web.
This section is for anyone looking to understand what twat-ez does, who can benefit from it, and how to quickly get it up and running.
At its heart, twat-ez aims to make Python scripting more robust and less cumbersome. Whether you're writing standalone scripts or extending the twat application, twat-ez provides tools to handle common challenges:
- Effortless Dependency Management: Automatically install required Python packages for your scripts on-the-fly.
- Reliable Tool Discovery: Find system executables (like
git,pip, or custom tools) without worrying about diverse user environments. - Convenient Web Downloads: Fetch content from URLs with built-in support for redirects and flexible decoding.
twatEcosystem Enhancement: Serves as a plugin for thetwatapplication, extending its capabilities.
twat-ez is designed for:
- Python Developers & Scripters: Anyone writing Python scripts that need to interact with system tools, manage their own dependencies, or fetch data from the web.
- Users of the
twatEcosystem: Individuals leveraging thetwatapplication can benefit from the extended functionalitiestwat-ezprovides as a plugin. - Developers in Specialized Environments: Useful for those working with Python in environments that might have their own package management or specific path configurations (e.g., within applications like FontLab which bundle Python).
twat-ez offers several benefits to streamline your development workflow:
- Reduced Setup Friction: Scripts can define and install their own dependencies using the
@needsdecorator. This means users don't have to manuallypip installpackages before running your script. - Increased Script Portability: The enhanced tool discovery mechanism (
py_needs.which) reliably finds executables across different operating systems and common installation locations (including standard system paths, user binary directories, and XDG paths). - Simplified Web Interaction: The
download_urlfunction handles complexities like HTTP redirects and offers options for how content is returned (bytes or string), trying to use efficient QtNetwork libraries if available. - Extensibility for
twat: As a plugin, it seamlessly integrates with and expands thetwatapplication's feature set. - Modern and Maintained: Built with modern Python practices, type-hinted, linted, tested, and actively maintained.
@needsDecorator: Just decorate a function with@needs(["package_a", "package_b"]), andtwat-ezwill ensure these packages are installed (usinguv) before your function runs.- Smart Executable Locator (
py_needs.which): An improvedwhichcommand that searches an extensive set of paths, making your scripts more resilient to different environment setups. - Resilient URL Downloader (
py_needs.download_url): A simple function to get content from the web, with automatic redirect following and fallback mechanisms. twatPlugin Integration: Designed to work as part of thetwatapplication.
You can install twat-ez using pip or uv:
Using pip:
pip install twat-ezUsing uv:
uv pip install twat-eztwat-ez can be used as a plugin for the twat application or as a standalone Python library, primarily through its py_needs module.
Once installed, twat-ez is available as a plugin within the twat ecosystem. The pyproject.toml registers an entry point ez = "twat_ez". The specific way to interact with it as a plugin will depend on the twat application's plugin management system.
(Refer to the twat application's documentation for details on how it discovers and uses plugins.)
The py_needs module is the workhorse of twat-ez. Import it into your scripts:
from twat_ez import py_needsHere are some common use cases:
Reliably locate executables on the system, searching standard paths, user-specific paths (like ~/.local/bin), and XDG directories.
# Find the 'git' executable
git_path = py_needs.which("git")
if git_path:
print(f"Found git at: {git_path}")
else:
print("git not found.")
# Find pip (will try to bootstrap with ensurepip if not found and not discoverable)
pip_path = py_needs.which_pip()
if pip_path:
print(f"Found pip at: {pip_path}")
else:
print("pip not found or bootstrap failed.")
# Find uv (will attempt to install uv via pip if not found)
uv_path = py_needs.which_uv()
if uv_path:
print(f"Found uv at: {uv_path}")
else:
print("uv not found and could not be installed.")Ensure your script's Python package dependencies are met automatically. The @needs decorator checks for specified packages and, if missing, installs them using uv.
from twat_ez.py_needs import needs
@needs(["requests", "pydantic==2.*"]) # Specify packages, optionally with version constraints
def fetch_todo_and_validate():
import requests
from pydantic import BaseModel
class Todo(BaseModel):
userId: int
id: int
title: str
completed: bool
response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
response.raise_for_status() # Ensure the request was successful
todo_data = response.json()
todo = Todo(**todo_data)
print(f"Fetched Todo: {todo.title} (Completed: {todo.completed})")
fetch_todo_and_validate()Targeted Installation with @needs(target=True):
By default, @needs installs packages into the current Python environment's site-packages. If you want to install packages to a custom location (e.g., a user-specific directory for tools, separate from project virtual environments), use target=True.
The installation path for target=True is determined by the UV_INSTALL_TARGET environment variable. If not set, it defaults to a standard user site-packages directory (e.g., ~/.local/lib/pythonX.Y/site-packages on Linux, or a FontLab-specific path if detected).
from twat_ez.py_needs import needs
import subprocess
@needs(["cowsay"], target=True) # For demo; 'cowsay' is usually a system package
def cowsay_a_message():
# 'cowsay' (if it were a Python CLI tool) would be installed to UV_INSTALL_TARGET.
try:
# Ensure the bin dir of UV_INSTALL_TARGET is in your PATH
subprocess.run(["cowsay", "Hello from twat-ez!"], check=True)
except FileNotFoundError:
print("cowsay command not found. Is UV_INSTALL_TARGET/bin in your PATH?")
except subprocess.CalledProcessError as e:
print(f"cowsay execution failed: {e}")
# To control the installation path for target=True:
# export UV_INSTALL_TARGET="/path/to/my/tools/python_libs" # (Linux/macOS)
# $env:UV_INSTALL_TARGET = "C:\\path\\to\\my\\tools\\python_libs" # (Windows PowerShell)
# python your_script.py
# cowsay_a_message() # Uncomment to runImportant: For executables installed with target=True to be runnable directly, the bin directory of your UV_INSTALL_TARGET (e.g., /path/to/my/tools/python_libs/bin) must be in your system's PATH environment variable.
Fetch content from URLs, with automatic redirect handling. It prioritizes PythonQt.QtNetwork if available (often in environments like FontLab), falling back to Python's built-in urllib.
# Download as bytes (mode=0)
try:
binary_content = py_needs.download_url("https://via.placeholder.com/150", mode=0)
with open("placeholder_image.png", "wb") as f:
f.write(binary_content)
print("Image downloaded successfully as placeholder_image.png.")
except RuntimeError as e:
print(f"Download error: {e}")
# Download as string (mode=2, forces UTF-8 decoding)
try:
text_content = py_needs.download_url("https://jsonplaceholder.typicode.com/todos/1", mode=2)
print(f"Text content (first 100 chars): {text_content[:100]}...")
except RuntimeError as e:
print(f"Download error: {e}")
except UnicodeDecodeError:
print("Failed to decode content as UTF-8.")
# Download as string, fallback to bytes on decode error (mode=1, default)
try:
flexible_content = py_needs.download_url("https://example.com")
if isinstance(flexible_content, str):
print(f"Content from example.com (string, first 100 chars): {flexible_content[:100]}...")
else:
print(f"Content from example.com (bytes, could not decode as UTF-8): {len(flexible_content)} bytes")
except RuntimeError as e:
print(f"Download error: {e}")The py_needs.py module itself can be invoked as a script using uv run (due to its embedded script metadata) or python -m twat_ez.py_needs. This exposes its functions (like download_url, which, etc.) as CLI commands, powered by the fire library.
Example:
To use the download_url function from the command line:
# Using uv run (if you have the source code)
uv run ./src/twat_ez/py_needs.py download_url --url="https://example.com" --mode=2
# Using python -m (after installing twat-ez)
python -m twat_ez.py_needs download_url --url="https://example.com" --mode=2This will print the content of example.com to standard output. You can explore other functions similarly. Use --help to see the available commands and their options:
uv run ./src/twat_ez/py_needs.py -- --help
python -m twat_ez.py_needs -- --help(Note: The double dash -- is used with uv run to separate arguments for uv run itself from arguments for the script. For python -m, it's used to signal end of options for python and start of options for the script if the script itself parses options in a way that might conflict.)
This section provides a more detailed look into the internal workings of twat-ez, particularly the py_needs.py module, and outlines the project's structure and contribution guidelines.
The primary logic of twat-ez resides in src/twat_ez/py_needs.py. This module is designed to be highly functional, potentially even usable as a standalone script in some contexts, thanks to its uv run compatibility.
py_needs.py is structured into several key areas:
- Path Providers & Environment Functions: Manages system path discovery and environment interactions.
- Utility Functions: Core helpers for tasks like executable verification.
- URL Download Functions: Implements network content retrieval.
- UV Installation Helpers: Manages the
uvpackage manager lifecycle and relatedpipdiscovery. - Decorators & Main Function: Includes the
@needsdecorator and the CLI entry point.
A robust mechanism for finding executables is crucial for scripts that need to call external tools.
-
build_extended_path(): This is the cornerstone of executable discovery. It constructs a comprehensivePATHstring by concatenating paths from several sources in a specific order:- The current
PATHenvironment variable. - XDG specification paths (see
get_xdg_paths()). - System-specific common binary locations (see
get_system_specific_paths()). - Default Python paths (
os.defpath). - Paths from any custom path providers registered via
register_path_provider(). The resulting list is deduplicated while preserving order, and only includes existing directories. This function's output is LRU cached viafunctools.lru_cache.
- The current
-
get_xdg_paths(): Retrieves paths based on the XDG Base Directory Specification. It checksXDG_BIN_HOMEand the parentbindirectory ofXDG_DATA_HOME(e.g.,$XDG_DATA_HOME/../bin). If these are not set, it defaults to~/.local/binif it exists. -
get_system_specific_paths(): Provides a list of common executable locations tailored to the operating system:- macOS: Includes
/usr/local/bin,/opt/homebrew/bin(for Apple Silicon Homebrew), standard system bins, and Xcode paths. - Windows: Includes user AppData paths, System32, PowerShell paths, and Chocolatey paths.
- Linux/Other: Includes standard system bins and
/snap/binif it exists.
- macOS: Includes
-
which(cmd, mode=os.F_OK | os.X_OK, path=None, verify=True): This is an enhanced version ofshutil.which. It uses the path string generated bybuild_extended_path()(or a custom one if provided) to search for the commandcmd.- If
verify=True(default), after finding an executable, it callsverify_executable()on it. If verification fails,whichreturnsNone. - The result is LRU cached.
- If
-
verify_executable(path_to_exe): Performs basic security and sanity checks on a potential executable:- Ensures the path exists and is a regular file.
- On Unix-like systems, checks if the file is world-writable (mode
0o002), returningFalse(unsafe) if it is. - (Currently, Windows checks are minimal beyond existence and file type).
-
which_pip(): Locates thepipexecutable.- It first calls
py_needs.which("pip"). - If not found, it attempts to bootstrap
pipusingensurepip.bootstrap(). - After attempting bootstrap, it tries
py_needs.which("pip")again. - It also tries to locate
pipviaimportlib.util.find_spec("pip")as a final fallback.
- The result is LRU cached.
- It first calls
-
which_uv(): Locates theuvexecutable.- It first calls
py_needs.which("uv"). - If
uvis not found, it attempts to installuvfor the current user by callingpip install --user uv(using thepipfound bywhich_pip()). - After an installation attempt, it calls
py_needs.which("uv")again.
- The result is LRU cached.
- It first calls
-
FontLab Integration (
_get_fontlab_site_packages()andget_site_packages_path()):_get_fontlab_site_packages(): Checks if running within FontLab (viaimport fontlab) and if so, constructs the path to FontLab's specificsite-packagesdirectory if it's insys.path.get_site_packages_path(): Uses_get_fontlab_site_packages()first. If FontLab is not detected or its site-packages aren't relevant, it falls back tosite.getusersitepackages(). This path is used as the default forUV_INSTALL_TARGET.
The @needs decorator enables functions to declare their Python package dependencies, which are then auto-installed if missing.
-
Workflow:
- When a function decorated with
@needs(mods_list, target=False)is called, it iterates throughmods_list. - For each module name,
importlib.util.find_spec(mod_name)checks if the module is installed and importable. - Missing modules are collected. If any,
_install_with_uv(missing_list, target_flag)is invoked. - After a successful installation,
_import_modules(missing_list)attempts to import them, raising an error if they're still unavailable.
- When a function decorated with
-
_install_with_uv(missing_packages, target_flag):- Locates
uvusingwhich_uv(). - Constructs the command:
uv pip install <packages...>. - If
target_flagisTrue:- Appends
--target <path>to theuvcommand. The<path>is from theUV_INSTALL_TARGETenvironment variable, defaulting to the output ofget_site_packages_path().
- Appends
- If
target_flagisFalse:- Appends
--python <sys.executable>to install into the current Python environment.
- Appends
- Uses
subprocess.run()for installation. Failures raise aRuntimeError.
- Locates
-
UV_INSTALL_TARGETEnvironment Variable: Controls the installation directory for@needs(target=True). This is useful for creating isolated tool-specific environments.
Provides a resilient method to fetch content from HTTP/HTTPS URLs.
-
Priority System & Fallback:
- Attempts
download_url_qt()ifPythonQt.QtNetworkis importable (common in FontLab). - If
PythonQtis unavailable ordownload_url_qt()fails, it falls back todownload_url_py().
- Successful downloads are LRU cached.
- Attempts
-
download_url_qt(url, mode, max_redir):- Uses
PythonQt.QtNetwork.QNetworkAccessManagerandPythonQt.QtCore.QEventLoopfor synchronous handling of Qt's asynchronous network operations. - Manually handles HTTP redirects (301, 302, 303, 307, 308) up to
max_redirtimes.
- Uses
-
download_url_py(url, mode, max_redir):- Uses Python's
urllib.request.build_opener()andurlopen(). urllibhandles redirects automatically.max_redirmainly ensures interface consistency.
- Uses Python's
-
bin_or_str(data_bytes, mode): Converts downloadedbytesbased onmode:mode=0: Rawbytes.mode=1(default): UTF-8str; falls back tobytesonUnicodeDecodeError.mode=2: UTF-8str; raisesUnicodeDecodeErroron failure.
Several functions in py_needs.py use @lru_cache for performance by memoizing results:
download_url_qt,download_url_py,download_urlwhich_uv,which_pip,whichbuild_extended_pathCaches can be cleared programmatically (e.g.,py_needs.which.cache_clear()).
py_needs.py has a shebang (#!/usr/bin/env -S uv run) and an embedded /// script ... /// block (specifying fire as a dependency). This enables execution via uv run ./src/twat_ez/py_needs.py <command> [args...].
The if __name__ == "__main__": block calls main(), which is set up to use fire. fire.Fire() (implicitly called) exposes the public functions of py_needs.py (e.g., download_url, which) as CLI commands.
src/twat_ez/__init__.py: Contains the package version (__version__), dynamically sourced fromimportlib.metadata.version.pyproject.toml: Central configuration for build and packaging:- Build System:
hatchlingbackend withhatch-vcsfor Git tag-based dynamic versioning. The version is written tosrc/twat_ez/__version__.pybyhatch-vcsduring build. - Metadata: Defines name (
twat-ez), Python version (>=3.10), license (MIT). - Dependencies: Runtime (
twat>=1.8.1) and optional (dev,test). - Entry Points: Registers
twat-ezas atwatplugin:[project.entry-points."twat.plugins"]withez = "twat_ez". - Hatch (
tool.hatch): Configuresuvas the installer for Hatch environments. Defines environments (default,lint) and scripts for tasks like testing (test,test-cov), type checking (type-check), and linting (lint).
- Build System:
- Tests (
tests/test_twat_ez.py): Pytest-based unit and integration tests.
This project follows modern Python practices for quality and maintainability.
- Hatch: Used for environment management. Install Hatch, then run
hatch shell. This usesuvto create/update a virtual environment with all dependencies. - Pre-commit Hooks: Configured in
.pre-commit-config.yamlfor automated linting/formatting. Install withpre-commit install.
- Ruff: For linting and formatting. Configured in
pyproject.toml([tool.ruff]).- Check:
hatch run lint:style - Format & Fix:
hatch run lint:fmt
- Check:
- MyPy: For static type checking. Configured in
pyproject.toml([tool.mypy]).- Check:
hatch run type-check(orhatch run lint:typing).
- Check:
- Type Hinting: All new code should be fully type-hinted.
- Pytest: Tests are in the
testsdirectory.- Run:
hatch run test - Coverage:
hatch run test-cov. Configured inpyproject.toml([tool.coverage]).
- Run:
- New features and bug fixes require corresponding tests.
- Versioning:
hatch-vcsderives versions from Git tags (Semantic Versioning, e.g.,v0.1.0). - Releases: Pushing a
v*.*.*tag tomainon GitHub triggers therelease.ymlGitHub Action to build and publish to PyPI.
- Strive for clear, descriptive messages. Consider Conventional Commits (e.g.,
feat: ...,fix: ...).
- Develop on feature branches (from
main). - Submit changes via Pull Requests to
main. PRs are reviewed and must pass CI checks (GitHub Actionspush.yml).
- Use GitHub Issues for bugs, features, and discussions.
- MIT License. Contributions are accepted under this license. See LICENSE file.