Skip to content

Commit d8f4456

Browse files
committed
added the foundation for lazy references in ForeignKey columns
1 parent 391bd47 commit d8f4456

File tree

7 files changed

+327
-73
lines changed

7 files changed

+327
-73
lines changed

piccolo/columns/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
UUID,
1818
Varchar,
1919
)
20-
from .base import Column, ForeignKeyMeta, Selectable # noqa: F401
21-
from .base import OnDelete, OnUpdate # noqa: F401
20+
from .base import ( # noqa: F401
21+
Column,
22+
ForeignKeyMeta,
23+
Selectable,
24+
OnDelete,
25+
OnUpdate,
26+
)
2227
from .combination import And, Or, Where # noqa: F401
28+
from .reference import LazyTableReference # noqa: F401

piccolo/columns/base.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datetime
66
import decimal
77
from enum import Enum
8+
import inspect
89
import typing as t
910

1011
from piccolo.columns.operators.comparison import (
@@ -24,12 +25,13 @@
2425
)
2526
from piccolo.columns.combination import Where
2627
from piccolo.columns.defaults.base import Default
28+
from piccolo.columns.reference import LazyTableReference
2729
from piccolo.querystring import QueryString
2830
from piccolo.utils.warnings import colored_warning
2931

30-
if t.TYPE_CHECKING:
31-
from piccolo.table import Table # noqa
32-
from .column_types import ForeignKey # noqa
32+
if t.TYPE_CHECKING: # pragma: no cover
33+
from piccolo.table import Table
34+
from piccolo.columns.column_types import ForeignKey
3335

3436

3537
class OnDelete(str, Enum):
@@ -62,11 +64,32 @@ def __repr__(self):
6264

6365
@dataclass
6466
class ForeignKeyMeta:
65-
references: t.Type[Table]
67+
references: t.Union[t.Type[Table], LazyTableReference]
6668
on_delete: OnDelete
6769
on_update: OnUpdate
6870
proxy_columns: t.List[Column] = field(default_factory=list)
6971

72+
@property
73+
def resolved_references(self) -> t.Type[Table]:
74+
"""
75+
Evaluates the ``references`` attribute if it's a LazyTableReference,
76+
raising a ``ValueError`` if it fails, otherwise returns a ``Table``
77+
subclass.
78+
"""
79+
from piccolo.table import Table
80+
81+
if isinstance(self.references, LazyTableReference):
82+
return self.references.resolve()
83+
elif inspect.isclass(self.references) and issubclass(
84+
self.references, Table
85+
):
86+
return self.references
87+
else:
88+
raise ValueError(
89+
"The references attribute is neither a Table sublclass or a "
90+
"LazyTableReference instance."
91+
)
92+
7093

