We will be using `Advanced Pydantic` project for all examples except for the last section
where we switch to the `Trame With Pydantic` project.
## Why Data Validation Matters
Data validation is the process of ensuring that data meets certain criteria before it\'s processed by your application. It\'s a crucial step in building robust and reliable software. Without proper data validation, your application could be vulnerable to:
@@ -80,11 +58,23 @@ Key Features of Pydantic:
***Improved Code Readability:** Makes code easier to understand and maintain by explicitly defining data models. Type hints make it clear what type of data is expected for each field.
## Setup I
Let\'s start by setting up a new application from the template.
Create a CLI project answering the `copier` questions, use `Advanced Pydantic` for the project name and use defaults for all other questions.
Pydantic uses Python type hints to define data models. When you create an instance of a Pydantic model, Pydantic automatically validates the input data against the defined types and constraints.
Here\'s a simple example (add this code to `advanced_pydantic/src/advanced_pydantic/main.py`):
Here\'s a simple example (add this code to `src/advanced_pydantic/main.py`):
```python
frompydanticimportBaseModel,Field
@@ -98,11 +88,12 @@ In this example, we define a `User` model with two fields: `id` and `name`. We u
When you create an instance of the `User` model, Pydantic automatically validates the input data.
Add the code to the main function in `advanced_pydantic/src/advanced_pydantic/main.py`
Modify the main function in `src/advanced_pydantic/main.py`
```python
frompydanticimportValidationError
defmain()->None:
try:
user=User(id=0,name="")
print(user)
@@ -123,7 +114,7 @@ If the input data is invalid, Pydantic raises a `ValidationError` exception with
When working with structured data, it\'s common to have nested objects. For example, a User model from the above example might have multiple Address entries. In Pydantic, we can achieve this by creating nested models.
1. Creating the Address Model (add code to `advanced_pydantic/src/advanced_pydantic/main.py`).
1. Creating the Address Model (add code to `src/advanced_pydantic/main.py`).
The Address model represents a simple address with three fields:
@@ -143,9 +134,9 @@ class Address(BaseModel):
type:Literal["home","work"]=Field()
```
2. Using the Address Model as a Nested Field (add code to `advanced_pydantic/src/advanced_pydantic/main.py`).
2. Using the Address Model as a Nested Field (modify User model in `src/advanced_pydantic/main.py`).
The User model contains:
The User model now contains:
- id: An integer that must be greater than 0 (default is 1).
- name: A required string with at least 1 character (default is "someName").
@@ -160,9 +151,11 @@ class User(BaseModel):
addresses:List[Address]=Field(min_length=1)
```
now you can try to test the model. Add the code to the main function in `advanced_pydantic/src/advanced_pydantic/main.py`
now you can try to test the model. Modify the main function in `src/advanced_pydantic/main.py`
```python
defmain()->None:
# Example input
user_data={
"id":1,
@@ -193,7 +186,7 @@ For easier integration with the NOVA framework, where model field information is
Sometimes, simple validation like checking the minimum length is not enough. In such cases, you can write a custom validation function for a specific field.
For example, let\'s say we have a User model where only even IDs are allowed. We can enforce this constraint using the `@field_validator decorator` (`advanced_pydantic/src/advanced_pydantic/main.py`):
For example, let\'s say we have a User model where only even IDs are allowed. We can enforce this constraint using the `@field_validator decorator` (modify `src/advanced_pydantic/main.py`):
```python
frompydanticimportBaseModel,Field,field_validator
@@ -209,6 +202,7 @@ class User(BaseModel):
raiseValueError(f"{value} is not an even number")
returnvalue
defmain()->None:
# Example input
user_data={
"id":1,
@@ -217,7 +211,6 @@ user_data = {
user=User.model_validate(user_data)
print(user)
```
This code will raise a ValueError because the provided id (1) is not an even number.
@@ -232,7 +225,7 @@ Note that we used the mode="**after**" option for the validator. This ensures th
In some cases, you may need to validate the entire model, not just individual fields. This can be done by writing a custom validation function for the whole model using the `@model_validator` decorator.
For example, let\'s say we have a User model where the name and id must meet specific conditions together. For instance, we only allow users with even IDs to have names that start with a capital letter. We can enforce this logic using a @model_validator (`advanced_pydantic/src/advanced_pydantic/main.py`):
For example, let\'s say we have a User model where the name and id must meet specific conditions together. For instance, we only allow users with even IDs to have names that start with a capital letter. We can enforce this logic using a @model_validator (modify `src/advanced_pydantic/main.py`):
```python
frompydanticimportBaseModel,Field,model_validator
@@ -249,6 +242,7 @@ class User(BaseModel):
returnself
defmain()->None:
# Example input
user_data={
"id":2,
@@ -261,11 +255,23 @@ print(user)
This code will raise a ValueError because the name ("alice") does not start with a capital letter, while the id is even.
## Setup II
Now, let\'s create a simple Trame GUI, use `Trame With Pydantic` for the project name,
set up a Trame-based GUI application based on MVVM and use defaults for all other questions.
One of the great features of the NOVA Framework is that it allows to leverage Pydantic models to automatically validation UI elements. Let\'s walk through what that looks like in code.
First, let\'s add the following model (`pydantic_mvvm/src/trame_with_pydantic/app/models/settings.py`):
First, let\'s add the following model (create `src/trame_with_pydantic/app/models/settings.py`):
```python
frompydanticimportBaseModel,Field
@@ -274,7 +280,7 @@ class SettingsModel(BaseModel):
port:int=Field(default=8080,gt=0,lt=65536,title="Port Number",description="The port to listen on.",examples=["12345"])
```
Then in your viewmodel, you create binding for this model (`pydantic_mvvm/src/trame_with_pydantic/app/view_models/main.py`, clean up code created by tempalte engine, we don't need it for this example):
Then in your viewmodel, you create binding for this model (modify `src/trame_with_pydantic/app/view_models/main.py`, clean up code created by tempalte engine, we don't need it for this example):
```python
fromtypingimportAny,Dict
@@ -291,7 +297,7 @@ class MainViewModel:
self.settings_bind.update_view(self.settings)
```
And in your view, remove all other fields and add the following InputField (`pydantic_mvvm/src/trame_with_pydantic/app/views/main.py`):
And in your view, remove all other fields and add the following InputField (modify `src/trame_with_pydantic/app/views/main.py`):
```python
...
@@ -308,7 +314,7 @@ In that fashion, the `InputField` seamlessly pulls information from your code\'s
### Using callbacks in ViewModel to react to validation errors
Sometimes, you may want to respond to UI validation errors beyond just marking a field as invalid (which happens automatically). In such cases, you can add a callback to the `new_bind` function (`pydantic_mvvm/src/trame_with_pydantic/app/view_models/main.py`):
Sometimes, you may want to respond to UI validation errors beyond just marking a field as invalid (which happens automatically). In such cases, you can add a callback to the `new_bind` function (modify `src/trame_with_pydantic/app/view_models/main.py`):