Dataclasses Integration
factorio was designed with dataclasses in mind. This is the most straightforward integration since dataclasses are simple Python objects with no external dependencies.
Basic Setup
Define Your Dataclasses
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class User:
name: str
email: str
age: int
created_at: datetime = field(default_factory=datetime.utcnow)
bio: Optional[str] = None
@dataclass
class Address:
street: str
city: str
country: str
zipcode: str
@dataclass
class UserWithAddress:
name: str
email: str
address: Address
Create Factories
from factorio import fields
from factorio.factories import Factory
class UserFactory(Factory[User]):
name = fields.TextField("name")
email = fields.TextField("email")
age = fields.IntegerField(min_value=18, max_value=65)
bio = fields.TextField("paragraph", nb_sentences=2)
class AddressFactory(Factory[Address]):
street = fields.TextField("street_address")
city = fields.TextField("city")
country = fields.TextField("country")
zipcode = fields.TextField("postcode")
class UserWithAddressFactory(Factory[UserWithAddress]):
name = fields.TextField("name")
email = fields.TextField("email")
address = fields.FactoryField(AddressFactory)
Usage Examples
Simple Object Creation
def test_basic_user():
user = UserFactory.build()
assert isinstance(user, User)
assert user.name
assert "@" in user.email
assert 18 <= user.age <= 65
assert isinstance(user.created_at, datetime)
Nested Objects
def test_user_with_address():
user = UserWithAddressFactory.build()
assert isinstance(user, UserWithAddress)
assert isinstance(user.address, Address)
assert user.address.city
assert user.address.country
Field Overrides
def test_custom_user():
user = UserFactory.build(
name="John Doe",
age=30,
email="john@example.com"
)
assert user.name == "John Doe"
assert user.age == 30
assert user.email == "john@example.com"
Advanced Patterns
Default Values
Dataclasses support default values, which work seamlessly with factorio:
@dataclass
class Product:
name: str
price: float
in_stock: bool = True
quantity: int = 0
tags: list[str] = field(default_factory=list)
class ProductFactory(Factory[Product]):
name = fields.TextField("word").capitalize()
price = fields.FloatField(min_value=0.01, max_value=999.99)
# Don't define in_stock, quantity, or tags - use defaults
def test_defaults_preserved():
product = ProductFactory.build()
assert product.in_stock is True # Default
assert product.quantity == 0 # Default
assert product.tags == [] # Default from factory
Note: If you want to override a field that has a default, just define it in the factory:
class CustomProductFactory(Factory[Product]):
name = fields.TextField("word")
price = fields.FloatField(min_value=1, max_value=100)
in_stock = fields.BooleanField(truth_probability=80) # Override default
quantity = fields.IntegerField(min_value=0, max_value=100) # Override default
Mutable Default Arguments
Be careful with mutable defaults in dataclasses:
@dataclass
class ShoppingCart:
items: list[str] = field(default_factory=list) # ✅ Correct
# items: list[str] = [] # ❌ Wrong - shared mutable default!
class ShoppingCartFactory(Factory[ShoppingCart]):
items = fields.ListField(fields.TextField("word"), length=3)
def test_separate_instances():
cart1 = ShoppingCartFactory.build()
cart2 = ShoppingCartFactory.build()
# Each cart has its own items list
assert cart1.items is not cart2.items
cart1.items.append("extra")
assert len(cart1.items) == 4
assert len(cart2.items) == 3
Post-Initialization Processing
Use __post_init__ for validation or computed fields:
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius < 0:
raise ValueError("Radius cannot be negative")
self.area = 3.14159 * self.radius ** 2
class CircleFactory(Factory[Circle]):
radius = fields.FloatField(min_value=0.1, max_value=100.0)
def test_circle_validation():
circle = CircleFactory.build()
assert circle.radius > 0
assert hasattr(circle, 'area')
assert circle.area > 0
def test_invalid_radius_rejected():
try:
circle = CircleFactory.build(radius=-5)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "negative" in str(e).lower()
Inheritance
Dataclasses support inheritance, and so do factories:
@dataclass
class Vehicle:
make: str
model: str
year: int
@dataclass
class Car(Vehicle):
num_doors: int
fuel_type: str
@dataclass
class ElectricCar(Car):
battery_capacity: float # kWh
charging_time: float # hours
class VehicleFactory(Factory[Vehicle]):
make = fields.ChoiceField(["Toyota", "Honda", "Ford"])
model = fields.TextField("word").capitalize()
year = fields.IntegerField(min_value=2000, max_value=2024)
class CarFactory(VehicleFactory):
num_doors = fields.ChoiceField([2, 4])
fuel_type = fields.ChoiceField(["gasoline", "diesel", "hybrid"])
class ElectricCarFactory(CarFactory):
battery_capacity = fields.FloatField(min_value=40.0, max_value=100.0)
charging_time = fields.FloatField(min_value=0.5, max_value=8.0)
fuel_type = fields.ConstantField("electric") # Override parent
def test_inheritance():
car = CarFactory.build()
assert isinstance(car, Car)
assert car.num_doors in [2, 4]
electric = ElectricCarFactory.build()
assert isinstance(electric, ElectricCar)
assert electric.fuel_type == "electric"
assert 40.0 <= electric.battery_capacity <= 100.0
Testing Patterns
Comparison and Equality
@dataclass
class Point:
x: float
y: float
class PointFactory(Factory[Point]):
x = fields.FloatField(min_value=-100, max_value=100)
y = fields.FloatField(min_value=-100, max_value=100)
def test_point_equality():
p1 = PointFactory.build(x=10.0, y=20.0)
p2 = PointFactory.build(x=10.0, y=20.0)
p3 = PointFactory.build(x=15.0, y=20.0)
assert p1 == p2 # Same values
assert p1 != p3 # Different x
Sorting
@dataclass(order=True)
class Student:
grade: int
name: str
class StudentFactory(Factory[Student]):
grade = fields.IntegerField(min_value=1, max_value=12)
name = fields.TextField("name")
def test_student_sorting():
students = [StudentFactory.build() for _ in range(10)]
# Sort by grade (primary), then name (secondary)
sorted_students = sorted(students)
assert sorted_students[0].grade <= sorted_students[-1].grade
Conversion to Dict/Tuple
from dataclasses import asdict, astuple
def test_conversion():
user = UserFactory.build()
# Convert to dict
user_dict = asdict(user)
assert isinstance(user_dict, dict)
assert user_dict["name"] == user.name
# Convert to tuple
user_tuple = astuple(user)
assert isinstance(user_tuple, tuple)
assert user_tuple[0] == user.name # First field
Complete Example
from dataclasses import dataclass, field
from datetime import datetime, date
from typing import Optional
from factorio import fields
from factorio.factories import Factory
# Domain models
@dataclass
class Book:
title: str
author: str
isbn: str
published_date: date
pages: int
genre: str
description: Optional[str] = None
@dataclass
class LibraryMember:
name: str
email: str
member_since: date
borrowed_books: list[str] = field(default_factory=list)
max_books: int = 5
@dataclass
class Loan:
book_isbn: str
member_email: str
loan_date: date
due_date: date
returned: bool = False
# Factories
class BookFactory(Factory[Book]):
title = fields.TextField("sentence", nb_words=4)
author = fields.TextField("name")
isbn = fields.TextField("isbn13")
published_date = fields.DateField(
min_date=date(1900, 1, 1),
max_date=date(2024, 12, 31)
)
pages = fields.IntegerField(min_value=50, max_value=1000)
genre = fields.ChoiceField([
"fiction", "non-fiction", "mystery", "sci-fi", "romance"
])
description = fields.TextField("paragraph", nb_sentences=2)
class LibraryMemberFactory(Factory[LibraryMember]):
name = fields.TextField("name")
email = fields.TextField("safe_email")
member_since = fields.DateField(
min_date=date(2020, 1, 1),
max_date=date(2024, 12, 31)
)
borrowed_books = fields.ListField(
fields.TextField("isbn13"),
length=2,
variation=1
)
max_books = fields.IntegerField(min_value=3, max_value=10)
class LoanFactory(Factory[Loan]):
book_isbn = fields.TextField("isbn13")
member_email = fields.TextField("safe_email")
loan_date = fields.DateField()
due_date = fields.DateField() # Should be after loan_date
returned = fields.BooleanField(truth_probability=60)
# Tests
def test_book_creation():
book = BookFactory.build()
assert book.title
assert book.author
assert len(book.isbn) == 13 # ISBN-13 format
assert 50 <= book.pages <= 1000
assert book.genre in ["fiction", "non-fiction", "mystery", "sci-fi", "romance"]
def test_member_with_books():
member = LibraryMemberFactory.build()
assert member.name
assert "@" in member.email
assert 1 <= len(member.borrowed_books) <= 3
assert all(isinstance(isbn, str) for isbn in member.borrowed_books)
def test_loan_workflow():
loan = LoanFactory.build()
assert loan.book_isbn
assert "@" in loan.member_email
assert loan.due_date >= loan.loan_date # Due date after loan date
assert isinstance(loan.returned, bool)
def test_bulk_operations():
books = [BookFactory.build() for _ in range(20)]
# All books are valid
assert all(book.title for book in books)
assert all(book.pages > 0 for book in books)
# Unique titles (likely, but not guaranteed)
titles = [book.title for book in books]
unique_titles = set(titles)
# With 20 books, we probably have some duplicates
assert len(unique_titles) >= 10 # At least half are unique
Best Practices
Use Type Hints
# ✅ Good - clear types
@dataclass
class User:
name: str
age: int
email: str
# ❌ Avoid - missing type hints
@dataclass
class BadUser:
name: Any # Too vague
age = None # No type annotation
Validate in __post_init__
@dataclass
class Temperature:
celsius: float
def __post_init__(self):
if self.celsius < -273.15:
raise ValueError("Below absolute zero!")
class TemperatureFactory(Factory[Temperature]):
celsius = fields.FloatField(min_value=-100, max_value=100)
# Factory respects validation
temp = TemperatureFactory.build()
assert temp.celsius >= -273.15
Keep Factories Simple
# ✅ Simple factory
class UserFactory(Factory[User]):
name = fields.TextField("name")
email = fields.TextField("email")
# ❌ Overly complex
class ComplicatedUserFactory(Factory[User]):
name = fields.StringField(
min_chars=5,
max_chars=20,
prefix="USR-"
)
email = fields.StringField(
min_chars=10,
max_chars=30,
suffix="@test.local"
)
# Just use TextField for realistic data!
Document Field Choices
class ProductFactory(Factory[Product]):
# Price in cents to avoid floating point issues
price_cents = fields.IntegerField(min_value=100, max_value=99999)
# Categories match our database enum
category = fields.ChoiceField([
"electronics",
"books",
"clothing",
"home_garden"
])
Common Pitfalls
⚠️ Frozen Dataclasses
@dataclass(frozen=True)
class ImmutablePoint:
x: float
y: float
class PointFactory(Factory[ImmutablePoint]):
x = fields.FloatField()
y = fields.FloatField()
def test_frozen():
point = PointFactory.build()
# Can't modify frozen dataclass
try:
point.x = 10.0
assert False, "Should raise FrozenInstanceError"
except Exception:
pass # Expected
⚠️ Missing Required Fields
@dataclass
class Person:
name: str # Required
age: int # Required
# ❌ Missing required field in factory
class BadPersonFactory(Factory[Person]):
name = fields.TextField("name")
# Forgot age!
# ✅ Include all required fields
class GoodPersonFactory(Factory[Person]):
name = fields.TextField("name")
age = fields.IntegerField(min_value=0, max_value=120)
⚠️ Shared Mutable State
@dataclass
class Team:
members: list[str] = field(default_factory=list) # ✅ Correct
# ❌ Wrong - don't do this in dataclasses
# members: list[str] = []
def test_no_shared_state():
team1 = TeamFactory.build()
team2 = TeamFactory.build()
team1.members.append("Alice")
assert "Alice" not in team2.members # Separate lists