Skip to content

Commit 52ef8dc

Browse files
authored
Add command to generate gino.ext.* stubs (#674)
* run `python -m gino.ext stub` to generate stubs * fixes #673
1 parent 7f09156 commit 52ef8dc

File tree

8 files changed

+195
-8
lines changed

8 files changed

+195
-8
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,7 @@ target/
6969
.DS_Store
7070

7171
# VS Code
72-
.vscode
72+
.vscode
73+
74+
# extension stub files
75+
src/gino/ext/*.pyi

poetry.lock

Lines changed: 49 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pytest-asyncio = "^0.10.0"
5656
pytest-mock = "^3.0.0"
5757
pytest-cov = "^2.8.1"
5858
black = { version = "^19.10b0", python = ">=3.6" }
59+
mypy = "^0.770"
5960

6061
# docs
6162
sphinx = "^3.0.3"

src/gino/ext/__main__.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Generate typing stubs for extensions.
2+
3+
$ python -m gino.ext
4+
5+
"""
6+
import sys
7+
import os
8+
9+
try:
10+
from importlib.metadata import entry_points
11+
except ImportError:
12+
from importlib_metadata import entry_points
13+
14+
15+
if __name__ == "__main__":
16+
base_dir = os.path.dirname(os.path.abspath(__file__))
17+
cmd = sys.argv[1] if len(sys.argv) == 2 else ""
18+
eps = list(entry_points().get("gino.extensions", []))
19+
20+
if cmd == "stub":
21+
added = False
22+
for ep in eps:
23+
path = os.path.join(base_dir, ep.name + ".pyi")
24+
if not os.path.exists(path):
25+
added = True
26+
print("Adding " + path)
27+
with open(path, "w") as f:
28+
f.write("from " + ep.value + " import *")
29+
if not added:
30+
print("Stub files are up to date.")
31+
32+
elif cmd == "clean":
33+
removed = False
34+
for filename in os.listdir(base_dir):
35+
if filename.endswith(".pyi"):
36+
removed = True
37+
path = os.path.join(base_dir, filename)
38+
print("Removing " + path)
39+
os.remove(path)
40+
if not removed:
41+
print("No stub files found.")
42+
43+
elif cmd == "list":
44+
name_size = max(len(ep.name) for ep in eps)
45+
value_size = max(len(ep.value) for ep in eps)
46+
for ep in eps:
47+
path = os.path.join(base_dir, ep.name + ".pyi")
48+
if not os.path.exists(path):
49+
path = "no stub file"
50+
print(
51+
"%s -> gino.ext.%s (%s)"
52+
% (ep.value.ljust(value_size), ep.name.ljust(name_size), path)
53+
)
54+
55+
else:
56+
print("Manages GINO extensions:")
57+
print()
58+
print(" python -m gino.ext COMMAND")
59+
print()
60+
print("Available commands:")
61+
print()
62+
print(" stub Generate gino/ext/*.pyi stub files for type checking.")
63+
print(" clean Remove the generated stub files.")
64+
print(" list List installed GINO extensions.")

src/gino/ext/py.typed

Whitespace-only changes.

tests/stub1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s1 = "111"

tests/stub2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s2 = 222

tests/test_ext.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
import importlib
33
import sys
44
import pytest
5+
import runpy
6+
from mypy.build import build
7+
from mypy.modulefinder import BuildSource
8+
from mypy.options import Options
59

610

711
def installed():
@@ -65,3 +69,74 @@ def test_import_error():
6569
with pytest.raises(ImportError, match="gino-nonexist"):
6670
# noinspection PyUnresolvedReferences
6771
from gino.ext import nonexist
72+
73+
74+
@pytest.fixture
75+
def extensions(mocker):
76+
EntryPoint = collections.namedtuple("EntryPoint", ["name", "value"])
77+
importlib_metadata = mocker.Mock()
78+
importlib_metadata.entry_points = lambda: {
79+
"gino.extensions": [
80+
EntryPoint("demo1", "tests.stub1"),
81+
EntryPoint("demo2", "tests.stub2"),
82+
]
83+
}
84+
mocker.patch.dict("sys.modules", {"importlib.metadata": importlib_metadata})
85+
86+
87+
def test_list(mocker, extensions):
88+
mocker.patch("sys.argv", ["", "list"])
89+
stdout = mocker.patch("sys.stdout.write")
90+
runpy.run_module("gino.ext", run_name="__main__")
91+
out = "".join(args[0][0] for args in stdout.call_args_list)
92+
assert "tests.stub1" in out
93+
assert "tests.stub2" in out
94+
assert "gino.ext.demo1" in out
95+
assert "gino.ext.demo2" in out
96+
assert out.count("no stub file") == 2
97+
98+
mocker.patch("sys.argv", [""])
99+
runpy.run_module("gino.ext", run_name="__main__")
100+
101+
102+
def test_type_check(mocker, extensions):
103+
mocker.patch("sys.argv", ["", "clean"])
104+
runpy.run_module("gino.ext", run_name="__main__")
105+
106+
result = build(
107+
[BuildSource(None, None, "from gino.ext.demo3 import s3")], Options()
108+
)
109+
assert result.errors
110+
111+
result = build(
112+
[BuildSource(None, None, "from gino.ext.demo1 import s1")], Options()
113+
)
114+
assert result.errors
115+
116+
mocker.patch("sys.argv", ["", "stub"])
117+
runpy.run_module("gino.ext", run_name="__main__")
118+
runpy.run_module("gino.ext", run_name="__main__")
119+
120+
try:
121+
result = build(
122+
[BuildSource(None, None, "from gino.ext.demo1 import s1")], Options()
123+
)
124+
assert not result.errors
125+
126+
result = build(
127+
[BuildSource(None, None, "from gino.ext.demo1 import s2")], Options()
128+
)
129+
assert result.errors
130+
131+
result = build(
132+
[BuildSource(None, None, "from gino.ext.demo2 import s2")], Options()
133+
)
134+
assert not result.errors
135+
136+
result = build(
137+
[BuildSource(None, None, "from gino.ext.demo2 import s1")], Options()
138+
)
139+
assert result.errors
140+
finally:
141+
mocker.patch("sys.argv", ["", "clean"])
142+
runpy.run_module("gino.ext", run_name="__main__")

0 commit comments

Comments
 (0)