Skip to content

Commit c8e27ba

Browse files
authored
Field refactor (tortoise#206)
Split fields into many modules for categorisation Remove unnecessary __slots__ from Fields, as this complicates work, and saves almost no memory as Fields only get instantiated as class variables (hence does not accumulate) PyLint plugin now uses simplified Field-type inference and generates reverse FK's correctly. Move DDL definition of fields from generate_schema into Fields Make fields support overriding on a per-dialect level Extend schema discover to know about DB types for ANY db type. Make schema generator use types specified in Fields Have fields define generated PK's Have fields define custom emulation rules (e.g. DecimalField and SQLite)
1 parent 10a7cd0 commit c8e27ba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1477
-1339
lines changed

CHANGELOG.rst

+13-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@
33
Changelog
44
=========
55
Unreleased
6-
------
7-
- Check whether charset name is available
6+
----------
7+
8+
* Refactored Fields:
9+
10+
Fields have been refactored, for better maintenance. There should be no change for most users.
11+
12+
- More accurate auto-completion.
13+
- Fields now contain their own SQL schema by dialect, which significantly simpliefies adding field types.
14+
- ``describe_model()`` now returns the DB type, and dialect overrides.
15+
16+
- ``JSONField`` will now automatically use ``python-rapidjson`` as an accelerator if it is available.
17+
- ``DecimalField`` and aggregations on it, now behaves much more like expected on SQLite (#256)
18+
- Check whether charset name is valid for the MySQL connection (#261)
819

920
0.15.4
1021
------

docs/examples/basic.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Recursive Relations
9898

9999
.. _example_enum_fields:
100100

101-
===============
101+
==================
102102
Enumeration Fields
103-
===============
103+
==================
104104
.. literalinclude:: ../../examples/enum_fields.py

docs/fields.rst

+11-56
Original file line numberDiff line numberDiff line change
@@ -23,74 +23,29 @@ Fields are defined as properties of a ``Model`` class object:
2323
Reference
2424
=========
2525

26-
.. autoclass:: tortoise.fields.Field
27-
:members:
26+
Here is the list of fields available with custom options of these fields:
27+
28+
Base Field
29+
----------
2830

29-
Here is the list of fields available at the moment with custom options of these fields:
31+
.. automodule:: tortoise.fields.base
32+
:members:
33+
:undoc-members:
3034

3135
Data Fields
3236
-----------
3337

34-
.. autoclass:: tortoise.fields.IntField
35-
:exclude-members: to_db_value, to_python_value
36-
37-
.. autoclass:: tortoise.fields.BigIntField
38-
:exclude-members: to_db_value, to_python_value
39-
40-
.. autoclass:: tortoise.fields.SmallIntField
41-
:exclude-members: to_db_value, to_python_value
42-
43-
.. autoclass:: tortoise.fields.CharField
44-
:exclude-members: to_db_value, to_python_value
45-
46-
.. autoclass:: tortoise.fields.TextField
47-
:exclude-members: to_db_value, to_python_value
48-
49-
.. autoclass:: tortoise.fields.BooleanField
50-
:exclude-members: to_db_value, to_python_value
51-
52-
.. autoclass:: tortoise.fields.DecimalField
53-
:exclude-members: to_db_value, to_python_value
54-
55-
.. autoclass:: tortoise.fields.DatetimeField
56-
:exclude-members: to_db_value, to_python_value
57-
58-
.. autoclass:: tortoise.fields.DateField
59-
:exclude-members: to_db_value, to_python_value
60-
61-
.. autoclass:: tortoise.fields.TimeDeltaField
62-
:exclude-members: to_db_value, to_python_value
63-
64-
.. autoclass:: tortoise.fields.FloatField
65-
:exclude-members: to_db_value, to_python_value
66-
67-
.. autoclass:: tortoise.fields.JSONField
68-
:exclude-members: to_db_value, to_python_value
69-
70-
.. autoclass:: tortoise.fields.UUIDField
71-
:exclude-members: to_db_value, to_python_value
72-
73-
.. autoclass:: tortoise.fields.IntEnumField
74-
:exclude-members: to_db_value, to_python_value
75-
76-
.. autoclass:: tortoise.fields.CharEnumField
38+
.. automodule:: tortoise.fields.data
39+
:members:
7740
:exclude-members: to_db_value, to_python_value
7841

79-
8042
Relational Fields
8143
-----------------
8244

83-
.. autoclass:: tortoise.fields.ForeignKeyField
45+
.. automodule:: tortoise.fields.relational
46+
:members: ForeignKeyField, OneToOneField, ManyToManyField
8447
:exclude-members: to_db_value, to_python_value
8548

86-
.. autoclass:: tortoise.fields.OneToOneField
87-
88-
.. autofunction:: tortoise.fields.ManyToManyField
89-
90-
.. autodata:: tortoise.fields.ForeignKeyRelation
91-
92-
.. autodata:: tortoise.fields.ForeignKeyNullableRelation
93-
9449
Extending A Field
9550
=================
9651

docs/getting_started.rst

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ Apart from ``asyncpg`` there is also support for ``sqlite`` through ``aiosqlite`
2626
``mysql`` through ``aiomysql``.
2727
You can easily implement more backends if there is appropriate ``asyncio`` driver for this db.
2828

29+
Optional Accelerators
30+
---------------------
31+
The following libraries can be used as accelerators:
32+
33+
* `python-rapidjson <https://pypi.org/project/python-rapidjson/>`_: Automatically used if installed for JSON SerDes.
34+
* `uvloop <https://pypi.org/project/uvloop/>`_: Shown to improve performance, but needs to be set up.
35+
Please look at ``uvloop`` docuemntation for more info.
36+
If you use a framework, it may already use it.
37+
2938
Tutorial
3039
========
3140

docs/query.rst

+14-2
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,21 @@ Foreign Key
150150

151151
Tortoise ORM provides an API for working with FK relations
152152

153-
.. autoclass:: tortoise.fields.ReverseRelation
153+
.. autoclass:: tortoise.fields.relational.ReverseRelation
154154
:members:
155155

156+
.. autodata:: tortoise.fields.relational.ForeignKeyNullableRelation
157+
158+
.. autodata:: tortoise.fields.relational.ForeignKeyRelation
159+
160+
.. _one_to_one:
161+
162+
One to One
163+
==========
164+
165+
.. autodata:: tortoise.fields.relational.OneToOneNullableRelation
166+
167+
.. autodata:: tortoise.fields.relational.OneToOneRelation
156168

157169
.. _many_to_many:
158170

@@ -161,7 +173,7 @@ Many to Many
161173

162174
Tortoise ORM provides an API for working with M2M relations
163175

164-
.. autoclass:: tortoise.fields.ManyToManyRelation
176+
.. autoclass:: tortoise.fields.relational.ManyToManyRelation
165177
:members:
166178
:inherited-members:
167179

requirements.txt

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pypika>=0.35.8
22
ciso8601>=2.1.1
33
aiosqlite>=0.10.0
4+
typing-extensions>=3.7
5+
6+
# For Python3.6 compatibility
47
aiocontextvars>=0.2.2;python_version<"3.7"
5-
dataclasses>=0.6;python_version<"3.7"
6-
typing-extensions

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[flake8]
22
max-line-length = 100
33
exclude =
4-
ignore = W503
54
per-file-ignores =
65
tortoise/__init__.py:F401
6+
tortoise/fields/__init__.py:F401
77
tests/schema/test_generate_schema.py:E501
88

99
[isort]

tests/backends/__init__.py

Whitespace-only changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

tests/backends/test_mysql.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Test some mysql-specific features
3+
"""
4+
import ssl
5+
6+
from tortoise import Tortoise
7+
from tortoise.contrib import test
8+
9+
10+
class TestMySQL(test.SimpleTestCase):
11+
async def setUp(self):
12+
if Tortoise._inited:
13+
await self._tearDownDB()
14+
self.db_config = test.getDBConfig(app_label="models", modules=["tests.testmodels"])
15+
if self.db_config["connections"]["models"]["engine"] != "tortoise.backends.mysql":
16+
raise test.SkipTest("MySQL only")
17+
18+
async def tearDown(self) -> None:
19+
if Tortoise._inited:
20+
await Tortoise._drop_databases()
21+
22+
async def test_bad_charset(self):
23+
self.db_config["connections"]["models"]["credentials"]["charset"] = "terrible"
24+
with self.assertRaisesRegex(ConnectionError, "Unknown charset"):
25+
await Tortoise.init(self.db_config)
26+
27+
async def test_ssl_true(self):
28+
self.db_config["connections"]["models"]["credentials"]["ssl"] = True
29+
try:
30+
await Tortoise.init(self.db_config)
31+
except (ConnectionError, ssl.SSLError):
32+
pass
33+
else:
34+
self.assertFalse(True, "Expected ConnectionError or SSLError")
35+
36+
async def test_ssl_custom(self):
37+
# Expect connectionerror or pass
38+
ctx = ssl.create_default_context()
39+
ctx.check_hostname = False
40+
ctx.verify_mode = ssl.CERT_NONE
41+
42+
self.db_config["connections"]["models"]["credentials"]["ssl"] = ctx
43+
try:
44+
await Tortoise.init(self.db_config, _create_db=True)
45+
except ConnectionError:
46+
pass

tests/test_postgres.py tests/backends/test_postgres.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Test some postgres-specific features
2+
Test some PostgreSQL-specific features
33
"""
44
import ssl
55

@@ -9,7 +9,7 @@
99
from tortoise.exceptions import OperationalError
1010

1111

12-
class TestTwoDatabases(test.SimpleTestCase):
12+
class TestPostgreSQL(test.SimpleTestCase):
1313
async def setUp(self):
1414
if Tortoise._inited:
1515
await self._tearDownDB()
File renamed without changes.

tests/fields/test_decimal.py

+51
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from tortoise import fields
55
from tortoise.contrib import test
66
from tortoise.exceptions import ConfigurationError, IntegrityError
7+
from tortoise.functions import Avg, Max, Sum
78

89

910
class TestDecimalFields(test.TestCase):
@@ -62,3 +63,53 @@ async def test_values_list(self):
6263
"decimal", "decimal_nodec"
6364
)
6465
self.assertEqual(list(values[0]), [Decimal("1.2346"), 19])
66+
67+
async def test_order_by(self):
68+
await testmodels.DecimalFields.create(decimal=Decimal("0"), decimal_nodec=1)
69+
await testmodels.DecimalFields.create(decimal=Decimal("9.99"), decimal_nodec=1)
70+
await testmodels.DecimalFields.create(decimal=Decimal("27.27"), decimal_nodec=1)
71+
values = (
72+
await testmodels.DecimalFields.all()
73+
.order_by("decimal")
74+
.values_list("decimal", flat=True)
75+
)
76+
self.assertEqual(values, [Decimal("0"), Decimal("9.99"), Decimal("27.27")])
77+
78+
async def test_aggregate_sum(self):
79+
await testmodels.DecimalFields.create(decimal=Decimal("0"), decimal_nodec=1)
80+
await testmodels.DecimalFields.create(decimal=Decimal("9.99"), decimal_nodec=1)
81+
await testmodels.DecimalFields.create(decimal=Decimal("27.27"), decimal_nodec=1)
82+
values = (
83+
await testmodels.DecimalFields.all()
84+
.annotate(sum_decimal=Sum("decimal"))
85+
.values("sum_decimal")
86+
)
87+
self.assertEqual(
88+
values[0], {"sum_decimal": Decimal("37.26")},
89+
)
90+
91+
async def test_aggregate_avg(self):
92+
await testmodels.DecimalFields.create(decimal=Decimal("0"), decimal_nodec=1)
93+
await testmodels.DecimalFields.create(decimal=Decimal("9.99"), decimal_nodec=1)
94+
await testmodels.DecimalFields.create(decimal=Decimal("27.27"), decimal_nodec=1)
95+
values = (
96+
await testmodels.DecimalFields.all()
97+
.annotate(avg_decimal=Avg("decimal"))
98+
.values("avg_decimal")
99+
)
100+
self.assertEqual(
101+
values[0], {"avg_decimal": Decimal("12.42")},
102+
)
103+
104+
async def test_aggregate_max(self):
105+
await testmodels.DecimalFields.create(decimal=Decimal("0"), decimal_nodec=1)
106+
await testmodels.DecimalFields.create(decimal=Decimal("9.99"), decimal_nodec=1)
107+
await testmodels.DecimalFields.create(decimal=Decimal("27.27"), decimal_nodec=1)
108+
values = (
109+
await testmodels.DecimalFields.all()
110+
.annotate(max_decimal=Max("decimal"))
111+
.values("max_decimal")
112+
)
113+
self.assertEqual(
114+
values[0], {"max_decimal": Decimal("27.27")},
115+
)

tests/model_setup/test_init.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async def test_dup3_init(self):
121121

122122
async def test_generated_nonint(self):
123123
with self.assertRaisesRegex(
124-
ConfigurationError, "Generated primary key allowed only for IntField and BigIntField"
124+
ConfigurationError, "Field 'val' \\(CharField\\) can't be DB-generated"
125125
):
126126
await Tortoise.init(
127127
{

tests/requirements-pypy.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ cffi==1.13.2 # via cryptography
1414
chardet==3.0.4 # via requests
1515
ciso8601==2.1.2
1616
coverage==4.5.4 # via coveralls, pytest-cov
17-
coveralls==1.8.2
17+
coveralls==1.9.1
1818
cryptography==2.8 # via pymysql
1919
docopt==0.6.2 # via coveralls
2020
execnet==1.7.1 # via pytest-xdist
@@ -32,7 +32,7 @@ pytest-cov==2.8.1
3232
pytest-forked==1.1.3 # via pytest-xdist
3333
pytest-xdist==1.30.0
3434
pytest==5.3.1
35-
pyyaml==5.1.2
35+
pyyaml==5.2
3636
requests==2.22.0 # via coveralls
3737
six==1.13.0 # via cryptography, packaging, pytest-xdist
3838
typing-extensions==3.7.4.1

tests/requirements.in

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ pytest-cov
3737
# Pypi
3838
twine<2.0
3939

40+
# Optional accelerators
41+
uvloop>=0.12.0; sys_platform != "win32" and implementation_name == "cpython"
42+
python-rapidjson; implementation_name == "cpython"
43+
4044
# Sample integration - Quart
4145
wsproto;python_version>="3.7"
4246
hypercorn;python_version>="3.7"
4347
quart;python_version>="3.7"
4448

4549
# Sample integration - Sanic
4650
sanic
47-
uvloop>=0.12.0; sys_platform != "win32" and implementation_name == "cpython"
4851

4952
# Sample integration - Starlette
5053
starlette

tests/requirements.txt

+5-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ click==7.0 # via black, pip-tools, quart, uvicorn
2828
cloud-sptheme==1.10.0
2929
colorama==0.4.1 # via green
3030
coverage==4.5.4 # via coveralls, green, nose2, pytest-cov
31-
coveralls==1.8.2
31+
coveralls==1.9.1
3232
cryptography==2.8 # via pymysql
3333
docopt==0.6.2 # via coveralls
3434
docutils==0.15.2
@@ -49,7 +49,7 @@ hypercorn==0.9.0 ; python_version >= "3.7"
4949
hyperframe==5.2.0 # via h2
5050
idna==2.8 # via httpcore, requests
5151
imagesize==1.1.0 # via sphinx
52-
importlib-metadata==0.23 # via pluggy, pytest, tox
52+
importlib-metadata==1.1.0 # via pluggy, pytest, tox
5353
isort==4.3.21 # via flake8-isort, pylint
5454
itsdangerous==1.1.0 # via quart
5555
jinja2==2.10.3 # via quart, sphinx
@@ -82,8 +82,9 @@ pytest-cov==2.8.1
8282
pytest-forked==1.1.3 # via pytest-xdist
8383
pytest-xdist==1.30.0
8484
pytest==5.3.1
85+
python-rapidjson==0.9.1 ; implementation_name == "cpython"
8586
pytz==2019.3 # via babel
86-
pyyaml==5.1.2
87+
pyyaml==5.2
8788
quart==0.10.0 ; python_version >= "3.7"
8889
readme-renderer==24.0 # via twine
8990
regex==2019.11.1 # via black
@@ -102,7 +103,7 @@ starlette==0.13.0
102103
stevedore==1.31.0 # via bandit
103104
testfixtures==6.10.3 # via flake8-isort
104105
toml==0.10.0 # via black, hypercorn, tox
105-
tox==3.14.1
106+
tox==3.14.2
106107
tqdm==4.40.0 # via twine
107108
twine==1.15.0
108109
typed-ast==1.4.0 # via astroid, black, mypy

tests/utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)