Skip to content

Commit 9ae5cf2

Browse files
LazyLoad database engines (piccolo-orm#147)
* LazyLoader draft * modify extra requirements * fix github workflow * Update the installation docs * throw specific errors if driver is not installed * use `str` on exception for backwards compatibility * increase `lazy_laoder.py` coverage * mock module instead of object * set `postgres_only` and `sqlite_only` for tests * increase coverage of `postgres.py` and `sqlite.py` * update the installation message for the playground * change LazyLoader super method * Update README * Remove Travis badge and add other badges * add codecov badge * update asgi template requirements.txt file to install asyncpg Co-authored-by: Daniel Townsend <[email protected]>
1 parent eea5c7a commit 9ae5cf2

File tree

17 files changed

+148
-25
lines changed

17 files changed

+148
-25
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ jobs:
6464
python -m pip install --upgrade pip
6565
pip install -r requirements/requirements.txt
6666
pip install -r requirements/test-requirements.txt
67+
pip install -r requirements/extras/postgres.txt
6768
- name: Setup postgres
6869
run: |
6970
export PGPASSWORD=postgres
@@ -99,6 +100,7 @@ jobs:
99100
python -m pip install --upgrade pip
100101
pip install -r requirements/requirements.txt
101102
pip install -r requirements/test-requirements.txt
103+
pip install -r requirements/extras/sqlite.txt
102104
- name: Test with pytest, SQLite
103105
run: ./scripts/test-sqlite.sh
104106
- name: Upload coverage

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# Piccolo
22

3-
[![Build Status](https://travis-ci.com/piccolo-orm/piccolo.svg?branch=master)](https://travis-ci.com/piccolo-orm/piccolo)
3+
![Tests](https://github.com/piccolo-orm/piccolo/actions/workflows/tests.yaml/badge.svg)
4+
![Release](https://github.com/piccolo-orm/piccolo/actions/workflows/release.yaml/badge.svg)
45
[![Documentation Status](https://readthedocs.org/projects/piccolo-orm/badge/?version=latest)](https://piccolo-orm.readthedocs.io/en/latest/?badge=latest)
6+
[![PyPI](https://img.shields.io/pypi/v/piccolo?color=%2334D058&label=pypi)](https://pypi.org/project/piccolo/)
57
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/piccolo-orm/piccolo.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/piccolo-orm/piccolo/context:python)
68
[![Total alerts](https://img.shields.io/lgtm/alerts/g/piccolo-orm/piccolo.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/piccolo-orm/piccolo/alerts/)
9+
[![codecov](https://codecov.io/gh/piccolo-orm/piccolo/branch/master/graph/badge.svg?token=V19CWH7MXX)](https://codecov.io/gh/piccolo-orm/piccolo)
710

811
A fast, user friendly ORM and query builder which supports asyncio. [Read the docs](https://piccolo-orm.readthedocs.io/en/latest/).
912

@@ -66,8 +69,16 @@ await b.remove().run()
6669

6770
## Installation
6871

72+
Installing with PostgreSQL driver:
73+
74+
```
75+
pip install piccolo[postgres]
76+
```
77+
78+
Installing with SQLite driver:
79+
6980
```
70-
pip install piccolo
81+
pip install piccolo[sqlite]
7182
```
7283

7384
## Building a web app?

docs/src/piccolo/getting_started/installing_piccolo.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Now install piccolo, ideally inside a `virtualenv <https://docs.python-guide.org
2121
# The important bit:
2222
pip install piccolo
2323
24+
# Install Piccolo with PostgreSQL driver:
25+
pip install piccolo[postgres]
26+
27+
# Install Piccolo with SQLite driver:
28+
pip install piccolo[sqlite]
29+
2430
# Optional: orjson for improved JSON serialisation performance
2531
pip install piccolo[orjson]
2632
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{{ router }}
22
{{ server }}
33
jinja2
4-
piccolo
4+
piccolo[postgres]
55
piccolo_admin

piccolo/apps/playground/commands/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ def run(
145145
import IPython # type: ignore
146146
except ImportError:
147147
sys.exit(
148-
"Install iPython using `pip install ipython` to use this feature."
148+
"Install iPython using `pip install piccolo[playground,sqlite]` "
149+
"to use this feature."
149150
)
150151

151152
if engine.upper() == "POSTGRES":

piccolo/conf/apps.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def get_piccolo_conf_module(
316316

317317
try:
318318
module = t.cast(PiccoloConfModule, import_module(module_name))
319-
except ModuleNotFoundError:
319+
except ModuleNotFoundError as exc:
320320
if self.diagnose:
321321
colored_warning(
322322
(
@@ -326,7 +326,19 @@ def get_piccolo_conf_module(
326326
level=Level.high,
327327
)
328328
print(traceback.format_exc())
329-
return None
329+
330+
if str(exc) == "No module named 'asyncpg'":
331+
raise ModuleNotFoundError(
332+
"PostgreSQL driver not found. "
333+
"Try running 'pip install piccolo[postgres]'"
334+
)
335+
elif str(exc) == "No module named 'aiosqlite'":
336+
raise ModuleNotFoundError(
337+
"SQLite driver not found. "
338+
"Try running 'pip install piccolo[sqlite]'"
339+
)
340+
else:
341+
raise exc
330342
else:
331343
return module
332344

piccolo/engine/postgres.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@
44
import typing as t
55
from dataclasses import dataclass
66

7-
import asyncpg # type: ignore
8-
from asyncpg.connection import Connection # type: ignore
9-
from asyncpg.cursor import Cursor # type: ignore
10-
from asyncpg.exceptions import InsufficientPrivilegeError # type: ignore
11-
from asyncpg.pool import Pool # type: ignore
12-
137
from piccolo.engine.base import Batch, Engine
148
from piccolo.engine.exceptions import TransactionError
159
from piccolo.query.base import Query
1610
from piccolo.querystring import QueryString
11+
from piccolo.utils.lazy_loader import LazyLoader
1712
from piccolo.utils.sync import run_sync
1813
from piccolo.utils.warnings import Level, colored_string, colored_warning
1914

15+
asyncpg = LazyLoader("asyncpg", globals(), "asyncpg")
16+
17+
if t.TYPE_CHECKING: # pragma: no cover
18+
from asyncpg.connection import Connection # type: ignore
19+
from asyncpg.cursor import Cursor # type: ignore
20+
from asyncpg.pool import Pool # type: ignore
21+
2022

2123
@dataclass
2224
class AsyncBatch(Batch):
@@ -283,7 +285,7 @@ async def prep_database(self):
283285
await self._run_in_new_connection(
284286
f'CREATE EXTENSION IF NOT EXISTS "{extension}"',
285287
)
286-
except InsufficientPrivilegeError:
288+
except asyncpg.exceptions.InsufficientPrivilegeError:
287289
print(
288290
colored_string(
289291
f"=> Unable to create {extension} extension - some "

piccolo/engine/sqlite.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@
99
from dataclasses import dataclass
1010
from decimal import Decimal
1111

12-
from aiosqlite import Connection, Cursor, connect
13-
1412
from piccolo.engine.base import Batch, Engine
1513
from piccolo.engine.exceptions import TransactionError
1614
from piccolo.query.base import Query
1715
from piccolo.querystring import QueryString
1816
from piccolo.utils.encoding import dump_json, load_json
17+
from piccolo.utils.lazy_loader import LazyLoader
1918
from piccolo.utils.sync import run_sync
2019

21-
if t.TYPE_CHECKING:
20+
aiosqlite = LazyLoader("aiosqlite", globals(), "aiosqlite")
21+
22+
23+
if t.TYPE_CHECKING: # pragma: no cover
24+
from aiosqlite import Connection, Cursor # type: ignore
25+
2226
from piccolo.table import Table
2327

2428
###############################################################################
@@ -414,7 +418,7 @@ async def batch(self, query: Query, batch_size: int = 100) -> AsyncBatch:
414418
###########################################################################
415419

416420
async def get_connection(self) -> Connection:
417-
connection = await connect(**self.connection_kwargs)
421+
connection = await aiosqlite.connect(**self.connection_kwargs)
418422
connection.row_factory = dict_factory # type: ignore
419423
await connection.execute("PRAGMA foreign_keys = 1")
420424
return connection
@@ -442,7 +446,7 @@ async def _run_in_new_connection(
442446
query_type: str = "generic",
443447
table: t.Optional[t.Type[Table]] = None,
444448
):
445-
async with connect(**self.connection_kwargs) as connection:
449+
async with aiosqlite.connect(**self.connection_kwargs) as connection:
446450
await connection.execute("PRAGMA foreign_keys = 1")
447451

448452
connection.row_factory = dict_factory # type: ignore

piccolo/utils/lazy_loader.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/util/lazy_loader.py
2+
from __future__ import absolute_import, division, print_function
3+
4+
import importlib
5+
import types
6+
import typing as t
7+
8+
9+
class LazyLoader(types.ModuleType):
10+
"""
11+
Lazily import a module.
12+
13+
`PostgresEngine` and `SQLiteEngine` are example use cases.
14+
"""
15+
16+
def __init__(self, local_name, parent_module_globals, name):
17+
self._local_name = local_name
18+
self._parent_module_globals = parent_module_globals
19+
20+
super().__init__(name)
21+
22+
def _load(self) -> types.ModuleType:
23+
try:
24+
# Import the target module and
25+
# insert it into the parent's namespace
26+
module = importlib.import_module(self.__name__)
27+
self._parent_module_globals[self._local_name] = module
28+
29+
# Update this object's dict so that
30+
# if someone keeps a reference to the
31+
# LazyLoader, lookups are efficient
32+
# (__getattr__ is only called on lookups that fail).
33+
self.__dict__.update(module.__dict__)
34+
35+
return module
36+
37+
except ModuleNotFoundError as exc:
38+
if str(exc) == "No module named 'asyncpg'":
39+
raise ModuleNotFoundError(
40+
"PostgreSQL driver not found. "
41+
"Try running 'pip install piccolo[postgres]'"
42+
)
43+
elif str(exc) == "No module named 'aiosqlite'":
44+
raise ModuleNotFoundError(
45+
"SQLite driver not found. "
46+
"Try running 'pip install piccolo[sqlite]'"
47+
)
48+
else:
49+
raise exc
50+
51+
def __getattr__(self, item) -> t.Any:
52+
module = self._load()
53+
return getattr(module, item)
54+
55+
def __dir__(self) -> t.List[str]:
56+
module = self._load()
57+
return dir(module)

requirements/dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
black>=21.7b0
22
ipdb==0.12.2
33
ipython==7.8.0
4+
flake8==3.8.4
45
isort==5.9.2
56
twine==3.1.1
67
mypy==0.782

0 commit comments

Comments
 (0)