Modern, extensible Python project management tool with comprehensive environment and build system support
—
Extensible plugin architecture supporting environment types, publishers, project templates, and environment collectors. Provides hook specifications and plugin manager for registration and discovery.
Central manager for plugin discovery, registration, and lifecycle management across all plugin types.
class PluginManager:
"""
Plugin manager for discovering and managing hatch plugins.
Handles registration and discovery of environment, publisher, template,
and environment collector plugins through the hook system.
"""
def __init__(self):
"""Initialize plugin manager."""
def initialize(self) -> None:
"""
Initialize plugin system and discover available plugins.
This method must be called before using any plugin functionality.
"""
def hatch_register_environment(self) -> dict[str, type]:
"""
Get registered environment plugins.
Returns:
Dict mapping plugin names to environment plugin classes
"""
def hatch_register_publisher(self) -> dict[str, type]:
"""
Get registered publisher plugins.
Returns:
Dict mapping plugin names to publisher plugin classes
"""
def hatch_register_template(self) -> dict[str, type]:
"""
Get registered template plugins.
Returns:
Dict mapping plugin names to template plugin classes
"""
def hatch_register_environment_collector(self) -> dict[str, type]:
"""
Get registered environment collector plugins.
Returns:
Dict mapping plugin names to collector plugin classes
"""
def list_name_plugin(self) -> list[tuple[str, str]]:
"""
List all registered plugins with their types.
Returns:
List of (plugin_name, plugin_type) tuples
"""
def get_plugin(self, plugin_type: str, plugin_name: str) -> type | None:
"""
Get specific plugin class by type and name.
Args:
plugin_type (str): Type of plugin ('environment', 'publisher', etc.)
plugin_name (str): Name of the plugin
Returns:
Plugin class or None if not found
"""Hook specifications define the plugin registration interface that plugins must implement to be discovered by hatch.
def hatch_register_environment():
"""
Hook for registering environment plugins.
Environment plugins must implement the EnvironmentInterface and provide
isolated execution environments for project operations.
Returns:
Dict mapping environment type names to plugin classes
"""
def hatch_register_environment_collector():
"""
Hook for registering environment collector plugins.
Environment collectors discover and provide environment configurations
from various sources like Docker Compose, tox.ini, etc.
Returns:
Dict mapping collector names to collector classes
"""
def hatch_register_publisher():
"""
Hook for registering publisher plugins.
Publisher plugins handle uploading packages to various indices
and repositories with authentication and metadata support.
Returns:
Dict mapping publisher names to publisher classes
"""
def hatch_register_template():
"""
Hook for registering template plugins.
Template plugins provide project scaffolding and initialization
templates for creating new projects with predefined structures.
Returns:
Dict mapping template names to template classes
"""
def hatch_register_version_scheme():
"""
Hook for registering version scheme plugins.
Version scheme plugins handle version parsing, comparison,
and manipulation for different versioning systems.
Returns:
Dict mapping scheme names to version scheme classes
"""Base interface that all environment plugins must implement to provide environment management capabilities.
class EnvironmentInterface:
"""
Base interface for environment plugins.
Environment plugins provide isolated execution environments for
project operations including dependency management and command execution.
"""
PLUGIN_NAME: str = '' # Must be overridden by implementations
def __init__(self, root, metadata, name, config, matrix_variables, data_directory, platform, verbosity, app):
"""
Initialize environment plugin.
Args:
root: Project root directory
metadata: Project metadata
name (str): Environment name
config (dict): Environment configuration
matrix_variables (dict): Matrix variables for this environment
data_directory: Data directory for environment storage
platform: Platform utilities
verbosity (int): Verbosity level
app: Application instance
"""
# Core abstract methods that must be implemented
def exists(self) -> bool:
"""Check if environment exists."""
def create(self) -> None:
"""Create the environment."""
def remove(self) -> None:
"""Remove the environment."""
def install_project(self) -> None:
"""Install project in the environment."""
def install_project_dev_mode(self) -> None:
"""Install project in development mode."""
def dependencies_in_sync(self) -> bool:
"""Check if dependencies are synchronized."""
def sync_dependencies(self) -> None:
"""Synchronize dependencies with configuration."""Base interface that all publisher plugins must implement to handle package publishing to various repositories.
class PublisherInterface:
"""
Base interface for publisher plugins.
Publisher plugins handle uploading built packages to package repositories
with authentication, metadata validation, and upload progress tracking.
"""
PLUGIN_NAME: str = '' # Must be overridden by implementations
def __init__(self, app, root, cache_dir, project_config, plugin_config):
"""
Initialize publisher plugin.
Args:
app: Application instance
root: Project root directory
cache_dir: Cache directory for temporary files
project_config (dict): Project publishing configuration
plugin_config (dict): Plugin-specific configuration
"""
@property
def app(self):
"""Application instance."""
@property
def root(self):
"""Project root directory."""
@property
def project_config(self) -> dict:
"""Project publishing configuration."""
@property
def plugin_config(self) -> dict:
"""Plugin-specific configuration."""
@property
def disable(self) -> bool:
"""Whether this publisher is disabled."""
def publish(self, artifacts: list[str], options: dict) -> None:
"""
Publish artifacts to repository.
Args:
artifacts (list[str]): List of artifact file paths to publish
options (dict): Publishing options and metadata
"""Base interface that all template plugins must implement to provide project scaffolding capabilities.
class TemplateInterface:
"""
Base interface for template plugins.
Template plugins provide project initialization and scaffolding
capabilities for creating new projects with predefined structures.
"""
PLUGIN_NAME: str = '' # Must be overridden by implementations
PRIORITY: int = 100 # Plugin priority for ordering
def __init__(self, plugin_config: dict, cache_dir, creation_time):
"""
Initialize template plugin.
Args:
plugin_config (dict): Plugin configuration
cache_dir: Cache directory for template files
creation_time: Template creation timestamp
"""
def initialize_config(self, config: dict) -> None:
"""
Initialize template configuration with user input.
Args:
config (dict): Configuration dictionary to populate
"""
def get_files(self, config: dict) -> list:
"""
Get list of template files to create.
Args:
config (dict): Template configuration
Returns:
List of File objects representing template files
"""
def finalize_files(self, config: dict, files: list) -> None:
"""
Finalize template files after creation.
Args:
config (dict): Template configuration
files (list): List of created File objects
"""Base interface for environment collector plugins that discover environment configurations from external sources.
class EnvironmentCollectorInterface:
"""
Base interface for environment collector plugins.
Environment collectors discover and provide environment configurations
from external sources like Docker Compose files, tox.ini, etc.
"""
PLUGIN_NAME: str = '' # Must be overridden by implementations
def __init__(self, root, config):
"""
Initialize environment collector plugin.
Args:
root: Project root directory
config (dict): Collector configuration
"""
@property
def root(self):
"""Project root directory."""
@property
def config(self) -> dict:
"""Collector configuration."""
def get_initial_config(self) -> dict[str, dict]:
"""
Get initial environment configurations.
Returns:
Dict mapping environment names to configurations
"""
def finalize_config(self, config: dict[str, dict]) -> None:
"""
Finalize environment configurations.
Args:
config (dict): Environment configurations to finalize
"""
def finalize_environments(self, config: dict[str, dict]) -> None:
"""
Finalize environment setup after configuration.
Args:
config (dict): Final environment configurations
"""Utilities for registering and discovering plugins both from entry points and direct registration.
def register_plugin(plugin_type: str, plugin_name: str, plugin_class: type) -> None:
"""
Register plugin programmatically.
Args:
plugin_type (str): Type of plugin ('environment', 'publisher', etc.)
plugin_name (str): Unique name for the plugin
plugin_class (type): Plugin implementation class
"""
def discover_plugins() -> dict[str, dict[str, type]]:
"""
Discover plugins from entry points and registered plugins.
Returns:
Dict mapping plugin types to dicts of plugin name -> class mappings
"""
def validate_plugin(plugin_class: type, plugin_type: str) -> list[str]:
"""
Validate plugin implementation against interface requirements.
Args:
plugin_class (type): Plugin class to validate
plugin_type (str): Expected plugin type
Returns:
List of validation errors (empty if valid)
"""
def load_plugin_config(plugin_name: str, config_section: dict) -> dict:
"""
Load and validate plugin configuration.
Args:
plugin_name (str): Name of the plugin
config_section (dict): Configuration section for plugin
Returns:
Validated plugin configuration
"""from hatch.env.plugin.interface import EnvironmentInterface
class DockerEnvironment(EnvironmentInterface):
"""Docker-based environment plugin."""
PLUGIN_NAME = 'docker'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.container_name = f'hatch-{self.name}-{self.root.name}'
def exists(self) -> bool:
"""Check if Docker container exists."""
result = self.platform.run_command(
['docker', 'inspect', self.container_name],
capture_output=True
)
return result.returncode == 0
def create(self) -> None:
"""Create Docker container environment."""
# Get Docker image from config
image = self.config.get('image', 'python:3.11')
# Create container
self.platform.run_command([
'docker', 'create',
'--name', self.container_name,
'--volume', f'{self.root}:/workspace',
'--workdir', '/workspace',
image,
'sleep', 'infinity'
])
# Start container
self.platform.run_command([
'docker', 'start', self.container_name
])
# Install project dependencies
self.sync_dependencies()
def remove(self) -> None:
"""Remove Docker container."""
self.platform.run_command([
'docker', 'rm', '-f', self.container_name
])
def sync_dependencies(self) -> None:
"""Install dependencies in container."""
if self.dependencies:
pip_cmd = ['pip', 'install'] + self.dependencies
self.run_command(pip_cmd)
def run_command(self, command: list[str], **kwargs):
"""Execute command in Docker container."""
docker_cmd = [
'docker', 'exec',
'-w', '/workspace',
self.container_name
] + command
return self.platform.run_command(docker_cmd, **kwargs)
# Register the plugin
def hatch_register_environment():
return {'docker': DockerEnvironment}from hatch.publish.plugin.interface import PublisherInterface
import httpx
class CustomIndexPublisher(PublisherInterface):
"""Publisher for custom package index."""
PLUGIN_NAME = 'custom-index'
def publish(self, artifacts: list[str], options: dict) -> None:
"""Publish packages to custom index."""
# Get index URL from config
index_url = self.plugin_config.get('url', 'https://pypi.example.com')
username = self.plugin_config.get('username')
password = self.plugin_config.get('password')
# Create HTTP client
client = httpx.Client()
for artifact_path in artifacts:
self.app.display_info(f'Uploading {artifact_path}...')
# Prepare upload data
with open(artifact_path, 'rb') as f:
files = {'file': f}
data = {
'name': self.project_config.get('name'),
'version': self.project_config.get('version')
}
# Upload artifact
response = client.post(
f'{index_url}/upload',
files=files,
data=data,
auth=(username, password) if username else None
)
if response.status_code != 200:
self.app.abort(f'Upload failed: {response.text}')
self.app.display_success(f'Successfully uploaded {artifact_path}')
# Register the plugin
def hatch_register_publisher():
return {'custom-index': CustomIndexPublisher}from hatch.template.plugin.interface import TemplateInterface
from hatch.template import File
class FastAPITemplate(TemplateInterface):
"""FastAPI project template."""
PLUGIN_NAME = 'fastapi'
PRIORITY = 50
def initialize_config(self, config: dict) -> None:
"""Initialize template configuration."""
config.setdefault('package_name', config['project_name'].replace('-', '_'))
config.setdefault('use_database', False)
config.setdefault('use_auth', False)
def get_files(self, config: dict) -> list:
"""Get template files to create."""
files = []
package_name = config['package_name']
# Main application file
files.append(File(
f'{package_name}/main.py',
'''from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
'''
))
# Requirements file
requirements = ['fastapi>=0.68.0', 'uvicorn[standard]>=0.15.0']
if config['use_database']:
requirements.append('sqlalchemy>=1.4.0')
if config['use_auth']:
requirements.append('python-jose[cryptography]>=3.3.0')
files.append(File(
'requirements.txt',
'\n'.join(requirements) + '\n'
))
# pyproject.toml
files.append(File(
'pyproject.toml',
f'''[project]
name = "{config['project_name']}"
version = "0.1.0"
description = "FastAPI application"
dependencies = {requirements}
[project.scripts]
dev = "uvicorn {package_name}.main:app --reload"
'''
))
return files
def finalize_files(self, config: dict, files: list) -> None:
"""Finalize template after file creation."""
# Could add post-processing here
pass
# Register the plugin
def hatch_register_template():
return {'fastapi': FastAPITemplate}from hatch.plugin.manager import PluginManager
# Initialize plugin manager
plugins = PluginManager()
plugins.initialize()
# Discover available plugins
env_plugins = plugins.hatch_register_environment()
publisher_plugins = plugins.hatch_register_publisher()
template_plugins = plugins.hatch_register_template()
print(f"Environment plugins: {list(env_plugins.keys())}")
print(f"Publisher plugins: {list(publisher_plugins.keys())}")
print(f"Template plugins: {list(template_plugins.keys())}")
# Get specific plugin
docker_plugin = plugins.get_plugin('environment', 'docker')
if docker_plugin:
print(f"Found Docker plugin: {docker_plugin.PLUGIN_NAME}")
# List all plugins
all_plugins = plugins.list_name_plugin()
for name, plugin_type in all_plugins:
print(f"{plugin_type}: {name}")Install with Tessl CLI
npx tessl i tessl/pypi-hatch