Merge pull request #28 from collerek/test_field_infos

Change model definition notation to support type hints and static checks
This commit is contained in:
collerek
2020-11-01 18:43:48 +07:00
committed by GitHub
84 changed files with 1503 additions and 659 deletions

View File

@ -27,7 +27,7 @@ script:
- DATABASE_URL=postgresql://localhost/test_database scripts/test.sh - DATABASE_URL=postgresql://localhost/test_database scripts/test.sh
- DATABASE_URL=mysql://localhost/test_database scripts/test.sh - DATABASE_URL=mysql://localhost/test_database scripts/test.sh
- DATABASE_URL=sqlite:///test.db scripts/test.sh - DATABASE_URL=sqlite:///test.db scripts/test.sh
- mypy --config-file mypy.ini ormar - mypy --config-file mypy.ini ormar tests
after_script: after_script:
- codecov - codecov

View File

@ -75,8 +75,8 @@ class Album(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(length=100) name: str = ormar.String(length=100)
class Track(ormar.Model): class Track(ormar.Model):
@ -85,10 +85,10 @@ class Track(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album) album= ormar.ForeignKey(Album)
title: ormar.String(length=100) title: str = ormar.String(length=100)
position: ormar.Integer() position: int = ormar.Integer()
# Create some records to work with. # Create some records to work with.

View File

@ -44,9 +44,9 @@ git checkout -b my-new-feature-branch
# 5. Formatting and linting # 5. Formatting and linting
# ormar uses black for formatting, flake8 for linting and mypy for type hints check # ormar uses black for formatting, flake8 for linting and mypy for type hints check
# run all of the following as all those calls will be run on travis after every push # run all of the following as all those calls will be run on travis after every push
black ormar black ormar tests
flake8 ormar flake8 ormar
mypy --config-file mypy.ini ormar mypy --config-file mypy.ini ormar tests
# 6. Run tests # 6. Run tests
# on localhost all tests are run against sglite backend # on localhost all tests are run against sglite backend

View File

@ -78,7 +78,7 @@ Used in sql only.
Sample usage: Sample usage:
```Python hl_lines="19-21" ```Python hl_lines="21-23"
--8<-- "../docs_src/fields/docs004.py" --8<-- "../docs_src/fields/docs004.py"
``` ```

View File

@ -75,8 +75,11 @@ class Album(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) # note that type hints are optional so
name: ormar.String(length=100) # id = ormar.Integer(primary_key=True)
# is also valid
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(length=100)
class Track(ormar.Model): class Track(ormar.Model):
@ -85,10 +88,10 @@ class Track(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album) album: Optional[Album] =ormar.ForeignKey(Album)
title: ormar.String(length=100) title: str = ormar.String(length=100)
position: ormar.Integer() position: int = ormar.Integer()
# Create some records to work with. # Create some records to work with.

View File

@ -34,7 +34,7 @@ By default if you assign primary key to `Integer` field, the `autoincrement` opt
You can disable by passing `autoincremant=False`. You can disable by passing `autoincremant=False`.
```Python ```Python
id: ormar.Integer(primary_key=True, autoincrement=False) id: int = ormar.Integer(primary_key=True, autoincrement=False)
``` ```
### Fields names vs Column names ### Fields names vs Column names
@ -50,14 +50,44 @@ Here you have a sample model with changed names
``` ```
Note that you can also change the ForeignKey column name Note that you can also change the ForeignKey column name
```Python hl_lines="9" ```Python hl_lines="21"
--8<-- "../docs_src/models/docs009.py" --8<-- "../docs_src/models/docs009.py"
``` ```
But for now you cannot change the ManyToMany column names as they go through other Model anyway. But for now you cannot change the ManyToMany column names as they go through other Model anyway.
```Python hl_lines="18" ```Python hl_lines="28"
--8<-- "../docs_src/models/docs010.py" --8<-- "../docs_src/models/docs010.py"
``` ```
### Type Hints & Legacy
Before version 0.4.0 `ormar` supported only one way of defining `Fields` on a `Model` using python type hints as pydantic.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
But that didn't play well with static type checkers like `mypy` and `pydantic` PyCharm plugin.
Therefore from version >=0.4.0 `ormar` switched to new notation.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
Note that type hints are **optional** so perfectly valid `ormar` code can look like this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
!!!warning
Even if you use type hints **`ormar` does not use them to construct `pydantic` fields!**
Type hints are there only to support static checkers and linting,
`ormar` construct annotations used by `pydantic` from own fields.
### Database initialization/ migrations ### Database initialization/ migrations
Note that all examples assume that you already have a database. Note that all examples assume that you already have a database.
@ -133,6 +163,20 @@ Created instance needs to be passed to every `Model` with `Meta` class `metadata
You need to create the `MetaData` instance **only once** and use it for all models. You need to create the `MetaData` instance **only once** and use it for all models.
You can create several ones if you want to use multiple databases. You can create several ones if you want to use multiple databases.
#### Best practice
Only thing that `ormar` expects is a class with name `Meta` and two class variables: `metadata` and `databases`.
So instead of providing the same parameters over and over again for all models you should creata a class and subclass it in all models.
```Python hl_lines="14 20 33"
--8<-- "../docs_src/models/docs013.py"
```
!!!warning
You need to subclass your `MainMeta` class in each `Model` class as those classes store configuration variables
that otherwise would be overwritten by each `Model`.
### Table Names ### Table Names
By default table name is created from Model class name as lowercase name plus 's'. By default table name is created from Model class name as lowercase name plus 's'.
@ -278,7 +322,7 @@ To access ormar `Fields` you can use `Model.Meta.model_fields` parameter
For example to list table model fields you can: For example to list table model fields you can:
```Python hl_lines="19" ```Python hl_lines="20"
--8<-- "../docs_src/models/docs005.py" --8<-- "../docs_src/models/docs005.py"
``` ```

28
docs/mypy.md Normal file
View File

@ -0,0 +1,28 @@
To provide better errors check you should use mypy with pydantic [plugin][plugin]
Note that legacy model declaration type will raise static type analyzers errors.
So you **cannot use the old notation** like this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
Instead switch to notation introduced in version 0.4.0.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs012.py"
```
Note that above example is not using the type hints, so further operations with mypy might fail, depending on the context.
Preferred notation should look liked this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
[plugin]: https://pydantic-docs.helpmanual.io/mypy_plugin/

18
docs/plugin.md Normal file
View File

@ -0,0 +1,18 @@
While `ormar` will work with any IDE there is a PyCharm `pydantic` plugin that enhances the user experience for this IDE.
Plugin is available on the JetBrains Plugins Repository for PyCharm: [plugin page][plugin page].
You can install the plugin for free from the plugin marketplace
(PyCharm's Preferences -> Plugin -> Marketplace -> search "pydantic").
!!!note
For plugin to work properly you need to provide valid type hints for model fields.
!!!info
Plugin supports type hints, argument inspection and more but mainly only for __init__ methods
More information can be found on the
[official plugin page](https://plugins.jetbrains.com/plugin/12861-pydantic)
and [github repository](https://github.com/koxudaxi/pydantic-pycharm-plugin).
[plugin page]: https://plugins.jetbrains.com/plugin/12861-pydantic

View File

@ -2,10 +2,13 @@
## QuerySet ## QuerySet
Each Model is auto registered with a QuerySet that represents the underlaying query and it's options. Each Model is auto registered with a `QuerySet` that represents the underlaying query and it's options.
Most of the methods are also available through many to many relation interface. Most of the methods are also available through many to many relation interface.
!!!info
To see which one are supported and how to construct relations visit [relations][relations].
Given the Models like this Given the Models like this
```Python ```Python
@ -95,74 +98,24 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows updated. Return number of rows updated.
```python hl_lines="24-28" ```Python hl_lines="26-28"
import databases --8<-- "../docs_src/queries/docs002.py"
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# queryset needs to be filtered before deleting to prevent accidental overwrite
# to update whole database table each=True needs to be provided as a safety switch
await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all()
assert len(all_books) == 3
``` ```
!!!warning
Queryset needs to be filtered before updating to prevent accidental overwrite.
To update whole database table `each=True` needs to be provided as a safety switch
### update_or_create ### update_or_create
`update_or_create(**kwargs) -> Model` `update_or_create(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one. Updates the model, or in case there is no match in database creates a new one.
```python hl_lines="24-30" ```Python hl_lines="26-32"
import databases --8<-- "../docs_src/queries/docs003.py"
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# if not exist the instance will be persisted in db
vol2 = await Book.objects.update_or_create(title="Volume II", author='Anonymous', genre='Fiction')
assert await Book.objects.count() == 1
# if pk or pkname passed in kwargs (like id here) the object will be updated
assert await Book.objects.update_or_create(id=vol2.id, genre='Historic')
assert await Book.objects.count() == 1
``` ```
!!!note !!!note
@ -177,36 +130,8 @@ Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed. A valid list of `Model` objects needs to be passed.
```python hl_lines="20-26" ```python hl_lines="21-27"
import databases --8<-- "../docs_src/queries/docs004.py"
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class ToDo(ormar.Model):
class Meta:
tablename = "todos"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
text: ormar.String(max_length=500)
completed: ormar.Boolean(default=False)
# create multiple instances at once with bulk_create
await ToDo.objects.bulk_create(
[
ToDo(text="Buy the groceries."),
ToDo(text="Call Mum.", completed=True),
ToDo(text="Send invoices.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 3
``` ```
### bulk_update ### bulk_update
@ -245,34 +170,8 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows deleted. Return number of rows deleted.
```python hl_lines="23-27" ```python hl_lines="26-30"
import databases --8<-- "../docs_src/queries/docs005.py"
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# delete accepts kwargs that will be used in filter
# acting in same way as queryset.filter(**kwargs).delete()
await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all()
assert len(all_books) == 2
``` ```
### all ### all
@ -453,76 +352,8 @@ has_sample = await Book.objects.filter(title='Sample').exists()
With `fields()` you can select subset of model columns to limit the data load. With `fields()` you can select subset of model columns to limit the data load.
```python hl_lines="48 60 61 67" ```python hl_lines="47 59 60 66"
import databases --8<-- "../docs_src/queries/docs006.py"
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
founded: ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
manufacturer: ormar.ForeignKey(Company)
name: ormar.String(max_length=100)
year: ormar.Integer(nullable=True)
gearbox_type: ormar.String(max_length=20, nullable=True)
gears: ormar.Integer(nullable=True)
aircon_type: ormar.String(max_length=20, nullable=True)
# build some sample data
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6,
aircon_type='Auto')
# select manufacturer but only name - to include related models use notation {model_name}__{column}
all_cars = await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__name']).all()
for car in all_cars:
# excluded columns will yield None
assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type'])
# included column on related models will be available, pk column is always included
# even if you do not include it in fields list
assert car.manufacturer.name == 'Toyota'
# also in the nested related models - you cannot exclude pk - it's always auto added
assert car.manufacturer.founded is None
# fields() can be called several times, building up the columns to select
# models selected in select_related but with no columns in fields list implies all fields
all_cars = await Car.objects.select_related('manufacturer').fields('id').fields(
['name']).all()
# all fiels from company model are selected
assert all_cars[0].manufacturer.name == 'Toyota'
assert all_cars[0].manufacturer.founded == 1937
# cannot exclude mandatory model columns - company__name in this example
await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__founded']).all()
# will raise pydantic ValidationError as company.name is required
``` ```
!!!warning !!!warning
@ -540,3 +371,4 @@ await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company_
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()` Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
[models]: ./models.md [models]: ./models.md
[relations]: ./relations.md

View File

@ -15,7 +15,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
To define a relation add `ForeignKey` field that points to related `Model`. To define a relation add `ForeignKey` field that points to related `Model`.
```Python hl_lines="27" ```Python hl_lines="29"
--8<-- "../docs_src/fields/docs003.py" --8<-- "../docs_src/fields/docs003.py"
``` ```
@ -25,7 +25,7 @@ To define a relation add `ForeignKey` field that points to related `Model`.
By default it's child (source) `Model` name + s, like courses in snippet below: By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="27 33" ```Python hl_lines="29 35"
--8<-- "../docs_src/fields/docs001.py" --8<-- "../docs_src/fields/docs001.py"
``` ```
@ -33,7 +33,7 @@ By default it's child (source) `Model` name + s, like courses in snippet below:
But you can overwrite this name by providing `related_name` parameter like below: But you can overwrite this name by providing `related_name` parameter like below:
```Python hl_lines="27 33" ```Python hl_lines="29 35"
--8<-- "../docs_src/fields/docs002.py" --8<-- "../docs_src/fields/docs002.py"
``` ```
@ -49,7 +49,7 @@ You have several ways to set-up a relationship connection.
The most obvious one is to pass a related `Model` instance to the constructor. The most obvious one is to pass a related `Model` instance to the constructor.
```Python hl_lines="32-33" ```Python hl_lines="34-35"
--8<-- "../docs_src/relations/docs001.py" --8<-- "../docs_src/relations/docs001.py"
``` ```
@ -57,7 +57,7 @@ The most obvious one is to pass a related `Model` instance to the constructor.
You can setup the relation also with just the pk column value of the related model. You can setup the relation also with just the pk column value of the related model.
```Python hl_lines="35-36" ```Python hl_lines="37-38"
--8<-- "../docs_src/relations/docs001.py" --8<-- "../docs_src/relations/docs001.py"
``` ```
@ -67,7 +67,7 @@ Next option is with a dictionary of key-values of the related model.
You can build the dictionary yourself or get it from existing model with `dict()` method. You can build the dictionary yourself or get it from existing model with `dict()` method.
```Python hl_lines="38-39" ```Python hl_lines="40-41"
--8<-- "../docs_src/relations/docs001.py" --8<-- "../docs_src/relations/docs001.py"
``` ```
@ -75,7 +75,7 @@ You can build the dictionary yourself or get it from existing model with `dict()
Finally you can explicitly set it to None (default behavior if no value passed). Finally you can explicitly set it to None (default behavior if no value passed).
```Python hl_lines="41-42" ```Python hl_lines="43-44"
--8<-- "../docs_src/relations/docs001.py" --8<-- "../docs_src/relations/docs001.py"
``` ```
@ -121,7 +121,11 @@ await news.posts.add(post)
Otherwise an IntegrityError will be raised by your database driver library. Otherwise an IntegrityError will be raised by your database driver library.
#### Creating new related `Model` instances #### create()
Create related `Model` directly from parent `Model`.
The link table is automatically populated, as well as relation ids in the database.
```python ```python
# Creating columns object from instance: # Creating columns object from instance:
@ -136,15 +140,27 @@ assert len(await post.categories.all()) == 2
To learn more about available QuerySet methods visit [queries][queries] To learn more about available QuerySet methods visit [queries][queries]
#### Removing related models #### remove()
Removal of the related model one by one.
Removes also the relation in the database.
```python ```python
# Removal of the relationship by one
await news.posts.remove(post) await news.posts.remove(post)
# or all at once ```
#### clear()
Removal all related models in one call.
Removes also the relation in the database.
```python
await news.posts.clear() await news.posts.clear()
``` ```
#### All other queryset methods #### Other queryset methods
When access directly the related `ManyToMany` field returns the list of related models. When access directly the related `ManyToMany` field returns the list of related models.
@ -164,7 +180,18 @@ news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido assert news_posts[0].author == guido
``` ```
Currently supported methods are:
!!!tip !!!tip
To learn more about available QuerySet methods visit [queries][queries] To learn more about available QuerySet methods visit [queries][queries]
##### get()
##### all()
##### filter()
##### select_related()
##### limit()
##### offset()
##### count()
##### exists()
[queries]: ./queries.md [queries]: ./queries.md

View File

@ -1,6 +1,22 @@
# 0.4.0
* Changed notation in Model definition -> now use name = ormar.Field() not name: ormar.Field()
* Note that old notation is still supported but deprecated and will not play nice with static checkers like mypy and pydantic pycharm plugin
* Type hint docs and test
* Use mypy for tests also not, only ormar package
* Fix scale and precision translation with max_digits and decimal_places pydantic Decimal field
* Update docs - add best practices for dependencies
* Refactor metaclass and model_fields to play nice with type hints
* Add mypy and pydantic plugin to docs
* Expand the docs on ManyToMany relation
# 0.3.11
* Fix setting server_default as default field value in python
# 0.3.10 # 0.3.10
* Fix * Fix postgresql check to avoid exceptions with drivers not installed if using different backend
# 0.3.9 # 0.3.9

0
docs_src/__init__.py Normal file
View File

View File

View File

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
import databases import databases
import sqlalchemy import sqlalchemy
@ -32,8 +32,8 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Item(ormar.Model): class Item(ormar.Model):
@ -42,9 +42,9 @@ class Item(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@app.get("/items/", response_model=List[Item]) @app.get("/items/", response_model=List[Item])

View File

View File

@ -0,0 +1,17 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
class Meta:
database = database
metadata = metadata
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)

View File

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import sqlalchemy import sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
@ -21,14 +23,14 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
department: ormar.ForeignKey(Department) department: Optional[Department] = ormar.ForeignKey(Department)
department = Department(name='Science') department = Department(name="Science")
course = Course(name='Math', completed=False, department=department) course = Course(name="Math", completed=False, department=department)
print(department.courses[0]) print(department.courses[0])
# Will produce: # Will produce:

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import sqlalchemy import sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
@ -21,14 +23,14 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
department: ormar.ForeignKey(Department, related_name="my_courses") department: Optional[Department] = ormar.ForeignKey(Department, related_name="my_courses")
department = Department(name='Science') department = Department(name="Science")
course = Course(name='Math', completed=False, department=department) course = Course(name="Math", completed=False, department=department)
print(department.my_courses[0]) print(department.my_courses[0])
# Will produce: # Will produce:

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import sqlalchemy import sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
@ -21,7 +23,7 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
department: ormar.ForeignKey(Department) department: Optional[Department] = ormar.ForeignKey(Department)

View File

@ -1,3 +1,5 @@
from datetime import datetime
import databases import databases
import sqlalchemy import sqlalchemy
from sqlalchemy import func, text from sqlalchemy import func, text
@ -14,8 +16,8 @@ class Product(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
company: ormar.String(max_length=200, server_default='Acme') company: str = ormar.String(max_length=200, server_default="Acme")
sort_order: ormar.Integer(server_default=text("10")) sort_order: int = ormar.Integer(server_default=text("10"))
created: ormar.DateTime(server_default=func.now()) created: datetime = ormar.DateTime(server_default=func.now())

View File

View File

@ -12,6 +12,6 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)

View File

@ -15,6 +15,6 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)

View File

@ -12,9 +12,9 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
print(Course.__fields__) print(Course.__fields__)

View File

@ -8,13 +8,13 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
class Meta: class Meta(ormar.ModelMeta): # note you don't have to subclass - but it's recommended for ide completion and mypy
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
print(Course.Meta.table.columns) print(Course.Meta.table.columns)

View File

@ -8,15 +8,16 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
class Meta: class Meta(ormar.ModelMeta):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
print({x:v.__dict__ for x,v in Course.Meta.model_fields.items()})
print({x: v.__dict__ for x, v in Course.Meta.model_fields.items()})
""" """
Will produce: Will produce:
{'completed': mappingproxy({'autoincrement': False, {'completed': mappingproxy({'autoincrement': False,

View File

@ -14,8 +14,8 @@ class Course(ormar.Model):
# define your constraints in Meta class of the model # define your constraints in Meta class of the model
# it's a list that can contain multiple constraints # it's a list that can contain multiple constraints
# hera a combination of name and column will have to be unique in db # hera a combination of name and column will have to be unique in db
constraints = [ormar.UniqueColumns('name', 'completed')] constraints = [ormar.UniqueColumns("name", "completed")]
id = ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)

View File

@ -12,9 +12,9 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id = ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
course = Course(name="Painting for dummies", completed=False) course = Course(name="Painting for dummies", completed=False)

View File

@ -13,7 +13,7 @@ class Child(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name='child_id', primary_key=True) id: int = ormar.Integer(name="child_id", primary_key=True)
first_name: ormar.String(name='fname', max_length=100) first_name: str = ormar.String(name="fname", max_length=100)
last_name: ormar.String(name='lname', max_length=100) last_name: str = ormar.String(name="lname", max_length=100)
born_year: ormar.Integer(name='year_born', nullable=True) born_year: int = ormar.Integer(name="year_born", nullable=True)

View File

@ -1,9 +1,21 @@
from typing import Optional
import databases
import sqlalchemy
import ormar
from .docs010 import Artist # previous example
database = databases.Database("sqlite:///test.db", force_rollback=True)
metadata = sqlalchemy.MetaData()
class Album(ormar.Model): class Album(ormar.Model):
class Meta: class Meta:
tablename = "music_albums" tablename = "music_albums"
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name='album_id', primary_key=True) id: int = ormar.Integer(name="album_id", primary_key=True)
name: ormar.String(name='album_name', max_length=100) name: str = ormar.String(name="album_name", max_length=100)
artist: ormar.ForeignKey(Artist, name='artist_id') artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id")

View File

@ -1,3 +1,13 @@
import databases
import sqlalchemy
import ormar
from .docs008 import Child
database = databases.Database("sqlite:///test.db", force_rollback=True)
metadata = sqlalchemy.MetaData()
class ArtistChildren(ormar.Model): class ArtistChildren(ormar.Model):
class Meta: class Meta:
tablename = "children_x_artists" tablename = "children_x_artists"
@ -11,8 +21,8 @@ class Artist(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name='artist_id', primary_key=True) id: int = ormar.Integer(name="artist_id", primary_key=True)
first_name: ormar.String(name='fname', max_length=100) first_name: str = ormar.String(name="fname", max_length=100)
last_name: ormar.String(name='lname', max_length=100) last_name: str = ormar.String(name="lname", max_length=100)
born_year: ormar.Integer(name='year') born_year: int = ormar.Integer(name="year")
children: ormar.ManyToMany(Child, through=ArtistChildren) children = ormar.ManyToMany(Child, through=ArtistChildren)

View File

@ -0,0 +1,19 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
class Meta:
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
completed: ormar.Boolean(default=False)
c1 = Course()

View File

@ -0,0 +1,17 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
class Meta:
database = database
metadata = metadata
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)

View File

@ -0,0 +1,38 @@
from typing import Optional
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///test.db", force_rollback=True)
metadata = sqlalchemy.MetaData()
# note that you do not have to subclass ModelMeta,
# it's useful for type hints and code completion
class MainMeta(ormar.ModelMeta):
metadata = metadata
database = database
class Artist(ormar.Model):
class Meta(MainMeta):
# note that tablename is optional
# if not provided ormar will user class.__name__.lower()+'s'
# -> artists in this example
pass
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=100)
last_name: str = ormar.String(max_length=100)
born_year: int = ormar.Integer(name="year")
class Album(ormar.Model):
class Meta(MainMeta):
pass
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
artist: Optional[Artist] = ormar.ForeignKey(Artist)

View File

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import ormar import ormar
import sqlalchemy import sqlalchemy
@ -12,8 +14,8 @@ class Album(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Track(ormar.Model): class Track(ormar.Model):
@ -22,7 +24,7 @@ class Track(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album) album: Optional[Album] = ormar.ForeignKey(Album)
title: ormar.String(max_length=100) title: str = ormar.String(max_length=100)
position: ormar.Integer() position: int = ormar.Integer()

View File

@ -0,0 +1,28 @@
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction',
choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all()
assert len(all_books) == 3

View File

@ -0,0 +1,32 @@
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction',
choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# if not exist the instance will be persisted in db
vol2 = await Book.objects.update_or_create(title="Volume II", author='Anonymous', genre='Fiction')
assert await Book.objects.count() == 1
# if pk or pkname passed in kwargs (like id here) the object will be updated
assert await Book.objects.update_or_create(id=vol2.id, genre='Historic')
assert await Book.objects.count() == 1

View File

@ -0,0 +1,30 @@
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class ToDo(ormar.Model):
class Meta:
tablename = "todos"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
completed = ormar.Boolean(default=False)
# create multiple instances at once with bulk_create
await ToDo.objects.bulk_create(
[
ToDo(text="Buy the groceries."),
ToDo(text="Call Mum.", completed=True),
ToDo(text="Send invoices.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 3

View File

@ -0,0 +1,30 @@
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction',
choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# delete accepts kwargs that will be used in filter
# acting in same way as queryset.filter(**kwargs).delete()
await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all()
assert len(all_books) == 2

View File

@ -0,0 +1,67 @@
import databases
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
founded: int = ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
manufacturer = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
gearbox_type: str = ormar.String(max_length=20, nullable=True)
gears: int = ormar.Integer(nullable=True)
aircon_type: str = ormar.String(max_length=20, nullable=True)
# build some sample data
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6,
aircon_type='Auto')
# select manufacturer but only name - to include related models use notation {model_name}__{column}
all_cars = await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__name']).all()
for car in all_cars:
# excluded columns will yield None
assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type'])
# included column on related models will be available, pk column is always included
# even if you do not include it in fields list
assert car.manufacturer.name == 'Toyota'
# also in the nested related models - you cannot exclude pk - it's always auto added
assert car.manufacturer.founded is None
# fields() can be called several times, building up the columns to select
# models selected in select_related but with no columns in fields list implies all fields
all_cars = await Car.objects.select_related('manufacturer').fields('id').fields(
['name']).all()
# all fiels from company model are selected
assert all_cars[0].manufacturer.name == 'Toyota'
assert all_cars[0].manufacturer.founded == 1937
# cannot exclude mandatory model columns - company__name in this example
await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__founded']).all()
# will raise pydantic ValidationError as company.name is required

View File

View File

@ -1,3 +1,5 @@
from typing import Optional, Dict, Union
import databases import databases
import sqlalchemy import sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Course(ormar.Model): class Course(ormar.Model):
@ -21,22 +23,22 @@ class Course(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
department: ormar.ForeignKey(Department) department: Optional[Union[Department, Dict]] = ormar.ForeignKey(Department)
department = Department(name='Science') department = Department(name="Science")
# set up a relation with actual Model instance # set up a relation with actual Model instance
course = Course(name='Math', completed=False, department=department) course = Course(name="Math", completed=False, department=department)
# set up relation with only related model pk value # set up relation with only related model pk value
course2 = Course(name='Math II', completed=False, department=department.pk) course2 = Course(name="Math II", completed=False, department=department.pk)
# set up a relation with dictionary corresponding to related model # set up a relation with dictionary corresponding to related model
course3 = Course(name='Math III', completed=False, department=department.dict()) course3 = Course(name="Math III", completed=False, department=department.dict())
# explicitly set up None # explicitly set up None
course4 = Course(name='Math III', completed=False, department=None) course4 = Course(name="Math III", completed=False, department=None)

View File

@ -1,3 +1,5 @@
from typing import Optional, Union, List
import databases import databases
import ormar import ormar
import sqlalchemy import sqlalchemy
@ -12,9 +14,9 @@ class Author(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
first_name: ormar.String(max_length=80) first_name: str = ormar.String(max_length=80)
last_name: ormar.String(max_length=80) last_name: str = ormar.String(max_length=80)
class Category(ormar.Model): class Category(ormar.Model):
@ -23,8 +25,8 @@ class Category(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=40) name: str = ormar.String(max_length=40)
class PostCategory(ormar.Model): class PostCategory(ormar.Model):
@ -42,7 +44,9 @@ class Post(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
title: ormar.String(max_length=200) title: str = ormar.String(max_length=200)
categories: ormar.ManyToMany(Category, through=PostCategory) categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
author: ormar.ForeignKey(Author) Category, through=PostCategory
)
author: Optional[Author] = ormar.ForeignKey(Author)

View File

@ -8,6 +8,8 @@ nav:
- Relations: relations.md - Relations: relations.md
- Queries: queries.md - Queries: queries.md
- Use with Fastapi: fastapi.md - Use with Fastapi: fastapi.md
- Use with mypy: mypy.md
- PyCharm plugin: plugin.md
- Contributing: contributing.md - Contributing: contributing.md
- Release Notes: releases.md - Release Notes: releases.md
repo_name: collerek/ormar repo_name: collerek/ormar

View File

@ -1,5 +1,10 @@
[mypy] [mypy]
python_version = 3.8 python_version = 3.8
plugins = pydantic.mypy
[mypy-sqlalchemy.*] [mypy-sqlalchemy.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-tests.test_model_definition.*]
ignore_errors = True

View File

@ -1,5 +1,6 @@
from ormar.exceptions import ModelDefinitionError, ModelNotSet, MultipleMatches, NoMatch from ormar.exceptions import ModelDefinitionError, ModelNotSet, MultipleMatches, NoMatch
from ormar.fields import ( from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
from ormar.fields import ( # noqa: I100
BigInteger, BigInteger,
Boolean, Boolean,
Date, Date,
@ -17,6 +18,7 @@ from ormar.fields import (
UniqueColumns, UniqueColumns,
) )
from ormar.models import Model from ormar.models import Model
from ormar.models.metaclass import ModelMeta
from ormar.queryset import QuerySet from ormar.queryset import QuerySet
from ormar.relations import RelationType from ormar.relations import RelationType
@ -28,8 +30,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType() Undefined = UndefinedType()
__version__ = "0.4.0"
__version__ = "0.3.11"
__all__ = [ __all__ = [
"Integer", "Integer",
"BigInteger", "BigInteger",
@ -54,4 +55,7 @@ __all__ = [
"Undefined", "Undefined",
"UUID", "UUID",
"UniqueColumns", "UniqueColumns",
"QuerySetProtocol",
"RelationProtocol",
"ModelMeta",
] ]

View File

@ -1,5 +1,6 @@
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
import pydantic
import sqlalchemy import sqlalchemy
from pydantic import Field, typing from pydantic import Field, typing
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
@ -11,7 +12,7 @@ if TYPE_CHECKING: # pragma no cover
from ormar.models import NewBaseModel from ormar.models import NewBaseModel
class BaseField: class BaseField(FieldInfo):
__type__ = None __type__ = None
column_type: sqlalchemy.Column column_type: sqlalchemy.Column
@ -32,6 +33,28 @@ class BaseField:
default: Any default: Any
server_default: Any server_default: Any
@classmethod
def is_valid_field_info_field(cls, field_name: str) -> bool:
return (
field_name not in ["default", "default_factory"]
and not field_name.startswith("__")
and hasattr(cls, field_name)
)
@classmethod
def convert_to_pydantic_field_info(cls, allow_null: bool = False) -> FieldInfo:
base = cls.default_value()
if base is None:
base = (
FieldInfo(default=None)
if (cls.nullable or allow_null)
else FieldInfo(default=pydantic.fields.Undefined)
)
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 @classmethod
def default_value(cls, use_server: bool = False) -> Optional[FieldInfo]: def default_value(cls, use_server: bool = False) -> Optional[FieldInfo]:
if cls.is_auto_primary_key(): if cls.is_auto_primary_key():

View File

@ -1,4 +1,4 @@
from typing import Any, Generator, List, Optional, TYPE_CHECKING, Type, Union from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
import sqlalchemy import sqlalchemy
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
@ -37,10 +37,16 @@ def ForeignKey( # noqa CFQ002
virtual: bool = False, virtual: bool = False,
onupdate: str = None, onupdate: str = None,
ondelete: str = None, ondelete: str = None,
) -> Type["ForeignKeyField"]: ) -> Any:
fk_string = to.Meta.tablename + "." + to.get_column_alias(to.Meta.pkname) fk_string = to.Meta.tablename + "." + to.get_column_alias(to.Meta.pkname)
to_field = to.__fields__[to.Meta.pkname] to_field = to.Meta.model_fields[to.Meta.pkname]
__type__ = (
Union[to_field.__type__, to]
if not nullable
else Optional[Union[to_field.__type__, to]]
)
namespace = dict( namespace = dict(
__type__=__type__,
to=to, to=to,
name=name, name=name,
nullable=nullable, nullable=nullable,
@ -50,7 +56,7 @@ def ForeignKey( # noqa CFQ002
) )
], ],
unique=unique, unique=unique,
column_type=to_field.type_.column_type, column_type=to_field.column_type,
related_name=related_name, related_name=related_name,
virtual=virtual, virtual=virtual,
primary_key=False, primary_key=False,
@ -58,7 +64,6 @@ def ForeignKey( # noqa CFQ002
pydantic_only=False, pydantic_only=False,
default=None, default=None,
server_default=None, server_default=None,
__pydantic_model__=to,
) )
return type("ForeignKey", (ForeignKeyField, BaseField), namespace) return type("ForeignKey", (ForeignKeyField, BaseField), namespace)
@ -70,14 +75,6 @@ class ForeignKeyField(BaseField):
related_name: str related_name: str
virtual: bool virtual: bool
@classmethod
def __get_validators__(cls) -> Generator:
yield cls.validate
@classmethod
def validate(cls, value: Any) -> Any:
return value
@classmethod @classmethod
def _extract_model_from_sequence( def _extract_model_from_sequence(
cls, value: List, child: "Model", to_register: bool cls, value: List, child: "Model", to_register: bool

View File

@ -1,5 +1,6 @@
from typing import Dict, TYPE_CHECKING, Type from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
import ormar
from ormar.fields import BaseField from ormar.fields import BaseField
from ormar.fields.foreign_key import ForeignKeyField from ormar.fields.foreign_key import ForeignKeyField
@ -15,17 +16,25 @@ def ManyToMany(
*, *,
name: str = None, name: str = None,
unique: bool = False, unique: bool = False,
related_name: str = None,
virtual: bool = False, virtual: bool = False,
) -> Type["ManyToManyField"]: **kwargs: Any
to_field = to.__fields__[to.Meta.pkname] ) -> Any:
to_field = to.Meta.model_fields[to.Meta.pkname]
related_name = kwargs.pop("related_name", None)
nullable = kwargs.pop("nullable", True)
__type__ = (
Union[to_field.__type__, to, List[to]] # type: ignore
if not nullable
else Optional[Union[to_field.__type__, to, List[to]]] # type: ignore
)
namespace = dict( namespace = dict(
__type__=__type__,
to=to, to=to,
through=through, through=through,
name=name, name=name,
nullable=True, nullable=True,
unique=unique, unique=unique,
column_type=to_field.type_.column_type, column_type=to_field.column_type,
related_name=related_name, related_name=related_name,
virtual=virtual, virtual=virtual,
primary_key=False, primary_key=False,
@ -33,20 +42,10 @@ def ManyToMany(
pydantic_only=False, pydantic_only=False,
default=None, default=None,
server_default=None, server_default=None,
__pydantic_model__=to,
# __origin__=List,
# __args__=[Optional[to]]
) )
return type("ManyToMany", (ManyToManyField, BaseField), namespace) return type("ManyToMany", (ManyToManyField, BaseField), namespace)
class ManyToManyField(ForeignKeyField): class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol):
through: Type["Model"] through: Type["Model"]
@classmethod
def __modify_schema__(cls, field_schema: Dict) -> None:
field_schema["type"] = "array"
field_schema["title"] = cls.name.title()
field_schema["definitions"] = {f"{cls.to.__name__}": cls.to.schema()}
field_schema["items"] = {"$ref": f"{REF_PREFIX}{cls.to.__name__}"}

View File

@ -1,7 +1,7 @@
import datetime import datetime
import decimal import decimal
import uuid import uuid
from typing import Any, Optional, Type from typing import Any, Optional, TYPE_CHECKING, Type
import pydantic import pydantic
import sqlalchemy import sqlalchemy
@ -20,7 +20,7 @@ def is_field_nullable(
class ModelFieldFactory: class ModelFieldFactory:
_bases: Any = BaseField _bases: Any = (BaseField,)
_type: Any = None _type: Any = None
def __new__(cls, *args: Any, **kwargs: Any) -> Type[BaseField]: # type: ignore def __new__(cls, *args: Any, **kwargs: Any) -> Type[BaseField]: # type: ignore
@ -56,8 +56,7 @@ class ModelFieldFactory:
pass pass
class String(ModelFieldFactory): class String(ModelFieldFactory, str):
_bases = (pydantic.ConstrainedStr, BaseField)
_type = str _type = str
def __new__( # type: ignore # noqa CFQ002 def __new__( # type: ignore # noqa CFQ002
@ -95,8 +94,7 @@ class String(ModelFieldFactory):
) )
class Integer(ModelFieldFactory): class Integer(ModelFieldFactory, int):
_bases = (pydantic.ConstrainedInt, BaseField)
_type = int _type = int
def __new__( # type: ignore def __new__( # type: ignore
@ -130,8 +128,7 @@ class Integer(ModelFieldFactory):
return sqlalchemy.Integer() return sqlalchemy.Integer()
class Text(ModelFieldFactory): class Text(ModelFieldFactory, str):
_bases = (pydantic.ConstrainedStr, BaseField)
_type = str _type = str
def __new__( # type: ignore def __new__( # type: ignore
@ -153,8 +150,7 @@ class Text(ModelFieldFactory):
return sqlalchemy.Text() return sqlalchemy.Text()
class Float(ModelFieldFactory): class Float(ModelFieldFactory, float):
_bases = (pydantic.ConstrainedFloat, BaseField)
_type = float _type = float
def __new__( # type: ignore def __new__( # type: ignore
@ -182,17 +178,23 @@ class Float(ModelFieldFactory):
return sqlalchemy.Float() return sqlalchemy.Float()
class Boolean(ModelFieldFactory): if TYPE_CHECKING: # pragma: nocover
_bases = (int, BaseField)
_type = bool
@classmethod def Boolean(**kwargs: Any) -> bool:
def get_column_type(cls, **kwargs: Any) -> Any: pass
return sqlalchemy.Boolean()
class DateTime(ModelFieldFactory): else:
_bases = (datetime.datetime, BaseField)
class Boolean(ModelFieldFactory, int):
_type = bool
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
return sqlalchemy.Boolean()
class DateTime(ModelFieldFactory, datetime.datetime):
_type = datetime.datetime _type = datetime.datetime
@classmethod @classmethod
@ -200,8 +202,7 @@ class DateTime(ModelFieldFactory):
return sqlalchemy.DateTime() return sqlalchemy.DateTime()
class Date(ModelFieldFactory): class Date(ModelFieldFactory, datetime.date):
_bases = (datetime.date, BaseField)
_type = datetime.date _type = datetime.date
@classmethod @classmethod
@ -209,8 +210,7 @@ class Date(ModelFieldFactory):
return sqlalchemy.Date() return sqlalchemy.Date()
class Time(ModelFieldFactory): class Time(ModelFieldFactory, datetime.time):
_bases = (datetime.time, BaseField)
_type = datetime.time _type = datetime.time
@classmethod @classmethod
@ -218,8 +218,7 @@ class Time(ModelFieldFactory):
return sqlalchemy.Time() return sqlalchemy.Time()
class JSON(ModelFieldFactory): class JSON(ModelFieldFactory, pydantic.Json):
_bases = (pydantic.Json, BaseField)
_type = pydantic.Json _type = pydantic.Json
@classmethod @classmethod
@ -227,8 +226,7 @@ class JSON(ModelFieldFactory):
return sqlalchemy.JSON() return sqlalchemy.JSON()
class BigInteger(Integer): class BigInteger(Integer, int):
_bases = (pydantic.ConstrainedInt, BaseField)
_type = int _type = int
def __new__( # type: ignore def __new__( # type: ignore
@ -262,8 +260,7 @@ class BigInteger(Integer):
return sqlalchemy.BigInteger() return sqlalchemy.BigInteger()
class Decimal(ModelFieldFactory): class Decimal(ModelFieldFactory, decimal.Decimal):
_bases = (pydantic.ConstrainedDecimal, BaseField)
_type = decimal.Decimal _type = decimal.Decimal
def __new__( # type: ignore # noqa CFQ002 def __new__( # type: ignore # noqa CFQ002
@ -290,14 +287,14 @@ class Decimal(ModelFieldFactory):
kwargs["le"] = kwargs["maximum"] kwargs["le"] = kwargs["maximum"]
if kwargs.get("max_digits"): if kwargs.get("max_digits"):
kwargs["scale"] = kwargs["max_digits"] kwargs["precision"] = kwargs["max_digits"]
elif kwargs.get("scale"): elif kwargs.get("precision"):
kwargs["max_digits"] = kwargs["scale"] kwargs["max_digits"] = kwargs["precision"]
if kwargs.get("decimal_places"): if kwargs.get("decimal_places"):
kwargs["precision"] = kwargs["decimal_places"] kwargs["scale"] = kwargs["decimal_places"]
elif kwargs.get("precision"): elif kwargs.get("scale"):
kwargs["decimal_places"] = kwargs["precision"] kwargs["decimal_places"] = kwargs["scale"]
return super().__new__(cls, **kwargs) return super().__new__(cls, **kwargs)
@ -317,8 +314,7 @@ class Decimal(ModelFieldFactory):
) )
class UUID(ModelFieldFactory): class UUID(ModelFieldFactory, uuid.UUID):
_bases = (uuid.UUID, BaseField)
_type = uuid.UUID _type = uuid.UUID
@classmethod @classmethod

View File

@ -1,11 +1,13 @@
import logging import logging
import warnings
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union
import databases import databases
import pydantic import pydantic
import sqlalchemy import sqlalchemy
from pydantic import BaseConfig from pydantic import BaseConfig
from pydantic.fields import FieldInfo, ModelField from pydantic.fields import ModelField
from pydantic.utils import lenient_issubclass
from sqlalchemy.sql.schema import ColumnCollectionConstraint from sqlalchemy.sql.schema import ColumnCollectionConstraint
import ormar # noqa I100 import ormar # noqa I100
@ -179,44 +181,49 @@ def register_relation_in_alias_manager(
def populate_default_pydantic_field_value( def populate_default_pydantic_field_value(
type_: Type[BaseField], field: str, attrs: dict ormar_field: Type[BaseField], field_name: str, attrs: dict
) -> dict: ) -> dict:
def_value = type_.default_value() curr_def_value = attrs.get(field_name, ormar.Undefined)
curr_def_value = attrs.get(field, "NONE") if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
if curr_def_value == "NONE" and isinstance(def_value, FieldInfo): curr_def_value = ormar.Undefined
attrs[field] = def_value if curr_def_value is None:
elif curr_def_value == "NONE" and type_.nullable: attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
attrs[field] = FieldInfo(default=None) else:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
return attrs return attrs
def populate_pydantic_default_values(attrs: Dict) -> Dict: def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
for field, type_ in attrs["__annotations__"].items(): model_fields = {}
if issubclass(type_, BaseField): potential_fields = {
if type_.name is None: k: v
type_.name = field for k, v in attrs["__annotations__"].items()
attrs = populate_default_pydantic_field_value(type_, field, attrs) if lenient_issubclass(v, BaseField)
return attrs
def extract_annotations_and_default_vals(attrs: dict, bases: Tuple) -> dict:
attrs["__annotations__"] = attrs.get("__annotations__") or bases[0].__dict__.get(
"__annotations__", {}
)
attrs = populate_pydantic_default_values(attrs)
return attrs
def populate_meta_orm_model_fields(
attrs: dict, new_model: Type["Model"]
) -> Type["Model"]:
model_fields = {
field_name: field
for field_name, field in attrs["__annotations__"].items()
if issubclass(field, BaseField)
} }
new_model.Meta.model_fields = model_fields if potential_fields:
return new_model warnings.warn(
"Using ormar.Fields as type Model annotation has been deprecated,"
" check documentation of current version",
DeprecationWarning,
)
potential_fields.update(
{k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
)
for field_name, field in potential_fields.items():
if field.name is None:
field.name = field_name
attrs = populate_default_pydantic_field_value(field, field_name, attrs)
model_fields[field_name] = field
attrs["__annotations__"][field_name] = field.__type__
return attrs, model_fields
def extract_annotations_and_default_vals(attrs: dict) -> Tuple[Dict, Dict]:
key = "__annotations__"
attrs[key] = attrs.get(key, {})
attrs, model_fields = populate_pydantic_default_values(attrs)
return attrs, model_fields
def populate_meta_tablename_columns_and_pk( def populate_meta_tablename_columns_and_pk(
@ -261,7 +268,7 @@ def populate_meta_sqlalchemy_table_if_required(
def get_pydantic_base_orm_config() -> Type[BaseConfig]: def get_pydantic_base_orm_config() -> Type[BaseConfig]:
class Config(BaseConfig): class Config(BaseConfig):
orm_mode = True orm_mode = True
arbitrary_types_allowed = True # arbitrary_types_allowed = True
return Config return Config
@ -305,7 +312,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
) -> "ModelMetaclass": ) -> "ModelMetaclass":
attrs["Config"] = get_pydantic_base_orm_config() attrs["Config"] = get_pydantic_base_orm_config()
attrs["__name__"] = name attrs["__name__"] = name
attrs = extract_annotations_and_default_vals(attrs, bases) attrs, model_fields = extract_annotations_and_default_vals(attrs)
new_model = super().__new__( # type: ignore new_model = super().__new__( # type: ignore
mcs, name, bases, attrs mcs, name, bases, attrs
) )
@ -313,7 +320,8 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
if hasattr(new_model, "Meta"): if hasattr(new_model, "Meta"):
if not hasattr(new_model.Meta, "constraints"): if not hasattr(new_model.Meta, "constraints"):
new_model.Meta.constraints = [] new_model.Meta.constraints = []
new_model = populate_meta_orm_model_fields(attrs, new_model) if not hasattr(new_model.Meta, "model_fields"):
new_model.Meta.model_fields = model_fields
new_model = populate_meta_tablename_columns_and_pk(name, new_model) new_model = populate_meta_tablename_columns_and_pk(name, new_model)
new_model = populate_meta_sqlalchemy_table_if_required(new_model) new_model = populate_meta_sqlalchemy_table_if_required(new_model)
expand_reverse_relationships(new_model) expand_reverse_relationships(new_model)
@ -322,7 +330,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
if new_model.Meta.pkname not in attrs["__annotations__"]: if new_model.Meta.pkname not in attrs["__annotations__"]:
field_name = new_model.Meta.pkname field_name = new_model.Meta.pkname
field = Integer(name=field_name, primary_key=True) field = Integer(name=field_name, primary_key=True)
attrs["__annotations__"][field_name] = field attrs["__annotations__"][field_name] = Optional[int] # type: ignore
populate_default_pydantic_field_value( populate_default_pydantic_field_value(
field, field_name, attrs # type: ignore field, field_name, attrs # type: ignore
) )

View File

@ -1,11 +1,12 @@
import itertools import itertools
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, TypeVar
import sqlalchemy import sqlalchemy
import ormar.queryset # noqa I100 import ormar.queryset # noqa I100
from ormar.fields.many_to_many import ManyToManyField from ormar.fields.many_to_many import ManyToManyField
from ormar.models import NewBaseModel # noqa I100 from ormar.models import NewBaseModel # noqa I100
from ormar.models.metaclass import ModelMeta
def group_related_list(list_: List) -> Dict: def group_related_list(list_: List) -> Dict:
@ -23,18 +24,34 @@ def group_related_list(list_: List) -> Dict:
return test_dict return test_dict
if TYPE_CHECKING: # pragma nocover
from ormar import QuerySet
T = TypeVar("T", bound="Model")
class Model(NewBaseModel): class Model(NewBaseModel):
__abstract__ = False __abstract__ = False
if TYPE_CHECKING: # pragma nocover
Meta: ModelMeta
objects: "QuerySet"
def __repr__(self) -> str: # pragma nocover
attrs_to_include = ["tablename", "columns", "pkname"]
_repr = {k: v for k, v in self.Meta.model_fields.items()}
for atr in attrs_to_include:
_repr[atr] = getattr(self.Meta, atr)
return f"{self.__class__.__name__}({str(_repr)})"
@classmethod @classmethod
def from_row( # noqa CCR001 def from_row( # noqa CCR001
cls, cls: Type[T],
row: sqlalchemy.engine.ResultProxy, row: sqlalchemy.engine.ResultProxy,
select_related: List = None, select_related: List = None,
related_models: Any = None, related_models: Any = None,
previous_table: str = None, previous_table: str = None,
fields: List = None, fields: List = None,
) -> Optional["Model"]: ) -> Optional[T]:
item: Dict[str, Any] = {} item: Dict[str, Any] = {}
select_related = select_related or [] select_related = select_related or []
@ -66,7 +83,9 @@ class Model(NewBaseModel):
item, row, table_prefix, fields, nested=table_prefix != "" item, row, table_prefix, fields, nested=table_prefix != ""
) )
instance = cls(**item) if item.get(cls.Meta.pkname, None) is not None else None instance: Optional[T] = cls(**item) if item.get(
cls.Meta.pkname, None
) is not None else None
return instance return instance
@classmethod @classmethod
@ -124,7 +143,7 @@ class Model(NewBaseModel):
return item return item
async def save(self) -> "Model": async def save(self: T) -> T:
self_fields = self._extract_model_db_fields() self_fields = self._extract_model_db_fields()
if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement: if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement:
@ -137,7 +156,7 @@ class Model(NewBaseModel):
setattr(self, self.Meta.pkname, item_id) setattr(self, self.Meta.pkname, item_id)
return self return self
async def update(self, **kwargs: Any) -> "Model": async def update(self: T, **kwargs: Any) -> T:
if kwargs: if kwargs:
new_values = {**self.dict(), **kwargs} new_values = {**self.dict(), **kwargs}
self.from_dict(new_values) self.from_dict(new_values)
@ -151,13 +170,13 @@ class Model(NewBaseModel):
await self.Meta.database.execute(expr) await self.Meta.database.execute(expr)
return self return self
async def delete(self) -> int: async def delete(self: T) -> int:
expr = self.Meta.table.delete() expr = self.Meta.table.delete()
expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname))) expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname)))
result = await self.Meta.database.execute(expr) result = await self.Meta.database.execute(expr)
return result return result
async def load(self) -> "Model": async def load(self: T) -> T:
expr = self.Meta.table.select().where(self.pk_column == self.pk) expr = self.Meta.table.select().where(self.pk_column == self.pk)
row = await self.Meta.database.fetch_one(expr) row = await self.Meta.database.fetch_one(expr)
if not row: # pragma nocover if not row: # pragma nocover

View File

@ -1,5 +1,5 @@
import inspect import inspect
from typing import Dict, List, Set, TYPE_CHECKING, Type, TypeVar, Union from typing import Dict, List, Sequence, Set, TYPE_CHECKING, Type, TypeVar, Union
import ormar import ormar
from ormar.exceptions import RelationshipInstanceError from ormar.exceptions import RelationshipInstanceError
@ -11,6 +11,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar import Model from ormar import Model
from ormar.models import NewBaseModel from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
Field = TypeVar("Field", bound=BaseField) Field = TypeVar("Field", bound=BaseField)
@ -135,7 +137,7 @@ class ModelTableProxy:
if field.to == related.__class__ or field.to.Meta == related.Meta: if field.to == related.__class__ or field.to.Meta == related.Meta:
return name return name
# fallback for not registered relation # fallback for not registered relation
if register_missing: if register_missing: # pragma nocover
expand_reverse_relationships(related.__class__) # type: ignore expand_reverse_relationships(related.__class__) # type: ignore
return ModelTableProxy.resolve_relation_name( return ModelTableProxy.resolve_relation_name(
item, related, register_missing=False item, related, register_missing=False
@ -177,7 +179,7 @@ class ModelTableProxy:
return new_kwargs return new_kwargs
@classmethod @classmethod
def merge_instances_list(cls, result_rows: List["Model"]) -> List["Model"]: def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
merged_rows: List["Model"] = [] merged_rows: List["Model"] = []
for index, model in enumerate(result_rows): for index, model in enumerate(result_rows):
if index > 0 and model is not None and model.pk == merged_rows[-1].pk: if index > 0 and model is not None and model.pk == merged_rows[-1].pk:

View File

@ -5,11 +5,12 @@ from typing import (
Any, Any,
Callable, Callable,
Dict, Dict,
List,
Mapping, Mapping,
Optional, Optional,
Sequence,
TYPE_CHECKING, TYPE_CHECKING,
Type, Type,
TypeVar,
Union, Union,
) )
@ -27,7 +28,9 @@ from ormar.relations.alias_manager import AliasManager
from ormar.relations.relation_manager import RelationsManager from ormar.relations.relation_manager import RelationsManager
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar.models.model import Model from ormar import Model
T = TypeVar("T", bound=Model)
IntStr = Union[int, str] IntStr = Union[int, str]
DictStrAny = Dict[str, Any] DictStrAny = Dict[str, Any]
@ -52,7 +55,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
Meta: ModelMeta Meta: ModelMeta
# noinspection PyMissingConstructor # noinspection PyMissingConstructor
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None: # type: ignore
object.__setattr__(self, "_orm_id", uuid.uuid4().hex) object.__setattr__(self, "_orm_id", uuid.uuid4().hex)
object.__setattr__(self, "_orm_saved", False) object.__setattr__(self, "_orm_saved", False)
@ -73,7 +76,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
if "pk" in kwargs: if "pk" in kwargs:
kwargs[self.Meta.pkname] = kwargs.pop("pk") kwargs[self.Meta.pkname] = kwargs.pop("pk")
# build the models to set them and validate but don't register # build the models to set them and validate but don't register
kwargs = { new_kwargs = {
k: self._convert_json( k: self._convert_json(
k, k,
self.Meta.model_fields[k].expand_relationship( self.Meta.model_fields[k].expand_relationship(
@ -85,7 +88,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
} }
values, fields_set, validation_error = pydantic.validate_model( values, fields_set, validation_error = pydantic.validate_model(
self, kwargs # type: ignore self, new_kwargs # type: ignore
) )
if validation_error and not pk_only: if validation_error and not pk_only:
raise validation_error raise validation_error
@ -96,7 +99,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
# register the columns models after initialization # register the columns models after initialization
for related in self.extract_related_names(): for related in self.extract_related_names():
self.Meta.model_fields[related].expand_relationship( self.Meta.model_fields[related].expand_relationship(
kwargs.get(related), self, to_register=True new_kwargs.get(related), self, to_register=True
) )
def __setattr__(self, name: str, value: Any) -> None: # noqa CCR001 def __setattr__(self, name: str, value: Any) -> None: # noqa CCR001
@ -133,7 +136,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
def _extract_related_model_instead_of_field( def _extract_related_model_instead_of_field(
self, item: str self, item: str
) -> Optional[Union["Model", List["Model"]]]: ) -> Optional[Union["T", Sequence["T"]]]:
alias = self.get_column_alias(item) alias = self.get_column_alias(item)
if alias in self._orm: if alias in self._orm:
return self._orm.get(alias) return self._orm.get(alias)
@ -170,7 +173,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
def db_backend_name(cls) -> str: def db_backend_name(cls) -> str:
return cls.Meta.database._backend._dialect.name return cls.Meta.database._backend._dialect.name
def remove(self, name: "Model") -> None: def remove(self, name: "T") -> None:
self._orm.remove_parent(self, name) self._orm.remove_parent(self, name)
def dict( # noqa A003 def dict( # noqa A003

View File

@ -0,0 +1,4 @@
from ormar.protocols.queryset_protocol import QuerySetProtocol
from ormar.protocols.relation_protocol import RelationProtocol
__all__ = ["QuerySetProtocol", "RelationProtocol"]

View File

@ -0,0 +1,46 @@
from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Union
try:
from typing import Protocol
except ImportError: # pragma: nocover
from typing_extensions import Protocol # type: ignore
if TYPE_CHECKING: # noqa: C901; #pragma nocover
from ormar import QuerySet, Model
class QuerySetProtocol(Protocol): # pragma: nocover
def filter(self, **kwargs: Any) -> "QuerySet": # noqa: A003, A001
...
def select_related(self, related: Union[List, str]) -> "QuerySet":
...
async def exists(self) -> bool:
...
async def count(self) -> int:
...
async def clear(self) -> int:
...
def limit(self, limit_count: int) -> "QuerySet":
...
def offset(self, offset: int) -> "QuerySet":
...
async def first(self, **kwargs: Any) -> "Model":
...
async def get(self, **kwargs: Any) -> "Model":
...
async def all( # noqa: A003, A001
self, **kwargs: Any
) -> Sequence[Optional["Model"]]:
...
async def create(self, **kwargs: Any) -> "Model":
...

View File

@ -0,0 +1,17 @@
from typing import TYPE_CHECKING, Type, Union
try:
from typing import Protocol
except ImportError: # pragma: nocover
from typing_extensions import Protocol # type: ignore
if TYPE_CHECKING: # pragma: nocover
from ormar import Model
class RelationProtocol(Protocol): # pragma: nocover
def add(self, child: "Model") -> None:
...
def remove(self, child: Union["Model", Type["Model"]]) -> None:
...

View File

@ -1,4 +1,4 @@
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Type, Union
import databases import databases
import sqlalchemy import sqlalchemy
@ -39,7 +39,7 @@ class QuerySet:
def __get__( def __get__(
self, self,
instance: Union["QuerySet", "QuerysetProxy"], instance: Optional[Union["QuerySet", "QuerysetProxy"]],
owner: Union[Type["Model"], Type["QuerysetProxy"]], owner: Union[Type["Model"], Type["QuerysetProxy"]],
) -> "QuerySet": ) -> "QuerySet":
if issubclass(owner, ormar.Model): if issubclass(owner, ormar.Model):
@ -59,7 +59,7 @@ class QuerySet:
raise ValueError("Model class of QuerySet is not initialized") raise ValueError("Model class of QuerySet is not initialized")
return self.model_cls return self.model_cls
def _process_query_result_rows(self, rows: List) -> List[Optional["Model"]]: def _process_query_result_rows(self, rows: List) -> Sequence[Optional["Model"]]:
result_rows = [ result_rows = [
self.model.from_row( self.model.from_row(
row, select_related=self._select_related, fields=self._columns row, select_related=self._select_related, fields=self._columns
@ -87,7 +87,7 @@ class QuerySet:
return new_kwargs return new_kwargs
@staticmethod @staticmethod
def check_single_result_rows_count(rows: List[Optional["Model"]]) -> None: def check_single_result_rows_count(rows: Sequence[Optional["Model"]]) -> None:
if not rows or rows[0] is None: if not rows or rows[0] is None:
raise NoMatch() raise NoMatch()
if len(rows) > 1: if len(rows) > 1:
@ -267,7 +267,7 @@ class QuerySet:
model = await self.get(pk=kwargs[pk_name]) model = await self.get(pk=kwargs[pk_name])
return await model.update(**kwargs) return await model.update(**kwargs)
async def all(self, **kwargs: Any) -> List[Optional["Model"]]: # noqa: A003 async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003
if kwargs: if kwargs:
return await self.filter(**kwargs).all() return await self.filter(**kwargs).all()

View File

@ -1,4 +1,4 @@
from typing import Any, List, Optional, TYPE_CHECKING, Union from typing import Any, List, Optional, Sequence, TYPE_CHECKING, TypeVar, Union
import ormar import ormar
@ -7,8 +7,10 @@ if TYPE_CHECKING: # pragma no cover
from ormar.models import Model from ormar.models import Model
from ormar.queryset import QuerySet from ormar.queryset import QuerySet
T = TypeVar("T", bound=Model)
class QuerysetProxy:
class QuerysetProxy(ormar.QuerySetProtocol):
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
relation: "Relation" relation: "Relation"
@ -26,27 +28,28 @@ class QuerysetProxy:
def queryset(self, value: "QuerySet") -> None: def queryset(self, value: "QuerySet") -> None:
self._queryset = value self._queryset = value
def _assign_child_to_parent(self, child: Optional["Model"]) -> None: def _assign_child_to_parent(self, child: Optional["T"]) -> None:
if child: if child:
owner = self.relation._owner owner = self.relation._owner
rel_name = owner.resolve_relation_name(owner, child) rel_name = owner.resolve_relation_name(owner, child)
setattr(owner, rel_name, child) setattr(owner, rel_name, child)
def _register_related(self, child: Union["Model", List[Optional["Model"]]]) -> None: def _register_related(self, child: Union["T", Sequence[Optional["T"]]]) -> None:
if isinstance(child, list): if isinstance(child, list):
for subchild in child: for subchild in child:
self._assign_child_to_parent(subchild) self._assign_child_to_parent(subchild)
else: else:
assert isinstance(child, ormar.Model)
self._assign_child_to_parent(child) self._assign_child_to_parent(child)
async def create_through_instance(self, child: "Model") -> None: async def create_through_instance(self, child: "T") -> None:
queryset = ormar.QuerySet(model_cls=self.relation.through) queryset = ormar.QuerySet(model_cls=self.relation.through)
owner_column = self.relation._owner.get_name() owner_column = self.relation._owner.get_name()
child_column = child.get_name() child_column = child.get_name()
kwargs = {owner_column: self.relation._owner, child_column: child} kwargs = {owner_column: self.relation._owner, child_column: child}
await queryset.create(**kwargs) await queryset.create(**kwargs)
async def delete_through_instance(self, child: "Model") -> None: async def delete_through_instance(self, child: "T") -> None:
queryset = ormar.QuerySet(model_cls=self.relation.through) queryset = ormar.QuerySet(model_cls=self.relation.through)
owner_column = self.relation._owner.get_name() owner_column = self.relation._owner.get_name()
child_column = child.get_name() child_column = child.get_name()
@ -88,7 +91,7 @@ class QuerysetProxy:
self._register_related(get) self._register_related(get)
return get return get
async def all(self, **kwargs: Any) -> List[Optional["Model"]]: # noqa: A003 async def all(self, **kwargs: Any) -> Sequence[Optional["Model"]]: # noqa: A003
all_items = await self.queryset.all(**kwargs) all_items = await self.queryset.all(**kwargs)
self._register_related(all_items) self._register_related(all_items)
return all_items return all_items

View File

@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Type, Union from typing import List, Optional, TYPE_CHECKING, Type, TypeVar, Union
import ormar # noqa I100 import ormar # noqa I100
from ormar.exceptions import RelationshipInstanceError # noqa I100 from ormar.exceptions import RelationshipInstanceError # noqa I100
@ -11,6 +11,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar.relations import RelationsManager from ormar.relations import RelationsManager
from ormar.models import NewBaseModel from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
class RelationType(Enum): class RelationType(Enum):
PRIMARY = 1 PRIMARY = 1
@ -23,15 +25,15 @@ class Relation:
self, self,
manager: "RelationsManager", manager: "RelationsManager",
type_: RelationType, type_: RelationType,
to: Type["Model"], to: Type["T"],
through: Type["Model"] = None, through: Type["T"] = None,
) -> None: ) -> None:
self.manager = manager self.manager = manager
self._owner: "Model" = manager.owner self._owner: "Model" = manager.owner
self._type: RelationType = type_ self._type: RelationType = type_
self.to: Type["Model"] = to self.to: Type["T"] = to
self.through: Optional[Type["Model"]] = through self.through: Optional[Type["T"]] = through
self.related_models: Optional[Union[RelationProxy, "Model"]] = ( self.related_models: Optional[Union[RelationProxy, "T"]] = (
RelationProxy(relation=self) RelationProxy(relation=self)
if type_ in (RelationType.REVERSE, RelationType.MULTIPLE) if type_ in (RelationType.REVERSE, RelationType.MULTIPLE)
else None else None
@ -50,7 +52,7 @@ class Relation:
self.related_models.pop(ind) self.related_models.pop(ind)
return None return None
def add(self, child: "Model") -> None: def add(self, child: "T") -> None:
relation_name = self._owner.resolve_relation_name(self._owner, child) relation_name = self._owner.resolve_relation_name(self._owner, child)
if self._type == RelationType.PRIMARY: if self._type == RelationType.PRIMARY:
self.related_models = child self.related_models = child
@ -77,7 +79,7 @@ class Relation:
self.related_models.pop(position) # type: ignore self.related_models.pop(position) # type: ignore
del self._owner.__dict__[relation_name][position] del self._owner.__dict__[relation_name][position]
def get(self) -> Optional[Union[List["Model"], "Model"]]: def get(self) -> Optional[Union[List["T"], "T"]]:
return self.related_models return self.related_models
def __repr__(self) -> str: # pragma no cover def __repr__(self) -> str: # pragma no cover

View File

@ -1,4 +1,4 @@
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union from typing import Dict, List, Optional, Sequence, TYPE_CHECKING, Type, TypeVar, Union
from weakref import proxy from weakref import proxy
from ormar.fields import BaseField from ormar.fields import BaseField
@ -14,6 +14,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar import Model from ormar import Model
from ormar.models import NewBaseModel from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
class RelationsManager: class RelationsManager:
def __init__( def __init__(
@ -46,7 +48,7 @@ class RelationsManager:
def __contains__(self, item: str) -> bool: def __contains__(self, item: str) -> bool:
return item in self._related_names return item in self._related_names
def get(self, name: str) -> Optional[Union[List["Model"], "Model"]]: def get(self, name: str) -> Optional[Union["T", Sequence["T"]]]:
relation = self._relations.get(name, None) relation = self._relations.get(name, None)
if relation is not None: if relation is not None:
return relation.get() return relation.get()

View File

@ -72,6 +72,6 @@ class RelationProxy(list):
if self.relation._type == ormar.RelationType.MULTIPLE: if self.relation._type == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.create_through_instance(item) await self.queryset_proxy.create_through_instance(item)
rel_name = item.resolve_relation_name(item, self._owner) rel_name = item.resolve_relation_name(item, self._owner)
if rel_name not in item._orm: if rel_name not in item._orm: # pragma nocover
item._orm._add_relation(item.Meta.model_fields[rel_name]) item._orm._add_relation(item.Meta.model_fields[rel_name])
setattr(item, rel_name, self._owner) setattr(item, rel_name, self._owner)

View File

@ -3,6 +3,7 @@ databases[postgresql]
databases[mysql] databases[mysql]
pydantic pydantic
sqlalchemy sqlalchemy
typing_extensions
# Async database drivers # Async database drivers
aiomysql aiomysql

View File

@ -51,7 +51,7 @@ setup(
packages=get_packages(PACKAGE), packages=get_packages(PACKAGE),
package_data={PACKAGE: ["py.typed"]}, package_data={PACKAGE: ["py.typed"]},
data_files=[("", ["LICENSE.md"])], data_files=[("", ["LICENSE.md"])],
install_requires=["databases", "pydantic>=1.5", "sqlalchemy"], install_requires=["databases", "pydantic>=1.5", "sqlalchemy", "typing_extensions"],
extras_require={ extras_require={
"postgresql": ["asyncpg", "psycopg2"], "postgresql": ["asyncpg", "psycopg2"],
"mysql": ["aiomysql", "pymysql"], "mysql": ["aiomysql", "pymysql"],

View File

@ -1,3 +1,5 @@
from typing import Optional, Union, List
import databases import databases
import pytest import pytest
import sqlalchemy import sqlalchemy
@ -15,10 +17,10 @@ class Child(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name="child_id", primary_key=True) id: int = ormar.Integer(name="child_id", primary_key=True)
first_name: ormar.String(name="fname", max_length=100) first_name: str = ormar.String(name="fname", max_length=100)
last_name: ormar.String(name="lname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100)
born_year: ormar.Integer(name="year_born", nullable=True) born_year: int = ormar.Integer(name="year_born", nullable=True)
class ArtistChildren(ormar.Model): class ArtistChildren(ormar.Model):
@ -34,11 +36,13 @@ class Artist(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name="artist_id", primary_key=True) id: int = ormar.Integer(name="artist_id", primary_key=True)
first_name: ormar.String(name="fname", max_length=100) first_name: str = ormar.String(name="fname", max_length=100)
last_name: ormar.String(name="lname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100)
born_year: ormar.Integer(name="year") born_year: int = ormar.Integer(name="year")
children: ormar.ManyToMany(Child, through=ArtistChildren) children: Optional[Union[Child, List[Child]]] = ormar.ManyToMany(
Child, through=ArtistChildren
)
class Album(ormar.Model): class Album(ormar.Model):
@ -47,9 +51,9 @@ class Album(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(name="album_id", primary_key=True) id: int = ormar.Integer(name="album_id", primary_key=True)
name: ormar.String(name="album_name", max_length=100) name: str = ormar.String(name="album_name", max_length=100)
artist: ormar.ForeignKey(Artist, name="artist_id") artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id")
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

@ -2,6 +2,7 @@ import datetime
import os import os
import databases import databases
import pydantic
import pytest import pytest
import sqlalchemy import sqlalchemy
@ -22,14 +23,14 @@ class Example(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=200, default="aaa") name: str = ormar.String(max_length=200, default="aaa")
created: ormar.DateTime(default=datetime.datetime.now) created: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
created_day: ormar.Date(default=datetime.date.today) created_day: datetime.date = ormar.Date(default=datetime.date.today)
created_time: ormar.Time(default=time) created_time: datetime.time = ormar.Time(default=time)
description: ormar.Text(nullable=True) description: str = ormar.Text(nullable=True)
value: ormar.Float(nullable=True) value: float = ormar.Float(nullable=True)
data: ormar.JSON(default={}) data: pydantic.Json = ormar.JSON(default={})
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

View File

@ -1,4 +1,4 @@
from typing import List from typing import List, Union, Optional
import databases import databases
import pytest import pytest
@ -38,8 +38,8 @@ class Category(ormar.Model):
class Meta(LocalMeta): class Meta(LocalMeta):
tablename = "categories" tablename = "categories"
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class ItemsXCategories(ormar.Model): class ItemsXCategories(ormar.Model):
@ -51,9 +51,9 @@ class Item(ormar.Model):
class Meta(LocalMeta): class Meta(LocalMeta):
pass pass
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
categories: ormar.ManyToMany(Category, through=ItemsXCategories) categories = ormar.ManyToMany(Category, through=ItemsXCategories)
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")
@ -77,7 +77,7 @@ async def create_item(item: Item):
@app.post("/items/add_category/", response_model=Item) @app.post("/items/add_category/", response_model=Item)
async def create_item(item: Item, category: Category): async def add_item_category(item: Item, category: Category):
await item.categories.add(category) await item.categories.add(category)
return item return item
@ -125,7 +125,9 @@ def test_all_endpoints():
def test_schema_modification(): def test_schema_modification():
schema = Item.schema() schema = Item.schema()
assert schema["properties"]["categories"]["type"] == "array" assert any(
x.get("type") == "array" for x in schema["properties"]["categories"]["anyOf"]
)
assert schema["properties"]["categories"]["title"] == "Categories" assert schema["properties"]["categories"]["title"] == "Categories"

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import sqlalchemy import sqlalchemy
from fastapi import FastAPI from fastapi import FastAPI
@ -18,8 +20,8 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Item(ormar.Model): class Item(ormar.Model):
@ -28,9 +30,9 @@ class Item(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@app.post("/items/", response_model=Item) @app.post("/items/", response_model=Item)

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import pytest import pytest
import sqlalchemy import sqlalchemy
@ -16,8 +18,8 @@ class Album(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Track(ormar.Model): class Track(ormar.Model):
@ -26,10 +28,10 @@ class Track(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album) album: Optional[Album] = ormar.ForeignKey(Album)
title: ormar.String(max_length=100) title: str = ormar.String(max_length=100)
position: ormar.Integer() position: int = ormar.Integer()
class Cover(ormar.Model): class Cover(ormar.Model):
@ -38,9 +40,9 @@ class Cover(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album, related_name="cover_pictures") album: Optional[Album] = ormar.ForeignKey(Album, related_name="cover_pictures")
title: ormar.String(max_length=100) title: str = ormar.String(max_length=100)
class Organisation(ormar.Model): class Organisation(ormar.Model):
@ -49,8 +51,12 @@ class Organisation(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
ident: ormar.String(max_length=100, choices=["ACME Ltd", "Other ltd"]) ident: str = ormar.String(max_length=100, choices=["ACME Ltd", "Other ltd"])
class Organization(object):
pass
class Team(ormar.Model): class Team(ormar.Model):
@ -59,9 +65,9 @@ class Team(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
org: ormar.ForeignKey(Organisation) org: Optional[Organisation] = ormar.ForeignKey(Organisation)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Member(ormar.Model): class Member(ormar.Model):
@ -70,9 +76,9 @@ class Member(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
team: ormar.ForeignKey(Team) team: Optional[Team] = ormar.ForeignKey(Team)
email: ormar.String(max_length=100) email: str = ormar.String(max_length=100)
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
from typing import List, Union, Optional
import databases import databases
import pytest import pytest
@ -18,9 +19,9 @@ class Author(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
first_name: ormar.String(max_length=80) first_name: str = ormar.String(max_length=80)
last_name: ormar.String(max_length=80) last_name: str = ormar.String(max_length=80)
class Category(ormar.Model): class Category(ormar.Model):
@ -29,8 +30,8 @@ class Category(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=40) name: str = ormar.String(max_length=40)
class PostCategory(ormar.Model): class PostCategory(ormar.Model):
@ -46,10 +47,12 @@ class Post(ormar.Model):
database = database database = database
metadata = metadata metadata = metadata
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
title: ormar.String(max_length=200) title: str = ormar.String(max_length=200)
categories: ormar.ManyToMany(Category, through=PostCategory) categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
author: ormar.ForeignKey(Author) Category, through=PostCategory
)
author: Optional[Author] = ormar.ForeignKey(Author)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")

View File

@ -1,13 +1,17 @@
# type: ignore
import asyncio
import datetime import datetime
import decimal import decimal
import pydantic import pydantic
import pytest import pytest
import sqlalchemy import sqlalchemy
import typing
import ormar.fields as fields import ormar
from ormar.exceptions import ModelDefinitionError from ormar.exceptions import ModelDefinitionError
from ormar.models import Model from ormar.models import Model
from tests.settings import DATABASE_URL
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
@ -17,18 +21,18 @@ class ExampleModel(Model):
tablename = "example" tablename = "example"
metadata = metadata metadata = metadata
test: fields.Integer(primary_key=True) test: int = ormar.Integer(primary_key=True)
test_string: fields.String(max_length=250) test_string: str = ormar.String(max_length=250)
test_text: fields.Text(default="") test_text: str = ormar.Text(default="")
test_bool: fields.Boolean(nullable=False) test_bool: bool = ormar.Boolean(nullable=False)
test_float: fields.Float() = None test_float: ormar.Float() = None # type: ignore
test_datetime: fields.DateTime(default=datetime.datetime.now) test_datetime = ormar.DateTime(default=datetime.datetime.now)
test_date: fields.Date(default=datetime.date.today) test_date = ormar.Date(default=datetime.date.today)
test_time: fields.Time(default=datetime.time) test_time = ormar.Time(default=datetime.time)
test_json: fields.JSON(default={}) test_json = ormar.JSON(default={})
test_bigint: fields.BigInteger(default=0) test_bigint: int = ormar.BigInteger(default=0)
test_decimal: fields.Decimal(scale=10, precision=2) test_decimal = ormar.Decimal(scale=2, precision=10)
test_decimal2: fields.Decimal(max_digits=10, decimal_places=2) test_decimal2 = ormar.Decimal(max_digits=10, decimal_places=2)
fields_to_check = [ fields_to_check = [
@ -46,11 +50,26 @@ fields_to_check = [
class ExampleModel2(Model): class ExampleModel2(Model):
class Meta: class Meta:
tablename = "example2" tablename = "examples"
metadata = metadata metadata = metadata
test: fields.Integer(primary_key=True) test: int = ormar.Integer(primary_key=True)
test_string: fields.String(max_length=250) test_string: str = ormar.String(max_length=250)
@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
loop.close()
@pytest.fixture(autouse=True, scope="module")
async def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.fixture() @pytest.fixture()
@ -117,29 +136,33 @@ def test_sqlalchemy_table_is_created(example):
assert all([field in example.Meta.table.columns for field in fields_to_check]) assert all([field in example.Meta.table.columns for field in fields_to_check])
def test_no_pk_in_model_definition(): @typing.no_type_check
with pytest.raises(ModelDefinitionError): def test_no_pk_in_model_definition(): # type: ignore
with pytest.raises(ModelDefinitionError): # type: ignore
class ExampleModel2(Model): class ExampleModel2(Model): # type: ignore
class Meta: class Meta:
tablename = "example3" tablename = "example2"
metadata = metadata metadata = metadata
test_string: fields.String(max_length=250) test_string: str = ormar.String(max_length=250) # type: ignore
@typing.no_type_check
def test_two_pks_in_model_definition(): def test_two_pks_in_model_definition():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
@typing.no_type_check
class ExampleModel2(Model): class ExampleModel2(Model):
class Meta: class Meta:
tablename = "example3" tablename = "example3"
metadata = metadata metadata = metadata
id: fields.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
test_string: fields.String(max_length=250, primary_key=True) test_string: str = ormar.String(max_length=250, primary_key=True)
@typing.no_type_check
def test_setting_pk_column_as_pydantic_only_in_model_definition(): def test_setting_pk_column_as_pydantic_only_in_model_definition():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
@ -148,9 +171,10 @@ def test_setting_pk_column_as_pydantic_only_in_model_definition():
tablename = "example4" tablename = "example4"
metadata = metadata metadata = metadata
test: fields.Integer(primary_key=True, pydantic_only=True) test: int = ormar.Integer(primary_key=True, pydantic_only=True)
@typing.no_type_check
def test_decimal_error_in_model_definition(): def test_decimal_error_in_model_definition():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
@ -159,9 +183,10 @@ def test_decimal_error_in_model_definition():
tablename = "example5" tablename = "example5"
metadata = metadata metadata = metadata
test: fields.Decimal(primary_key=True) test: decimal.Decimal = ormar.Decimal(primary_key=True)
@typing.no_type_check
def test_string_error_in_model_definition(): def test_string_error_in_model_definition():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
@ -170,9 +195,10 @@ def test_string_error_in_model_definition():
tablename = "example6" tablename = "example6"
metadata = metadata metadata = metadata
test: fields.String(primary_key=True) test: str = ormar.String(primary_key=True)
@typing.no_type_check
def test_json_conversion_in_model(): def test_json_conversion_in_model():
with pytest.raises(pydantic.ValidationError): with pytest.raises(pydantic.ValidationError):
ExampleModel( ExampleModel(

View File

@ -1,6 +1,6 @@
import asyncio import asyncio
import uuid import uuid
from datetime import datetime import datetime
from typing import List from typing import List
import databases import databases
@ -22,8 +22,8 @@ class JsonSample(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
test_json: ormar.JSON(nullable=True) test_json = ormar.JSON(nullable=True)
class UUIDSample(ormar.Model): class UUIDSample(ormar.Model):
@ -32,8 +32,8 @@ class UUIDSample(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.UUID(primary_key=True, default=uuid.uuid4) id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
test_text: ormar.Text() test_text: str = ormar.Text()
class User(ormar.Model): class User(ormar.Model):
@ -42,8 +42,8 @@ class User(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100, default="") name: str = ormar.String(max_length=100, default="")
class Product(ormar.Model): class Product(ormar.Model):
@ -52,11 +52,11 @@ class Product(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
rating: ormar.Integer(minimum=1, maximum=5) rating: int = ormar.Integer(minimum=1, maximum=5)
in_stock: ormar.Boolean(default=False) in_stock: bool = ormar.Boolean(default=False)
last_delivery: ormar.Date(default=datetime.now) last_delivery: datetime.date = ormar.Date(default=datetime.datetime.now)
country_name_choices = ("Canada", "Algeria", "United States") country_name_choices = ("Canada", "Algeria", "United States")
@ -70,12 +70,12 @@ class Country(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String( name: str = ormar.String(
max_length=9, choices=country_name_choices, default="Canada", max_length=9, choices=country_name_choices, default="Canada",
) )
taxed: ormar.Boolean(choices=country_taxed_choices, default=True) taxed: bool = ormar.Boolean(choices=country_taxed_choices, default=True)
country_code: ormar.Integer( country_code: int = ormar.Integer(
minimum=0, maximum=1000, choices=country_country_code_choices, default=1 minimum=0, maximum=1000, choices=country_country_code_choices, default=1
) )
@ -98,9 +98,9 @@ async def create_test_database():
def test_model_class(): def test_model_class():
assert list(User.Meta.model_fields.keys()) == ["id", "name"] assert list(User.Meta.model_fields.keys()) == ["id", "name"]
assert issubclass(User.Meta.model_fields["id"], pydantic.ConstrainedInt) assert issubclass(User.Meta.model_fields["id"], pydantic.fields.FieldInfo)
assert User.Meta.model_fields["id"].primary_key is True assert User.Meta.model_fields["id"].primary_key is True
assert issubclass(User.Meta.model_fields["name"], pydantic.ConstrainedStr) assert issubclass(User.Meta.model_fields["name"], pydantic.fields.FieldInfo)
assert User.Meta.model_fields["name"].max_length == 100 assert User.Meta.model_fields["name"].max_length == 100
assert isinstance(User.Meta.table, sqlalchemy.Table) assert isinstance(User.Meta.table, sqlalchemy.Table)
@ -215,7 +215,7 @@ async def test_model_filter():
assert product.pk is not None assert product.pk is not None
assert product.name == "T-Shirt" assert product.name == "T-Shirt"
assert product.rating == 5 assert product.rating == 5
assert product.last_delivery == datetime.now().date() assert product.last_delivery == datetime.datetime.now().date()
products = await Product.objects.all(rating__gte=2, in_stock=True) products = await Product.objects.all(rating__gte=2, in_stock=True)
assert len(products) == 2 assert len(products) == 2

View File

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
import databases import databases
import pytest import pytest
@ -35,8 +35,8 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Item(ormar.Model): class Item(ormar.Model):
@ -45,9 +45,9 @@ class Item(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
from typing import Optional
import databases import databases
import pytest import pytest
@ -17,8 +18,8 @@ class Department(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True, autoincrement=False) id: int = ormar.Integer(primary_key=True, autoincrement=False)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class SchoolClass(ormar.Model): class SchoolClass(ormar.Model):
@ -27,8 +28,8 @@ class SchoolClass(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Category(ormar.Model): class Category(ormar.Model):
@ -37,9 +38,9 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
department: ormar.ForeignKey(Department, nullable=False) department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
class Student(ormar.Model): class Student(ormar.Model):
@ -48,10 +49,10 @@ class Student(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
schoolclass: ormar.ForeignKey(SchoolClass) schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model): class Teacher(ormar.Model):
@ -60,10 +61,10 @@ class Teacher(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
schoolclass: ormar.ForeignKey(SchoolClass) schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")

View File

@ -0,0 +1,374 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
import ormar
from ormar.exceptions import NoMatch, MultipleMatches, RelationshipInstanceError
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Album(ormar.Model):
class Meta:
tablename = "albums"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Track(ormar.Model):
class Meta:
tablename = "tracks"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
title: str = ormar.String(max_length=100)
position: int = ormar.Integer()
class Cover(ormar.Model):
class Meta:
tablename = "covers"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album, related_name="cover_pictures")
title: str = ormar.String(max_length=100)
class Organisation(ormar.Model):
class Meta:
tablename = "org"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
ident: str = ormar.String(max_length=100, choices=["ACME Ltd", "Other ltd"])
class Team(ormar.Model):
class Meta:
tablename = "teams"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
org: Optional[Organisation] = ormar.ForeignKey(Organisation)
name: str = ormar.String(max_length=100)
class Member(ormar.Model):
class Meta:
tablename = "members"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
team: Optional[Team] = ormar.ForeignKey(Team)
email: str = ormar.String(max_length=100)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_wrong_query_foreign_key_type():
async with database:
with pytest.raises(RelationshipInstanceError):
Track(title="The Error", album="wrong_pk_type")
@pytest.mark.asyncio
async def test_setting_explicitly_empty_relation():
async with database:
track = Track(album=None, title="The Bird", position=1)
assert track.album is None
@pytest.mark.asyncio
async def test_related_name():
async with database:
async with database.transaction(force_rollback=True):
album = await Album.objects.create(name="Vanilla")
await Cover.objects.create(album=album, title="The cover file")
assert len(album.cover_pictures) == 1
@pytest.mark.asyncio
async def test_model_crud():
async with database:
async with database.transaction(force_rollback=True):
album = Album(name="Jamaica")
await album.save()
track1 = Track(album=album, title="The Bird", position=1)
track2 = Track(album=album, title="Heart don't stand a chance", position=2)
track3 = Track(album=album, title="The Waters", position=3)
await track1.save()
await track2.save()
await track3.save()
track = await Track.objects.get(title="The Bird")
assert track.album.pk == album.pk
assert isinstance(track.album, ormar.Model)
assert track.album.name is None
await track.album.load()
assert track.album.name == "Jamaica"
assert len(album.tracks) == 3
assert album.tracks[1].title == "Heart don't stand a chance"
album1 = await Album.objects.get(name="Jamaica")
assert album1.pk == album.pk
assert album1.tracks == []
await Track.objects.create(
album={"id": track.album.pk}, title="The Bird2", position=4
)
@pytest.mark.asyncio
async def test_select_related():
async with database:
async with database.transaction(force_rollback=True):
album = Album(name="Malibu")
await album.save()
track1 = Track(album=album, title="The Bird", position=1)
track2 = Track(album=album, title="Heart don't stand a chance", position=2)
track3 = Track(album=album, title="The Waters", position=3)
await track1.save()
await track2.save()
await track3.save()
fantasies = Album(name="Fantasies")
await fantasies.save()
track4 = Track(album=fantasies, title="Help I'm Alive", position=1)
track5 = Track(album=fantasies, title="Sick Muse", position=2)
track6 = Track(album=fantasies, title="Satellite Mind", position=3)
await track4.save()
await track5.save()
await track6.save()
track = await Track.objects.select_related("album").get(title="The Bird")
assert track.album.name == "Malibu"
tracks = await Track.objects.select_related("album").all()
assert len(tracks) == 6
@pytest.mark.asyncio
async def test_model_removal_from_relations():
async with database:
async with database.transaction(force_rollback=True):
album = Album(name="Chichi")
await album.save()
track1 = Track(album=album, title="The Birdman", position=1)
track2 = Track(album=album, title="Superman", position=2)
track3 = Track(album=album, title="Wonder Woman", position=3)
await track1.save()
await track2.save()
await track3.save()
assert len(album.tracks) == 3
await album.tracks.remove(track1)
assert len(album.tracks) == 2
assert track1.album is None
await track1.update()
track1 = await Track.objects.get(title="The Birdman")
assert track1.album is None
await album.tracks.add(track1)
assert len(album.tracks) == 3
assert track1.album == album
await track1.update()
track1 = await Track.objects.select_related("album__tracks").get(
title="The Birdman"
)
album = await Album.objects.select_related("tracks").get(name="Chichi")
assert track1.album == album
track1.remove(album)
assert track1.album is None
assert len(album.tracks) == 2
track2.remove(album)
assert track2.album is None
assert len(album.tracks) == 1
@pytest.mark.asyncio
async def test_fk_filter():
async with database:
async with database.transaction(force_rollback=True):
malibu = Album(name="Malibu%")
await malibu.save()
await Track.objects.create(album=malibu, title="The Bird", position=1)
await Track.objects.create(
album=malibu, title="Heart don't stand a chance", position=2
)
await Track.objects.create(album=malibu, title="The Waters", position=3)
fantasies = await Album.objects.create(name="Fantasies")
await Track.objects.create(
album=fantasies, title="Help I'm Alive", position=1
)
await Track.objects.create(album=fantasies, title="Sick Muse", position=2)
await Track.objects.create(
album=fantasies, title="Satellite Mind", position=3
)
tracks = (
await Track.objects.select_related("album")
.filter(album__name="Fantasies")
.all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = (
await Track.objects.select_related("album")
.filter(album__name__icontains="fan")
.all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = await Track.objects.filter(album__name__contains="Fan").all()
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = await Track.objects.filter(album__name__contains="Malibu%").all()
assert len(tracks) == 3
tracks = (
await Track.objects.filter(album=malibu).select_related("album").all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Malibu%"
tracks = await Track.objects.select_related("album").all(album=malibu)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Malibu%"
@pytest.mark.asyncio
async def test_multiple_fk():
async with database:
async with database.transaction(force_rollback=True):
acme = await Organisation.objects.create(ident="ACME Ltd")
red_team = await Team.objects.create(org=acme, name="Red Team")
blue_team = await Team.objects.create(org=acme, name="Blue Team")
await Member.objects.create(team=red_team, email="a@example.org")
await Member.objects.create(team=red_team, email="b@example.org")
await Member.objects.create(team=blue_team, email="c@example.org")
await Member.objects.create(team=blue_team, email="d@example.org")
other = await Organisation.objects.create(ident="Other ltd")
team = await Team.objects.create(org=other, name="Green Team")
await Member.objects.create(team=team, email="e@example.org")
members = (
await Member.objects.select_related("team__org")
.filter(team__org__ident="ACME Ltd")
.all()
)
assert len(members) == 4
for member in members:
assert member.team.org.ident == "ACME Ltd"
@pytest.mark.asyncio
async def test_wrong_choices():
async with database:
async with database.transaction(force_rollback=True):
with pytest.raises(ValueError):
await Organisation.objects.create(ident="Test 1")
@pytest.mark.asyncio
async def test_pk_filter():
async with database:
async with database.transaction(force_rollback=True):
fantasies = await Album.objects.create(name="Test")
track = await Track.objects.create(
album=fantasies, title="Test1", position=1
)
await Track.objects.create(album=fantasies, title="Test2", position=2)
await Track.objects.create(album=fantasies, title="Test3", position=3)
tracks = (
await Track.objects.select_related("album").filter(pk=track.pk).all()
)
assert len(tracks) == 1
tracks = (
await Track.objects.select_related("album")
.filter(position=2, album__name="Test")
.all()
)
assert len(tracks) == 1
@pytest.mark.asyncio
async def test_limit_and_offset():
async with database:
async with database.transaction(force_rollback=True):
fantasies = await Album.objects.create(name="Limitless")
await Track.objects.create(
id=None, album=fantasies, title="Sample", position=1
)
await Track.objects.create(album=fantasies, title="Sample2", position=2)
await Track.objects.create(album=fantasies, title="Sample3", position=3)
tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1
assert tracks[0].title == "Sample"
tracks = await Track.objects.limit(1).offset(1).all()
assert len(tracks) == 1
assert tracks[0].title == "Sample2"
@pytest.mark.asyncio
async def test_get_exceptions():
async with database:
async with database.transaction(force_rollback=True):
fantasies = await Album.objects.create(name="Test")
with pytest.raises(NoMatch):
await Album.objects.get(name="Test2")
await Track.objects.create(album=fantasies, title="Test1", position=1)
await Track.objects.create(album=fantasies, title="Test2", position=2)
await Track.objects.create(album=fantasies, title="Test3", position=3)
with pytest.raises(MultipleMatches):
await Track.objects.select_related("album").get(album=fantasies)
@pytest.mark.asyncio
async def test_wrong_model_passed_as_fk():
async with database:
async with database.transaction(force_rollback=True):
with pytest.raises(RelationshipInstanceError):
org = await Organisation.objects.create(ident="ACME Ltd")
await Track.objects.create(album=org, title="Test1", position=1)

View File

@ -21,8 +21,8 @@ class Model(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.String(primary_key=True, default=key, max_length=8) id: str = ormar.String(primary_key=True, default=key, max_length=8)
name: ormar.String(max_length=32) name: str = ormar.String(max_length=32)
@pytest.fixture(autouse=True, scope="function") @pytest.fixture(autouse=True, scope="function")

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import pytest import pytest
import sqlalchemy import sqlalchemy
@ -16,10 +18,10 @@ class Book(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
title: ormar.String(max_length=200) title: str = ormar.String(max_length=200)
author: ormar.String(max_length=100) author: str = ormar.String(max_length=100)
genre: ormar.String( genre: str = ormar.String(
max_length=100, max_length=100,
default="Fiction", default="Fiction",
choices=["Fiction", "Adventure", "Historic", "Fantasy"], choices=["Fiction", "Adventure", "Historic", "Fantasy"],
@ -32,9 +34,9 @@ class ToDo(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
text: ormar.String(max_length=500) text: str = ormar.String(max_length=500)
completed: ormar.Boolean(default=False) completed: bool = ormar.Boolean(default=False)
class Category(ormar.Model): class Category(ormar.Model):
@ -43,8 +45,8 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=500) name: str = ormar.String(max_length=500)
class Note(ormar.Model): class Note(ormar.Model):
@ -53,9 +55,9 @@ class Note(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
text: ormar.String(max_length=500) text: str = ormar.String(max_length=500)
category: ormar.ForeignKey(Category) category: Optional[Category] = ormar.ForeignKey(Category)
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

@ -1,4 +1,5 @@
import asyncio import asyncio
from typing import Optional
import databases import databases
import pytest import pytest
@ -17,8 +18,8 @@ class Department(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True, autoincrement=False) id: int = ormar.Integer(primary_key=True, autoincrement=False)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class SchoolClass(ormar.Model): class SchoolClass(ormar.Model):
@ -27,9 +28,9 @@ class SchoolClass(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
department: ormar.ForeignKey(Department, nullable=False) department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
class Category(ormar.Model): class Category(ormar.Model):
@ -38,8 +39,8 @@ class Category(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
class Student(ormar.Model): class Student(ormar.Model):
@ -48,10 +49,10 @@ class Student(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
schoolclass: ormar.ForeignKey(SchoolClass) schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model): class Teacher(ormar.Model):
@ -60,10 +61,10 @@ class Teacher(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
schoolclass: ormar.ForeignKey(SchoolClass) schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: ormar.ForeignKey(Category, nullable=True) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases import databases
import pydantic import pydantic
import pytest import pytest
@ -16,9 +18,9 @@ class Company(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100, nullable=False) name: str = ormar.String(max_length=100, nullable=False)
founded: ormar.Integer(nullable=True) founded: int = ormar.Integer(nullable=True)
class Car(ormar.Model): class Car(ormar.Model):
@ -27,13 +29,13 @@ class Car(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
manufacturer: ormar.ForeignKey(Company) manufacturer: Optional[Company] = ormar.ForeignKey(Company)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
year: ormar.Integer(nullable=True) year: int = ormar.Integer(nullable=True)
gearbox_type: ormar.String(max_length=20, nullable=True) gearbox_type: str = ormar.String(max_length=20, nullable=True)
gears: ormar.Integer(nullable=True) gears: int = ormar.Integer(nullable=True)
aircon_type: ormar.String(max_length=20, nullable=True) aircon_type: str = ormar.String(max_length=20, nullable=True)
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")

View File

@ -20,11 +20,11 @@ class Product(ormar.Model):
metadata = metadata metadata = metadata
database = database database = database
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
company: ormar.String(max_length=200, server_default='Acme') company: str = ormar.String(max_length=200, server_default="Acme")
sort_order: ormar.Integer(server_default=text("10")) sort_order: int = ormar.Integer(server_default=text("10"))
created: ormar.DateTime(server_default=func.now()) created: datetime = ormar.DateTime(server_default=func.now())
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -44,42 +44,44 @@ async def create_test_database():
def test_table_defined_properly(): def test_table_defined_properly():
assert Product.Meta.model_fields['created'].nullable assert Product.Meta.model_fields["created"].nullable
assert not Product.__fields__['created'].required assert not Product.__fields__["created"].required
assert Product.Meta.table.columns['created'].server_default.arg.name == 'now' assert Product.Meta.table.columns["created"].server_default.arg.name == "now"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_model_creation(): async def test_model_creation():
async with database: async with database:
async with database.transaction(force_rollback=True): async with database.transaction(force_rollback=True):
p1 = Product(name='Test') p1 = Product(name="Test")
assert p1.created is None assert p1.created is None
await p1.save() await p1.save()
await p1.load() await p1.load()
assert p1.created is not None assert p1.created is not None
assert p1.company == 'Acme' assert p1.company == "Acme"
assert p1.sort_order == 10 assert p1.sort_order == 10
date = datetime.strptime('2020-10-27 11:30', '%Y-%m-%d %H:%M') date = datetime.strptime("2020-10-27 11:30", "%Y-%m-%d %H:%M")
p3 = await Product.objects.create(name='Test2', created=date, company='Roadrunner', sort_order=1) p3 = await Product.objects.create(
name="Test2", created=date, company="Roadrunner", sort_order=1
)
assert p3.created is not None assert p3.created is not None
assert p3.created == date assert p3.created == date
assert p1.created != p3.created assert p1.created != p3.created
assert p3.company == 'Roadrunner' assert p3.company == "Roadrunner"
assert p3.sort_order == 1 assert p3.sort_order == 1
p3 = await Product.objects.get(name='Test2') p3 = await Product.objects.get(name="Test2")
assert p3.company == 'Roadrunner' assert p3.company == "Roadrunner"
assert p3.sort_order == 1 assert p3.sort_order == 1
time.sleep(1) time.sleep(1)
p2 = await Product.objects.create(name='Test3') p2 = await Product.objects.create(name="Test3")
assert p2.created is not None assert p2.created is not None
assert p2.company == 'Acme' assert p2.company == "Acme"
assert p2.sort_order == 10 assert p2.sort_order == 10
if Product.db_backend_name() != 'postgresql': if Product.db_backend_name() != "postgresql":
# postgres use transaction timestamp so it will remain the same # postgres use transaction timestamp so it will remain the same
assert p1.created != p2.created # pragma nocover assert p1.created != p2.created # pragma nocover

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
import sqlite3 import sqlite3
import asyncpg import asyncpg # type: ignore
import databases import databases
import pymysql import pymysql
import pytest import pytest
@ -21,9 +21,9 @@ class Product(ormar.Model):
database = database database = database
constraints = [ormar.UniqueColumns("name", "company")] constraints = [ormar.UniqueColumns("name", "company")]
id: ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: ormar.String(max_length=100) name: str = ormar.String(max_length=100)
company: ormar.String(max_length=200) company: str = ormar.String(max_length=200)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")