How to test a BaseModel class with fields retrieved from a method? - python

I find myself writing tests for methods for two classes, one of which has str fields that it retrieves from another BaseModel class:
class Person(BaseModel):
firstname: Optional[str] = settings.get_settings().firstname
lastname: Optional[str] = settings.get_settings().lastname
Now, when I try to create the fixture I get this problem
#test file
#pytest.fixture
def person_class():
person = Person(
firstname="Marco",
lastname="Marche"
)
return person()
Error:
pydantic.error_wrappers.ValidationError: 31 validation errors for Settings
Settings is a BaseSettings class.
class Settings(BaseSettings):
firstname: str,
lastname: str,
address: str,
telephone: str
#lru_cache
def get_settings() -> Settings:
return Settings()
How can I solve it by creating a "fake" Person class with fields entered by me?

I'm assuming that you have a typo in your Settings definition and there should be no commas in the firstname, lastname, and address lines.
The error you're getting is most likely because your Settings class requires a value for firstname, lastname, and the other fields but you didn't provide any.
I think the easiest solution would be to give it some fake values in the instantiation in your get_settings() function, e.g.:
#lru_cache
def get_settings() -> Settings:
return Settings(
firstname="fake firstname",
lastname="fake lastname",
address="fake address",
telephone="fake telephone",
)
Pydantic does provide the construct() method on the Models which allows to create models without validation, however, I don't think this would work in your situation because you are getting the default values for Person from Settings which, in a case like yours, is still being evaluated.

Related

Pydantic - Use schema field as field type in another schema

Is this concept somehow possible in Pydantic:
## one file
class StatusOut(BaseModel):
id: int
name: str
## another file
class UserOut(BaseModel):
name: str
status_name: StatusOut.name ## IS THIS ACHIEVABLE???
I get an error "AttributeError: type object 'StatusOut' has no attribute 'name'"
So the first and only thing I got in mind is to override validation with #validator(...) decorator.
from pydantic import BaseModel, validator
class StatusOut(BaseModel):
id: int
name: str
class UserOut(BaseModel):
name: str
status_name: StatusOut
#validator("status_name")
def redefine_status_name(cls, v):
return v.name
status = StatusOut(id=1, name="status")
user = UserOut(name="test", status_name=status)
print(user.json()) # {"name": "test", "status_name": "status"}
I guess there is no need to explain what I did, I just returned StatusOut().name instead of StatusOut itself
Although I do not suggest doing that, as it makes status_name's real type not the one that is declared and might confuse you in future

SqlModel : Fastapi AttributeError: type object 'AddressBaseCore' has no attribute '__config__'

