From 867fc691f70f97b425470bf0f92e3294ea8d204d Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 11 Aug 2020 17:34:19 +0200 Subject: [PATCH] refactor fields into a package --- .coverage | Bin 53248 -> 53248 bytes orm/__init__.py | 3 +- orm/fields.py | 275 --------------------------------- orm/fields/__init__.py | 31 ++++ orm/fields/base.py | 107 +++++++++++++ orm/fields/foreign_key.py | 91 +++++++++++ orm/fields/model_fields.py | 86 +++++++++++ orm/models.py | 3 +- orm/queryset.py | 6 +- orm/relations.py | 2 +- tests/test_fastapi_usage.py | 3 +- tests/test_foreign_keys.py | 7 +- tests/test_same_table_joins.py | 11 +- 13 files changed, 335 insertions(+), 290 deletions(-) delete mode 100644 orm/fields.py create mode 100644 orm/fields/__init__.py create mode 100644 orm/fields/base.py create mode 100644 orm/fields/foreign_key.py create mode 100644 orm/fields/model_fields.py diff --git a/.coverage b/.coverage index 0c8e0091151cb63f7feb13a3d4d9a61ed1db424a..26e4f9cb550ed84f7390d87f992de099209690d8 100644 GIT binary patch delta 1173 zcmZ8hZA@EL7`}Jswx#X8=e>8pP-Y1ltvV5u@-S{^2!v=p%-`MRZ&gMG?cD0etrojsjCdQ#=vej7%NJ7;aMi=2QMB?sJgiRH;F zjt9w%Lr*T-dCrWORuXcQk{|5?Ya;y)i||gb)8qF#eYB0$c*$YaRTm(pOEZFd)bx}AF>RV$n3vDCd!5=0dTj>EAI))F5|Gve}C`T#J+(c*|bQ3dA9o=7TYF{4vx@r4sTz4bc1~ zI+X)yJJ6KWGmmpd8|u<1i{f#g&R^jETnZ{so8~xjz>$x}us=je*#YT)>FA$9POZA| zfaSk{BF*wdxmCh#3}j#fR$vZ(f+&0r7vXi#;S^rOzumlU7a3Qv(e0aQ(tQHc{S$UYs$p#ZD)9a zV?)MLVWn1CIX=;@=V=+{O~q!qR+xh~|8Y+w?{2A++4*erY1VGivBsf?Oov95%t-%s zAaP^t{JG|VRCZuS30+U8w$kajq>|X3+Klb&+UED}4F4*tm(zibzxx){#By>vn2HBN zdrQmH+1E0wZ{h|cJ;oC<1_w?}ul}H9>VvA+Ul*HDUOmf9j}7<+lHK8@nOJi2Msnr% zXyQa`AR+Jgc9Pl6tW-ctA1zqCd166Lr9KS2v}GtXQ0fW8vA!9Xy6BCqFYl;C5cQON zME~tlcU^ea6&S|^#wys4-RAH8G(9Z5tu9Xn)oL4e>JXz3;tw^JT7jz*P@; z&S(@Yg$^M-Dn+b!gE|ATx_(E6e)oQTBPs&rn}7-a5NvYF?kO%PCnSj}(8 zE5}PD9Oif(t@?|!*u*1R38>z+{ur;O6@ap=6(N3uM2^4mHM9&+r|@e-8Ll#LGz+Nq z)q)!sG+Bpjri;4%VJ(9@4ca2Dl!OY#e8JSyWy8meej;e4kOLV}B^2RDIDtMwCwd5v z!eZ_-_n5O1K?8^1pKhk14s_z}9h>otQHjs-QkjkH<(k3Zf3TQV?E$^(U~E;r5m?qi zh*wJ#SU6Kd%d5dDg3F1qik4M@_SHi5pD~NiovoBG|bMesEk2H(i~&8 zGqz@@60TVLr1xQKCO<9sW37=X6)0UTfQixijF-vOs zOJe-VRN|QGqf$-r!+m1?l diff --git a/orm/__init__.py b/orm/__init__.py index 39adee8..773ab78 100644 --- a/orm/__init__.py +++ b/orm/__init__.py @@ -6,13 +6,13 @@ from orm.fields import ( DateTime, Decimal, Float, - ForeignKey, Integer, JSON, String, Text, Time, ) +from orm.fields.foreign_key import ForeignKey from orm.models import Model __version__ = "0.0.1" @@ -28,7 +28,6 @@ __all__ = [ "Date", "Decimal", "Float", - "ForeignKey", "Model", "ModelDefinitionError", "ModelNotSet", diff --git a/orm/fields.py b/orm/fields.py deleted file mode 100644 index 980e334..0000000 --- a/orm/fields.py +++ /dev/null @@ -1,275 +0,0 @@ -import datetime -import decimal -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, Union - -import orm -from orm.exceptions import ModelDefinitionError, RelationshipInstanceError - -from pydantic import BaseModel, Json - -import sqlalchemy - -if TYPE_CHECKING: # pragma no cover - from orm.models import Model - - -class RequiredParams: - def __init__(self, *args: str) -> None: - self._required = list(args) - - def __call__(self, model_field_class: Type["BaseField"]) -> Type["BaseField"]: - old_init = model_field_class.__init__ - model_field_class._old_init = old_init - - def __init__(instance: "BaseField", *args: Any, **kwargs: Any) -> None: - super(instance.__class__, instance).__init__(*args, **kwargs) - for arg in self._required: - if arg not in kwargs: - raise ModelDefinitionError( - f"{instance.__class__.__name__} field requires parameter: {arg}" - ) - setattr(instance, arg, kwargs.pop(arg)) - - model_field_class.__init__ = __init__ - return model_field_class - - -class BaseField: - __type__ = None - - def __init__(self, *args: Any, **kwargs: Any) -> None: - name = kwargs.pop("name", None) - args = list(args) - if args: - if isinstance(args[0], str): - if name is not None: - raise ModelDefinitionError( - "Column name cannot be passed positionally and as a keyword." - ) - name = args.pop(0) - - self.name = name - self._populate_from_kwargs(kwargs) - - def _populate_from_kwargs(self, kwargs: Dict) -> None: - self.primary_key = kwargs.pop("primary_key", False) - self.autoincrement = kwargs.pop( - "autoincrement", self.primary_key and self.__type__ == int - ) - - self.nullable = kwargs.pop("nullable", not self.primary_key) - self.default = kwargs.pop("default", None) - self.server_default = kwargs.pop("server_default", None) - - self.index = kwargs.pop("index", None) - self.unique = kwargs.pop("unique", None) - - self.pydantic_only = kwargs.pop("pydantic_only", False) - if self.pydantic_only and self.primary_key: - raise ModelDefinitionError("Primary key column cannot be pydantic only.") - - @property - def is_required(self) -> bool: - return ( - not self.nullable and not self.has_default and not self.is_auto_primary_key - ) - - @property - def default_value(self) -> Any: - default = self.default if self.default is not None else self.server_default - return default() if callable(default) else default - - @property - def has_default(self) -> bool: - return self.default is not None or self.server_default is not None - - @property - def is_auto_primary_key(self) -> bool: - if self.primary_key: - return self.autoincrement - return False - - def get_column(self, name: str = None) -> sqlalchemy.Column: - self.name = self.name or name - constraints = self.get_constraints() - return sqlalchemy.Column( - self.name, - self.get_column_type(), - *constraints, - primary_key=self.primary_key, - autoincrement=self.autoincrement, - nullable=self.nullable, - index=self.index, - unique=self.unique, - default=self.default, - server_default=self.server_default, - ) - - def get_column_type(self) -> sqlalchemy.types.TypeEngine: - raise NotImplementedError() # pragma: no cover - - def get_constraints(self) -> Optional[List]: - return [] - - def expand_relationship(self, value: Any, child: "Model") -> Any: - return value - - -@RequiredParams("length") -class String(BaseField): - __type__ = str - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.String(self.length) - - -class Integer(BaseField): - __type__ = int - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Integer() - - -class Text(BaseField): - __type__ = str - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Text() - - -class Float(BaseField): - __type__ = float - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Float() - - -class Boolean(BaseField): - __type__ = bool - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Boolean() - - -class DateTime(BaseField): - __type__ = datetime.datetime - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.DateTime() - - -class Date(BaseField): - __type__ = datetime.date - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Date() - - -class Time(BaseField): - __type__ = datetime.time - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.Time() - - -class JSON(BaseField): - __type__ = Json - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.JSON() - - -class BigInteger(BaseField): - __type__ = int - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.BigInteger() - - -@RequiredParams("length", "precision") -class Decimal(BaseField): - __type__ = decimal.Decimal - - def get_column_type(self) -> sqlalchemy.Column: - return sqlalchemy.DECIMAL(self.length, self.precision) - - -def create_dummy_instance(fk: Type["Model"], pk: int = None) -> "Model": - init_dict = {fk.__pkname__: pk or -1} - init_dict = { - **init_dict, - **{ - k: create_dummy_instance(v.to) - for k, v in fk.__model_fields__.items() - if isinstance(v, ForeignKey) and not v.nullable and not v.virtual - }, - } - return fk(**init_dict) - - -class ForeignKey(BaseField): - def __init__( - self, - to: Type["Model"], - name: str = None, - related_name: str = None, - nullable: bool = True, - virtual: bool = False, - ) -> None: - super().__init__(nullable=nullable, name=name) - self.virtual = virtual - self.related_name = related_name - self.to = to - - @property - def __type__(self) -> Type[BaseModel]: - return self.to.__pydantic_model__ - - def get_constraints(self) -> List[sqlalchemy.schema.ForeignKey]: - fk_string = self.to.__tablename__ + "." + self.to.__pkname__ - return [sqlalchemy.schema.ForeignKey(fk_string)] - - def get_column_type(self) -> sqlalchemy.Column: - to_column = self.to.__model_fields__[self.to.__pkname__] - return to_column.get_column_type() - - def expand_relationship( - self, value: Any, child: "Model" - ) -> Union["Model", List["Model"]]: - - if isinstance(value, orm.models.Model) and not isinstance(value, self.to): - raise RelationshipInstanceError( - f"Relationship error - expecting: {self.to.__name__}, " - f"but {value.__class__.__name__} encountered." - ) - - if isinstance(value, list) and not isinstance(value, self.to): - model = [self.expand_relationship(val, child) for val in value] - return model - - if isinstance(value, self.to): - model = value - elif isinstance(value, dict): - model = self.to(**value) - else: - if not isinstance(value, self.to.pk_type()): - raise RelationshipInstanceError( - f"Relationship error - ForeignKey {self.to.__name__} " - f"is of type {self.to.pk_type()} " - f"of type {self.__type__} " - f"while {type(value)} passed as a parameter." - ) - model = create_dummy_instance(fk=self.to, pk=value) - - self.add_to_relationship_registry(model, child) - - return model - - def add_to_relationship_registry(self, model: "Model", child: "Model") -> None: - model._orm_relationship_manager.add_relation( - model.__class__.__name__.lower(), - child.__class__.__name__.lower(), - model, - child, - virtual=self.virtual, - ) diff --git a/orm/fields/__init__.py b/orm/fields/__init__.py new file mode 100644 index 0000000..75a1b7d --- /dev/null +++ b/orm/fields/__init__.py @@ -0,0 +1,31 @@ +from orm.fields.model_fields import ( + BigInteger, + Boolean, + Date, + DateTime, + Decimal, + String, + Integer, + Text, + Float, + Time, + JSON, +) +from orm.fields.foreign_key import ForeignKey +from orm.fields.base import BaseField + +__all__ = [ + "Decimal", + "BigInteger", + "Boolean", + "Date", + "DateTime", + "String", + "JSON", + "Integer", + "Text", + "Float", + "Time", + "ForeignKey", + "BaseField", +] diff --git a/orm/fields/base.py b/orm/fields/base.py new file mode 100644 index 0000000..32c0f13 --- /dev/null +++ b/orm/fields/base.py @@ -0,0 +1,107 @@ +from typing import Type, Any, Dict, Optional, List + +import sqlalchemy + +from orm import ModelDefinitionError + + +class RequiredParams: + def __init__(self, *args: str) -> None: + self._required = list(args) + + def __call__(self, model_field_class: Type["BaseField"]) -> Type["BaseField"]: + old_init = model_field_class.__init__ + model_field_class._old_init = old_init + + def __init__(instance: "BaseField", *args: Any, **kwargs: Any) -> None: + super(instance.__class__, instance).__init__(*args, **kwargs) + for arg in self._required: + if arg not in kwargs: + raise ModelDefinitionError( + f"{instance.__class__.__name__} field requires parameter: {arg}" + ) + setattr(instance, arg, kwargs.pop(arg)) + + model_field_class.__init__ = __init__ + return model_field_class + + +class BaseField: + __type__ = None + + def __init__(self, *args: Any, **kwargs: Any) -> None: + name = kwargs.pop("name", None) + args = list(args) + if args: + if isinstance(args[0], str): + if name is not None: + raise ModelDefinitionError( + "Column name cannot be passed positionally and as a keyword." + ) + name = args.pop(0) + + self.name = name + self._populate_from_kwargs(kwargs) + + def _populate_from_kwargs(self, kwargs: Dict) -> None: + self.primary_key = kwargs.pop("primary_key", False) + self.autoincrement = kwargs.pop( + "autoincrement", self.primary_key and self.__type__ == int + ) + + self.nullable = kwargs.pop("nullable", not self.primary_key) + self.default = kwargs.pop("default", None) + self.server_default = kwargs.pop("server_default", None) + + self.index = kwargs.pop("index", None) + self.unique = kwargs.pop("unique", None) + + self.pydantic_only = kwargs.pop("pydantic_only", False) + if self.pydantic_only and self.primary_key: + raise ModelDefinitionError("Primary key column cannot be pydantic only.") + + @property + def is_required(self) -> bool: + return ( + not self.nullable and not self.has_default and not self.is_auto_primary_key + ) + + @property + def default_value(self) -> Any: + default = self.default if self.default is not None else self.server_default + return default() if callable(default) else default + + @property + def has_default(self) -> bool: + return self.default is not None or self.server_default is not None + + @property + def is_auto_primary_key(self) -> bool: + if self.primary_key: + return self.autoincrement + return False + + def get_column(self, name: str = None) -> sqlalchemy.Column: + self.name = self.name or name + constraints = self.get_constraints() + return sqlalchemy.Column( + self.name, + self.get_column_type(), + *constraints, + primary_key=self.primary_key, + autoincrement=self.autoincrement, + nullable=self.nullable, + index=self.index, + unique=self.unique, + default=self.default, + server_default=self.server_default, + ) + + def get_column_type(self) -> sqlalchemy.types.TypeEngine: + raise NotImplementedError() # pragma: no cover + + def get_constraints(self) -> Optional[List]: + return [] + + def expand_relationship(self, value: Any, child: "Model") -> Any: + return value diff --git a/orm/fields/foreign_key.py b/orm/fields/foreign_key.py new file mode 100644 index 0000000..1496fc9 --- /dev/null +++ b/orm/fields/foreign_key.py @@ -0,0 +1,91 @@ +from typing import Type, List, Any, Union, TYPE_CHECKING + +import sqlalchemy +from pydantic import BaseModel + +import orm +from orm.exceptions import RelationshipInstanceError +from orm.fields.base import BaseField + +if TYPE_CHECKING: # pragma no cover + from orm.models import Model + + +def create_dummy_instance(fk: Type["Model"], pk: int = None) -> "Model": + init_dict = {fk.__pkname__: pk or -1} + init_dict = { + **init_dict, + **{ + k: create_dummy_instance(v.to) + for k, v in fk.__model_fields__.items() + if isinstance(v, ForeignKey) and not v.nullable and not v.virtual + }, + } + return fk(**init_dict) + + +class ForeignKey(BaseField): + def __init__( + self, + to: Type["Model"], + name: str = None, + related_name: str = None, + nullable: bool = True, + virtual: bool = False, + ) -> None: + super().__init__(nullable=nullable, name=name) + self.virtual = virtual + self.related_name = related_name + self.to = to + + @property + def __type__(self) -> Type[BaseModel]: + return self.to.__pydantic_model__ + + def get_constraints(self) -> List[sqlalchemy.schema.ForeignKey]: + fk_string = self.to.__tablename__ + "." + self.to.__pkname__ + return [sqlalchemy.schema.ForeignKey(fk_string)] + + def get_column_type(self) -> sqlalchemy.Column: + to_column = self.to.__model_fields__[self.to.__pkname__] + return to_column.get_column_type() + + def expand_relationship( + self, value: Any, child: "Model" + ) -> Union["Model", List["Model"]]: + + if isinstance(value, orm.models.Model) and not isinstance(value, self.to): + raise RelationshipInstanceError( + f"Relationship error - expecting: {self.to.__name__}, " + f"but {value.__class__.__name__} encountered." + ) + + if isinstance(value, list) and not isinstance(value, self.to): + model = [self.expand_relationship(val, child) for val in value] + return model + + if isinstance(value, self.to): + model = value + elif isinstance(value, dict): + model = self.to(**value) + else: + if not isinstance(value, self.to.pk_type()): + raise RelationshipInstanceError( + f"Relationship error - ForeignKey {self.to.__name__} " + f"is of type {self.to.pk_type()} " + f"while {type(value)} passed as a parameter." + ) + model = create_dummy_instance(fk=self.to, pk=value) + + self.add_to_relationship_registry(model, child) + + return model + + def add_to_relationship_registry(self, model: "Model", child: "Model") -> None: + model._orm_relationship_manager.add_relation( + model.__class__.__name__.lower(), + child.__class__.__name__.lower(), + model, + child, + virtual=self.virtual, + ) diff --git a/orm/fields/model_fields.py b/orm/fields/model_fields.py new file mode 100644 index 0000000..3ddae06 --- /dev/null +++ b/orm/fields/model_fields.py @@ -0,0 +1,86 @@ +import datetime +import decimal + +import sqlalchemy +from pydantic import Json + +from orm.fields.base import BaseField, RequiredParams + + +@RequiredParams("length") +class String(BaseField): + __type__ = str + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.String(self.length) + + +class Integer(BaseField): + __type__ = int + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Integer() + + +class Text(BaseField): + __type__ = str + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Text() + + +class Float(BaseField): + __type__ = float + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Float() + + +class Boolean(BaseField): + __type__ = bool + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Boolean() + + +class DateTime(BaseField): + __type__ = datetime.datetime + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.DateTime() + + +class Date(BaseField): + __type__ = datetime.date + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Date() + + +class Time(BaseField): + __type__ = datetime.time + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.Time() + + +class JSON(BaseField): + __type__ = Json + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.JSON() + + +class BigInteger(BaseField): + __type__ = int + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.BigInteger() + + +@RequiredParams("length", "precision") +class Decimal(BaseField): + __type__ = decimal.Decimal + + def get_column_type(self) -> sqlalchemy.Column: + return sqlalchemy.DECIMAL(self.length, self.precision) diff --git a/orm/models.py b/orm/models.py index 94047d2..9fde6e7 100644 --- a/orm/models.py +++ b/orm/models.py @@ -9,7 +9,8 @@ import databases import orm.queryset as qry from orm.exceptions import ModelDefinitionError -from orm.fields import BaseField, ForeignKey +from orm import ForeignKey +from orm.fields.base import BaseField from orm.relations import RelationshipManager import pydantic diff --git a/orm/queryset.py b/orm/queryset.py index ae3cac2..ea6c264 100644 --- a/orm/queryset.py +++ b/orm/queryset.py @@ -13,9 +13,10 @@ from typing import ( import databases import orm +import orm.fields.foreign_key from orm import ForeignKey from orm.exceptions import MultipleMatches, NoMatch, QueryDefinitionError -from orm.fields import BaseField +from orm.fields.base import BaseField import sqlalchemy from sqlalchemy import text @@ -79,7 +80,8 @@ class Query: if ( not self.model_cls.__model_fields__[key].nullable and isinstance( - self.model_cls.__model_fields__[key], orm.fields.ForeignKey + self.model_cls.__model_fields__[key], + orm.fields.foreign_key.ForeignKey, ) and key not in self._select_related ): diff --git a/orm/relations.py b/orm/relations.py index f541dfe..08f49fc 100644 --- a/orm/relations.py +++ b/orm/relations.py @@ -5,7 +5,7 @@ from random import choices from typing import Dict, List, TYPE_CHECKING, Union from weakref import proxy -from orm.fields import ForeignKey +from orm import ForeignKey if TYPE_CHECKING: # pragma no cover from orm.models import FakePydantic, Model diff --git a/tests/test_fastapi_usage.py b/tests/test_fastapi_usage.py index 1c3d5dd..bef1607 100644 --- a/tests/test_fastapi_usage.py +++ b/tests/test_fastapi_usage.py @@ -4,6 +4,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient import orm +import orm.fields.foreign_key from tests.settings import DATABASE_URL app = FastAPI() @@ -28,7 +29,7 @@ class Item(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(length=100) - category = orm.ForeignKey(Category, nullable=True) + category = orm.fields.foreign_key.ForeignKey(Category, nullable=True) @app.post("/items/", response_model=Item) diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index 660f3d4..6b69507 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -3,6 +3,7 @@ import pytest import sqlalchemy import orm +import orm.fields.foreign_key from orm.exceptions import NoMatch, MultipleMatches, RelationshipInstanceError from tests.settings import DATABASE_URL @@ -25,7 +26,7 @@ class Track(orm.Model): __database__ = database id = orm.Integer(primary_key=True) - album = orm.ForeignKey(Album) + album = orm.fields.foreign_key.ForeignKey(Album) title = orm.String(length=100) position = orm.Integer() @@ -45,7 +46,7 @@ class Team(orm.Model): __database__ = database id = orm.Integer(primary_key=True) - org = orm.ForeignKey(Organisation) + org = orm.fields.foreign_key.ForeignKey(Organisation) name = orm.String(length=100) @@ -55,7 +56,7 @@ class Member(orm.Model): __database__ = database id = orm.Integer(primary_key=True) - team = orm.ForeignKey(Team) + team = orm.fields.foreign_key.ForeignKey(Team) email = orm.String(length=100) diff --git a/tests/test_same_table_joins.py b/tests/test_same_table_joins.py index 8c67f4b..4c27446 100644 --- a/tests/test_same_table_joins.py +++ b/tests/test_same_table_joins.py @@ -5,6 +5,7 @@ import pytest import sqlalchemy import orm +import orm.fields.foreign_key from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL, force_rollback=True) @@ -27,7 +28,7 @@ class SchoolClass(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(length=100) - department = orm.ForeignKey(Department, nullable=False) + department = orm.fields.foreign_key.ForeignKey(Department, nullable=False) class Category(orm.Model): @@ -46,8 +47,8 @@ class Student(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(length=100) - schoolclass = orm.ForeignKey(SchoolClass) - category = orm.ForeignKey(Category, nullable=True) + schoolclass = orm.fields.foreign_key.ForeignKey(SchoolClass) + category = orm.fields.foreign_key.ForeignKey(Category, nullable=True) class Teacher(orm.Model): @@ -57,8 +58,8 @@ class Teacher(orm.Model): id = orm.Integer(primary_key=True) name = orm.String(length=100) - schoolclass = orm.ForeignKey(SchoolClass) - category = orm.ForeignKey(Category, nullable=True) + schoolclass = orm.fields.foreign_key.ForeignKey(SchoolClass) + category = orm.fields.foreign_key.ForeignKey(Category, nullable=True) @pytest.fixture(scope='module')