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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fec057cc40cd256c02ea2452e2016e0dc14ef37a5c0f38cf1b2f65fe502d5e2
|
|
| MD5 |
a798dff7415f362b3fcd6bc5d53003fc
|
|
| BLAKE2b-256 |
a56bbe6c35dbb5a22e988b8c2e4621b1fb9e4a4a1c319701d3fd038b295f6338
|
File details
Details for the file pydantic_builder-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pydantic_builder-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
327b3c6162af33bb5ea0da37557a837f525bac281ceaf442295c1550bfe99c90
|
|
| MD5 |
ec6937ea84c8b99e5354bf9d7be0efd9
|
|
| BLAKE2b-256 |
9a62d8874003f44641e6c63ff2ad9c778b552eb79bd4b42adfdb41d08129d5f2
|