Changing pydantic model Field() arguments with class variables for Fastapi - python

I'm a little new to tinkering with class inheritance in python, particularly when it comes down to using class attributes. In this case I am using a class attribute to change an argument in pydantic's Field() function. This wouldn't be too hard to do if my class contained it's own constructor, however, my class User1 is inheriting this from pydantic's BaseModel.
The idea is that I would like to be able to change the class attribute prior to creating the instance.
Please see some example code below:
from pydantic import Basemodel, Field
class User1(BaseModel):
_set_ge = None # create class attribute
item: float = Field(..., ge=_set_ge)
# avoid overriding BaseModel's __init__
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
User1._set_ge = 0 # setting the class attribute to a new value
instance = User1(item=-1)
print(instance) # item=-1.0
When creating the instance using instance = User1(item=-1) I would expect a validation error to be thrown, but it instead passes validation and simply returns the item value.
If I had my own constructor there would be little issue in changing the _set_ge, but as User1 inheriting this constructor from BaseModel, things are a little more complicated.
The eventual aim is to add this class to a fastapi endpoint as follows:
from fastapi import Fastapi
from schemas import User1
class NewUser1(User1):
pass
NewUser1._set_ge = 0
#app.post("/")
def endpoint(request: NewUser1):
return User1.item
To reduce code duplication, I aimed to use this method to easily change Field() arguments. If there is a better way, I'd be glad to consider that too.
This question is quite closely related to this unanswered one.

In the end, the #validator proposal by #hernán-alarcón is probably the best way to do this. For example:
from pydantic import Basemodel, Field, NumberNotGeError
from typing import ClassVar
class User(BaseModel):
_set_ge = ClassVar[float] # added the ClassVar typing to make clearer, but the underscore should be sufficient
item: float = Field(...)
#validator('item')
def limits(cls, v):
limit_number = cls._set_ge
if v >= limit_number:
return v
else:
raise NumberNotGeError(limit_value=limit_number)
class User1(User)
_set_ge = 0 # setting the class attribute to a new value
instance = User1(item=-1) # raises the error

Related

How Generic class inheritance works in python?

I have the following pieces of code:
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: ModelType):
self.model = model
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
pass
Although I know the purpose of this code, I'm a bit confused about how it works underneath.
CRUDBase inherits from Generic but how do you get the typevars? ie [ModelType, CreateSchemaType, UpdateSchemaType] which is in python? a generic builder?
Similarly CRUDUser inherits from CRUDBase (and in turn from Generic) and receives the same "list" [User, UserCreate, UserUpdate].

Using the class name for class_registry's AutoRegister

I want to use the AutoRegister feature of python's class registry (https://pypi.org/project/class-registry/). I made a minimal working example:
from class_registry import AutoRegister, ClassRegistry
from abc import ABC, abstractmethod
my_registry = ClassRegistry("registry_name", unique=True)
class MyBaseClass(ABC, metaclass=AutoRegister(my_registry)):
#property
#abstractmethod
def registry_name(self):
raise NotImplementedError
class MyClassA(MyBaseClass):
registry_name = "MyClassA"
class MyClassB(MyBaseClass):
registry_name = "MyClassB"
a = my_registry.get("MyClassA")
b = my_registry.get("MyClassB")
However, I find it inelegant, to have to specify the registry_name for each subclass - which in my case is always identical to the actual class name. I'd rather use the class name itself for the registry.
I tried to solve it using MyBaseClass's __init__ following this suggestion https://stackoverflow.com/a/44705886/5539351:
class MyBaseClass(ABC, metaclass=AutoRegister(my_registry)):
def __init__(self) -> None:
super().__init__()
self.registry_name = self.__class__.__name__
But it seems to me, that the registration happens before/without __init__, so I get the error
AttributeError: type object 'MyBaseClass' has no attribute 'name'
The following question goes in a related direction, but it didn't help me, because it is still required to specify the registry-key for each class:
Registry pattern with __init_subclass__ and sub-classable registry
Now, I am out of ideas and thankful for any input.

Type variable ... is unbound

