Skip to content

Commit 45775a7

Browse files
authored
1184 Be able to auto register apps (#1182)
* auto register apps * update docs * add tests * move `get_app_identifier` out * rename var to `app_module` * fix test * add docs for root
1 parent 9f9febb commit 45775a7

File tree

5 files changed

+198
-7
lines changed

5 files changed

+198
-7
lines changed

docs/src/piccolo/projects_and_apps/piccolo_apps.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Run the following command within your project:
1818

1919
.. code-block:: bash
2020
21-
piccolo app new my_app
21+
piccolo app new my_app --register
2222
2323
2424
Where ``my_app`` is your new app's name. This will create a folder like this:
@@ -34,7 +34,8 @@ Where ``my_app`` is your new app's name. This will create a folder like this:
3434
3535
3636
It's important to register your new app with the ``APP_REGISTRY`` in
37-
``piccolo_conf.py``.
37+
``piccolo_conf.py``. If you used the ``--register`` flag, then this is done
38+
automatically. Otherwise, add it manually:
3839

3940
.. code-block:: python
4041
@@ -45,6 +46,18 @@ It's important to register your new app with the ``APP_REGISTRY`` in
4546
Anytime you invoke the ``piccolo`` command, you will now be able to perform
4647
operations on your app, such as :ref:`Migrations`.
4748

49+
root
50+
~~~~
51+
52+
By default the app is created in the current directory. If you want the app to
53+
be in a sub folder, you can use the ``--root`` option:
54+
55+
.. code-block:: bash
56+
57+
piccolo app new my_app --register --root=./apps
58+
59+
The app will then be created in the ``apps`` folder.
60+
4861
-------------------------------------------------------------------------------
4962

5063
AppConfig

piccolo/apps/app/commands/new.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import importlib
44
import os
5+
import pathlib
56
import sys
67
import typing as t
78

89
import black
910
import jinja2
1011

12+
from piccolo.conf.apps import PiccoloConfUpdater
13+
1114
TEMPLATE_DIRECTORY = os.path.join(
1215
os.path.dirname(os.path.abspath(__file__)), "templates"
1316
)
@@ -30,7 +33,11 @@ def module_exists(module_name: str) -> bool:
3033
return True
3134

3235

33-
def new_app(app_name: str, root: str = "."):
36+
def get_app_module(app_name: str, root: str) -> str:
37+
return ".".join([*pathlib.Path(root).parts, app_name, "piccolo_app"])
38+
39+
40+
def new_app(app_name: str, root: str = ".", register: bool = False):
3441
print(f"Creating {app_name} app ...")
3542

3643
app_root = os.path.join(root, app_name)
@@ -69,8 +76,12 @@ def new_app(app_name: str, root: str = "."):
6976
with open(os.path.join(migrations_folder_path, "__init__.py"), "w"):
7077
pass
7178

79+
if register:
80+
app_module = get_app_module(app_name=app_name, root=root)
81+
PiccoloConfUpdater().register_app(app_module=app_module)
82+
7283

73-
def new(app_name: str, root: str = "."):
84+
def new(app_name: str, root: str = ".", register: bool = False):
7485
"""
7586
Creates a new Piccolo app.
7687
@@ -79,6 +90,8 @@ def new(app_name: str, root: str = "."):
7990
:param root:
8091
Where to create the app e.g. ./my/folder. By default it creates the
8192
app in the current directory.
93+
:param register:
94+
If True, the app is registered automatically in piccolo_conf.py.
8295
8396
"""
84-
new_app(app_name=app_name, root=root)
97+
new_app(app_name=app_name, root=root, register=register)

piccolo/conf/apps.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import ast
34
import inspect
45
import itertools
56
import os
@@ -11,6 +12,8 @@
1112
from importlib import import_module
1213
from types import ModuleType
1314

15+
import black
16+
1417
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
1518
from piccolo.engine.base import Engine
1619
from piccolo.table import Table
@@ -437,6 +440,17 @@ def get_piccolo_conf_module(
437440
else:
438441
return module
439442

443+
def get_piccolo_conf_path(self) -> str:
444+
piccolo_conf_module = self.get_piccolo_conf_module()
445+
446+
if piccolo_conf_module is None:
447+
raise ModuleNotFoundError("piccolo_conf.py not found.")
448+
449+
module_file_path = piccolo_conf_module.__file__
450+
assert module_file_path
451+
452+
return module_file_path
453+
440454
def get_app_registry(self) -> AppRegistry:
441455
"""
442456
Returns the ``AppRegistry`` instance within piccolo_conf.
@@ -583,3 +597,88 @@ def get_table_classes(
583597
tables.extend(app_config.table_classes)
584598

585599
return tables
600+
601+
602+
###############################################################################
603+
604+
605+
class PiccoloConfUpdater:
606+
607+
def __init__(self, piccolo_conf_path: t.Optional[str] = None):
608+
"""
609+
:param piccolo_conf_path:
610+
The path to the piccolo_conf.py (e.g. `./piccolo_conf.py`). If not
611+
passed in, we use our ``Finder`` class to get it.
612+
"""
613+
self.piccolo_conf_path = (
614+
piccolo_conf_path or Finder().get_piccolo_conf_path()
615+
)
616+
617+
def _modify_app_registry_src(self, src: str, app_module: str) -> str:
618+
"""
619+
:param src:
620+
The contents of the ``piccolo_conf.py`` file.
621+
:param app_module:
622+
The app to add to the registry e.g. ``'music.piccolo_app'``.
623+
:returns:
624+
Updated Python source code string.
625+
626+
"""
627+
ast_root = ast.parse(src)
628+
629+
parsing_successful = False
630+
631+
for node in ast.walk(ast_root):
632+
if isinstance(node, ast.Call):
633+
if (
634+
isinstance(node.func, ast.Name)
635+
and node.func.id == "AppRegistry"
636+
):
637+
if len(node.keywords) > 0:
638+
keyword = node.keywords[0]
639+
if keyword.arg == "apps":
640+
apps = keyword.value
641+
if isinstance(apps, ast.List):
642+
apps.elts.append(
643+
ast.Constant(app_module, kind="str")
644+
)
645+
parsing_successful = True
646+
break
647+
648+
if not parsing_successful:
649+
raise SyntaxError(
650+
"Unable to parse piccolo_conf.py - `AppRegistry(apps=...)` "
651+
"not found)."
652+
)
653+
654+
new_contents = ast.unparse(ast_root)
655+
656+
formatted_contents = black.format_str(
657+
new_contents, mode=black.FileMode(line_length=80)
658+
)
659+
660+
return formatted_contents
661+
662+
def register_app(self, app_module: str):
663+
"""
664+
Adds the given app to the ``AppRegistry`` in ``piccolo_conf.py``.
665+
666+
This is used by command line tools like:
667+
668+
.. code-block:: bash
669+
670+
piccolo app new my_app --register
671+
672+
:param app_module:
673+
The module of the app, e.g. ``'music.piccolo_app'``.
674+
675+
"""
676+
with open(self.piccolo_conf_path) as f:
677+
piccolo_conf_src = f.read()
678+
679+
new_contents = self._modify_app_registry_src(
680+
src=piccolo_conf_src, app_module=app_module
681+
)
682+
683+
with open(self.piccolo_conf_path, "wt") as f:
684+
f.write(new_contents)

tests/apps/app/commands/test_new.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import tempfile
44
from unittest import TestCase
55

6-
from piccolo.apps.app.commands.new import module_exists, new
6+
from piccolo.apps.app.commands.new import get_app_module, module_exists, new
77

88

99
class TestModuleExists(TestCase):
@@ -43,3 +43,21 @@ def test_new_with_clashing_name(self):
4343
"A module called sys already exists"
4444
)
4545
)
46+
47+
48+
class TestGetAppIdentifier(TestCase):
49+
50+
def test_get_app_module(self):
51+
"""
52+
Make sure the the ``root`` argument is handled correctly.
53+
"""
54+
self.assertEqual(
55+
get_app_module(app_name="music", root="."),
56+
"music.piccolo_app",
57+
)
58+
59+
for root in ("apps", "./apps", "./apps/"):
60+
self.assertEqual(
61+
get_app_module(app_name="music", root=root),
62+
"apps.music.piccolo_app",
63+
)

tests/conf/test_apps.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
from __future__ import annotations
22

33
import pathlib
4+
import tempfile
45
from unittest import TestCase
56

67
from piccolo.apps.user.tables import BaseUser
7-
from piccolo.conf.apps import AppConfig, AppRegistry, Finder, table_finder
8+
from piccolo.conf.apps import (
9+
AppConfig,
10+
AppRegistry,
11+
Finder,
12+
PiccoloConfUpdater,
13+
table_finder,
14+
)
815
from tests.example_apps.mega.tables import MegaTable, SmallTable
916
from tests.example_apps.music.tables import (
1017
Band,
@@ -310,3 +317,44 @@ def test_sort_app_configs(self):
310317
self.assertListEqual(
311318
[i.app_name for i in sorted_app_configs], ["app_2", "app_1"]
312319
)
320+
321+
322+
class TestPiccoloConfUpdater(TestCase):
323+
324+
def test_modify_app_registry_src(self):
325+
"""
326+
Make sure the `piccolo_conf.py` source code can be modified
327+
successfully.
328+
"""
329+
updater = PiccoloConfUpdater()
330+
331+
new_src = updater._modify_app_registry_src(
332+
src="APP_REGISTRY = AppRegistry(apps=[])",
333+
app_module="music.piccolo_app",
334+
)
335+
self.assertEqual(
336+
new_src.strip(),
337+
'APP_REGISTRY = AppRegistry(apps=["music.piccolo_app"])',
338+
)
339+
340+
def test_register_app(self):
341+
"""
342+
Make sure the new contents get written to disk.
343+
"""
344+
temp_dir = tempfile.gettempdir()
345+
piccolo_conf_path = pathlib.Path(temp_dir) / "piccolo_conf.py"
346+
347+
src = "APP_REGISTRY = AppRegistry(apps=[])"
348+
349+
with open(piccolo_conf_path, "wt") as f:
350+
f.write(src)
351+
352+
updater = PiccoloConfUpdater(piccolo_conf_path=str(piccolo_conf_path))
353+
updater.register_app(app_module="music.piccolo_app")
354+
355+
with open(piccolo_conf_path) as f:
356+
contents = f.read().strip()
357+
358+
self.assertEqual(
359+
contents, 'APP_REGISTRY = AppRegistry(apps=["music.piccolo_app"])'
360+
)

0 commit comments

Comments
 (0)