Skip to content

Commit c107b68

Browse files
committed
added table_finder
1 parent 489b504 commit c107b68

File tree

6 files changed

+168
-22
lines changed

6 files changed

+168
-22
lines changed

docs/src/piccolo/projects_and_apps/piccolo_apps.rst

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
.. _PiccoloApps:
22

3+
############
34
Piccolo Apps
4-
============
5+
############
56

67
By leveraging Piccolo apps you can:
78

8-
* Modularise your code.
9-
* Share your apps with other Piccolo users.
10-
* Unlock some useful functionality like auto migrations.
9+
* Modularise your code.
10+
* Share your apps with other Piccolo users.
11+
* Unlock some useful functionality like auto migrations.
1112

13+
-------------------------------------------------------------------------------
14+
15+
***************
1216
Creating an app
13-
---------------
17+
***************
1418

1519
Run the following command within your project:
1620

@@ -19,7 +23,7 @@ Run the following command within your project:
1923
piccolo app new my_app
2024
2125
22-
This will create a folder like this:
26+
Where `my_app` is your new app's name. This will create a folder like this:
2327

2428
.. code-block:: bash
2529
@@ -43,8 +47,11 @@ It's important to register your new app with the ``APP_REGISTRY`` in
4347
Anytime you invoke the `piccolo` command, you will now be able to perform
4448
operations on your app, such as :ref:`Migrations`.
4549

50+
-------------------------------------------------------------------------------
51+
52+
*********
4653
AppConfig
47-
---------
54+
*********
4855

4956
Inside your app's `piccolo_app.py` file is an ``AppConfig`` instance. This is
5057
how you customise your app's settings.
@@ -75,7 +82,7 @@ how you customise your app's settings.
7582
)
7683
7784
app_name
78-
~~~~~~~~
85+
========
7986

8087
This is used to identify your app, when using the `piccolo` CLI, for example:
8188

@@ -84,24 +91,55 @@ This is used to identify your app, when using the `piccolo` CLI, for example:
8491
piccolo migrations forwards blog
8592
8693
migrations_folder_path
87-
~~~~~~~~~~~~~~~~~~~~~~
94+
======================
8895

8996
Specifies where your app's migrations are stored. By default, a folder called
9097
`piccolo_migrations` is used.
9198

9299
table_classes
93-
~~~~~~~~~~~~~
100+
=============
101+
102+
Use this to register your app's ``Table`` subclasses. This is important for
103+
auto migrations (see :ref:`Migrations`).
104+
105+
You can register them manually, see the example above, or can use
106+
``table_finder``.
107+
108+
table_finder
109+
------------
110+
111+
Instead of manually registering ``Table`` subclasses, you can use
112+
``table_finder`` to automatically import any ``Table`` subclasses from a given
113+
list of modules.
114+
115+
.. code-block:: python
116+
117+
from piccolo.conf.apps import table_finder
118+
119+
APP_CONFIG = AppConfig(
120+
app_name='blog',
121+
migrations_folder_path=os.path.join(CURRENT_DIRECTORY, 'piccolo_migrations'),
122+
table_classes=table_finder(modules=['blog.tables']),
123+
migration_dependencies=[],
124+
commands=[]
125+
)
126+
127+
The module path should be from the root of the project (the same directory as
128+
your ``piccolo_conf.py`` file, rather than a relative path).
129+
130+
.. currentmodule:: piccolo.conf.apps
131+
132+
.. autofunction:: table_finder
94133

95-
Use this to register your app's tables. This is important for auto migrations (see :ref:`Migrations`).
96134

97135
migration_dependencies
98-
~~~~~~~~~~~~~~~~~~~~~~
136+
======================
99137

100138
Used to specify other Piccolo apps whose migrations need to be run before the
101139
current app's migrations.
102140

103141
commands
104-
~~~~~~~~
142+
========
105143

106144
You can register functions and coroutines, which are automatically added to
107145
the `piccolo` CLI.
@@ -155,8 +193,11 @@ app.
155193
Piccolo itself is bundled with several apps - have a look at the source code
156194
for inspiration.
157195

196+
-------------------------------------------------------------------------------
197+
198+
************
158199
Sharing Apps
159-
------------
200+
************
160201

161202
By breaking up your project into apps, the project becomes more maintainable.
162203
You can also share these apps between projects, and they can even be installed

docs/src/piccolo/schema/column_types.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ Example
390390
Type
391391
====
392392

393-
``UUID`` uses the ``UUID`` Python type for values.
393+
``UUID`` uses the ``UUID`` type for values.
394394

395395
.. code-block:: python
396396

piccolo/conf/apps.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,58 @@
33
from importlib import import_module
44
import typing as t
55