I have a base class BaseTemplateData which inherits from pydantic.BaseModel. Then, I want to have a property of the BaseTemplate class which stores a type of a child class of BaseTemplateData.
I'm doing the following, but I'm getting a mypy error saying Type variable "file.path.TemplateDataType" is unbound, when I'm explicitly passing a bound parameter to the TypeVar call.
I also would like to have another class BaseTemplate2 which property doesn't store the type itself, but an instance of a child class of BaseTemplateData. Would the approach be correct?
Any help would be much appreciated. Thanks!
from typing import Type, TypeVar
from pydantic import BaseModel
class BaseTemplateData(BaseModel):
"""
Base class for all templates.
"""
TemplateDataType = TypeVar("TemplateDataType", bound=BaseTemplateData)
class BaseTemplate(BaseModel):
"""
Template class for email templates
"""
data_model: Type[TemplateDataType]
class BaseTemplate2(BaseModel):
"""
Template class for email templates 2
"""
data_model: TemplateDataType
I'm not so familiar with pythons Typing and can't imagine what you trying to solve by this code. But I don't understand why you need TemplateDataType?
Your expectations are:
I want to have a property of the BaseTemplate class which stores a type of a child class of BaseTemplateData
I also would like to have another class BaseTemplate2 which property doesn't store the type itself, but an instance of a child class of BaseTemplateData
Okay:
from typing import Type
from pydantic import BaseModel
class BaseTemplateData(BaseModel):
"""
Base class for all templates.
"""
class ChildBaseTemplateData(BaseTemplateData):
"""
Child class for Base class.
"""
class BaseTemplate(BaseModel):
"""
Template class for email templates
"""
data_model: Type[BaseTemplateData]
class BaseTemplate2(BaseModel):
"""
Template class for email templates 2
"""
data_model: BaseTemplateData
a = BaseTemplate()
# it is okay. Because a.data_model is a "type of a child class of BaseTemplateData"
a.data_model = ChildBaseTemplateData
b = BaseTemplate2()
# it is okay too. Because b.data_model is "an instance of a child class of BaseTemplateData"
b.data_model = ChildBaseTemplateData()
c = BaseTemplate2()
# error: Incompatible types in assignment (expression has type "str", variable has type "BaseTemplateData")
c.data_model = "string"
Here is a good explanation about TypeVar
Also TypeVar is using with Generics.
Here is no Generics in your example
Proper usage of TypeVar will be something like:
from typing import Type, TypeVar, Generic
A = TypeVar("A", int, str)
class B(Generic[A]):
pass
# Ok
a = B[int]
# Error: error: Value of type variable "A" of "B" cannot be "float"
b = B[float]

How to hint the type of Django's model field `objects` to a dynamically generated class?

I have a Team model in my Django project. I create its custom model manager with QuerySet.as_manager().
class TeamQuerySet(models.QuerySet):
def active(self) -> "models.QuerySet[Team]":
return self.filter(is_active=True)
class Team(models.Model):
is_active = models.BooleanField()
objects = TeamQuerySet.as_manager()
When I try to execute Team.objects.active(), mypy gives the following error:
error: "Manager[Any]" has no attribute "active"
In [5]: Team.objects
Out[5]: <django.db.models.manager.ManagerFromTeamQuerySet at 0x10eee1f70>
If I was explicitly defining a TeamManager class, there would be not a problem. How can I hint the type of Django model field objects to a dynamically generated class?
Based on the Manager[Any] I assume you are already using django-stubs.
Unfortunately it looks like there's an open issue to make QuerySet.as_manager generic over the model it's attached to that has not been resolved yet.
Even if the PR addressing the issue got merged I'm afraid it wouldn't address your immediate issue because the as_manager needs to be generic over the generic QuerySet subclass used to create the manager in order for both .active to be available and attributes relating to Team be available.
In this regard this other PR, which is unfortunately quite stale, seems to properly address your issue.
I've worked around this with a little switch-a-roo for MyPy's sake:
_Q = TypeVar("_Q", bound="WorkflowQuerySet")
class WorkflowQuerySet(models.QuerySet["WorkflowModel"]):
"""
Queryset for workflow objects.
"""
def count_objects(self) -> int:
raise NotImplementedError
def latest_objects(self: _Q) -> _Q:
raise NotImplementedError
if TYPE_CHECKING:
# Create a type MyPy understands
class WorkflowManager(models.Manager["WorkflowModel"]):
def count_objects(self) -> int:
...
def latest_objects(self) -> _Q:
...
else:
WorkflowManager = WorkflowQuerySet.as_manager
class WorkflowModel(models.Model):
"""
A model that has workflow.
"""
objects = WorkflowManager()
Here is my answer using generics and typevar
from typing import Generic, TypeVar
from django.db import models
class BookQueryset(models.QuerySet['Book']):
...
class Book(models.Model):
objects: BookQueryset = BookQueryset.as_manager()
book = Book.objects.all()[0]
If you inspect book is type Book

python: how to access class.__dict__ from class variable?

