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=mysql://localhost/test_database 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:
- codecov

View File

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

View File

@ -44,9 +44,9 @@ git checkout -b my-new-feature-branch
# 5. Formatting and linting
# 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
black ormar
black ormar tests
flake8 ormar
mypy --config-file mypy.ini ormar
mypy --config-file mypy.ini ormar tests
# 6. Run tests
# on localhost all tests are run against sglite backend

View File

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

View File

@ -75,8 +75,11 @@ class Album(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(length=100)
# note that type hints are optional so
# 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):
@ -85,10 +88,10 @@ class Track(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album)
title: ormar.String(length=100)
position: ormar.Integer()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] =ormar.ForeignKey(Album)
title: str = ormar.String(length=100)
position: int = ormar.Integer()
# 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`.
```Python
id: ormar.Integer(primary_key=True, autoincrement=False)
id: int = ormar.Integer(primary_key=True, autoincrement=False)
```
### 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
```Python hl_lines="9"
```Python hl_lines="21"
--8<-- "../docs_src/models/docs009.py"
```
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"
```
### 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
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 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
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:
```Python hl_lines="19"
```Python hl_lines="20"
--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
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.
!!!info
To see which one are supported and how to construct relations visit [relations][relations].
Given the Models like this
```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.
```python hl_lines="24-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: 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
```Python hl_lines="26-28"
--8<-- "../docs_src/queries/docs002.py"
```
!!!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(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one.
```python hl_lines="24-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: 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
```Python hl_lines="26-32"
--8<-- "../docs_src/queries/docs003.py"
```
!!!note
@ -177,36 +130,8 @@ Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="20-26"
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: 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
```python hl_lines="21-27"
--8<-- "../docs_src/queries/docs004.py"
```
### 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.
```python hl_lines="23-27"
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: 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
```python hl_lines="26-30"
--8<-- "../docs_src/queries/docs005.py"
```
### 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.
```python hl_lines="48 60 61 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: 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
```python hl_lines="47 59 60 66"
--8<-- "../docs_src/queries/docs006.py"
```
!!!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()`
[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`.
```Python hl_lines="27"
```Python hl_lines="29"
--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:
```Python hl_lines="27 33"
```Python hl_lines="29 35"
--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:
```Python hl_lines="27 33"
```Python hl_lines="29 35"
--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.
```Python hl_lines="32-33"
```Python hl_lines="34-35"
--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.
```Python hl_lines="35-36"
```Python hl_lines="37-38"
--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.
```Python hl_lines="38-39"
```Python hl_lines="40-41"
--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).
```Python hl_lines="41-42"
```Python hl_lines="43-44"
--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.
#### 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
# 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]
#### Removing related models
#### remove()
Removal of the related model one by one.
Removes also the relation in the database.
```python
# Removal of the relationship by one
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()
```
#### All other queryset methods
#### Other queryset methods
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
```
Currently supported methods are:
!!!tip
To learn more about available QuerySet methods visit [queries][queries]
##### get()
##### all()
##### filter()
##### select_related()
##### limit()
##### offset()
##### count()
##### exists()
[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
* Fix
* Fix postgresql check to avoid exceptions with drivers not installed if using different backend
# 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 sqlalchemy
@ -32,8 +32,8 @@ class Category(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Item(ormar.Model):
@ -42,9 +42,9 @@ class Item(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@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 sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
@ -21,14 +23,14 @@ class Course(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
completed: ormar.Boolean(default=False)
department: ormar.ForeignKey(Department)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
department: Optional[Department] = ormar.ForeignKey(Department)
department = Department(name='Science')
course = Course(name='Math', completed=False, department=department)
department = Department(name="Science")
course = Course(name="Math", completed=False, department=department)
print(department.courses[0])
# Will produce:

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

@ -8,13 +8,13 @@ metadata = sqlalchemy.MetaData()
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
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
completed: ormar.Boolean(default=False)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
print(Course.Meta.table.columns)

View File

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

View File

@ -14,8 +14,8 @@ class Course(ormar.Model):
# define your constraints in Meta class of the model
# it's a list that can contain multiple constraints
# 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)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)

View File

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

View File

@ -13,7 +13,7 @@ class Child(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(name='child_id', primary_key=True)
first_name: ormar.String(name='fname', max_length=100)
last_name: ormar.String(name='lname', max_length=100)
born_year: ormar.Integer(name='year_born', nullable=True)
id: int = ormar.Integer(name="child_id", primary_key=True)
first_name: str = ormar.String(name="fname", max_length=100)
last_name: str = ormar.String(name="lname", max_length=100)
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 Meta:
tablename = "music_albums"
metadata = metadata
database = database
id: ormar.Integer(name='album_id', primary_key=True)
name: ormar.String(name='album_name', max_length=100)
artist: ormar.ForeignKey(Artist, name='artist_id')
id: int = ormar.Integer(name="album_id", primary_key=True)
name: str = ormar.String(name="album_name", max_length=100)
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 Meta:
tablename = "children_x_artists"
@ -11,8 +21,8 @@ class Artist(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(name='artist_id', primary_key=True)
first_name: ormar.String(name='fname', max_length=100)
last_name: ormar.String(name='lname', max_length=100)
born_year: ormar.Integer(name='year')
children: ormar.ManyToMany(Child, through=ArtistChildren)
id: int = ormar.Integer(name="artist_id", primary_key=True)
first_name: str = ormar.String(name="fname", max_length=100)
last_name: str = ormar.String(name="lname", max_length=100)
born_year: int = ormar.Integer(name="year")
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 ormar
import sqlalchemy
@ -12,8 +14,8 @@ class Album(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Track(ormar.Model):
@ -22,7 +24,7 @@ class Track(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
album: ormar.ForeignKey(Album)
title: ormar.String(max_length=100)
position: ormar.Integer()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
title: str = ormar.String(max_length=100)
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 sqlalchemy
@ -12,8 +14,8 @@ class Department(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
@ -21,22 +23,22 @@ class Course(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
completed: ormar.Boolean(default=False)
department: ormar.ForeignKey(Department)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
department: Optional[Union[Department, Dict]] = ormar.ForeignKey(Department)
department = Department(name='Science')
department = Department(name="Science")
# 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
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
course3 = Course(name='Math III', completed=False, department=department.dict())
course3 = Course(name="Math III", completed=False, department=department.dict())
# 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 ormar
import sqlalchemy
@ -12,9 +14,9 @@ class Author(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
first_name: ormar.String(max_length=80)
last_name: ormar.String(max_length=80)
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
class Category(ormar.Model):
@ -23,8 +25,8 @@ class Category(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=40)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=40)
class PostCategory(ormar.Model):
@ -42,7 +44,9 @@ class Post(ormar.Model):
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
categories: ormar.ManyToMany(Category, through=PostCategory)
author: ormar.ForeignKey(Author)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
Category, through=PostCategory
)
author: Optional[Author] = ormar.ForeignKey(Author)

View File

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

View File

@ -1,5 +1,10 @@
[mypy]
python_version = 3.8
plugins = pydantic.mypy
[mypy-sqlalchemy.*]
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.fields import (
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
from ormar.fields import ( # noqa: I100
BigInteger,
Boolean,
Date,
@ -17,6 +18,7 @@ from ormar.fields import (
UniqueColumns,
)
from ormar.models import Model
from ormar.models.metaclass import ModelMeta
from ormar.queryset import QuerySet
from ormar.relations import RelationType
@ -28,8 +30,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
__version__ = "0.3.11"
__version__ = "0.4.0"
__all__ = [
"Integer",
"BigInteger",
@ -54,4 +55,7 @@ __all__ = [
"Undefined",
"UUID",
"UniqueColumns",
"QuerySetProtocol",
"RelationProtocol",
"ModelMeta",
]

View File

@ -1,5 +1,6 @@
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
import pydantic
import sqlalchemy
from pydantic import Field, typing
from pydantic.fields import FieldInfo
@ -11,7 +12,7 @@ if TYPE_CHECKING: # pragma no cover
from ormar.models import NewBaseModel
class BaseField:
class BaseField(FieldInfo):
__type__ = None
column_type: sqlalchemy.Column
@ -32,6 +33,28 @@ class BaseField:
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
def default_value(cls, use_server: bool = False) -> Optional[FieldInfo]:
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
from sqlalchemy import UniqueConstraint
@ -37,10 +37,16 @@ def ForeignKey( # noqa CFQ002
virtual: bool = False,
onupdate: str = None,
ondelete: str = None,
) -> Type["ForeignKeyField"]:
) -> Any:
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(
__type__=__type__,
to=to,
name=name,
nullable=nullable,
@ -50,7 +56,7 @@ def ForeignKey( # noqa CFQ002
)
],
unique=unique,
column_type=to_field.type_.column_type,
column_type=to_field.column_type,
related_name=related_name,
virtual=virtual,
primary_key=False,
@ -58,7 +64,6 @@ def ForeignKey( # noqa CFQ002
pydantic_only=False,
default=None,
server_default=None,
__pydantic_model__=to,
)
return type("ForeignKey", (ForeignKeyField, BaseField), namespace)
@ -70,14 +75,6 @@ class ForeignKeyField(BaseField):
related_name: str
virtual: bool
@classmethod
def __get_validators__(cls) -> Generator:
yield cls.validate
@classmethod
def validate(cls, value: Any) -> Any:
return value
@classmethod
def _extract_model_from_sequence(
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.foreign_key import ForeignKeyField
@ -15,17 +16,25 @@ def ManyToMany(
*,
name: str = None,
unique: bool = False,
related_name: str = None,
virtual: bool = False,
) -> Type["ManyToManyField"]:
to_field = to.__fields__[to.Meta.pkname]
**kwargs: Any
) -> 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(
__type__=__type__,
to=to,
through=through,
name=name,
nullable=True,
unique=unique,
column_type=to_field.type_.column_type,
column_type=to_field.column_type,
related_name=related_name,
virtual=virtual,
primary_key=False,
@ -33,20 +42,10 @@ def ManyToMany(
pydantic_only=False,
default=None,
server_default=None,
__pydantic_model__=to,
# __origin__=List,
# __args__=[Optional[to]]
)
return type("ManyToMany", (ManyToManyField, BaseField), namespace)
class ManyToManyField(ForeignKeyField):
class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol):
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 decimal
import uuid
from typing import Any, Optional, Type
from typing import Any, Optional, TYPE_CHECKING, Type
import pydantic
import sqlalchemy
@ -20,7 +20,7 @@ def is_field_nullable(
class ModelFieldFactory:
_bases: Any = BaseField
_bases: Any = (BaseField,)
_type: Any = None
def __new__(cls, *args: Any, **kwargs: Any) -> Type[BaseField]: # type: ignore
@ -56,8 +56,7 @@ class ModelFieldFactory:
pass
class String(ModelFieldFactory):
_bases = (pydantic.ConstrainedStr, BaseField)
class String(ModelFieldFactory, str):
_type = str
def __new__( # type: ignore # noqa CFQ002
@ -95,8 +94,7 @@ class String(ModelFieldFactory):
)
class Integer(ModelFieldFactory):
_bases = (pydantic.ConstrainedInt, BaseField)
class Integer(ModelFieldFactory, int):
_type = int
def __new__( # type: ignore
@ -130,8 +128,7 @@ class Integer(ModelFieldFactory):
return sqlalchemy.Integer()
class Text(ModelFieldFactory):
_bases = (pydantic.ConstrainedStr, BaseField)
class Text(ModelFieldFactory, str):
_type = str
def __new__( # type: ignore
@ -153,8 +150,7 @@ class Text(ModelFieldFactory):
return sqlalchemy.Text()
class Float(ModelFieldFactory):
_bases = (pydantic.ConstrainedFloat, BaseField)
class Float(ModelFieldFactory, float):
_type = float
def __new__( # type: ignore
@ -182,8 +178,15 @@ class Float(ModelFieldFactory):
return sqlalchemy.Float()
class Boolean(ModelFieldFactory):
_bases = (int, BaseField)
if TYPE_CHECKING: # pragma: nocover
def Boolean(**kwargs: Any) -> bool:
pass
else:
class Boolean(ModelFieldFactory, int):
_type = bool
@classmethod
@ -191,8 +194,7 @@ class Boolean(ModelFieldFactory):
return sqlalchemy.Boolean()
class DateTime(ModelFieldFactory):
_bases = (datetime.datetime, BaseField)
class DateTime(ModelFieldFactory, datetime.datetime):
_type = datetime.datetime
@classmethod
@ -200,8 +202,7 @@ class DateTime(ModelFieldFactory):
return sqlalchemy.DateTime()
class Date(ModelFieldFactory):
_bases = (datetime.date, BaseField)
class Date(ModelFieldFactory, datetime.date):
_type = datetime.date
@classmethod
@ -209,8 +210,7 @@ class Date(ModelFieldFactory):
return sqlalchemy.Date()
class Time(ModelFieldFactory):
_bases = (datetime.time, BaseField)
class Time(ModelFieldFactory, datetime.time):
_type = datetime.time
@classmethod
@ -218,8 +218,7 @@ class Time(ModelFieldFactory):
return sqlalchemy.Time()
class JSON(ModelFieldFactory):
_bases = (pydantic.Json, BaseField)
class JSON(ModelFieldFactory, pydantic.Json):
_type = pydantic.Json
@classmethod
@ -227,8 +226,7 @@ class JSON(ModelFieldFactory):
return sqlalchemy.JSON()
class BigInteger(Integer):
_bases = (pydantic.ConstrainedInt, BaseField)
class BigInteger(Integer, int):
_type = int
def __new__( # type: ignore
@ -262,8 +260,7 @@ class BigInteger(Integer):
return sqlalchemy.BigInteger()
class Decimal(ModelFieldFactory):
_bases = (pydantic.ConstrainedDecimal, BaseField)
class Decimal(ModelFieldFactory, decimal.Decimal):
_type = decimal.Decimal
def __new__( # type: ignore # noqa CFQ002
@ -290,14 +287,14 @@ class Decimal(ModelFieldFactory):
kwargs["le"] = kwargs["maximum"]
if kwargs.get("max_digits"):
kwargs["scale"] = kwargs["max_digits"]
elif kwargs.get("scale"):
kwargs["max_digits"] = kwargs["scale"]
kwargs["precision"] = kwargs["max_digits"]
elif kwargs.get("precision"):
kwargs["max_digits"] = kwargs["precision"]
if kwargs.get("decimal_places"):
kwargs["precision"] = kwargs["decimal_places"]
elif kwargs.get("precision"):
kwargs["decimal_places"] = kwargs["precision"]
kwargs["scale"] = kwargs["decimal_places"]
elif kwargs.get("scale"):
kwargs["decimal_places"] = kwargs["scale"]
return super().__new__(cls, **kwargs)
@ -317,8 +314,7 @@ class Decimal(ModelFieldFactory):
)
class UUID(ModelFieldFactory):
_bases = (uuid.UUID, BaseField)
class UUID(ModelFieldFactory, uuid.UUID):
_type = uuid.UUID
@classmethod

View File

@ -1,11 +1,13 @@
import logging
import warnings
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union
import databases
import pydantic
import sqlalchemy
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
import ormar # noqa I100
@ -179,44 +181,49 @@ def register_relation_in_alias_manager(
def populate_default_pydantic_field_value(
type_: Type[BaseField], field: str, attrs: dict
ormar_field: Type[BaseField], field_name: str, attrs: dict
) -> dict:
def_value = type_.default_value()
curr_def_value = attrs.get(field, "NONE")
if curr_def_value == "NONE" and isinstance(def_value, FieldInfo):
attrs[field] = def_value
elif curr_def_value == "NONE" and type_.nullable:
attrs[field] = FieldInfo(default=None)
curr_def_value = attrs.get(field_name, ormar.Undefined)
if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
curr_def_value = ormar.Undefined
if curr_def_value is None:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
else:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
return attrs
def populate_pydantic_default_values(attrs: Dict) -> Dict:
for field, type_ in attrs["__annotations__"].items():
if issubclass(type_, BaseField):
if type_.name is None:
type_.name = field
attrs = populate_default_pydantic_field_value(type_, field, attrs)
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)
def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
model_fields = {}
potential_fields = {
k: v
for k, v in attrs["__annotations__"].items()
if lenient_issubclass(v, BaseField)
}
new_model.Meta.model_fields = model_fields
return new_model
if potential_fields:
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(
@ -261,7 +268,7 @@ def populate_meta_sqlalchemy_table_if_required(
def get_pydantic_base_orm_config() -> Type[BaseConfig]:
class Config(BaseConfig):
orm_mode = True
arbitrary_types_allowed = True
# arbitrary_types_allowed = True
return Config
@ -305,7 +312,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
) -> "ModelMetaclass":
attrs["Config"] = get_pydantic_base_orm_config()
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
mcs, name, bases, attrs
)
@ -313,7 +320,8 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
if hasattr(new_model, "Meta"):
if not hasattr(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_sqlalchemy_table_if_required(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__"]:
field_name = new_model.Meta.pkname
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(
field, field_name, attrs # type: ignore
)

View File

@ -1,11 +1,12 @@
import itertools
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, TypeVar
import sqlalchemy
import ormar.queryset # noqa I100
from ormar.fields.many_to_many import ManyToManyField
from ormar.models import NewBaseModel # noqa I100
from ormar.models.metaclass import ModelMeta
def group_related_list(list_: List) -> Dict:
@ -23,18 +24,34 @@ def group_related_list(list_: List) -> Dict:
return test_dict
if TYPE_CHECKING: # pragma nocover
from ormar import QuerySet
T = TypeVar("T", bound="Model")
class Model(NewBaseModel):
__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
def from_row( # noqa CCR001
cls,
cls: Type[T],
row: sqlalchemy.engine.ResultProxy,
select_related: List = None,
related_models: Any = None,
previous_table: str = None,
fields: List = None,
) -> Optional["Model"]:
) -> Optional[T]:
item: Dict[str, Any] = {}
select_related = select_related or []
@ -66,7 +83,9 @@ class Model(NewBaseModel):
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
@classmethod
@ -124,7 +143,7 @@ class Model(NewBaseModel):
return item
async def save(self) -> "Model":
async def save(self: T) -> T:
self_fields = self._extract_model_db_fields()
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)
return self
async def update(self, **kwargs: Any) -> "Model":
async def update(self: T, **kwargs: Any) -> T:
if kwargs:
new_values = {**self.dict(), **kwargs}
self.from_dict(new_values)
@ -151,13 +170,13 @@ class Model(NewBaseModel):
await self.Meta.database.execute(expr)
return self
async def delete(self) -> int:
async def delete(self: T) -> int:
expr = self.Meta.table.delete()
expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname)))
result = await self.Meta.database.execute(expr)
return result
async def load(self) -> "Model":
async def load(self: T) -> T:
expr = self.Meta.table.select().where(self.pk_column == self.pk)
row = await self.Meta.database.fetch_one(expr)
if not row: # pragma nocover

View File

@ -1,5 +1,5 @@
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
from ormar.exceptions import RelationshipInstanceError
@ -11,6 +11,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
Field = TypeVar("Field", bound=BaseField)
@ -135,7 +137,7 @@ class ModelTableProxy:
if field.to == related.__class__ or field.to.Meta == related.Meta:
return name
# fallback for not registered relation
if register_missing:
if register_missing: # pragma nocover
expand_reverse_relationships(related.__class__) # type: ignore
return ModelTableProxy.resolve_relation_name(
item, related, register_missing=False
@ -177,7 +179,7 @@ class ModelTableProxy:
return new_kwargs
@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"] = []
for index, model in enumerate(result_rows):
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,
Callable,
Dict,
List,
Mapping,
Optional,
Sequence,
TYPE_CHECKING,
Type,
TypeVar,
Union,
)
@ -27,7 +28,9 @@ from ormar.relations.alias_manager import AliasManager
from ormar.relations.relation_manager import RelationsManager
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]
DictStrAny = Dict[str, Any]
@ -52,7 +55,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
Meta: ModelMeta
# 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_saved", False)
@ -73,7 +76,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
if "pk" in kwargs:
kwargs[self.Meta.pkname] = kwargs.pop("pk")
# build the models to set them and validate but don't register
kwargs = {
new_kwargs = {
k: self._convert_json(
k,
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(
self, kwargs # type: ignore
self, new_kwargs # type: ignore
)
if validation_error and not pk_only:
raise validation_error
@ -96,7 +99,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
# register the columns models after initialization
for related in self.extract_related_names():
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
@ -133,7 +136,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
def _extract_related_model_instead_of_field(
self, item: str
) -> Optional[Union["Model", List["Model"]]]:
) -> Optional[Union["T", Sequence["T"]]]:
alias = self.get_column_alias(item)
if alias in self._orm:
return self._orm.get(alias)
@ -170,7 +173,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
def db_backend_name(cls) -> str:
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)
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 sqlalchemy
@ -39,7 +39,7 @@ class QuerySet:
def __get__(
self,
instance: Union["QuerySet", "QuerysetProxy"],
instance: Optional[Union["QuerySet", "QuerysetProxy"]],
owner: Union[Type["Model"], Type["QuerysetProxy"]],
) -> "QuerySet":
if issubclass(owner, ormar.Model):
@ -59,7 +59,7 @@ class QuerySet:
raise ValueError("Model class of QuerySet is not initialized")
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 = [
self.model.from_row(
row, select_related=self._select_related, fields=self._columns
@ -87,7 +87,7 @@ class QuerySet:
return new_kwargs
@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:
raise NoMatch()
if len(rows) > 1:
@ -267,7 +267,7 @@ class QuerySet:
model = await self.get(pk=kwargs[pk_name])
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:
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
@ -7,8 +7,10 @@ if TYPE_CHECKING: # pragma no cover
from ormar.models import Model
from ormar.queryset import QuerySet
T = TypeVar("T", bound=Model)
class QuerysetProxy:
class QuerysetProxy(ormar.QuerySetProtocol):
if TYPE_CHECKING: # pragma no cover
relation: "Relation"
@ -26,27 +28,28 @@ class QuerysetProxy:
def queryset(self, value: "QuerySet") -> None:
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:
owner = self.relation._owner
rel_name = owner.resolve_relation_name(owner, 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):
for subchild in child:
self._assign_child_to_parent(subchild)
else:
assert isinstance(child, ormar.Model)
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)
owner_column = self.relation._owner.get_name()
child_column = child.get_name()
kwargs = {owner_column: self.relation._owner, child_column: child}
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)
owner_column = self.relation._owner.get_name()
child_column = child.get_name()
@ -88,7 +91,7 @@ class QuerysetProxy:
self._register_related(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)
self._register_related(all_items)
return all_items

View File

@ -1,5 +1,5 @@
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
from ormar.exceptions import RelationshipInstanceError # noqa I100
@ -11,6 +11,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar.relations import RelationsManager
from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
class RelationType(Enum):
PRIMARY = 1
@ -23,15 +25,15 @@ class Relation:
self,
manager: "RelationsManager",
type_: RelationType,
to: Type["Model"],
through: Type["Model"] = None,
to: Type["T"],
through: Type["T"] = None,
) -> None:
self.manager = manager
self._owner: "Model" = manager.owner
self._type: RelationType = type_
self.to: Type["Model"] = to
self.through: Optional[Type["Model"]] = through
self.related_models: Optional[Union[RelationProxy, "Model"]] = (
self.to: Type["T"] = to
self.through: Optional[Type["T"]] = through
self.related_models: Optional[Union[RelationProxy, "T"]] = (
RelationProxy(relation=self)
if type_ in (RelationType.REVERSE, RelationType.MULTIPLE)
else None
@ -50,7 +52,7 @@ class Relation:
self.related_models.pop(ind)
return None
def add(self, child: "Model") -> None:
def add(self, child: "T") -> None:
relation_name = self._owner.resolve_relation_name(self._owner, child)
if self._type == RelationType.PRIMARY:
self.related_models = child
@ -77,7 +79,7 @@ class Relation:
self.related_models.pop(position) # type: ignore
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
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 ormar.fields import BaseField
@ -14,6 +14,8 @@ if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.models import NewBaseModel
T = TypeVar("T", bound=Model)
class RelationsManager:
def __init__(
@ -46,7 +48,7 @@ class RelationsManager:
def __contains__(self, item: str) -> bool:
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)
if relation is not None:
return relation.get()

View File

@ -72,6 +72,6 @@ class RelationProxy(list):
if self.relation._type == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.create_through_instance(item)
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])
setattr(item, rel_name, self._owner)

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import datetime
import os
import databases
import pydantic
import pytest
import sqlalchemy
@ -22,14 +23,14 @@ class Example(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=200, default="aaa")
created: ormar.DateTime(default=datetime.datetime.now)
created_day: ormar.Date(default=datetime.date.today)
created_time: ormar.Time(default=time)
description: ormar.Text(nullable=True)
value: ormar.Float(nullable=True)
data: ormar.JSON(default={})
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200, default="aaa")
created: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
created_day: datetime.date = ormar.Date(default=datetime.date.today)
created_time: datetime.time = ormar.Time(default=time)
description: str = ormar.Text(nullable=True)
value: float = ormar.Float(nullable=True)
data: pydantic.Json = ormar.JSON(default={})
@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 pytest
@ -38,8 +38,8 @@ class Category(ormar.Model):
class Meta(LocalMeta):
tablename = "categories"
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class ItemsXCategories(ormar.Model):
@ -51,9 +51,9 @@ class Item(ormar.Model):
class Meta(LocalMeta):
pass
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
categories: ormar.ManyToMany(Category, through=ItemsXCategories)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
categories = ormar.ManyToMany(Category, through=ItemsXCategories)
@pytest.fixture(autouse=True, scope="module")
@ -77,7 +77,7 @@ async def create_item(item: 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)
return item
@ -125,7 +125,9 @@ def test_all_endpoints():
def test_schema_modification():
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"

View File

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

View File

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

View File

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

View File

@ -1,13 +1,17 @@
# type: ignore
import asyncio
import datetime
import decimal
import pydantic
import pytest
import sqlalchemy
import typing
import ormar.fields as fields
import ormar
from ormar.exceptions import ModelDefinitionError
from ormar.models import Model
from tests.settings import DATABASE_URL
metadata = sqlalchemy.MetaData()
@ -17,18 +21,18 @@ class ExampleModel(Model):
tablename = "example"
metadata = metadata
test: fields.Integer(primary_key=True)
test_string: fields.String(max_length=250)
test_text: fields.Text(default="")
test_bool: fields.Boolean(nullable=False)
test_float: fields.Float() = None
test_datetime: fields.DateTime(default=datetime.datetime.now)
test_date: fields.Date(default=datetime.date.today)
test_time: fields.Time(default=datetime.time)
test_json: fields.JSON(default={})
test_bigint: fields.BigInteger(default=0)
test_decimal: fields.Decimal(scale=10, precision=2)
test_decimal2: fields.Decimal(max_digits=10, decimal_places=2)
test: int = ormar.Integer(primary_key=True)
test_string: str = ormar.String(max_length=250)
test_text: str = ormar.Text(default="")
test_bool: bool = ormar.Boolean(nullable=False)
test_float: ormar.Float() = None # type: ignore
test_datetime = ormar.DateTime(default=datetime.datetime.now)
test_date = ormar.Date(default=datetime.date.today)
test_time = ormar.Time(default=datetime.time)
test_json = ormar.JSON(default={})
test_bigint: int = ormar.BigInteger(default=0)
test_decimal = ormar.Decimal(scale=2, precision=10)
test_decimal2 = ormar.Decimal(max_digits=10, decimal_places=2)
fields_to_check = [
@ -46,11 +50,26 @@ fields_to_check = [
class ExampleModel2(Model):
class Meta:
tablename = "example2"
tablename = "examples"
metadata = metadata
test: fields.Integer(primary_key=True)
test_string: fields.String(max_length=250)
test: int = ormar.Integer(primary_key=True)
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()
@ -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])
def test_no_pk_in_model_definition():
with pytest.raises(ModelDefinitionError):
@typing.no_type_check
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:
tablename = "example3"
tablename = "example2"
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():
with pytest.raises(ModelDefinitionError):
@typing.no_type_check
class ExampleModel2(Model):
class Meta:
tablename = "example3"
metadata = metadata
id: fields.Integer(primary_key=True)
test_string: fields.String(max_length=250, primary_key=True)
id: int = ormar.Integer(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():
with pytest.raises(ModelDefinitionError):
@ -148,9 +171,10 @@ def test_setting_pk_column_as_pydantic_only_in_model_definition():
tablename = "example4"
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():
with pytest.raises(ModelDefinitionError):
@ -159,9 +183,10 @@ def test_decimal_error_in_model_definition():
tablename = "example5"
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():
with pytest.raises(ModelDefinitionError):
@ -170,9 +195,10 @@ def test_string_error_in_model_definition():
tablename = "example6"
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():
with pytest.raises(pydantic.ValidationError):
ExampleModel(

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
@ -16,10 +18,10 @@ class Book(ormar.Model):
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(
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"],
@ -32,9 +34,9 @@ class ToDo(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
text: ormar.String(max_length=500)
completed: ormar.Boolean(default=False)
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
completed: bool = ormar.Boolean(default=False)
class Category(ormar.Model):
@ -43,8 +45,8 @@ class Category(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=500)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=500)
class Note(ormar.Model):
@ -53,9 +55,9 @@ class Note(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
text: ormar.String(max_length=500)
category: ormar.ForeignKey(Category)
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
category: Optional[Category] = ormar.ForeignKey(Category)
@pytest.fixture(autouse=True, scope="module")

View File

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

View File

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

View File

@ -20,11 +20,11 @@ class Product(ormar.Model):
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
company: ormar.String(max_length=200, server_default='Acme')
sort_order: ormar.Integer(server_default=text("10"))
created: ormar.DateTime(server_default=func.now())
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
company: str = ormar.String(max_length=200, server_default="Acme")
sort_order: int = ormar.Integer(server_default=text("10"))
created: datetime = ormar.DateTime(server_default=func.now())
@pytest.fixture(scope="module")
@ -44,42 +44,44 @@ async def create_test_database():
def test_table_defined_properly():
assert Product.Meta.model_fields['created'].nullable
assert not Product.__fields__['created'].required
assert Product.Meta.table.columns['created'].server_default.arg.name == 'now'
assert Product.Meta.model_fields["created"].nullable
assert not Product.__fields__["created"].required
assert Product.Meta.table.columns["created"].server_default.arg.name == "now"
@pytest.mark.asyncio
async def test_model_creation():
async with database:
async with database.transaction(force_rollback=True):
p1 = Product(name='Test')
p1 = Product(name="Test")
assert p1.created is None
await p1.save()
await p1.load()
assert p1.created is not None
assert p1.company == 'Acme'
assert p1.company == "Acme"
assert p1.sort_order == 10
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)
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
)
assert p3.created is not None
assert p3.created == date
assert p1.created != p3.created
assert p3.company == 'Roadrunner'
assert p3.company == "Roadrunner"
assert p3.sort_order == 1
p3 = await Product.objects.get(name='Test2')
assert p3.company == 'Roadrunner'
p3 = await Product.objects.get(name="Test2")
assert p3.company == "Roadrunner"
assert p3.sort_order == 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.company == 'Acme'
assert p2.company == "Acme"
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
assert p1.created != p2.created # pragma nocover

View File

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