practica · temario
8.5tema 5 de 5

Setup del Proyecto con uv

Gestión moderna de dependencias en Python con uv: velocidad, reproducibilidad y convenciones DDD.

uv es un gestor de dependencias y entornos virtuales para Python escrito en Rust, diseñado para reemplazar pip, virtualenv, pip-tools y en muchos casos Poetry. Es entre 10x y 100x más rápido que pip para instalar dependencias gracias a su resolver paralelo y su caché agresiva. Más importante que la velocidad es que uv convierte la reproducibilidad en el comportamiento por defecto: el lockfile `uv.lock` garantiza que cualquier desarrollador del equipo, cualquier runner de CI y cualquier contenedor Docker instalará exactamente las mismas versiones.

El flujo de trabajo con uv se organiza alrededor de tres archivos: `pyproject.toml` (la declaración de dependencias directas y configuración de herramientas), `uv.lock` (el árbol completo de dependencias resueltas, commiteado al repositorio) y `.python-version` (la versión de Python que usa el proyecto). Estos tres archivos juntos eliminan el problema del 'funciona en mi máquina' porque el entorno es completamente determinista.

La configuración de herramientas de calidad de código vive también en `pyproject.toml` bajo sus respectivas secciones `[tool.ruff]`, `[tool.mypy]` y `[tool.pytest.ini_options]`. Centralizar la configuración en un solo archivo reduce la dispersión de archivos de configuración (`setup.cfg`, `.flake8`, `mypy.ini`, `pytest.ini`) que proliferaban en proyectos Python anteriores. Ruff es el linter y formatter moderno que reemplaza flake8, isort y black en un único binario.

El layout `src/` (también llamado 'src layout') es la convención correcta para proyectos Python con estructura DDD. Poner el código bajo `src/<project>/` en lugar de directamente en la raíz evita que Python importe el paquete desde el directorio de trabajo en lugar del paquete instalado. Esto es especialmente importante en proyectos DDD donde la estructura de módulos refleja la estructura de Bounded Contexts: un import accidental desde el directorio raíz puede enmascarar errores de dependencias.

El `Makefile` es la interfaz del desarrollador con el proyecto: `make test`, `make lint`, `make run`. Independientemente del gestor de dependencias o del framework, el Makefile provee un vocabulario consistente que funciona en cualquier máquina Unix. Para un equipo nuevo, `make test` es la primera orden que ejecutan: si funciona, el entorno está bien configurado. Los targets deben ser autodocumentados con comentarios `##` que aparecen al ejecutar `make help`.

uv integra nativamente el concepto de dependency groups para separar dependencias de producción (`fastapi`, `sqlalchemy`) de las de desarrollo (`pytest`, `ruff`, `mypy`). Esto es importante en DDD porque los tests y las herramientas de análisis estático son parte del proceso de diseño, no un accesorio. La separación entre `[project.dependencies]` y `[tool.uv.dev-dependencies]` hace que las imágenes Docker de producción no incluyan herramientas de test.

structure.txt
# Initialize a new project with uv
uv init <project>
cd <project>

# Add production dependencies
uv add fastapi sqlalchemy pydantic

# Add development dependencies (test, lint, type-check)
uv add --dev pytest pytest-cov ruff mypy

# Run commands inside the managed virtual environment
uv run pytest
uv run pytest --cov=src/<project> --cov-report=term-missing
uv run ruff check .
uv run ruff format .
uv run mypy src/

# Sync environment from lockfile (after git pull / fresh clone)
uv sync

# Add a specific version constraint
uv add 'sqlalchemy>=2.0,<3.0'

# Update all dependencies within their constraints
uv lock --upgrade

# Example pyproject.toml for a DDD project with src layout
[project]
name = "<project>"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.115",
    "sqlalchemy>=2.0",
    "pydantic>=2.0",
]

[tool.uv]
dev-dependencies = [
    "pytest>=8.0",
    "pytest-cov>=5.0",
    "ruff>=0.6",
    "mypy>=1.11",
]

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP"]
ignore = []

[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = false

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-v --tb=short"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/<project>"]
project/
<project>· Project Root
<project>· Source Package (src layout)
tests· Test Suite
unit· Unit Tests
integration· Integration Tests
e2e· End-to-End Tests

Debugging lab

Detecta y corrige el error o la violación de diseño.

0/5 tests passing0%
  1. 8.5.5.1

    # BAD: pyproject.toml missing src layout configuration for pytest # Tests fail with: ModuleNotFoundError: No module named '<project>' [project] name = "<project>" version = "0.1.0" requires-python = ">=3.12" dependencies = ["fastapi>=0.115", "sqlalchemy>=2.0"] [tool.pytest.ini_options] testpaths = ["tests"] # Missing pythonpath configuration — pytest can't find src/<project>

  2. 8.5.5.2

    # BAD: dev dependencies mixed with production dependencies [project] name = "<project>" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.115", "sqlalchemy>=2.0", "pydantic>=2.0", "pytest>=8.0", # <-- test tools in production deps! "ruff>=0.6", # <-- linter in production deps! "mypy>=1.11", # <-- type checker in production deps! ]

  3. 8.5.5.3

    # BAD: uv.lock is gitignored — installs are not reproducible # .gitignore __pycache__/ .venv/ *.pyc uv.lock # <-- THIS IS WRONG! Never gitignore the lockfile dist/ # Consequence: each developer/CI run resolves dependencies independently. # "works on my machine" problem returns. Minor version differences cause bugs.

  4. 8.5.5.4

    # BAD: mypy configured without strict mode — type errors silently ignored # pyproject.toml [tool.mypy] python_version = "3.12" # No strict mode — most type errors are not caught # ignore_missing_imports is also missing # As a result, the domain layer has many untyped functions: # def find_by_id(self, id): # id has no type annotation # return self._store.get(id) # return type is unknown

  5. 8.5.5.5

    # BAD: Makefile uses pip and activates venv manually — fragile # Makefile test: source .venv/bin/activate && pytest tests/ lint: source .venv/bin/activate && ruff check . setup: python -m venv .venv source .venv/bin/activate && pip install -r requirements.txt source .venv/bin/activate && pip install -r requirements-dev.txt # Problems: # - source doesn't work on all shells (Windows, fish) # - pip doesn't use uv.lock — installs are non-deterministic # - requires manual activation — error-prone