Skip to content

Commit c871f96

Browse files
authored
Merge pull request fastapi#4 from watkinsm/dynamic-model-creation
feat: add dynamic model creation
2 parents 4d20051 + 0b2a796 commit c871f96

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

sqlmodel/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,4 @@
137137
from .main import SQLModel as SQLModel
138138
from .main import Field as Field
139139
from .main import Relationship as Relationship
140+
from .main import create_model as create_model

sqlmodel/main.py

+43
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,46 @@ def _calculate_keys(
644644
@declared_attr # type: ignore
645645
def __tablename__(cls) -> str:
646646
return cls.__name__.lower()
647+
648+
649+
def create_model(
650+
model_name: str,
651+
field_definitions: Dict[str, Tuple[Any, Any]],
652+
*,
653+
__module__: str = __name__,
654+
**kwargs,
655+
) -> Type[SQLModelMetaclass]:
656+
"""
657+
Dynamically create a model, similar to the Pydantic `create_model()` method
658+
659+
:param model_name: name of the created model
660+
:param field_definitions: data fields of the create model
661+
:param __module__: module of the created model
662+
:param **kwargs: Other keyword arguments to pass to the metaclass constructor, e.g. table=True
663+
"""
664+
fields = {}
665+
annotations = {}
666+
667+
for f_name, f_def in field_definitions.items():
668+
if f_name.startswith("_"):
669+
raise ValueError("Field names may not start with an underscore")
670+
try:
671+
if isinstance(f_def, tuple) and len(f_def) > 1:
672+
f_annotation, f_value = f_def
673+
elif isinstance(f_def, tuple):
674+
f_annotation, f_value = f_def[0], Field(nullable=False)
675+
else:
676+
f_annotation, f_value = f_def, Field(nullable=False)
677+
except ValueError as e:
678+
raise ConfigError(
679+
"field_definitions values must be either a tuple of (<type_annotation>, <default_value>)"
680+
"or just a type annotation [or a 1-tuple of (<type_annotation>,)]"
681+
) from e
682+
683+
if f_annotation:
684+
annotations[f_name] = f_annotation
685+
fields[f_name] = f_value
686+
687+
namespace = {"__annotations__": annotations, "__module__": __module__, **fields}
688+
689+
return type(model_name, (SQLModel,), namespace, **kwargs)

tests/test_create_model.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Optional
2+
3+
from sqlmodel import Field, Session, SQLModel, create_engine, create_model
4+
5+
6+
def test_create_model(clear_sqlmodel):
7+
"""
8+
Test dynamic model creation, query, and deletion
9+
"""
10+
11+
hero = create_model(
12+
"Hero",
13+
{
14+
"id": (Optional[int], Field(default=None, primary_key=True)),
15+
"name": str,
16+
"secret_name": (str,), # test 1-tuple
17+
"age": (Optional[int], None),
18+
},
19+
table=True,
20+
)
21+
22+
hero_1 = hero(**{"name": "Deadpond", "secret_name": "Dive Wilson"})
23+
24+
engine = create_engine("sqlite://")
25+
26+
SQLModel.metadata.create_all(engine)
27+
with Session(engine) as session:
28+
session.add(hero_1)
29+
session.commit()
30+
session.refresh(hero_1)
31+
32+
with Session(engine) as session:
33+
query_hero = session.query(hero).first()
34+
assert query_hero
35+
assert query_hero.id == hero_1.id
36+
assert query_hero.name == hero_1.name
37+
assert query_hero.secret_name == hero_1.secret_name
38+
assert query_hero.age == hero_1.age
39+
40+
with Session(engine) as session:
41+
session.delete(hero_1)
42+
session.commit()
43+
44+
with Session(engine) as session:
45+
query_hero = session.query(hero).first()
46+
assert not query_hero

0 commit comments

Comments
 (0)