"Two heads are better than one, but two objects are just right!"
CloneCat is a Python framework that helps you create perfect clones
of your objects along with all their relationships.
Think copy.deepcopy() but with superpowers.
Ever tried to clone a complex object only to find that half its relationships went on vacation? CloneCat keeps the family together! It's like a family reunion, but for your data structures.
CloneCat has a clear interface to determine which relations should be copied, which relations should be cloned, and which relations should be ignored.
Additionally, CloneCat has built in validation that verifies that all relations in a dataclass are specified. When any attribute is left out, its corresponding CloneCat class cannot be used.
CopyCat works great with:
- dataclasses
- SQLAlchemy
- Django models
- Pydantic
- More frameworks can be added easily. See documentation below.
- ๐ Relationship Preservation: Keeps all your object relationships intact
- ๐โโ๏ธ Blazing Fast: Optimized for performance (okay, we tried our best)
- ๐ง Smart Detection: Automatically handles circular references without breaking a sweat
- ๐ฏ Type Safe: Full type hints because we're not animals
- ๐ก๏ธ Battle Tested: Comprehensive test suite (translation: we broke it many times)
- ๐ญ Customizable: Supports custom cloning strategies for picky objects
Using uv (because you're cool like that):
uv add clonecatOr with pip (if you must):
pip install clonecatGiven the following dataclasses:
import dataclasses
@dataclasses.dataclass
class Person:
id: int
first_name: str
last_name: str
When a person must be cloned (don't try this at home),
use the following CloneCat class:
from clonecat import CloneCat
from clonecat.inspectors.dataclass import DataclassInspector
class ClonePerson(CloneCat):
inspector_class = DataclassInspector
class Meta:
model = Person
ignore = {"id"}
copy = {"first_name", "last_name"}Let's break down the snippet above into several chunks:
class ClonePerson(CloneCat): All copy classes must inherit fromCloneCat.inspector_class = DataclassInspector: This tellsCloneCathow to interspect the dataclass, revealing its attributes.class Meta: AMeta-class is required, with at least themodel-parameter set.ignore = {"id"}: Optionally: specify keys that should NOT be copied.copy = {"first_name", "last_name"}: Optionally specify keys that should be copied.
Given an instance of Person (yes, also Elon Musk is human), clone this using:
elon_musk = Person(first_name="Elon", last_name="Musk")
elon_musk_clone = ClonePerson.clone(elon_musk, CloneCatRegistry())Forgot to say, but CloneCatRegistry() can be used
to keep track which new instances are created out of the existing instances.
It's nothing more than a glorified dictionary.
After cloning, the following assertions hold:
assert elon_musk.first_name == elon_musk_clone.first_name
assert elon_musk.last_name == elon_musk_clone.last_name
# ID is not copied, and therefore different:
assert elon_musk.id != elon_musk_clone.idIgnoring keys and copying are two options, but there is another one: cloning. This implies that a new instance is created out of the old instance. The relation remains intact.
Let's say Elon Musk favorite food is Pineapple Pizza. This is encoded with the following dataclasses:
import dataclasses
@dataclasses.dataclass
class Food:
name: str
@dataclasses.dataclass
class Person:
id: int
first_name: str
last_name: strAnd the corresponding CloneCat models:
from clonecat import CloneCat
from clonecat.inspectors.dataclass import DataclassInspector
class CloneFood(CloneCat):
inspector_class = DataclassInspector
class Meta:
model = Food
copy = {"name"}
class ClonePerson(CloneCat):
inspector_class = DataclassInspector
class Meta:
model = Person
ignore = {"id"}
copy = {"first_name", "last_name"}
favorite_food: FoodNow, it's time to show how this can be cloned:
pineapple_pizza = Food(name="Pineapple pizza")
elon_musk = Person(first_name="Elon", last_name="Musk", favorite_food=pineapple_pizza)
elon_musk_clone = ClonePerson.clone(elon_musk, CloneCatRegistry())Trust, but verify:
assert elon_musk.first_name == elon_musk_clone.first_name
assert elon_musk.last_name == elon_musk_clone.last_name
# Food is a different instance
assert elon_musk.favorite_food is not elon_musk_clone.favorite_food
# But they both love Pineapple Pizza
assert elon_musk.favorite_food.name == elon_musk_clone.favorite_food.nameThis section is a work in progress.
Run the test suite:
uv run pytestWith coverage:
uv run pytest --cov=clonecat --cov-report=htmlWe love contributions! Whether it's:
- ๐ Bug reports
- ๐ก Feature requests
- ๐ Documentation improvements
- ๐งช Test cases
- ๐จ Code improvements
Check out our Contributing Guide to get started.
MIT License - see LICENSE file for details.
- Inspired by the frustrations of
copy.deepcopy() - Built with love, coffee, and questionable life choices
- Special thanks to all the objects that sacrificed themselves during testing