I am new to fastapi and SQLModel, i was trying to implement some basic code from my existing lib, I have an Address Class
like
#dataclass
class Address(DataClassJsonMixin):
addr1: str
city: str
province: str
I simply want to create a class in SQLModel that connects to DB. I have only added a new column ID here. i am getting below error where i am not sure why is it asking for a config attribute.
class AddressMaster(SQLModel, Address):
id: int = Field(default=None, primary_key=True)
AttributeError: type object 'Address' has no attribute '__config__'
It's failing on config = getattr(base, "__config__") that has some information which I am not able to comprehand.
# Only one of the base classes (or the current one) should be a table model
# this allows FastAPI cloning a SQLModel for the response_model without
# trying to create a new SQLAlchemy, for a new table, with the same name, that
# triggers an error
try 1:
from sqlmodel import SQLModel, Field
from ...core import Address
from dataclasses import dataclass
#dataclass
class AddressDB(Address, SQLModel):
pass
# END AddressDB
class AddressMaster(AddressDB, table=True):
"""
Address Master Table
"""
id: int = Field(default=None, primary_key=True)
# END AddressMaster
Object Creation
objAd = AddressMaster.from_dict({"addr1": "Kashmir", "city": "Srinagar", "province": "Kashmir"})
There is an error in the semantics of your AdressMaster class.
If it is meant to be a class related to your DB. Then you have to specify in the first parameter either the class inheriting from a SQL model or from SQLmodel (And in this case, you should rewrite each attribute of your model within this class) directly. And it is necessary to pass it the argument table=True
class AddressMaster(Address, table=True):
id: int = Field(default=None, primary_key=True)
# Here the attributes will be inherited from your Adress class
# (provided that this one in its parentage is an inheritance link with a modelSQL)
Or
class AddressMaster(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
addr1: str
city: str
province: str
# Here, the class is independent from the other pydantic
# validation models since it inherits directly from SQLModel
In Try 1:
You are trying to pass two parameters to your AddressDB class, one of which is an SQLModel. However, SQLModel allows to override SQLAlchemy, and accepts as parameter only models from SQLAlchemy or Pydantic. During the initialization, it goes through the arguments passed in parameter and tries to call the method or attribute Config which exists in the pydantic and SQLAlchemy models. This is the source of your error since you pass in parameter a DataClassJsonMixin which has no Config method or attribute. This is the origin of your error.
How to solve it. You just have to not call DataClassJsonMixin which seems to me to encode / decode JSON data. However, this is a basic behavior of Pydantic (which is used behind SQLModel).
So if you use the first method shown above (i.e. inherited from a SQLModel), you just have to put your validation fields inside AddressDB and make this class inherit only from SQLModel
class AddressDB(SQLModel):
addr1: str
city: str
province: str

How to use a reserved keyword in pydantic model

I need to create a schema but it has a column called global, and when I try to write this, I got an error.
class User(BaseModel):
id:int
global:bool
I try to use another name, but gives another error when try to save in db.
It looks like you are using the pydantic module. You can't use the name global because it's a reserved keyword so you need to use this trick to convert it.
class User(BaseModel):
id: int
global_: bool
class Config:
fields = {
'global_': 'global'
}
or
class User(BaseModel):
id: int
global_: bool = Field(..., alias='global')
To create a class you have to use a dictionary (because User(id=1, global=False) also throws an error:
user = User(**{'id': 1, 'global': False})
To get data in correct schema use by_alias:
user.dict(by_alias=True)

Access Type Hints for attributes of a dataclass created in post_init

Python: 3.7+
I have a dataclass and a subclass of it as following:
from abc import ABC
from dataclasses import dataclass
from typing import Dict, List, Optional
from dbconn import DBConnector
#dataclass
class User:
uid: int
name: str
#dataclass
class Model(ABC):
database: DBConnector
user: User
def func(self, *args, **kwargs):
pass
#dataclass
class Command(Model):
message: Optional[str] = "Hello"
def __post_init__(self):
self.user_id: str = str(self.user.uid)
self.message = f"{self.user.name}: {self.message}"
I could get the type hint for database, user and message using typing.get_type_hints(Command).
How can I get the type hints for user_id?
One workaround would to be pass in the user.uid and user.name as separate params to Command but that's not pragmatic when User object has many useful attributes.
I believe the reason why it doesn't work in the first place is because init gets called at runtime and that's why type checking doesn't take those attrs into account. One possible solution would be to parse the ast of the class but I'm not sure if that's recommended and generic enough. If yes, would appreciate a working example.
Figured out a hacky solution by using inspect.get_source and regex matching Type Hinted attributes. Also had to convert dataclass into a normal class for the end Model.
from abc import ABC
from dataclasses import dataclass
import inspect
import re
from typing import Dict, List, Optional
from dbconn import DBConnector
#dataclass
class User:
uid: int
name: str
#dataclass
class Model(ABC):
database: DBConnector
user: User
def func(self, *args, **kwargs):
pass
def get_type_hints(self):
source = inspect.getsource(self.__class__)
# Only need type hinted attributes
patt = r"self\.(?P<name>.+):\s(?P<type>.+)\s="
attrs = re.findall(patt, source)
for attr in attrs:
yield attr + (getattr(self, attr[0]), )
class Command(Model):
message: Optional[str] = "Hello"
def __init__(
self, database: DBConnector,
user: User,
message: Optional[str] = "Hello"
):
super().__init__(database, user)
self.user_id: str = str(self.user.uid)
self.message: Optional[str] = f"{self.user.name}: {self.message}"
cmd = Command(DBConnector(), User(123, 'Stack Overflow'))
for attr in cmd.get_type_hints():
print(attr)
# Output
('user_id', 'str', '123')
('message', 'str', 'Stack Overflow: Hello')
If someone can come up with a more robust solution, I'm definitely interested in it. For now, I'll mark this as my answer, in case someone stumbles upon this and is ok with a hacky solution.

Is there any post_load in pydantic?

Previously I used the marshmallow library with the Flask. Some time ago I have tried FastAPI with Pydantic. At first glance pydantic seems similar to masrhmallow but on closer inspection they differ. And for me the main difference between them is post_load methods which are from marshmallow. I can't find any analogs for it in pydantic.
post_load is decorator for post-processing methods. Using it I can handle return object on my own, can do whatever I want:
class ProductSchema(Schema):
alias = fields.Str()
category = fields.Str()
brand = fields.Str()
#post_load
def check_alias(self, params, **kwargs):
"""One of the fields must be filled"""
if not any([params.get('alias'), params.get('category'), params.get('brand')]):
raise ValidationError('No alias provided', field='alias')
return params
Besides it used not only for validation. Code example is just for visual understanding, do not analyze it, I have just invented it.
So my question is:
is there any analog for post_load in pydantic?
It is not obvious but pydantic's validator returns value of the field. So there are two ways to handle post_load conversions: validator and
root_validator.
validator gets the field value as argument and returns its value.
root_validator is the same but manipulates with the whole object.
from pydantic import validator, root_validator
class PaymentStatusSchema(BaseModel):
order_id: str = Param(..., title="Order id in the shop")
order_number: str = Param(..., title="Order number in chronological order")
status: int = Param(..., title="Payment status")
#validator("status")
def convert_status(cls, status):
return "active" if status == 1 else "inactive"
#root_validator
def check_order_id(cls, values):
"""Check order id"""
if not values.get('orderNumber') and not values.get('mdOrder'):
raise HTTPException(status_code=400, detail='No order data provided')
return values
By default pydantic runs validators as post-processing methods. For pre-processing you should use validators with pre argument:
#root_validator(pre=True)
def check_order_id(cls, values):
"""Check order id"""
# some code here
return values
Yes, you can use Pydantic's #validator decorator to do pre-load, post-load, model validating etc.
Here is a Post load example
from pydantic import validator
class Person(BaseModel):
first_name: str
second_name: str
#validator("first_name")
def make_it_formal(cls, first_name):
return f"Mr. {first_name.capitalize()}"
p = Person(first_name="egvo", second_name="Example")
p.first_name
Out: Mr. Egvo
Alternatively, you can also override __init__ and post-process the instance there:
from pydantic import BaseModel
class ProductSchema(BaseModel):
alias: str
category: str
brand: str
def __init__(self, *args, **kwargs):
# Do Pydantic validation
super().__init__(*args, **kwargs)
# Do things after Pydantic validation
if not any([self.alias, self.category, self.brand]):
raise ValueError("No alias provided")
Though this happens outside of Pydantic's validation.

Categories

Resources