A brief guide to creating wheeled Python distributions.
Python wheel is a modern standard for Python binary package distribution, replacing the older egg format.
- Faster Installation: Pre-built packages install immediately without compilation
- Improved Security: Digital signatures and hash verification
- Better Consistency: Same binary works across compatible platforms
- No Build Tools Required: End users don’t need compilers or build dependencies
- Smaller Size: Excludes tests and documentation, reducing download size
Pure Python wheels contain only Python code (no compiled extensions). They use the “py3-none-any” tag indicating compatibility with any platform.
Filename format: {name}-{version}-py3-none-any.whl
Example: mypackage-1.0.0-py3-none-any.whl
Platform wheels contain compiled extensions and are specific to an operating system, architecture, and sometimes Python implementation.
Filename format: {name}-{version}-{python}-{abi}-{platform}.whl
Examples:
my_package-1.0.0-cp39-cp39-win_amd64.whl # Windows 64-bit, Python 3.9
my_package-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl # macOS, Python 3.9
my_package-1.0.0-cp39-cp39-manylinux_2_17_x86_64.whl # Linux, Python 3.9
Where:
cp39means CPython 3.9cp39(second) is the ABI tagwin_amd64,macosx_10_9_x86_64,manylinux_2_17_x86_64are platform tags
Question: Does your package have C extensions or compiled code?
YES: Use Platform Wheel. Build separately for each platform (examples: numpy, pandas) NO: Use Pure Python Wheel (py3-none-any). Single wheel works on all platforms.
Instead of calling
python, you may need to callpython3
Install these tools before starting:
python -m pip install --upgrade pip
pip install build wheel setuptools pytest requestspython3-venv - The built-in virtual environment module in Python 3 (successor to pyvenv).
# Debian/Ubuntu
sudo apt install python3-venv
# Fedora
sudo dnf install python3-venv# Basic usage (.venv - This is the standard convention for naming Virtual Environments)
python -m venv .venv
# With system-site-packages (gives access to system packages)
python -m venv --system-site-packages .venv# On Linux/macOS
source .venv/bin/activate
# On Windows (cmd.exe)
.venv\Scripts\activate.bat
# On Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Deactivate when done
deactivateHere’s a streamlined workflow that will make package development much easier:
- One-Time Setup
- Create venv
- Activate
- Install package in editable mode
- Daily Development Loop
- Edit src/
- pytest
- Test command
- Release
- Update version
- Build
- Test wheel
# =====================================
# ONE-TIME SETUP (run once per project)
# =====================================
# STEP 1: Create a Virtual Environment
python -m venv .venv
# STEP 2: Activate the Virtual Environment
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# STEP 3: Install Development Dependencies
python -m pip install --upgrade pip
pip install build pytest pytest-cov requests
# OR better yet (if you've set up dev extras in pyproject.toml):
pip install -e ".[dev]"
# STEP 4: Install Your Package in Editable Mode
pip install -e .
# =====================================
# DAILY DEVELOPMENT (repeat as needed)
# =====================================
# Edit your code in src/...
# Changes take effect immediately - no reinstall needed!
# Run tests directly
pytest
# Try your CLI command or import your package
your-command --help # Works with your latest code!
python -c "import yourpackage; print(yourpackage.something())"
# =====================================
# PREPARING A RELEASE (occasional)
# =====================================
# STEP 5: Update version in pyproject.toml or __init__.py
# STEP 6: Clean Build Artifacts
rm -rf build/ dist/ *.egg-info
# STEP 7: Build Distribution Packages
python -m build
# STEP 8: Test the Built Wheel (recommended)
# Create temporary environment to test the actual wheel
python -m venv test_env
source test_env/bin/activate
pip install dist/package_name-0.1.0-py3-none-any.whl
# Test it works
python -c "import package_name; print(package_name.__version__)"
deactivate
rm -rf test_env
# If you modify dependencies in pyproject.toml, refresh editable install:
pip install -e . --force-reinstallpip install -e . (the -e stands for “editable”):
- Instant feedback: When edit code, changes available immediately
- No reinstall loop: Avoid the tedious edit-uninstall-reinstall-test cycle
- System-wide access: Your package/CLI becomes importable everywhere
- Real source testing: Tests run against your actual source code
- Fast iteration: Perfect for active development
# Run all tests
pytest
# Run tests with coverage report
pytest --cov=your_package_name --cov-report=html
# Run specific test file
pytest tests/test_module.py
# Run tests in verbose mode
pytest -v
# Run tests and stop at first failure
pytest -x
# Run tests matching a pattern
pytest -k "test_something"# Version Bumping
# Update version in pyproject.toml or __init__.py, then:
pip install -e . # Refresh the editable install
# Complete Clean - Remove all generated and cached files
rm -rf build/ dist/ *.egg-info __pycache__/ .pytest_cache/ .coverage htmlcov/
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
# Checking Installed Package (to see installed version and location)
pip show your_package_name
# List all files in the installed package
pip show -f your_package_name
# Check what packages are installed
pip list
# Create requirements.txt from current environment
pip freeze > requirements.txtmy-package/ # Project root
├── pyproject.toml # Modern build configuration (preferred)
├── setup.py # Legacy build configuration (if needed)
├── setup.cfg # Additional configuration (optional)
├── README.md # Project documentation
├── LICENSE # License file
├── src/ # Source directory (recommended)
│ └── my_package/ # Package directory
│ ├── __init__.py # Makes it a Python package
│ ├── module.py # Package code
│ └── cli.py # Command-line interface
└── tests/ # Test directory
└── test_module.py # Test files
Why Use the =src= Layout?
The src layout (also called “src layout”) is considered best practice for several reasons:
- Prevents accidental imports: Can’t accidentally import from project root
- Clear separation: Distinguishes installed code from development code
- Better testing: Ensures tests run against installed package, not source
- Catches packaging errors: Missing files in setup will cause import errors
- Simpler editable installs: More predictable behavior with
pip install -e .
This is the modern, preferred approach defined by PEP 517/518.
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "1.0.0"
description = "A sample Python package"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
keywords = ["example", "tutorial"]
[project.urls]
"Homepage" = "https://github.com/yourusername/hello"
# Runtime dependencies
dependencies = [
"requests>=2.28.0",
"click>=8.0.0",
]
# This section is REQUIRED for pip install -e ".[dev]"
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=3.0",
"mypy>=1.0",
"build>=1.0",
]
docs = [
"sphinx>=4.0",
"sphinx-rtd-theme>=1.0",
]
# Define command-line scripts
# Format: command-name = "package.module:function"
# This creates an executable script called 'my-command'
[project.scripts]
my-command = "my_package.cli:main"
# Setuptools-specific configuration
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
# Pytest configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --strict-markers"
# Mypy type checking configuration
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_unimported = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
check_untyped_defs = true
strict_equality = true
# Coverage configuration
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/test_*.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]The modern, PEP 517-compliant way:
python -m buildThis creates both:
- A wheel file:
dist/my_package-1.0.0-py3-none-any.whl - A source distribution:
dist/my_package-1.0.0.tar.gz
# Build only wheel
python -m build --wheel
# Build only source distribution
python -m build --sdist# Build wheel
python setup.py bdist_wheel
# Build source distribution
python setup.py sdist
# Build both
python setup.py sdist bdist_wheelNote: Direct setup.py invocation is deprecated. Use python -m build instead.
Use pipx for installing CLI applications in isolated environments:
# Install pipx (if not already installed)
# Debian/Ubuntu
sudo apt install pipx
# Or via pip
python -m pip install --user pipx
python -m pipx ensurepath
# Install your package
pipx install dist/package_name-0.1.0-py3-none-any.whl
# Or install directly from PyPI
pipx install package_name
# Uninstall
pipx uninstall package_name
# List installed packages
pipx list
# Upgrade a package
pipx upgrade package_nameWhy pipx?
- Each application gets its own virtual environment
- No dependency conflicts between tools
- Commands available system-wide
- Perfect for CLI tools like black, pytest, flake8, etc.
# Install from PyPI
pip install package-name
# Install from local wheel
pip install dist/package_name-0.1.0-py3-none-any.whl
# Install from source (current directory)
pip install .
# Install in editable/development mode
pip install -e .
# Install with extra dependencies
pip install "package-name[dev]"
pip install -e ".[dev,docs]"
# Uninstall
pip uninstall package-name
# Upgrade to latest version
pip install --upgrade package-name
# Force reinstall (useful for testing)
pip install --force-reinstall package-name# Create requirements.txt from current environment
pip freeze > requirements.txt
# Install from requirements.txt
pip install -r requirements.txt
# Show installed package details
pip show package-name
# List all installed packages
pip list
# List outdated packages
pip list --outdatedA simple Python package for greeting messages.
hellolib/
├── Makefile
├── pyproject.toml
├── README.md
├── src/
│ └── hellolib/
│ ├── __init__.py
│ └── hello.py
└── tests/
├── __init__.py
├── test_version.py
└── test_hellolib.py
# Hellolib
A simple Python package for greeting messages.
## Installation
```bash
pip install hellolib
```
## Usage
```python
from hellolib.hello import say_hello, say_goodbye
print(say_hello()) # Hello, World!
print(say_hello("Python")) # Hello, Python!
print(say_goodbye("Friend")) # Goodbye, Friend!
```
## Development
```bash
# Create and activate environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install in development mode with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=hellolib --cov-report=html
# Build distribution
python -m build
# Clean up
deactivate
rm -rf .venv
```
## Testing the Built Package
```bash
# Create test environment
python -m venv test_env
source test_env/bin/activate # On Windows: test_env\Scripts\activate
# Install the wheel
pip install ./dist/hellolib-1.1.0-py3-none-any.whl
# Test it
python -c "from hellolib.hello import say_hello; print(say_hello('Wheel'))"
# Clean up
deactivate
rm -rf test_env
```[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hellolib"
version = "1.1.0"
description = "A simple greeting library"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
[project.urls]
"Homepage" = "https://github.com/yourusername/hello"
# Optional dependencies for development
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=3.0",
"mypy>=1.0",
"build>=1.0",
]
# Setuptools-specific configuration
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
# Pytest configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v"
# Mypy type checking configuration
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_unimported = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
check_untyped_defs = true
strict_equality = true"""Simple greeting library."""
try:
from importlib.metadata import version, PackageNotFoundError
__version__ = version(__name__)
except (ImportError, PackageNotFoundError):
__version__ = "unknown"
from hellolib.hello import (
say_hello,
say_goodbye,
greet_multiple,
format_greeting,
)
# Define public interface
__all__ = [
"__version__",
"say_hello",
"say_goodbye",
"greet_multiple",
"format_greeting",
]"""Greeting functions demonstrating Python 3.10+ features."""
def say_hello(name: str = "World") -> str:
"""Return a greeting message.
Args:
name: Name to greet. Defaults to "World".
Returns:
Greeting message
Examples:
>>> say_hello()
'Hello, World!'
>>> say_hello("Python")
'Hello, Python!'
"""
return f"Hello, {name}!"
def say_goodbye(name: str = "World") -> str:
"""Return a goodbye message.
Args:
name: Name to say goodbye to. Defaults to "World".
Returns:
Goodbye message
Examples:
>>> say_goodbye()
'Goodbye, World!'
>>> say_goodbye("Python")
'Goodbye, Python!'
"""
return f"Goodbye, {name}!"
def greet_multiple(
names: list[str] | tuple[str, ...],
greeting: str = "Hello"
) -> list[str]:
"""Greet multiple people with modern type hints.
Demonstrates Python 3.10+ features:
- Union types with | operator (PEP 604)
- Built-in generics like list[str], tuple[str, ...] (PEP 585)
Args:
names: List or tuple of names to greet
greeting: Greeting word to use. Defaults to "Hello".
Returns:
List of greeting messages
Examples:
>>> greet_multiple(["Alice", "Bob"])
['Hello, Alice!', 'Hello, Bob!']
>>> greet_multiple(("Charlie",), "Hi")
['Hi, Charlie!']
>>> greet_multiple([])
[]
"""
return [f"{greeting}, {name}!" for name in names]
def format_greeting(
name: str,
*,
style: str | None = None,
suffix: str = "!"
) -> str:
"""Format a greeting with various styles using pattern matching.
Demonstrates Python 3.10+ features:
- Union with None using | operator
- Keyword-only arguments with *
- Structural pattern matching (PEP 634)
Args:
name: Name to greet
style: Greeting style ('formal', 'casual', or None for default)
suffix: Punctuation to end with. Defaults to "!".
Returns:
Formatted greeting message
Examples:
>>> format_greeting("Alice")
'Hello, Alice!'
>>> format_greeting("Bob", style="formal")
'Good day, Bob!'
>>> format_greeting("Charlie", style="casual")
'Hey, Charlie!'
>>> format_greeting("Dave", style="casual", suffix=".")
'Hey, Dave.'
"""
match style:
case "formal":
greeting = "Good day"
case "casual":
greeting = "Hey"
case None:
greeting = "Hello"
case _:
greeting = "Hello"
return f"{greeting}, {name}{suffix}""""Tests package for hellolib."""
# This file can be empty - it just indicates tests is a package"""Tests for the hellolib package."""
from hellolib.hello import (
say_hello,
say_goodbye,
greet_multiple,
format_greeting,
)
def test_say_hello_default() -> None:
"""Test say_hello with default arguments."""
assert say_hello() == "Hello, World!"
def test_say_hello_custom() -> None:
"""Test say_hello with custom name."""
assert say_hello("Python") == "Hello, Python!"
def test_say_hello_empty_string() -> None:
"""Test say_hello with empty string."""
assert say_hello("") == "Hello, !"
def test_say_goodbye_default() -> None:
"""Test say_goodbye with default arguments."""
assert say_goodbye() == "Goodbye, World!"
def test_say_goodbye_custom() -> None:
"""Test say_goodbye with custom name."""
assert say_goodbye("Python") == "Goodbye, Python!"
def test_return_types() -> None:
"""Test that functions return strings."""
assert isinstance(say_hello(), str)
assert isinstance(say_goodbye(), str)
# Tests for Python 3.10+ features
def test_greet_multiple_list() -> None:
"""Test greet_multiple with list (demonstrates list[str] type hint)."""
result = greet_multiple(["Alice", "Bob"])
assert result == ["Hello, Alice!", "Hello, Bob!"]
def test_greet_multiple_tuple() -> None:
"""Test greet_multiple with tuple (demonstrates tuple[str, ...] type hint)."""
result = greet_multiple(("Charlie",))
assert result == ["Hello, Charlie!"]
def test_greet_multiple_empty() -> None:
"""Test greet_multiple with empty list."""
assert greet_multiple([]) == []
def test_greet_multiple_custom_greeting() -> None:
"""Test greet_multiple with custom greeting."""
result = greet_multiple(["Dave"], greeting="Hi")
assert result == ["Hi, Dave!"]
def test_format_greeting_default() -> None:
"""Test format_greeting with default style."""
assert format_greeting("Alice") == "Hello, Alice!"
def test_format_greeting_formal() -> None:
"""Test format_greeting with formal style (demonstrates pattern matching)."""
assert format_greeting("Bob", style="formal") == "Good day, Bob!"
def test_format_greeting_casual() -> None:
"""Test format_greeting with casual style (demonstrates pattern matching)."""
assert format_greeting("Charlie", style="casual") == "Hey, Charlie!"
def test_format_greeting_custom_suffix() -> None:
"""Test format_greeting with custom suffix."""
assert format_greeting("Dave", style="casual", suffix=".") == "Hey, Dave."
def test_format_greeting_unknown_style() -> None:
"""Test format_greeting with unknown style (demonstrates default case)."""
assert format_greeting("Eve", style="unknown") == "Hello, Eve!""""Test version information."""
from hellolib import __version__
def test_version_exists() -> None:
"""Test that version is defined."""
assert __version__ is not None
assert isinstance(__version__, str)
assert __version__ != "unknown"
def test_version_format() -> None:
"""Test version follows semantic versioning."""
parts = __version__.split(".")
assert len(parts) >= 2, "Version should have at least major.minor"
assert all(part.isdigit() for part in parts), "Version parts should be numeric"#!/usr/bin/env python3
"""Example usage of hellolib package."""
from hellolib.hello import say_hello, say_goodbye
# Default greeting
print(say_hello()) # Outputs: Hello, World!
# Custom greeting
print(say_hello("Python")) # Outputs: Hello, Python!
# Goodbye message
print(say_goodbye("Friend")) # Outputs: Goodbye, Friend!For development workflow, see hellolib/README.md
# After installing in dev mode (pip install -e .)
python ./use_hellolib_example.pyExpected output:
Hello, World!
Hello, Python!
Goodbye, Friend!
A Python CLI package for various greeting commands.
hello/
├── pyproject.toml
├── README.md
├── src/
│ └── hello/
│ ├── __init__.py
│ ├── cli.py
│ ├── handlers/
│ │ ├── __init__.py
│ │ ├── hi_handler.py
│ │ ├── bay_handler.py
│ │ ├── default_handler.py
│ │ └── info_handler.py
└── tests/
├── __init__.py
├── test_version.py
└── test_hello.py
# Hello
A simple Python CLI application for greeting commands.
## Installation
### System-wide Installation (Recommended for CLI tools)
```bash
pipx install hello
```
### Regular Installation
```bash
pip install hello
```
## Usage
After installation, you can use the package from the command line:
```bash
hello hi # Outputs: Hello
hello bay # Outputs: Good-bay
hello anything # Outputs: Have a nice day
hello --version # Shows version information
hello --help # Shows help information
```
## Development
```bash
# Create and activate environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install in development mode
pip install -e ".[dev]"
# Run all tests
pytest
# Run tests with coverage
pytest --cov=hello --cov-report=html
# Run specific test
pytest tests/test_hello.py -v
# Clean previous builds
rm -rf build/ dist/ *.egg-info
# Build distribution
python -m build
# Clean up
deactivate
rm -rf .venv
```
## Testing the Built Package
### Test environment
```bash
# Create test environment
python -m venv test_env
source test_env/bin/activate # On Windows: test_env\Scripts\activate
# Install the wheel
pip install ./dist/hello-1.0.0-py3-none-any.whl
# Test commands
hello hi
hello bay
hello --version
# Clean up
deactivate
rm -rf test_env
```
### System-wide Installation
```bash
# Install with pipx for testing
pipx install ./dist/hello-1.0.0-py3-none-any.whl
# Test commands
hello hi
hello bay
hello --version
# Uninstall
pipx uninstall hello
```[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hello"
version = "1.0.0"
description = "A CLI greeting application"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
[project.urls]
"Homepage" = "https://github.com/yourusername/hello"
# This section is REQUIRED for pip install -e ".[dev]"
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=3.0",
"mypy>=1.0",
"build>=1.0",
]
# This creates an executable command called 'hello'
# that calls the main() function in hello.cli module
[project.scripts]
hello = "hello.cli:main"
# Setuptools-specific configuration
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
include = ["hello*"]
# Pytest configuration
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
addopts = "-v"
# Mypy type checking configuration
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_unimported = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
check_untyped_defs = true
strict_equality = true"""Hello CLI application package."""
try:
from importlib.metadata import version, PackageNotFoundError
__version__ = version(__name__)
except (ImportError, PackageNotFoundError):
__version__ = "unknown"
# Define public interface
__all__ = ["__version__"]"""Command-line interface for the hello package."""
import sys
import argparse
from hello.handlers.hi_handler import handle_hi
from hello.handlers.bay_handler import handle_bay
from hello.handlers.default_handler import handle_default
from hello import __version__
def create_parser() -> argparse.ArgumentParser:
"""Create and configure argument parser.
Returns:
argparse.ArgumentParser: Configured ArgumentParser instance
"""
parser = argparse.ArgumentParser(
prog="hello", # Use command name, not module name (__name__ would be "hello.cli")
description="A simple greeting CLI application",
epilog="Examples:\n"
" hello # Default greeting\n"
" hello hi # Say hello\n"
" hello bay # Say goodbye\n"
" hello --version # Show version",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--version",
"-v",
action="version",
version=f"%(prog)s {__version__}",
help="Show program version and exit"
)
parser.add_argument(
"command",
nargs="?",
default="default",
help="Command to execute: 'hi', 'bay', or any other text for default message"
)
return parser
def main() -> int:
"""Main entry point for CLI.
Returns:
Exit code (0 for success, non-zero for error)
"""
parser = create_parser()
args = parser.parse_args()
try:
match args.command:
case "hi":
result = handle_hi()
case "bay":
result = handle_bay()
case _:
result = handle_default()
print(result)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())"""Handler modules for the hello package."""
from hello.handlers.hi_handler import handle_hi
from hello.handlers.bay_handler import handle_bay
from hello.handlers.default_handler import handle_default
from hello.handlers.info_handler import handle_info
__all__ = ["handle_hi", "handle_bay", "handle_default", "handle_info"]"""Handler for the 'hi' command."""
def handle_hi() -> str:
"""Return the 'Hello' greeting.
Returns:
The greeting message
"""
return "Hello""""Handler for the 'bay' command."""
def handle_bay() -> str:
"""Return the 'Good-bay' message.
Returns:
The farewell message
"""
return "Good-bay""""Default handler for unrecognized commands."""
def handle_default() -> str:
"""Return the default message.
Returns:
The default friendly message
"""
return "Have a nice day""""Handler demonstrating advanced Python 3.10+ features.
This handler showcases structural pattern matching capabilities
introduced in Python 3.10 (PEP 634).
"""
def handle_info(args: list[str] | None = None) -> str:
"""Process info command with structural pattern matching.
Demonstrates Python 3.10+ features:
- Union types with | operator (PEP 604)
- Built-in generics like list[str] (PEP 585)
- Structural pattern matching (PEP 634)
Args:
args: List of arguments to process. Defaults to None.
Returns:
Information message based on the arguments
Examples:
>>> handle_info(None)
'No information requested'
>>> handle_info([])
'No information requested'
>>> handle_info(['version'])
'Version: 1.0.0'
>>> handle_info(['python'])
'Python: 3.10+'
>>> handle_info(['author', 'email'])
'Contact: author at email'
"""
match args:
# Match None or empty list
case None | []:
return "No information requested"
# Match single item patterns
case ["version"]:
return "Version: 1.0.0"
case ["python"]:
return "Python: 3.10+"
case ["help"]:
return "Available info: version, python, author, help"
# Match two items for contact info
case ["author", email]:
return f"Contact: author at {email}"
# Match list with specific first element
case ["config", *rest]:
config_items = ", ".join(rest) if rest else "none"
return f"Configuration: {config_items}"
# Catch-all with guard (if clause)
case [first, *_] if len(args) > 3:
return f"Too many arguments (got {len(args)}, expected ≤ 3)"
# Final catch-all
case _:
items = ", ".join(args) if isinstance(args, list) else str(args)
return f"Unknown info request: {items}""""Tests package for hello CLI application."""
# This file can be empty - it just indicates tests is a package"""Tests for the hello package."""
from unittest.mock import patch
import pytest
from hello.cli import main
from hello.handlers.hi_handler import handle_hi
from hello.handlers.bay_handler import handle_bay
from hello.handlers.default_handler import handle_default
from hello.handlers.info_handler import handle_info
def test_handle_hi() -> None:
"""Test the hi handler."""
assert handle_hi() == "Hello"
def test_handle_bay() -> None:
"""Test the bay handler."""
assert handle_bay() == "Good-bay"
def test_handle_default() -> None:
"""Test the default handler."""
assert handle_default() == "Have a nice day"
@patch("sys.argv", ["hello", "hi"])
def test_cli_hi(capsys: pytest.CaptureFixture[str]) -> None:
"""Test CLI with 'hi' command."""
result = main()
captured = capsys.readouterr()
assert captured.out.strip() == "Hello"
assert result == 0
@patch("sys.argv", ["hello", "bay"])
def test_cli_bay(capsys: pytest.CaptureFixture[str]) -> None:
"""Test CLI with 'bay' command."""
result = main()
captured = capsys.readouterr()
assert captured.out.strip() == "Good-bay"
assert result == 0
@patch("sys.argv", ["hello", "unknown"])
def test_cli_default(capsys: pytest.CaptureFixture[str]) -> None:
"""Test CLI with an unknown command."""
result = main()
captured = capsys.readouterr()
assert captured.out.strip() == "Have a nice day"
assert result == 0
@patch("sys.argv", ["hello"])
def test_cli_no_command(capsys: pytest.CaptureFixture[str]) -> None:
"""Test CLI with no command (should use default)."""
result = main()
captured = capsys.readouterr()
assert captured.out.strip() == "Have a nice day"
assert result == 0
# Tests for info_handler demonstrating Python 3.10+ pattern matching
def test_info_handler_none() -> None:
"""Test info handler with None."""
assert handle_info(None) == "No information requested"
def test_info_handler_empty() -> None:
"""Test info handler with empty list."""
assert handle_info([]) == "No information requested"
def test_info_handler_version() -> None:
"""Test info handler requesting version."""
assert handle_info(["version"]) == "Version: 1.0.0"
def test_info_handler_python() -> None:
"""Test info handler requesting Python version."""
assert handle_info(["python"]) == "Python: 3.10+"
def test_info_handler_help() -> None:
"""Test info handler requesting help."""
assert handle_info(["help"]) == "Available info: version, python, author, help"
def test_info_handler_author() -> None:
"""Test info handler with author contact (demonstrates pattern with capture)."""
result = handle_info(["author", "test@example.com"])
assert result == "Contact: author at test@example.com"
def test_info_handler_config() -> None:
"""Test info handler with config (demonstrates wildcard pattern)."""
result = handle_info(["config", "debug", "verbose"])
assert result == "Configuration: debug, verbose"
def test_info_handler_config_empty() -> None:
"""Test info handler with config but no items."""
result = handle_info(["config"])
assert result == "Configuration: none"
def test_info_handler_too_many_args() -> None:
"""Test info handler with too many arguments (demonstrates guard clause)."""
result = handle_info(["a", "b", "c", "d"])
assert result == "Too many arguments (got 4, expected ≤ 3)"
def test_info_handler_unknown() -> None:
"""Test info handler with unknown request (demonstrates catch-all)."""
result = handle_info(["unknown", "request"])
assert result == "Unknown info request: unknown, request""""Test version information."""
import subprocess
import sys
from hello import __version__
def test_version_exists() -> None:
"""Test that version can be imported."""
assert __version__ is not None
assert isinstance(__version__, str)
assert __version__ != "unknown"
def test_version_format() -> None:
"""Test version follows semantic versioning."""
parts = __version__.split(".")
assert len(parts) >= 2, "Version should have at least major.minor"
assert all(part.isdigit() for part in parts), "Version parts should be numeric"
def test_cli_version_flag() -> None:
"""Test that CLI --version flag works."""
result = subprocess.run(
["hello", "--version"],
capture_output=True,
text=True,
check=False
)
# Check command succeeded
assert result.returncode == 0, f"CLI failed with: {result.stderr}"
# Check output contains version
output = result.stdout.strip()
assert "hello" in output.lower(), f"Expected 'hello' in output: {output}"
assert __version__ in output, f"Expected version {__version__} in output: {output}"
def test_cli_version_shorthand() -> None:
"""Test that CLI -v flag works."""
result = subprocess.run(
["hello", "-v"],
capture_output=True,
text=True,
check=False
)
# Check command succeeded
assert result.returncode == 0, f"CLI failed with: {result.stderr}"
# Check output contains version
output = result.stdout.strip()
assert __version__ in output, f"Expected version {__version__} in output: {output}"
def test_cli_module_version() -> None:
"""Test running CLI as module with --version."""
result = subprocess.run(
[sys.executable, "-m", "hello.cli", "--version"],
capture_output=True,
text=True,
check=False
)
assert result.returncode == 0, f"CLI module failed with: {result.stderr}"
output = result.stdout.strip()
assert __version__ in output, f"Expected version {__version__} in output: {output}"For development workflow and testing commands, see hello/README.md