diff --git a/.travis.yml b/.travis.yml index 6996e4f..f3c6d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index d6ff26c..c2b0daa 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/contributing.md b/docs/contributing.md index 690a2cf..90c957a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -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 diff --git a/docs/fields.md b/docs/fields.md index 6dc0a17..67bdc74 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -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" ``` diff --git a/docs/index.md b/docs/index.md index ca77b54..0b691dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -74,9 +74,12 @@ class Album(ormar.Model): tablename = "album" 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. diff --git a/docs/models.md b/docs/models.md index 3562a7b..5fdc950 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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" ``` diff --git a/docs/mypy.md b/docs/mypy.md new file mode 100644 index 0000000..4066e3d --- /dev/null +++ b/docs/mypy.md @@ -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/ \ No newline at end of file diff --git a/docs/plugin.md b/docs/plugin.md new file mode 100644 index 0000000..fff0590 --- /dev/null +++ b/docs/plugin.md @@ -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 \ No newline at end of file diff --git a/docs/queries.md b/docs/queries.md index c3f0268..77c6229 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -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 @@ -539,4 +370,5 @@ 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 \ No newline at end of file +[models]: ./models.md +[relations]: ./relations.md \ No newline at end of file diff --git a/docs/relations.md b/docs/relations.md index fd6a3e1..5bda020 100644 --- a/docs/relations.md +++ b/docs/relations.md @@ -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 \ No newline at end of file diff --git a/docs/releases.md b/docs/releases.md index ebc41e0..c24ec54 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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 diff --git a/docs_src/__init__.py b/docs_src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/fastapi/__init__.py b/docs_src/fastapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/fastapi/docs001.py b/docs_src/fastapi/docs001.py index 475756a..4d2087c 100644 --- a/docs_src/fastapi/docs001.py +++ b/docs_src/fastapi/docs001.py @@ -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]) diff --git a/docs_src/fastapi/mypy/__init__.py b/docs_src/fastapi/mypy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/fastapi/mypy/docs001.py b/docs_src/fastapi/mypy/docs001.py new file mode 100644 index 0000000..2c74a77 --- /dev/null +++ b/docs_src/fastapi/mypy/docs001.py @@ -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) diff --git a/docs_src/fields/__init__.py b/docs_src/fields/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/fields/docs001.py b/docs_src/fields/docs001.py index 7cb26f4..dd79791 100644 --- a/docs_src/fields/docs001.py +++ b/docs_src/fields/docs001.py @@ -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: diff --git a/docs_src/fields/docs002.py b/docs_src/fields/docs002.py index 4a0c61f..2432856 100644 --- a/docs_src/fields/docs002.py +++ b/docs_src/fields/docs002.py @@ -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: diff --git a/docs_src/fields/docs003.py b/docs_src/fields/docs003.py index a0cb2c4..5066f60 100644 --- a/docs_src/fields/docs003.py +++ b/docs_src/fields/docs003.py @@ -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) diff --git a/docs_src/fields/docs004.py b/docs_src/fields/docs004.py index b04cdb2..8a4e1ab 100644 --- a/docs_src/fields/docs004.py +++ b/docs_src/fields/docs004.py @@ -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()) diff --git a/docs_src/models/__init__.py b/docs_src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/models/docs001.py b/docs_src/models/docs001.py index 47b3581..36d7fd9 100644 --- a/docs_src/models/docs001.py +++ b/docs_src/models/docs001.py @@ -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) diff --git a/docs_src/models/docs002.py b/docs_src/models/docs002.py index 96ee368..3450dae 100644 --- a/docs_src/models/docs002.py +++ b/docs_src/models/docs002.py @@ -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) diff --git a/docs_src/models/docs003.py b/docs_src/models/docs003.py index 91a83cf..e79bfba 100644 --- a/docs_src/models/docs003.py +++ b/docs_src/models/docs003.py @@ -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__) diff --git a/docs_src/models/docs004.py b/docs_src/models/docs004.py index a1db655..cc8bce8 100644 --- a/docs_src/models/docs004.py +++ b/docs_src/models/docs004.py @@ -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) diff --git a/docs_src/models/docs005.py b/docs_src/models/docs005.py index ce81887..cd0fa9d 100644 --- a/docs_src/models/docs005.py +++ b/docs_src/models/docs005.py @@ -8,15 +8,16 @@ 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()}) + +print({x: v.__dict__ for x, v in Course.Meta.model_fields.items()}) """ Will produce: {'completed': mappingproxy({'autoincrement': False, @@ -63,4 +64,4 @@ Will produce: 'server_default': None, 'strip_whitespace': False, 'unique': False})} -""" \ No newline at end of file +""" diff --git a/docs_src/models/docs006.py b/docs_src/models/docs006.py index 7926cfb..7649c2c 100644 --- a/docs_src/models/docs006.py +++ b/docs_src/models/docs006.py @@ -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) diff --git a/docs_src/models/docs007.py b/docs_src/models/docs007.py index 751cace..9dfe28a 100644 --- a/docs_src/models/docs007.py +++ b/docs_src/models/docs007.py @@ -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) diff --git a/docs_src/models/docs008.py b/docs_src/models/docs008.py index 9a3d063..da2444c 100644 --- a/docs_src/models/docs008.py +++ b/docs_src/models/docs008.py @@ -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) diff --git a/docs_src/models/docs009.py b/docs_src/models/docs009.py index 0204feb..747f612 100644 --- a/docs_src/models/docs009.py +++ b/docs_src/models/docs009.py @@ -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") diff --git a/docs_src/models/docs010.py b/docs_src/models/docs010.py index 57febef..d63101e 100644 --- a/docs_src/models/docs010.py +++ b/docs_src/models/docs010.py @@ -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) diff --git a/docs_src/models/docs011.py b/docs_src/models/docs011.py new file mode 100644 index 0000000..962de1d --- /dev/null +++ b/docs_src/models/docs011.py @@ -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() \ No newline at end of file diff --git a/docs_src/models/docs012.py b/docs_src/models/docs012.py new file mode 100644 index 0000000..68e75b9 --- /dev/null +++ b/docs_src/models/docs012.py @@ -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) \ No newline at end of file diff --git a/docs_src/models/docs013.py b/docs_src/models/docs013.py new file mode 100644 index 0000000..8d584f3 --- /dev/null +++ b/docs_src/models/docs013.py @@ -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) diff --git a/docs_src/queries/__init__.py b/docs_src/queries/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/queries/docs001.py b/docs_src/queries/docs001.py index 2e4fe43..850030e 100644 --- a/docs_src/queries/docs001.py +++ b/docs_src/queries/docs001.py @@ -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() \ No newline at end of file + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + title: str = ormar.String(max_length=100) + position: int = ormar.Integer() diff --git a/docs_src/queries/docs002.py b/docs_src/queries/docs002.py new file mode 100644 index 0000000..d9cdeff --- /dev/null +++ b/docs_src/queries/docs002.py @@ -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 diff --git a/docs_src/queries/docs003.py b/docs_src/queries/docs003.py new file mode 100644 index 0000000..58e4f1b --- /dev/null +++ b/docs_src/queries/docs003.py @@ -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 diff --git a/docs_src/queries/docs004.py b/docs_src/queries/docs004.py new file mode 100644 index 0000000..6aaf01f --- /dev/null +++ b/docs_src/queries/docs004.py @@ -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 diff --git a/docs_src/queries/docs005.py b/docs_src/queries/docs005.py new file mode 100644 index 0000000..dca0757 --- /dev/null +++ b/docs_src/queries/docs005.py @@ -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 diff --git a/docs_src/queries/docs006.py b/docs_src/queries/docs006.py new file mode 100644 index 0000000..936c79f --- /dev/null +++ b/docs_src/queries/docs006.py @@ -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 diff --git a/docs_src/relations/__init__.py b/docs_src/relations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/relations/docs001.py b/docs_src/relations/docs001.py index 930e8d3..48307d2 100644 --- a/docs_src/relations/docs001.py +++ b/docs_src/relations/docs001.py @@ -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) diff --git a/docs_src/relations/docs002.py b/docs_src/relations/docs002.py index f78083a..8dd0566 100644 --- a/docs_src/relations/docs002.py +++ b/docs_src/relations/docs002.py @@ -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) diff --git a/mkdocs.yml b/mkdocs.yml index a7b99e3..9c3dec6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/mypy.ini b/mypy.ini index d9b0283..108c485 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,10 @@ [mypy] python_version = 3.8 +plugins = pydantic.mypy [mypy-sqlalchemy.*] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True + +[mypy-tests.test_model_definition.*] +ignore_errors = True + diff --git a/ormar/__init__.py b/ormar/__init__.py index ecead7c..b121bb1 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -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", ] diff --git a/ormar/fields/base.py b/ormar/fields/base.py index 0a47ae4..7b86277 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -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(): diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index 854f83d..26334b4 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -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 diff --git a/ormar/fields/many_to_many.py b/ormar/fields/many_to_many.py index 1f73a0d..d53106a 100644 --- a/ormar/fields/many_to_many.py +++ b/ormar/fields/many_to_many.py @@ -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__}"} diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index 5462865..f32b5d4 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -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,17 +178,23 @@ class Float(ModelFieldFactory): return sqlalchemy.Float() -class Boolean(ModelFieldFactory): - _bases = (int, BaseField) - _type = bool +if TYPE_CHECKING: # pragma: nocover - @classmethod - def get_column_type(cls, **kwargs: Any) -> Any: - return sqlalchemy.Boolean() + def Boolean(**kwargs: Any) -> bool: + pass -class DateTime(ModelFieldFactory): - _bases = (datetime.datetime, BaseField) +else: + + class Boolean(ModelFieldFactory, int): + _type = bool + + @classmethod + def get_column_type(cls, **kwargs: Any) -> Any: + return sqlalchemy.Boolean() + + +class DateTime(ModelFieldFactory, datetime.datetime): _type = datetime.datetime @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 diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index d7c57b9..0190a6b 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -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 ) diff --git a/ormar/models/model.py b/ormar/models/model.py index d0b07e4..a086a3b 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -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 diff --git a/ormar/models/modelproxy.py b/ormar/models/modelproxy.py index 56ee930..fb0e789 100644 --- a/ormar/models/modelproxy.py +++ b/ormar/models/modelproxy.py @@ -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: diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index faab1c1..3f48ddf 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -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 diff --git a/ormar/protocols/__init__.py b/ormar/protocols/__init__.py new file mode 100644 index 0000000..214cc10 --- /dev/null +++ b/ormar/protocols/__init__.py @@ -0,0 +1,4 @@ +from ormar.protocols.queryset_protocol import QuerySetProtocol +from ormar.protocols.relation_protocol import RelationProtocol + +__all__ = ["QuerySetProtocol", "RelationProtocol"] diff --git a/ormar/protocols/queryset_protocol.py b/ormar/protocols/queryset_protocol.py new file mode 100644 index 0000000..1320c2a --- /dev/null +++ b/ormar/protocols/queryset_protocol.py @@ -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": + ... diff --git a/ormar/protocols/relation_protocol.py b/ormar/protocols/relation_protocol.py new file mode 100644 index 0000000..b551b8d --- /dev/null +++ b/ormar/protocols/relation_protocol.py @@ -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: + ... diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index 1decda1..749eb75 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -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() diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index f71de87..8e2d6b4 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -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 diff --git a/ormar/relations/relation.py b/ormar/relations/relation.py index 91e00df..e09f00c 100644 --- a/ormar/relations/relation.py +++ b/ormar/relations/relation.py @@ -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 diff --git a/ormar/relations/relation_manager.py b/ormar/relations/relation_manager.py index 6e7eb24..0e36844 100644 --- a/ormar/relations/relation_manager.py +++ b/ormar/relations/relation_manager.py @@ -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() diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py index 29e4b97..806a07d 100644 --- a/ormar/relations/relation_proxy.py +++ b/ormar/relations/relation_proxy.py @@ -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) diff --git a/requirements.txt b/requirements.txt index e6b1752..e665778 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ databases[postgresql] databases[mysql] pydantic sqlalchemy +typing_extensions # Async database drivers aiomysql @@ -32,4 +33,4 @@ flake8-builtins flake8-variables-names flake8-cognitive-complexity flake8-functions -flake8-expression-complexity \ No newline at end of file +flake8-expression-complexity diff --git a/setup.py b/setup.py index 97a3145..02c016c 100644 --- a/setup.py +++ b/setup.py @@ -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"], diff --git a/tests/test_aliases.py b/tests/test_aliases.py index b48bb3f..81c4a8c 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -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") diff --git a/tests/test_columns.py b/tests/test_columns.py index b2a7bd0..73cf84a 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -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") diff --git a/tests/test_docs/__init__.py b/tests/test_docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fastapi_docs.py b/tests/test_fastapi_docs.py index 030815f..9fa503f 100644 --- a/tests/test_fastapi_docs.py +++ b/tests/test_fastapi_docs.py @@ -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" diff --git a/tests/test_fastapi_usage.py b/tests/test_fastapi_usage.py index fcbf479..503e582 100644 --- a/tests/test_fastapi_usage.py +++ b/tests/test_fastapi_usage.py @@ -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) diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index b85493d..e7bf4e5 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -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") diff --git a/tests/test_many_to_many.py b/tests/test_many_to_many.py index fc97b0d..3ddeb61 100644 --- a/tests/test_many_to_many.py +++ b/tests/test_many_to_many.py @@ -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") diff --git a/tests/test_model_definition.py b/tests/test_model_definition.py index 2f64259..5f6d360 100644 --- a/tests/test_model_definition.py +++ b/tests/test_model_definition.py @@ -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( diff --git a/tests/test_models.py b/tests/test_models.py index 47b5d50..490dedd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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 diff --git a/tests/test_more_reallife_fastapi.py b/tests/test_more_reallife_fastapi.py index b538b5c..fc3b1f7 100644 --- a/tests/test_more_reallife_fastapi.py +++ b/tests/test_more_reallife_fastapi.py @@ -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") diff --git a/tests/test_more_same_table_joins.py b/tests/test_more_same_table_joins.py index 0492d91..3fe22fe 100644 --- a/tests/test_more_same_table_joins.py +++ b/tests/test_more_same_table_joins.py @@ -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") diff --git a/tests/test_new_annotation_style.py b/tests/test_new_annotation_style.py new file mode 100644 index 0000000..28665a5 --- /dev/null +++ b/tests/test_new_annotation_style.py @@ -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) diff --git a/tests/test_non_integer_pkey.py b/tests/test_non_integer_pkey.py index d601e10..78fb179 100644 --- a/tests/test_non_integer_pkey.py +++ b/tests/test_non_integer_pkey.py @@ -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") diff --git a/tests/test_queryset_level_methods.py b/tests/test_queryset_level_methods.py index af8385e..5ea5443 100644 --- a/tests/test_queryset_level_methods.py +++ b/tests/test_queryset_level_methods.py @@ -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") diff --git a/tests/test_same_table_joins.py b/tests/test_same_table_joins.py index 33d2677..2375613 100644 --- a/tests/test_same_table_joins.py +++ b/tests/test_same_table_joins.py @@ -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") diff --git a/tests/test_selecting_subset_of_columns.py b/tests/test_selecting_subset_of_columns.py index dee9785..389648b 100644 --- a/tests/test_selecting_subset_of_columns.py +++ b/tests/test_selecting_subset_of_columns.py @@ -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") diff --git a/tests/test_server_default.py b/tests/test_server_default.py index b9106d0..d01fc40 100644 --- a/tests/test_server_default.py +++ b/tests/test_server_default.py @@ -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 diff --git a/tests/test_unique_constraints.py b/tests/test_unique_constraints.py index 7908ae4..3126d2a 100644 --- a/tests/test_unique_constraints.py +++ b/tests/test_unique_constraints.py @@ -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")