461 lines
9.9 KiB
Markdown
461 lines
9.9 KiB
Markdown
|
|
# Python Packaging and Project Setup
|
||
|
|
|
||
|
|
## Project Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
myproject/
|
||
|
|
├── pyproject.toml # Project metadata and dependencies
|
||
|
|
├── README.md # Project description
|
||
|
|
├── .gitignore # Git ignore patterns
|
||
|
|
├── .python-version # Python version for pyenv
|
||
|
|
├── src/
|
||
|
|
│ └── myproject/
|
||
|
|
│ ├── __init__.py # Package initialization
|
||
|
|
│ ├── py.typed # PEP 561 type marker
|
||
|
|
│ ├── core.py # Core functionality
|
||
|
|
│ └── utils.py # Utilities
|
||
|
|
├── tests/
|
||
|
|
│ ├── __init__.py
|
||
|
|
│ ├── conftest.py # Pytest configuration
|
||
|
|
│ └── test_core.py # Tests
|
||
|
|
└── docs/
|
||
|
|
└── index.md # Documentation
|
||
|
|
```
|
||
|
|
|
||
|
|
## Pyproject.toml Configuration
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[build-system]
|
||
|
|
requires = ["hatchling"]
|
||
|
|
build-backend = "hatchling.build"
|
||
|
|
|
||
|
|
[project]
|
||
|
|
name = "myproject"
|
||
|
|
version = "0.1.0"
|
||
|
|
description = "A Python project"
|
||
|
|
readme = "README.md"
|
||
|
|
requires-python = ">=3.11"
|
||
|
|
license = {text = "MIT"}
|
||
|
|
authors = [
|
||
|
|
{name = "Your Name", email = "you@example.com"}
|
||
|
|
]
|
||
|
|
keywords = ["python", "package"]
|
||
|
|
classifiers = [
|
||
|
|
"Development Status :: 4 - Beta",
|
||
|
|
"Intended Audience :: Developers",
|
||
|
|
"License :: OSI Approved :: MIT License",
|
||
|
|
"Programming Language :: Python :: 3.11",
|
||
|
|
"Programming Language :: Python :: 3.12",
|
||
|
|
"Typing :: Typed",
|
||
|
|
]
|
||
|
|
|
||
|
|
dependencies = [
|
||
|
|
"requests>=2.31.0",
|
||
|
|
"pydantic>=2.5.0",
|
||
|
|
]
|
||
|
|
|
||
|
|
[project.optional-dependencies]
|
||
|
|
dev = [
|
||
|
|
"pytest>=7.4.0",
|
||
|
|
"pytest-cov>=4.1.0",
|
||
|
|
"mypy>=1.7.0",
|
||
|
|
"black>=23.11.0",
|
||
|
|
"ruff>=0.1.6",
|
||
|
|
]
|
||
|
|
docs = [
|
||
|
|
"mkdocs>=1.5.0",
|
||
|
|
"mkdocs-material>=9.4.0",
|
||
|
|
]
|
||
|
|
|
||
|
|
[project.scripts]
|
||
|
|
myproject = "myproject.cli:main"
|
||
|
|
|
||
|
|
[project.urls]
|
||
|
|
Homepage = "https://github.com/username/myproject"
|
||
|
|
Documentation = "https://myproject.readthedocs.io"
|
||
|
|
Repository = "https://github.com/username/myproject"
|
||
|
|
Changelog = "https://github.com/username/myproject/blob/main/CHANGELOG.md"
|
||
|
|
|
||
|
|
# Tool configurations
|
||
|
|
[tool.black]
|
||
|
|
line-length = 100
|
||
|
|
target-version = ["py311"]
|
||
|
|
include = '\.pyi?$'
|
||
|
|
|
||
|
|
[tool.ruff]
|
||
|
|
line-length = 100
|
||
|
|
target-version = "py311"
|
||
|
|
select = [
|
||
|
|
"E", # pycodestyle errors
|
||
|
|
"W", # pycodestyle warnings
|
||
|
|
"F", # pyflakes
|
||
|
|
"I", # isort
|
||
|
|
"B", # flake8-bugbear
|
||
|
|
"C4", # flake8-comprehensions
|
||
|
|
"UP", # pyupgrade
|
||
|
|
]
|
||
|
|
ignore = []
|
||
|
|
|
||
|
|
[tool.ruff.per-file-ignores]
|
||
|
|
"__init__.py" = ["F401"] # Ignore unused imports in __init__.py
|
||
|
|
|
||
|
|
[tool.mypy]
|
||
|
|
python_version = "3.11"
|
||
|
|
strict = true
|
||
|
|
warn_return_any = true
|
||
|
|
warn_unused_configs = true
|
||
|
|
disallow_untyped_defs = true
|
||
|
|
|
||
|
|
[[tool.mypy.overrides]]
|
||
|
|
module = "third_party.*"
|
||
|
|
ignore_missing_imports = true
|
||
|
|
|
||
|
|
[tool.pytest.ini_options]
|
||
|
|
minversion = "7.0"
|
||
|
|
addopts = [
|
||
|
|
"-ra",
|
||
|
|
"--strict-markers",
|
||
|
|
"--strict-config",
|
||
|
|
"--cov=myproject",
|
||
|
|
"--cov-report=term-missing",
|
||
|
|
"--cov-report=html",
|
||
|
|
]
|
||
|
|
testpaths = ["tests"]
|
||
|
|
pythonpath = ["src"]
|
||
|
|
|
||
|
|
[tool.coverage.run]
|
||
|
|
source = ["src"]
|
||
|
|
branch = true
|
||
|
|
|
||
|
|
[tool.coverage.report]
|
||
|
|
exclude_lines = [
|
||
|
|
"pragma: no cover",
|
||
|
|
"def __repr__",
|
||
|
|
"raise AssertionError",
|
||
|
|
"raise NotImplementedError",
|
||
|
|
"if __name__ == .__main__.:",
|
||
|
|
"if TYPE_CHECKING:",
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
## Poetry Project Management
|
||
|
|
|
||
|
|
```toml
|
||
|
|
# pyproject.toml for Poetry
|
||
|
|
[tool.poetry]
|
||
|
|
name = "myproject"
|
||
|
|
version = "0.1.0"
|
||
|
|
description = "A Python project"
|
||
|
|
authors = ["Your Name <you@example.com>"]
|
||
|
|
readme = "README.md"
|
||
|
|
license = "MIT"
|
||
|
|
packages = [{include = "myproject", from = "src"}]
|
||
|
|
|
||
|
|
[tool.poetry.dependencies]
|
||
|
|
python = "^3.11"
|
||
|
|
requests = "^2.31.0"
|
||
|
|
pydantic = "^2.5.0"
|
||
|
|
|
||
|
|
[tool.poetry.group.dev.dependencies]
|
||
|
|
pytest = "^7.4.0"
|
||
|
|
pytest-cov = "^4.1.0"
|
||
|
|
mypy = "^1.7.0"
|
||
|
|
black = "^23.11.0"
|
||
|
|
ruff = "^0.1.6"
|
||
|
|
|
||
|
|
[tool.poetry.scripts]
|
||
|
|
myproject = "myproject.cli:main"
|
||
|
|
|
||
|
|
[build-system]
|
||
|
|
requires = ["poetry-core"]
|
||
|
|
build-backend = "poetry.core.masonry.api"
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Poetry commands
|
||
|
|
poetry init # Initialize new project
|
||
|
|
poetry add requests # Add dependency
|
||
|
|
poetry add --group dev pytest # Add dev dependency
|
||
|
|
poetry install # Install dependencies
|
||
|
|
poetry update # Update dependencies
|
||
|
|
poetry shell # Activate virtual environment
|
||
|
|
poetry run pytest # Run command in venv
|
||
|
|
poetry build # Build package
|
||
|
|
poetry publish # Publish to PyPI
|
||
|
|
poetry export -f requirements.txt --output requirements.txt
|
||
|
|
```
|
||
|
|
|
||
|
|
## Virtual Environments
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Using venv (built-in)
|
||
|
|
python -m venv .venv
|
||
|
|
source .venv/bin/activate # Linux/Mac
|
||
|
|
.venv\Scripts\activate # Windows
|
||
|
|
|
||
|
|
# Install in editable mode
|
||
|
|
pip install -e .
|
||
|
|
pip install -e ".[dev]" # With optional dependencies
|
||
|
|
|
||
|
|
# Using virtualenv
|
||
|
|
pip install virtualenv
|
||
|
|
virtualenv venv
|
||
|
|
source venv/bin/activate
|
||
|
|
|
||
|
|
# Using pyenv for Python version management
|
||
|
|
pyenv install 3.11.6
|
||
|
|
pyenv local 3.11.6 # Set for current directory
|
||
|
|
echo "3.11.6" > .python-version
|
||
|
|
```
|
||
|
|
|
||
|
|
## Package __init__.py
|
||
|
|
|
||
|
|
```python
|
||
|
|
# src/myproject/__init__.py
|
||
|
|
"""MyProject - A Python package."""
|
||
|
|
|
||
|
|
from myproject.core import main_function, CoreClass
|
||
|
|
from myproject.utils import helper_function
|
||
|
|
|
||
|
|
__version__ = "0.1.0"
|
||
|
|
__all__ = ["main_function", "CoreClass", "helper_function"]
|
||
|
|
|
||
|
|
# Package-level configuration
|
||
|
|
import logging
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
logger.addHandler(logging.NullHandler())
|
||
|
|
```
|
||
|
|
|
||
|
|
## Type Stub Files (py.typed)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# src/myproject/py.typed
|
||
|
|
# Empty file indicates package includes type hints
|
||
|
|
|
||
|
|
# src/myproject/__init__.pyi (optional stub file)
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
__version__: str
|
||
|
|
|
||
|
|
def main_function(arg: str) -> dict[str, Any]: ...
|
||
|
|
|
||
|
|
class CoreClass:
|
||
|
|
def __init__(self, name: str) -> None: ...
|
||
|
|
def process(self) -> str: ...
|
||
|
|
```
|
||
|
|
|
||
|
|
## CLI Entry Points
|
||
|
|
|
||
|
|
```python
|
||
|
|
# src/myproject/cli.py
|
||
|
|
import sys
|
||
|
|
from typing import NoReturn
|
||
|
|
|
||
|
|
def main() -> NoReturn:
|
||
|
|
"""Main CLI entry point."""
|
||
|
|
print("MyProject CLI")
|
||
|
|
sys.exit(0)
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|
||
|
|
```
|
||
|
|
|
||
|
|
## Requirements Files
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# requirements.txt - Production dependencies
|
||
|
|
requests>=2.31.0,<3.0.0
|
||
|
|
pydantic>=2.5.0,<3.0.0
|
||
|
|
|
||
|
|
# requirements-dev.txt - Development dependencies
|
||
|
|
-r requirements.txt
|
||
|
|
pytest>=7.4.0
|
||
|
|
pytest-cov>=4.1.0
|
||
|
|
mypy>=1.7.0
|
||
|
|
black>=23.11.0
|
||
|
|
ruff>=0.1.6
|
||
|
|
|
||
|
|
# Generate from Poetry
|
||
|
|
poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||
|
|
poetry export -f requirements.txt --with dev --output requirements-dev.txt
|
||
|
|
```
|
||
|
|
|
||
|
|
## Building and Distribution
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Build package
|
||
|
|
python -m build
|
||
|
|
|
||
|
|
# Check package
|
||
|
|
twine check dist/*
|
||
|
|
|
||
|
|
# Upload to PyPI
|
||
|
|
twine upload dist/*
|
||
|
|
|
||
|
|
# Upload to Test PyPI
|
||
|
|
twine upload --repository testpypi dist/*
|
||
|
|
|
||
|
|
# Install from Test PyPI
|
||
|
|
pip install --index-url https://test.pypi.org/simple/ myproject
|
||
|
|
```
|
||
|
|
|
||
|
|
## Setuptools Configuration (Legacy)
|
||
|
|
|
||
|
|
```python
|
||
|
|
# setup.py (if not using pyproject.toml)
|
||
|
|
from setuptools import setup, find_packages
|
||
|
|
|
||
|
|
setup(
|
||
|
|
name="myproject",
|
||
|
|
version="0.1.0",
|
||
|
|
packages=find_packages(where="src"),
|
||
|
|
package_dir={"": "src"},
|
||
|
|
python_requires=">=3.11",
|
||
|
|
install_requires=[
|
||
|
|
"requests>=2.31.0",
|
||
|
|
"pydantic>=2.5.0",
|
||
|
|
],
|
||
|
|
extras_require={
|
||
|
|
"dev": [
|
||
|
|
"pytest>=7.4.0",
|
||
|
|
"mypy>=1.7.0",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
entry_points={
|
||
|
|
"console_scripts": [
|
||
|
|
"myproject=myproject.cli:main",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Manifest for Package Data
|
||
|
|
|
||
|
|
```
|
||
|
|
# MANIFEST.in
|
||
|
|
include README.md
|
||
|
|
include LICENSE
|
||
|
|
include pyproject.toml
|
||
|
|
recursive-include src/myproject *.py
|
||
|
|
recursive-include src/myproject py.typed
|
||
|
|
recursive-include tests *.py
|
||
|
|
prune docs/_build
|
||
|
|
```
|
||
|
|
|
||
|
|
## Version Management
|
||
|
|
|
||
|
|
```python
|
||
|
|
# src/myproject/__version__.py
|
||
|
|
__version__ = "0.1.0"
|
||
|
|
|
||
|
|
# src/myproject/__init__.py
|
||
|
|
from myproject.__version__ import __version__
|
||
|
|
|
||
|
|
# Read version in pyproject.toml
|
||
|
|
import tomli
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
def get_version() -> str:
|
||
|
|
pyproject = Path(__file__).parent.parent / "pyproject.toml"
|
||
|
|
with open(pyproject, "rb") as f:
|
||
|
|
data = tomli.load(f)
|
||
|
|
return data["project"]["version"]
|
||
|
|
```
|
||
|
|
|
||
|
|
## Dependency Management Best Practices
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Pin dependencies for applications
|
||
|
|
requests==2.31.0
|
||
|
|
pydantic==2.5.2
|
||
|
|
|
||
|
|
# Use ranges for libraries
|
||
|
|
requests>=2.31.0,<3.0.0
|
||
|
|
pydantic>=2.5.0,<3.0.0
|
||
|
|
|
||
|
|
# Lock files
|
||
|
|
# Poetry: poetry.lock
|
||
|
|
# pip: requirements.txt with exact versions
|
||
|
|
pip freeze > requirements-lock.txt
|
||
|
|
|
||
|
|
# Update dependencies
|
||
|
|
poetry update
|
||
|
|
pip install --upgrade -r requirements.txt
|
||
|
|
```
|
||
|
|
|
||
|
|
## CI/CD Integration
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
# .github/workflows/test.yml
|
||
|
|
name: Tests
|
||
|
|
|
||
|
|
on: [push, pull_request]
|
||
|
|
|
||
|
|
jobs:
|
||
|
|
test:
|
||
|
|
runs-on: ubuntu-latest
|
||
|
|
strategy:
|
||
|
|
matrix:
|
||
|
|
python-version: ["3.11", "3.12"]
|
||
|
|
|
||
|
|
steps:
|
||
|
|
- uses: actions/checkout@v4
|
||
|
|
- name: Set up Python
|
||
|
|
uses: actions/setup-python@v4
|
||
|
|
with:
|
||
|
|
python-version: ${{ matrix.python-version }}
|
||
|
|
|
||
|
|
- name: Install dependencies
|
||
|
|
run: |
|
||
|
|
python -m pip install --upgrade pip
|
||
|
|
pip install -e ".[dev]"
|
||
|
|
|
||
|
|
- name: Run tests
|
||
|
|
run: |
|
||
|
|
pytest --cov --cov-report=xml
|
||
|
|
|
||
|
|
- name: Type check
|
||
|
|
run: mypy src
|
||
|
|
|
||
|
|
- name: Lint
|
||
|
|
run: |
|
||
|
|
black --check src tests
|
||
|
|
ruff check src tests
|
||
|
|
|
||
|
|
- name: Upload coverage
|
||
|
|
uses: codecov/codecov-action@v3
|
||
|
|
```
|
||
|
|
|
||
|
|
## Pre-commit Hooks
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
# .pre-commit-config.yaml
|
||
|
|
repos:
|
||
|
|
- repo: https://github.com/psf/black
|
||
|
|
rev: 23.11.0
|
||
|
|
hooks:
|
||
|
|
- id: black
|
||
|
|
|
||
|
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||
|
|
rev: v0.1.6
|
||
|
|
hooks:
|
||
|
|
- id: ruff
|
||
|
|
args: [--fix, --exit-non-zero-on-fix]
|
||
|
|
|
||
|
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||
|
|
rev: v1.7.1
|
||
|
|
hooks:
|
||
|
|
- id: mypy
|
||
|
|
additional_dependencies: [types-requests]
|
||
|
|
```
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Install pre-commit
|
||
|
|
pip install pre-commit
|
||
|
|
pre-commit install
|
||
|
|
|
||
|
|
# Run manually
|
||
|
|
pre-commit run --all-files
|
||
|
|
```
|