Skip to content

Commit 76737dc

Browse files
authored
1039 Improve LazyTableReference (#1040)
* add test * improve lazy table refs * improve docs for `LazyTableReference` * add docs for circular imports
1 parent 85dd176 commit 76737dc

File tree

9 files changed

+159
-10
lines changed

9 files changed

+159
-10
lines changed

docs/src/piccolo/projects_and_apps/included_apps.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Lets you scaffold an ASGI web app. See :ref:`ASGICommand`.
3838
3939
-------------------------------------------------------------------------------
4040

41+
.. _Fixtures:
42+
4143
fixtures
4244
~~~~~~~~
4345

docs/src/piccolo/schema/m2m.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ We create it in Piccolo like this:
5454
5555
5656
.. note::
57-
We use ``LazyTableReference`` because when Python evaluates ``Band`` and
58-
``Genre``, the ``GenreToBand`` class doesn't exist yet.
57+
We use :class:`LazyTableReference <piccolo.columns.reference.LazyTableReference>`
58+
because when Python evaluates ``Band`` and ``Genre``, the ``GenreToBand``
59+
class doesn't exist yet.
5960

6061
By using ``M2M`` it unlocks some powerful and convenient features.
6162

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
Avoiding circular imports
2+
=========================
3+
4+
How Python imports work
5+
-----------------------
6+
7+
When Python imports a file, it evaluates it from top to bottom.
8+
9+
With :class:`ForeignKey <piccolo.columns.column_types.ForeignKey>` columns we
10+
sometimes have to reference tables lower down in the file (which haven't been
11+
evaluated yet).
12+
13+
The solutions are:
14+
15+
* Try and move the referenced table to a different Python file.
16+
* Use :class:`LazyTableReference <piccolo.columns.reference.LazyTableReference>`
17+
18+
Import ``Table`` definitions as early as possible
19+
-------------------------------------------------
20+
21+
In the entrypoint to your app, at the top of the file, it's recommended to
22+
import your tables.
23+
24+
.. code-block:: python
25+
26+
# main.py
27+
from my_app.tables import Manager, Band
28+
29+
This ensures that the tables are imported, and setup correctly.
30+
31+
Keep table files focused
32+
------------------------
33+
34+
You should try and keep your ``tables.py`` files pretty focused (i.e.
35+
just contain your ``Table`` definitions).
36+
37+
If you have lots of logic alongside your ``Table`` definitions, it might cause
38+
your ``LazyTableReference`` references to evaluate too soon (causing circular
39+
import errors). An example of this is with
40+
:func:`create_pydantic_model <piccolo.utils.pydantic.create_pydantic_model>`:
41+
42+
.. literalinclude:: avoiding_circular_imports_src/tables.py
43+
44+
Simplify your schema if possible
45+
--------------------------------
46+
47+
Even with :class:`LazyTableReference <piccolo.columns.reference.LazyTableReference>`,
48+
you may run into some problems if your schema is really complicated.
49+
50+
An example is when you have two tables, and they have foreign keys to each other.
51+
52+
.. code-block:: python
53+
54+
class Band(Table):
55+
name = Varchar()
56+
manager = ForeignKey("Manager")
57+
58+
59+
class Manager(Table):
60+
name = Varchar()
61+
favourite_band = ForeignKey(Band)
62+
63+
64+
Piccolo should be able to create these tables, and query them. However, some
65+
Piccolo tooling may struggle - for example when loading :ref:`fixtures <Fixtures>`.
66+
67+
A joining table can help in these situations:
68+
69+
.. code-block:: python
70+
71+
class Band(Table):
72+
name = Varchar()
73+
manager = ForeignKey("Manager")
74+
75+
76+
class Manager(Table):
77+
name = Varchar()
78+
79+
80+
class ManagerFavouriteBand(Table):
81+
manager = ForeignKey(Manager, unique=True)
82+
band = ForeignKey(Band)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# tables.py
2+
3+
from piccolo.columns import ForeignKey, Varchar
4+
from piccolo.table import Table
5+
from piccolo.utils.pydantic import create_pydantic_model
6+
7+
8+
class Band(Table):
9+
name = Varchar()
10+
# This automatically gets converted into a LazyTableReference, because a
11+
# string is passed in:
12+
manager = ForeignKey("Manager")
13+
14+
15+
# This is not recommended, as it will cause the LazyTableReference to be
16+
# evaluated before Manager has imported.
17+
# Instead, move this to a separate file, or below Manager.
18+
BandModel = create_pydantic_model(Band)
19+
20+
21+
class Manager(Table):
22+
name = Varchar()

docs/src/piccolo/tutorials/deployment.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ This is a very simple Dockerfile, and illustrates the basics:
3535
.. code-block:: dockerfile
3636
3737
# Specify the base image:
38-
FROM python:3.10-slim-bullseye
38+
FROM python:3.12-bookworm
3939
4040
# Install the pip requirements:
4141
RUN pip install --upgrade pip
@@ -77,3 +77,11 @@ When we run the container (usually via `Kubernetes <https://kubernetes.io/>`_,
7777
`Docker Compose <https://docs.docker.com/compose/>`_, or similar),
7878
we can specify the database credentials using environment variables, which will
7979
be used by our application.
80+
81+
Accessing a local Postgres database
82+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
83+
84+
Bear in mind that if you have Postgres running locally on the server (i.e. on
85+
``localhost``), your Docker container won't automatically be able to access it.
86+
You can try Docker's host based networking, or just run Postgres within a
87+
Docker container.

docs/src/piccolo/tutorials/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ help you solve common problems:
1111
./using_sqlite_and_asyncio_effectively
1212
./deployment
1313
./fastapi
14+
./avoiding_circular_imports

piccolo/columns/column_types.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,7 +2183,7 @@ def __getattribute__(self, name: str) -> t.Union[Column, t.Any]:
21832183
# If the ForeignKey is using a lazy reference, we need to set the
21842184
# attributes here. Attributes starting with an underscore are
21852185
# unlikely to be column names.
2186-
if not name.startswith("__"):
2186+
if not name.startswith("_") and name not in dir(self):
21872187
try:
21882188
_foreign_key_meta = object.__getattribute__(
21892189
self, "_foreign_key_meta"
@@ -2196,12 +2196,9 @@ def __getattribute__(self, name: str) -> t.Union[Column, t.Any]:
21962196
):
21972197
object.__getattribute__(self, "set_proxy_columns")()
21982198

2199-
try:
2200-
value = object.__getattribute__(self, name)
2201-
except AttributeError:
2202-
raise AttributeError
2199+
value = object.__getattribute__(self, name)
22032200

2204-
if name == "_":
2201+
if name.startswith("_"):
22052202
return value
22062203

22072204
foreignkey_class: t.Type[ForeignKey] = object.__getattribute__(

piccolo/columns/reference.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class LazyTableReference:
3030
If specified, the ``Table`` subclass is imported from this path.
3131
For example, ``'my_app.tables'``.
3232
33+
.. hint::
34+
If the table is in the same file, you can pass in ``__name__``.
35+
3336
"""
3437

3538
table_class_name: str

tests/columns/test_reference.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,41 @@
55

66
from unittest import TestCase
77

8+
from piccolo.columns import ForeignKey, Varchar
89
from piccolo.columns.reference import LazyTableReference
10+
from piccolo.table import Table
11+
from tests.base import TableTest
912

1013

11-
class TestLazyTableReference(TestCase):
14+
class Band(Table):
15+
manager: ForeignKey["Manager"] = ForeignKey(
16+
LazyTableReference("Manager", module_path=__name__)
17+
)
18+
name = Varchar()
19+
20+
21+
class Manager(Table):
22+
name = Varchar()
23+
24+
25+
class TestQueries(TableTest):
26+
tables = [Band, Manager]
27+
28+
def setUp(self):
29+
super().setUp()
30+
manager = Manager({Manager.name: "Guido"})
31+
manager.save().run_sync()
32+
band = Band({Band.name: "Pythonistas", Band.manager: manager})
33+
band.save().run_sync()
34+
35+
def test_select(self):
36+
self.assertListEqual(
37+
Band.select(Band.name, Band.manager._.name).run_sync(),
38+
[{"name": "Pythonistas", "manager.name": "Guido"}],
39+
)
40+
41+
42+
class TestInit(TestCase):
1243
def test_init(self):
1344
"""
1445
A ``LazyTableReference`` must be passed either an ``app_name`` or
@@ -34,6 +65,8 @@ def test_init(self):
3465
module_path="tests.example_apps.music.tables",
3566
)
3667

68+
69+
class TestStr(TestCase):
3770
def test_str(self):
3871
self.assertEqual(
3972
LazyTableReference(

0 commit comments

Comments
 (0)