switch from class to instance fro fields

This commit is contained in:
collerek
2021-03-19 14:22:31 +01:00
parent 61c456a01f
commit 32695ffa1d
26 changed files with 329 additions and 411 deletions

View File

@ -1,7 +1,7 @@
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, Union
import sqlalchemy
from pydantic import Field, Json, typing
from pydantic import Json, typing
from pydantic.fields import FieldInfo, Required, Undefined
import ormar # noqa I101
@ -28,44 +28,62 @@ class BaseField(FieldInfo):
to pydantic field types like ConstrainedStr
"""
__type__ = None
related_name = None
def __init__(self, **kwargs: Any) -> None:
self.__type__: type = kwargs.pop("__type__", None)
self.related_name = kwargs.pop("related_name", None)
column_type: sqlalchemy.Column
constraints: List = []
name: str
alias: str
self.column_type: sqlalchemy.Column = kwargs.pop("column_type", None)
self.constraints: List = kwargs.pop("constraints", list())
self.name: str = kwargs.pop("name", None)
self.db_alias: str = kwargs.pop("alias", None)
primary_key: bool
autoincrement: bool
nullable: bool
index: bool
unique: bool
pydantic_only: bool
choices: typing.Sequence
self.primary_key: bool = kwargs.pop("primary_key", False)
self.autoincrement: bool = kwargs.pop("autoincrement", False)
self.nullable: bool = kwargs.pop("nullable", False)
self.index: bool = kwargs.pop("index", False)
self.unique: bool = kwargs.pop("unique", False)
self.pydantic_only: bool = kwargs.pop("pydantic_only", False)
self.choices: typing.Sequence = kwargs.pop("choices", False)
virtual: bool = False # ManyToManyFields and reverse ForeignKeyFields
is_multi: bool = False # ManyToManyField
is_relation: bool = False # ForeignKeyField + subclasses
is_through: bool = False # ThroughFields
self.virtual: bool = kwargs.pop(
"virtual", None
) # ManyToManyFields and reverse ForeignKeyFields
self.is_multi: bool = kwargs.pop("is_multi", None) # ManyToManyField
self.is_relation: bool = kwargs.pop(
"is_relation", None
) # ForeignKeyField + subclasses
self.is_through: bool = kwargs.pop("is_through", False) # ThroughFields
owner: Type["Model"]
to: Type["Model"]
through: Type["Model"]
self_reference: bool = False
self_reference_primary: Optional[str] = None
orders_by: Optional[List[str]] = None
related_orders_by: Optional[List[str]] = None
self.owner: Type["Model"] = kwargs.pop("owner", None)
self.to: Type["Model"] = kwargs.pop("to", None)
self.through: Type["Model"] = kwargs.pop("through", None)
self.self_reference: bool = kwargs.pop("self_reference", False)
self.self_reference_primary: Optional[str] = kwargs.pop(
"self_reference_primary", None
)
self.orders_by: Optional[List[str]] = kwargs.pop("orders_by", None)
self.related_orders_by: Optional[List[str]] = kwargs.pop(
"related_orders_by", None
)
encrypt_secret: str
encrypt_backend: EncryptBackends = EncryptBackends.NONE
encrypt_custom_backend: Optional[Type[EncryptBackend]] = None
self.encrypt_secret: str = kwargs.pop("encrypt_secret", None)
self.encrypt_backend: EncryptBackends = kwargs.pop(
"encrypt_backend", EncryptBackends.NONE
)
self.encrypt_custom_backend: Optional[Type[EncryptBackend]] = kwargs.pop(
"encrypt_custom_backend", None
)
default: Any
server_default: Any
self.ormar_default: Any = kwargs.pop("default", None)
self.server_default: Any = kwargs.pop("server_default", None)
@classmethod
def is_valid_uni_relation(cls) -> bool:
for name, value in kwargs.items():
setattr(self, name, value)
kwargs.update(self.get_pydantic_default())
super().__init__(**kwargs)
def is_valid_uni_relation(self) -> bool:
"""
Checks if field is a relation definition but only for ForeignKey relation,
so excludes ManyToMany fields, as well as virtual ForeignKey
@ -78,10 +96,9 @@ class BaseField(FieldInfo):
:return: result of the check
:rtype: bool
"""
return not cls.is_multi and not cls.virtual
return not self.is_multi and not self.virtual
@classmethod
def get_alias(cls) -> str:
def get_alias(self) -> str:
"""
Used to translate Model column names to database column names during db queries.
@ -89,73 +106,25 @@ class BaseField(FieldInfo):
otherwise field name in ormar/pydantic
:rtype: str
"""
return cls.alias if cls.alias else cls.name
return self.db_alias if self.db_alias else self.name
@classmethod
def is_valid_field_info_field(cls, field_name: str) -> bool:
"""
Checks if field belongs to pydantic FieldInfo
- used during setting default pydantic values.
Excludes defaults and alias as they are populated separately
(defaults) or not at all (alias)
:param field_name: field name of BaseFIeld
:type field_name: str
:return: True if field is present on pydantic.FieldInfo
:rtype: bool
"""
return (
field_name not in ["default", "default_factory", "alias", "allow_mutation"]
and not field_name.startswith("__")
and hasattr(cls, field_name)
and not callable(getattr(cls, field_name))
)
@classmethod
def get_base_pydantic_field_info(cls, allow_null: bool) -> FieldInfo:
def get_pydantic_default(self) -> Dict:
"""
Generates base pydantic.FieldInfo with only default and optionally
required to fix pydantic Json field being set to required=False.
Used in an ormar Model Metaclass.
:param allow_null: flag if the default value can be None
or if it should be populated by pydantic Undefined
:type allow_null: bool
:return: instance of base pydantic.FieldInfo
:rtype: pydantic.FieldInfo
"""
base = cls.default_value()
base = self.default_value()
if base is None:
base = (
FieldInfo(default=None)
if (cls.nullable or allow_null)
else FieldInfo(default=Undefined)
)
if cls.__type__ == Json and base.default is Undefined:
base.default = Required
base = dict(default=None) if self.nullable else dict(default=Undefined)
if self.__type__ == Json and base.get("default") is Undefined:
base["default"] = Required
return base
@classmethod
def convert_to_pydantic_field_info(cls, allow_null: bool = False) -> FieldInfo:
"""
Converts a BaseField into pydantic.FieldInfo
that is later easily processed by pydantic.
Used in an ormar Model Metaclass.
:param allow_null: flag if the default value can be None
or if it should be populated by pydantic Undefined
:type allow_null: bool
:return: actual instance of pydantic.FieldInfo with all needed fields populated
:rtype: pydantic.FieldInfo
"""
base = cls.get_base_pydantic_field_info(allow_null=allow_null)
for attr_name in FieldInfo.__dict__.keys():
if cls.is_valid_field_info_field(attr_name):
setattr(base, attr_name, cls.__dict__.get(attr_name))
return base
@classmethod
def default_value(cls, use_server: bool = False) -> Optional[FieldInfo]:
def default_value(self, use_server: bool = False) -> Optional[Dict]:
"""
Returns a FieldInfo instance with populated default
(static) or default_factory (function).
@ -173,17 +142,20 @@ class BaseField(FieldInfo):
which is returning a FieldInfo instance
:rtype: Optional[pydantic.FieldInfo]
"""
if cls.is_auto_primary_key():
return Field(default=None)
if cls.has_default(use_server=use_server):
default = cls.default if cls.default is not None else cls.server_default
if self.is_auto_primary_key():
return dict(default=None)
if self.has_default(use_server=use_server):
default = (
self.ormar_default
if self.ormar_default is not None
else self.server_default
)
if callable(default):
return Field(default_factory=default)
return Field(default=default)
return dict(default_factory=default)
return dict(default=default)
return None
@classmethod
def get_default(cls, use_server: bool = False) -> Any: # noqa CCR001
def get_default(self, use_server: bool = False) -> Any: # noqa CCR001
"""
Return default value for a field.
If the field is Callable the function is called and actual result is returned.
@ -195,18 +167,17 @@ class BaseField(FieldInfo):
:return: default value for the field if set, otherwise implicit None
:rtype: Any
"""
if cls.has_default():
if self.has_default():
default = (
cls.default
if cls.default is not None
else (cls.server_default if use_server else None)
self.ormar_default
if self.ormar_default is not None
else (self.server_default if use_server else None)
)
if callable(default):
default = default()
return default
@classmethod
def has_default(cls, use_server: bool = True) -> bool:
def has_default(self, use_server: bool = True) -> bool:
"""
Checks if the field has default value set.
@ -216,12 +187,11 @@ class BaseField(FieldInfo):
:return: result of the check if default value is set
:rtype: bool
"""
return cls.default is not None or (
cls.server_default is not None and use_server
return self.ormar_default is not None or (
self.server_default is not None and use_server
)
@classmethod
def is_auto_primary_key(cls) -> bool:
def is_auto_primary_key(self) -> bool:
"""
Checks if field is first a primary key and if it,
it's than check if it's set to autoincrement.
@ -230,12 +200,11 @@ class BaseField(FieldInfo):
:return: result of the check for primary key and autoincrement
:rtype: bool
"""
if cls.primary_key:
return cls.autoincrement
if self.primary_key:
return self.autoincrement
return False
@classmethod
def construct_constraints(cls) -> List:
def construct_constraints(self) -> List:
"""
Converts list of ormar constraints into sqlalchemy ForeignKeys.
Has to be done dynamically as sqlalchemy binds ForeignKey to the table.
@ -249,15 +218,14 @@ class BaseField(FieldInfo):
con.reference,
ondelete=con.ondelete,
onupdate=con.onupdate,
name=f"fk_{cls.owner.Meta.tablename}_{cls.to.Meta.tablename}"
f"_{cls.to.get_column_alias(cls.to.Meta.pkname)}_{cls.name}",
name=f"fk_{self.owner.Meta.tablename}_{self.to.Meta.tablename}"
f"_{self.to.get_column_alias(self.to.Meta.pkname)}_{self.name}",
)
for con in cls.constraints
for con in self.constraints
]
return constraints
@classmethod
def get_column(cls, name: str) -> sqlalchemy.Column:
def get_column(self, name: str) -> sqlalchemy.Column:
"""
Returns definition of sqlalchemy.Column used in creation of sqlalchemy.Table.
Populates name, column type constraints, as well as a number of parameters like
@ -268,24 +236,23 @@ class BaseField(FieldInfo):
:return: actual definition of the database column as sqlalchemy requires.
:rtype: sqlalchemy.Column
"""
if cls.encrypt_backend == EncryptBackends.NONE:
if self.encrypt_backend == EncryptBackends.NONE:
column = sqlalchemy.Column(
cls.alias or name,
cls.column_type,
*cls.construct_constraints(),
primary_key=cls.primary_key,
nullable=cls.nullable and not cls.primary_key,
index=cls.index,
unique=cls.unique,
default=cls.default,
server_default=cls.server_default,
self.db_alias or name,
self.column_type,
*self.construct_constraints(),
primary_key=self.primary_key,
nullable=self.nullable and not self.primary_key,
index=self.index,
unique=self.unique,
default=self.ormar_default,
server_default=self.server_default,
)
else:
column = cls._get_encrypted_column(name=name)
column = self._get_encrypted_column(name=name)
return column
@classmethod
def _get_encrypted_column(cls, name: str) -> sqlalchemy.Column:
def _get_encrypted_column(self, name: str) -> sqlalchemy.Column:
"""
Returns EncryptedString column type instead of actual column.
@ -294,29 +261,28 @@ class BaseField(FieldInfo):
:return: newly defined column
:rtype: sqlalchemy.Column
"""
if cls.primary_key or cls.is_relation:
if self.primary_key or self.is_relation:
raise ModelDefinitionError(
"Primary key field and relations fields" "cannot be encrypted!"
)
column = sqlalchemy.Column(
cls.alias or name,
self.db_alias or name,
EncryptedString(
_field_type=cls,
encrypt_secret=cls.encrypt_secret,
encrypt_backend=cls.encrypt_backend,
encrypt_custom_backend=cls.encrypt_custom_backend,
_field_type=self,
encrypt_secret=self.encrypt_secret,
encrypt_backend=self.encrypt_backend,
encrypt_custom_backend=self.encrypt_custom_backend,
),
nullable=cls.nullable,
index=cls.index,
unique=cls.unique,
default=cls.default,
server_default=cls.server_default,
nullable=self.nullable,
index=self.index,
unique=self.unique,
default=self.ormar_default,
server_default=self.server_default,
)
return column
@classmethod
def expand_relationship(
cls,
self,
value: Any,
child: Union["Model", "NewBaseModel"],
to_register: bool = True,
@ -339,21 +305,19 @@ class BaseField(FieldInfo):
"""
return value
@classmethod
def set_self_reference_flag(cls) -> None:
def set_self_reference_flag(self) -> None:
"""
Sets `self_reference` to True if field to and owner are same model.
:return: None
:rtype: None
"""
if cls.owner is not None and (
cls.owner == cls.to or cls.owner.Meta == cls.to.Meta
if self.owner is not None and (
self.owner == self.to or self.owner.Meta == self.to.Meta
):
cls.self_reference = True
cls.self_reference_primary = cls.name
self.self_reference = True
self.self_reference_primary = self.name
@classmethod
def has_unresolved_forward_refs(cls) -> bool:
def has_unresolved_forward_refs(self) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
@ -363,8 +327,7 @@ class BaseField(FieldInfo):
"""
return False
@classmethod
def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None:
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
"""
Evaluates the ForwardRef to actual Field based on global and local namespaces
@ -376,8 +339,7 @@ class BaseField(FieldInfo):
:rtype: None
"""
@classmethod
def get_related_name(cls) -> str:
def get_related_name(self) -> str:
"""
Returns name to use for reverse relation.
It's either set as `related_name` or by default it's owner model. get_name + 's'