I need to define a class variable, named "class". I want to do this directly in class namespace, not in a class method. Obviously, I cannot directly say:
class MyClass(object):
a = 1
b = 2
class = 3
So, I want to do something like:
class MyClass(object):
a = 1
b = 2
self.__dict__["class"] = 3
Where "self" should be replaced with a reference to the class. So, how do I refer to a class from class namespace?
NOTE: This question might seem contrived, but it stems from a practical goal.
In fact, MyClass is a Django REST Framework serializer and I need a "class" field to be defined on it, because this REST endpoint has to follow a certain protocol.
There's a metaclass defined for Serializers, which calls __new__() upon class creation and that __new__() aggregates all the fields, defined on class and populates a registry of fields with them. So, I have to define my variable class before the class is created. Also see: Django REST Framework: how to make verbose name of field differ from its field_name?
You could do:
class MyClass(object):
a = 1
b = 2
vars()['class'] = 3
But since class is a reserved keyword, then you have to access the variable using getattr and setattr, so that class remains a string.
>>> m = MyClass()
>>> getattr(m, 'class')
3
You can create your class from type and add the attribute class to the class dictionary:
>>> MyClass = type('MyClass', (), {'class': 3, 'a':1, 'b':2})
>>> getattr(MyClass, 'class')
3
You can't directly access the name class with a dot reference, you'll need to use getattr:
>>> MyClass.class
File "<stdin>", line 1
MyClass.class
^
SyntaxError: invalid syntax
FWIW, you can define the class methods like you would do conventionally and then bind them to the class later on.
Caveat: While this works, I wouldn't use this hack myself as the keyword class is too much of a keyword to tamper with.
You don't need to name the attribute class, which can lead to all kinds of problems. You can name the attribute class_, but still have it pull from a source attribute named class and render out to JSON as class.
You can do this by overriding the metaclass for Serializers. Here is an example of a serializers.py file (the models and classes are largely pulled straight from the tutorial).
The main magic is this section of the metaclass
# Remap fields (to use class instead of class_)
fields_ = []
for name, field in fields:
if name.endswith('_'):
name = name.rstrip('_')
fields_.append((name, field))
This takes any field you define in the serializer that ends in an underscore (ie. field_) and removes the underscore from the name when it binds the Fields and sets the _declared_fields attribute on the serializer.
from collections import OrderedDict
from rest_framework import serializers
from rest_framework.fields import Field
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class MyMeta(serializers.SerializerMetaclass):
#classmethod
def _get_declared_fields(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
# If this class is subclassing another Serializer, add that Serializer's
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to maintain the correct order of fields.
for base in reversed(bases):
if hasattr(base, '_declared_fields'):
fields = list(base._declared_fields.items()) + fields
# Remap fields (to use class instead of class_)
fields_ = []
for name, field in fields:
if name.endswith('_'):
name = name.rstrip('_')
fields_.append((name, field))
return OrderedDict(fields_)
class SnippetSerializer(serializers.Serializer):
__metaclass__ = MyMeta
pk = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
class_ = serializers.CharField(source='klass', label='class', default='blah')
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.
"""
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
instance.title = validated_data.get('title', instance.title)
instance.class_ = validated_data.get('class', instance.class_)
instance.save()
return instance
Here is the models.py file for reference (django doesn't allow field names to end in an underscore)
from django.db import models
class Snippet(models.Model):
title = models.CharField(max_length=100, blank=True, default='')
klass = models.CharField(max_length=100, default='yo')
This is how it looks from the django shell
$ python manage.py shell
>>> from snippets.models import Snippet
>>> from snippets.serializers import SnippetSerializer
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser
>>> snippet = Snippet(title='test')
>>> snippet.save()
>>> serializer = SnippetSerializer(snippet)
>>> serializer.data
{'title': u'test', 'pk': 6, 'class': u'yo'}
You cannot it while creating class - technically that object does not exist yet.
You could consider:
class MyClass(object):
a = 1
b = 2
# class is already created
MyClass.__dict__["class"] = 3
But MyClass.__dict__ is not a dict, but a dictproxy, and 'dictproxy' object does not support item assignment, so there would be TypeError raised.
Use '''setattr''' to set a class attribute immediately after you finish the class definition. Outside the definition, of course. Pass '''MyClass''' for parameter, and it will create an attribute of your class.
Dict of members should not be used, especially for modifying an object. In fact, it is rarely needed. Most of things (though not all) people usually intend it to do are better done by '''setattr''' and '''getattr'''.
Finally, as one of those who answered noticed, you do not really need a field named '''class''', but that's another story, different from your original question.

Categories

Resources