7194
@dataclass
7295
class ColumnMeta:
@@ -409,7 +432,7 @@ def querystring(self) -> QueryString:
409432
self, "_foreign_key_meta", None
410433
)
411434
if foreign_key_meta:
412-
tablename = foreign_key_meta.references._meta.tablename
435+
tablename = foreign_key_meta.resolved_references._meta.tablename
413436
on_delete = foreign_key_meta.on_delete.value
414437
on_update = foreign_key_meta.on_update.value
415438
query += (
@@ -436,4 +459,7 @@ def __str__(self):
436459
return self.querystring.__str__()
437460

438461
def __repr__(self):
439-
return f"{self._meta.name} - {self.__class__.__name__}"
462+
return (
463+
f"{self._meta.table.__name__}.{self._meta.name} - "
464+
f"{self.__class__.__name__}"
465+
)

piccolo/columns/column_types.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
from __future__ import annotations
2+
23
import copy
3-
from datetime import datetime, date, time, timedelta
44
import decimal
55
import typing as t
66
import uuid
7+
from datetime import date, datetime, time, timedelta
78

8-
from piccolo.columns.base import Column, OnDelete, OnUpdate, ForeignKeyMeta
9-
from piccolo.columns.operators.string import ConcatPostgres, ConcatSQLite
10-
from piccolo.columns.defaults.date import DateArg, DateNow, DateCustom
11-
from piccolo.columns.defaults.time import TimeArg, TimeNow, TimeCustom
9+
from piccolo.columns.base import Column, ForeignKeyMeta, OnDelete, OnUpdate
10+
from piccolo.columns.defaults.date import DateArg, DateCustom, DateNow
1211
from piccolo.columns.defaults.interval import IntervalArg, IntervalCustom
12+
from piccolo.columns.defaults.time import TimeArg, TimeCustom, TimeNow
1313
from piccolo.columns.defaults.timestamp import (
1414
TimestampArg,
15-
TimestampNow,
1615
TimestampCustom,
16+
TimestampNow,
1717
)
18-
from piccolo.columns.defaults.uuid import UUIDArg, UUID4
19-
from piccolo.querystring import Unquoted, QueryString
18+
from piccolo.columns.defaults.uuid import UUID4, UUIDArg
19+
from piccolo.columns.operators.string import ConcatPostgres, ConcatSQLite
20+
from piccolo.columns.reference import LazyTableReference
21+
from piccolo.querystring import QueryString, Unquoted
2022
from piccolo.utils.encoding import dump_json
2123

22-
if t.TYPE_CHECKING:
24+
if t.TYPE_CHECKING: # pragma: no cover
2325
from piccolo.table import Table
2426

2527

@@ -170,9 +172,7 @@ def column_type(self):
170172
def __add__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
171173
engine_type = self._meta.table._meta.db.engine_type
172174
return self.concat_delegate.get_querystring(
173-
column_name=self._meta.name,
174-
value=value,
175-
engine_type=engine_type,
175+
column_name=self._meta.name, value=value, engine_type=engine_type,
176176
)
177177

178178
def __radd__(self, value: t.Union[str, Varchar, Text]) -> QueryString:
@@ -970,7 +970,7 @@ class Band(Table):
970970

971971
def __init__(
972972
self,
973-
references: t.Union[t.Type[Table], str],
973+
references: t.Union[t.Type[Table], LazyTableReference, str],
974974
default: t.Union[int, None] = None,
975975
null: bool = True,
976976
on_delete: OnDelete = OnDelete.cascade,
@@ -979,13 +979,6 @@ def __init__(
979979
) -> None:
980980
self._validate_default(default, (int, None))
981981

982-
if isinstance(references, str):
983-
if references != "self":
984-
raise ValueError(
985-
"String values for 'references' currently only supports "
986-
"'self', which is a reference to the current table."
987-
)
988-
989982
kwargs.update(
990983
{
991984
"references": references,
@@ -995,14 +988,15 @@ def __init__(
995988
)
996989
super().__init__(default=default, null=null, **kwargs)
997990

998-
if t.TYPE_CHECKING:
991+
if t.TYPE_CHECKING: # pragma: no cover
999992
# This is here just for type inference - the actual value is set by
1000993
# the Table metaclass.
1001994
from piccolo.table import Table
1002995

1003-
self._foreign_key_meta = ForeignKeyMeta(
1004-
Table, OnDelete.cascade, OnUpdate.cascade
1005-
)
996+
if not hasattr(self, "_foreign_key_meta"):
997+
self._foreign_key_meta = ForeignKeyMeta(
998+
Table, OnDelete.cascade, OnUpdate.cascade
999+
)
10061000

10071001
def __getattribute__(self, name: str):
10081002
"""
@@ -1040,7 +1034,9 @@ def __getattribute__(self, name: str):
10401034
except Exception:
10411035
pass
10421036

1043-
for column in value._foreign_key_meta.references._meta.columns:
1037+
for (
1038+
column
1039+
) in value._foreign_key_meta.resolved_references._meta.columns:
10441040
_column: Column = copy.deepcopy(column)
10451041
_column._meta.call_chain = copy.copy(
10461042
new_column._meta.call_chain

piccolo/columns/reference.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Dataclasses for storing lazy references between ForeignKey columns and tables.
3+
"""
4+
from __future__ import annotations
5+
from dataclasses import dataclass, field
6+
import importlib
7+
import inspect
8+
import typing as t
9+
10+
if t.TYPE_CHECKING: # pragma: no cover
11+
from piccolo.columns.column_types import ForeignKey
12+
from piccolo.table import Table
13+
14+
15+
@dataclass
16+
class LazyTableReference:
17+
"""
18+
Holds a reference to a ``Table`` subclass. Used to avoid circular
19+
dependencies in the ``references`` argument of ``ForeignKey`` columns.
20+
21+
:param table_class_name:
22+
The name of the ``Table`` subclass. For example, 'Manager'.
23+
:param app_name:
24+
If specified, the ``Table`` subclass is imported from a Piccolo app
25+
with the given name.
26+
:param module_path:
27+
If specified, the ``Table`` subclass is imported from this path.
28+
For example, 'my_app.tables'.
29+
30+
"""
31+
32+
table_class_name: str
33+
app_name: t.Optional[str] = None
34+
module_path: t.Optional[str] = None
35+
36+
def __post_init__(self):
37+
if self.app_name is None and self.module_path is None:
38+
raise ValueError(
39+
"You must specify either app_name or module_path."
40+
)
41+
if self.app_name and self.module_path:
42+
raise ValueError(
43+
"Specify either app_name or module_path - not both."
44+
)
45+
46+
def resolve(self) -> t.Type[Table]:
47+
if self.app_name is not None:
48+
from piccolo.conf.apps import Finder
49+
50+
finder = Finder()
51+
return finder.get_table_with_name(
52+
app_name=self.app_name, table_class_name=self.table_class_name
53+
)
54+
55+
if self.module_path:
56+
module = importlib.import_module(self.module_path)
57+
table: t.Optional[t.Type[Table]] = getattr(
58+
module, self.table_class_name, None
59+
)
60+
61+
from piccolo.table import Table
62+
63+
if (
64+
table is not None
65+
and inspect.isclass(table)
66+
and issubclass(table, Table)
67+
):
68+
return table
69+
else:
70+
raise ValueError(
71+
f"Can't find a Table subclass called {self.app_name} "
72+
f"in {self.module_path}"
73+
)
74+
75+
raise ValueError("You must specify either app_name or module_path.")
76+
77+
def __str__(self):
78+
if self.app_name:
79+
return f"App {self.app_name}.{self.table_class_name}"
80+
elif self.module_path:
81+
return f"Module {self.module_path}.{self.table_class_name}"
82+
else:
83+
return "Unknown"
84+
85+
86+
@dataclass
87+
class LazyColumnReferenceStore:
88+
foreign_key_columns: t.List[ForeignKey] = field(default_factory=list)
89+
90+
def for_table(self, table: t.Type[Table]) -> t.List[ForeignKey]:
91+
return [
92+
i
93+
for i in self.foreign_key_columns
94+
if isinstance(i._foreign_key_meta.references, LazyTableReference)
95+
and i._foreign_key_meta.references.resolve() is table
96+
]
97+
98+
def for_tablename(self, tablename: str) -> t.List[ForeignKey]:
99+
return [
100+
i
101+
for i in self.foreign_key_columns
102+
if isinstance(i._foreign_key_meta.references, LazyTableReference)
103+
and i._foreign_key_meta.references.resolve()._meta.tablename
104+
== tablename
105+
]
106+
107+
108+
LAZY_COLUMN_REFERENCES: LazyColumnReferenceStore = LazyColumnReferenceStore()

piccolo/query/methods/select.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
from piccolo.querystring import QueryString
2020

21-
if t.TYPE_CHECKING:
21+
if t.TYPE_CHECKING: # pragma: no cover
2222
from piccolo.custom_types import Combinable
2323
from piccolo.table import Table # noqa
2424

@@ -182,7 +182,7 @@ def _get_joins(self, columns: t.Sequence[Selectable]) -> t.List[str]:
182182
left_tablename = key._meta.table._meta.tablename
183183

184184
right_tablename = (
185-
key._foreign_key_meta.references._meta.tablename
185+
key._foreign_key_meta.resolved_references._meta.tablename
186186
)
187187

188188
_joins.append(

0 commit comments

Comments
 (0)