6-
if t.TYPE_CHECKING:
7-
from piccolo.table import Table
6+
from piccolo.table import Table
7+
8+
9+
def table_finder(
10+
modules: t.Sequence[str],
11+
include_tags: t.Sequence[str] = ["__all__"],
12+
exclude_tags: t.Sequence[str] = [],
13+
) -> t.List[t.Type[Table]]:
14+
"""
15+
Rather than explicitly importing and registering table classes with the
16+
AppConfig, ``table_finder`` can be used instead. It imports any ``Table``
17+
subclasses in the given modules. Tags can be used to limit which ``Table``
18+
subclasses are imported.
19+
20+
:param modules:
21+
The module paths to check for ``Table`` subclasses. For example,
22+
['blog.tables']. The path should be from the root of your project,
23+
not a relative path.
24+
:param include_tags:
25+
If the ``Table`` subclass has one of these tags, it will be
26+
imported. The special tag '__all__' will import all ``Table``
27+
subclasses found.
28+
:param exclude_tags:
29+
If the ``Table`` subclass has any of these tags, it won't be
30+
imported. `exclude_tags` overrides `include_tags`.
31+
32+
"""
33+
table_subclasses: t.List[t.Type[Table]] = []
34+
35+
for module_path in modules:
36+
try:
37+
module = import_module(module_path)
38+
except ImportError as exception:
39+
print(f"Unable to import {module_path}")
40+
raise exception
41+
42+
object_names = [i for i in dir(module) if not i.startswith("_")]
43+
44+
for object_name in object_names:
45+
_object = getattr(module, object_name)
46+
if issubclass(_object, Table) and _object is not Table:
47+
table: Table = _object
48+
if exclude_tags and set(table._meta.tags).intersection(
49+
set(exclude_tags)
50+
):
51+
continue
52+
elif "__all__" in include_tags:
53+
table_subclasses.append(_object)
54+
elif set(table._meta.tags).intersection(set(include_tags)):
55+
table_subclasses.append(_object)
56+
57+
return table_subclasses
858

959

1060
@dataclass

piccolo/table.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class TableMeta:
4040
default_columns: t.List[Column] = field(default_factory=list)
4141
non_default_columns: t.List[Column] = field(default_factory=list)
4242
foreign_key_columns: t.List[ForeignKey] = field(default_factory=list)
43+
tags: t.List[str] = field(default_factory=list)
4344
_db: t.Optional[Engine] = None
4445

4546
# Records reverse foreign key relationships - i.e. when the current table
@@ -97,7 +98,7 @@ def __init_subclass__(
9798
cls,
9899
tablename: t.Optional[str] = None,
99100
db: t.Optional[Engine] = None,
100-
app: t.Optional[str] = None,
101+
tags: t.List[str] = [],
101102
):
102103
"""
103104
Automatically populate the _meta, which includes the tablename, and
@@ -133,7 +134,7 @@ def __init_subclass__(
133134

134135
if isinstance(column, PrimaryKey):
135136
# We want it at the start.
136-
columns = [column] + columns
137+
columns = [column] + columns # type: ignore
137138
default_columns.append(column)
138139
else:
139140
columns.append(column)
@@ -152,6 +153,7 @@ def __init_subclass__(
152153
default_columns=default_columns,
153154
non_default_columns=non_default_columns,
154155
foreign_key_columns=foreign_key_columns,
156+
tags=tags,
155157
_db=db,
156158
)
157159

tests/conf/test_apps.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,57 @@
11
from unittest import TestCase
22

3-
from piccolo.conf.apps import AppRegistry
3+
from piccolo.conf.apps import AppRegistry, AppConfig, table_finder
44

55

66
class TestAppRegistry(TestCase):
77
def test_init(self):
8-
AppRegistry()
8+
app_registry = AppRegistry(apps=["piccolo.apps.user.piccolo_app"])
9+
app_config = app_registry.get_app_config(app_name="user")
10+
self.assertTrue(isinstance(app_config, AppConfig))
11+
12+
13+
class TestTableFinder(TestCase):
14+
def test_table_finder(self):
15+
"""
16+
Should return all Table subclasses.
17+
"""
18+
tables = table_finder(modules=["tests.example_project.tables"])
19+
20+
table_class_names = [i.__name__ for i in tables]
21+
table_class_names.sort()
22+
23+
self.assertEqual(
24+
table_class_names,
25+
["Band", "Concert", "Manager", "Poster", "Ticket", "Venue"],
26+
)
27+
28+
def test_include_tags(self):
29+
"""
30+
Should return all Table subclasses with a matching tag.
31+
"""
32+
tables = table_finder(
33+
modules=["tests.example_project.tables"], include_tags=["special"]
34+
)
35+
36+
table_class_names = [i.__name__ for i in tables]
37+
table_class_names.sort()
38+
39+
self.assertEqual(
40+
table_class_names, ["Poster"],
41+
)
42+
43+
def test_exclude_tags(self):
44+
"""
45+
Should return all Table subclasses without the specified tags.
46+
"""
47+
tables = table_finder(
48+
modules=["tests.example_project.tables"], exclude_tags=["special"]
49+
)
50+
51+
table_class_names = [i.__name__ for i in tables]
52+
table_class_names.sort()
53+
54+
self.assertEqual(
55+
table_class_names,
56+
["Band", "Concert", "Manager", "Ticket", "Venue"],
57+
)

tests/example_project/tables.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,9 @@ class Ticket(Table):
3535
price = Numeric(digits=(5, 2))
3636

3737

38-
class Poster(Table):
38+
class Poster(Table, tags=["special"]):
39+
"""
40+
Has tags for tests which need it.
41+
"""
42+
3943
content = Text()

0 commit comments

Comments
 (0)