Skip to main content

A micro framework implementing a convenient type parameterized builder pattern for pydantic models

Project description

🏗️ Pydantic Builder

📋 Overview

Pydantic Builder is a micro-framework that simplifies the creation of test fixtures and object construction for Pydantic models. It implements a type-safe builder pattern that allows you to construct complex Pydantic model instances with a clean, fluent API.

Features

  • 🔒 Type-safe builder pattern with full generic type support
  • 🔗 Fluent API with method chaining for readable object construction
  • 🎯 Focus on what matters - only specify the fields you care about, use defaults for the rest
  • 🧪 Perfect for testing - reduces boilerplate in test fixtures
  • 💡 IDE-friendly - full autocompletion support for all model fields
  • 🎨 Clean test code - keeps tests focused on what's being tested, not setup

📥 Installation

💡 The package is available on PyPI:

pip install pydantic_builder

Requirements: Python >= 3.10

🚀 Usage

🔰 Basic Usage

Create a builder for any Pydantic model by extending AbstractBaseBuilder:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

# Your Pydantic model
class Point(BaseModel):
    x: int
    y: int

# Create a builder for it
class PointBuilder(AbstractBaseBuilder[Point]):
    @property
    def default_instance(self) -> Point:
        return Point(x=0, y=0)

# Use the builder
point = PointBuilder().build()
# Point(x=0, y=0)

# Override specific fields
point = PointBuilder().with_(x=10).build()
# Point(x=10, y=0)

# Chain multiple field updates
point = PointBuilder().with_(x=5).with_(y=3).build()
# Point(x=5, y=3)

🌟 Intermediate Usage - Simplifying Tests

The builder pattern really shines in tests, where you want to focus on specific scenarios without cluttering your test with irrelevant setup:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

class Range(BaseModel):
    min: float | None = None
    max: float | None = None
    
    def size(self) -> float:
        if self.min is None or self.max is None:
            return float("inf")
        return self.max - self.min

class RangeBuilder(AbstractBaseBuilder[Range]):
    @property
    def default_instance(self) -> Range:
        return Range(min=0, max=5)

# In your tests - only specify what's relevant to the test
def test_size_with_no_upper_bound():
    # Clear and focused - only the max=None matters for this test
    range_ = RangeBuilder().with_(max=None).build()
    assert range_.size() == float("inf")

def test_size_with_specific_bounds():
    # Chain multiple attributes cleanly
    range_ = RangeBuilder().with_(min=1).with_(max=10).build()
    assert range_.size() == 9

🔥 Advanced Usage - Composing Builders

For complex models with nested structures, builders can be composed together:

from pydantic import BaseModel
from pydantic_builder import AbstractBaseBuilder

class Ingredient(BaseModel):
    id: int
    name: str
    quantity: float
    unit: str
    optional: bool = False

class Recipe(BaseModel):
    title: str
    serving_size: int
    preparation_time: float
    cooking_time: float
    ingredients: list[Ingredient]
    steps: list[str] = []

    def scale_recipe(self, new_serving_size: int) -> Self:
        """Scale recipe ingredients based on new serving size."""
        if self.serving_size is None or self.serving_size == 0:
            raise ValueError("Cannot scale recipe without original serving size")

        scale_factor = new_serving_size / self.serving_size

        scaled_ingredients = []
        for ingredient in self.ingredients:
            scaled_ingredient = Ingredient(
                id=ingredient.id,
                name=ingredient.name,
                quantity=ingredient.quantity * scale_factor,
                unit=ingredient.unit,
                optional=ingredient.optional,
            )
            scaled_ingredients.append(scaled_ingredient)

        return Recipe(
            **self.model_dump(exclude={"serving_size", "ingredients"}),
            ingredients=scaled_ingredients,
            serving_size=new_serving_size,
        )

class IngredientBuilder(AbstractBaseBuilder[Ingredient]):
    @property
    def default_instance(self) -> Ingredient:
        return Ingredient(id=1, name="flour", quantity=100, unit="g")

class RecipeBuilder(AbstractBaseBuilder[Recipe]):
    @property
    def default_instance(self) -> Recipe:
        return Recipe(
            title="Default Recipe",
            serving_size=4,
            preparation_time=30,
            cooking_time=60,
            ingredients=[
                Ingredient(id=1, name="flour", quantity=100, unit="g"),
                Ingredient(id=2, name="sugar", quantity=50, unit="g"),
            ],
            steps=["Mix ingredients", "Bake for 30 minutes"],
        )

# Compose builders together for complex test scenarios
def test_recipe_with_custom_ingredients():
    recipe = (
        RecipeBuilder()
        .with_(
            title="Custom Cake",
            ingredients=[
                IngredientBuilder().with_(name="flour", quantity=200).build(),
                IngredientBuilder().with_(name="sugar", quantity=150).build(),
                IngredientBuilder().with_(name="eggs", quantity=3, unit="units").build(),
            ]
        )
        .build()
    )
    
    assert recipe.title == "Custom Cake"
    assert len(recipe.ingredients) == 3
    assert recipe.ingredients[0].quantity == 200


def test_recipe_scaling():

    # Arrange
    original_serving_size = 2 # 1st important value
    original_ingredient_quantity = 100 # 2nd important value
    new_serving_size = original_serving_size * 2 # 3rd important value

    recipe = RecipeBuilder()
    .with_(
        serving_size=original_serving_size # 1st important value
    )
    .with_(
        ingredients=[
            IngredientBuilder()
            .with_(
                quantity=original_ingredient_quantity # 2nd important value
            )
            .build()
            ]
    )
    .build()

    # Act
    scaled_recipe = recipe.scale_recipe(new_serving_size) # 3rd important value

    # Assert
    assert scaled_recipe.serving_size == new_serving_size
    assert scaled_recipe.ingredients[0].quantity == original_ingredient_quantity * 2
    

🎯 Why Use Builders?

Without builders:

def test_ingredient_conversion():
    # Have to specify every field, even irrelevant ones
    ingredient = Ingredient(
        id=1, # Don't care about id for this test
        name="flour",  # Don't care about name for this test
        quantity=1,
        unit="kg",
        optional=False  # Don't care about optional for this test
    )
    assert ingredient.convert_to_grams().quantity == 1000

With builders:

def test_ingredient_conversion():
    # Only specify what matters for this test
    ingredient = IngredientBuilder().with_(quantity=1, unit="kg").build()
    assert ingredient.convert_to_grams().quantity == 1000

🔍 Going Further

  • 🧪 See the test files for comprehensive usage examples
  • 💬 Open an issue for questions or feature requests

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_builder-0.1.0.tar.gz (6.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_builder-0.1.0-py3-none-any.whl (7.2 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_builder-0.1.0.tar.gz.

File metadata

  • Download URL: pydantic_builder-0.1.0.tar.gz
  • Upload date:
  • Size: 6.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.15

File hashes

Hashes for pydantic_builder-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7fec057cc40cd256c02ea2452e2016e0dc14ef37a5c0f38cf1b2f65fe502d5e2
MD5 a798dff7415f362b3fcd6bc5d53003fc
BLAKE2b-256 a56bbe6c35dbb5a22e988b8c2e4621b1fb9e4a4a1c319701d3fd038b295f6338

See more details on using hashes here.

File details

Details for the file pydantic_builder-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_builder-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 327b3c6162af33bb5ea0da37557a837f525bac281ceaf442295c1550bfe99c90
MD5 ec6937ea84c8b99e5354bf9d7be0efd9
BLAKE2b-256 9a62d8874003f44641e6c63ff2ad9c778b552eb79bd4b42adfdb41d08129d5f2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page