Skip to content

Commit f66cdb9

Browse files
authored
INTPYTHON-380 Add typing (#176)
1 parent edfb8c9 commit f66cdb9

16 files changed

+205
-96
lines changed

docs/conf.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
# Add any paths that contain custom static files (such as style sheets) here,
134134
# relative to this directory. They are copied after the builtin static files,
135135
# so a file named "default.css" will overwrite the builtin "default.css".
136-
html_static_path = []
136+
html_static_path: list[str] = []
137137

138138
# If not "", a "Last updated on:" timestamp is inserted at every page bottom,
139139
# using the given strftime format.
@@ -182,14 +182,14 @@
182182

183183
# -- Options for LaTeX output --------------------------------------------------
184184

185-
latex_elements = {
186-
# The paper size ("letterpaper" or "a4paper").
187-
# "papersize": "letterpaper",
188-
# The font size ("10pt", "11pt" or "12pt").
189-
# "pointsize": "10pt",
190-
# Additional stuff for the LaTeX preamble.
191-
# "preamble": "",
192-
}
185+
# latex_elements = {
186+
# The paper size ("letterpaper" or "a4paper").
187+
# "papersize": "letterpaper",
188+
# The font size ("10pt", "11pt" or "12pt").
189+
# "pointsize": "10pt",
190+
# Additional stuff for the LaTeX preamble.
191+
# "preamble": "",
192+
# }
193193

194194
# Grouping the document tree into LaTeX files. List of tuples
195195
# (source start file, target name, title, author, documentclass [howto/manual]).
@@ -228,9 +228,7 @@
228228

229229
# One entry per manual page. List of tuples
230230
# (source start file, name, description, authors, manual section).
231-
man_pages = [
232-
("index", "flask-pymongo", "Flask-PyMongo Documentation", ["Dan Crosta"], 1)
233-
]
231+
man_pages = [("index", "flask-pymongo", "Flask-PyMongo Documentation", ["Dan Crosta"], 1)]
234232

235233
# If true, show URL addresses after external links.
236234
# man_show_urls = False

examples/wiki/wiki.py

+19-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from __future__ import annotations
22

33
import re
4+
from typing import TYPE_CHECKING, Any
45

5-
import markdown2
6+
import markdown2 # type:ignore[import-untyped]
67
from flask import Flask, redirect, render_template, request, url_for
78

89
from flask_pymongo import PyMongo
910

11+
if TYPE_CHECKING:
12+
from werkzeug.wrappers.response import Response
13+
1014
app = Flask(__name__)
1115
mongo = PyMongo(app, "mongodb://localhost/wiki")
1216

@@ -15,17 +19,17 @@
1519

1620

1721
@app.route("/", methods=["GET"])
18-
def redirect_to_homepage():
22+
def redirect_to_homepage() -> Response:
1923
return redirect(url_for("show_page", pagepath="HomePage"))
2024

2125

2226
@app.template_filter()
23-
def totitle(value):
27+
def totitle(value: str) -> str:
2428
return " ".join(WIKIPART.findall(value))
2529

2630

2731
@app.template_filter()
28-
def wikify(value):
32+
def wikify(value: str) -> Any:
2933
parts = WIKIWORD.split(value)
3034
for i, part in enumerate(parts):
3135
if WIKIWORD.match(part):
@@ -36,20 +40,23 @@ def wikify(value):
3640

3741

3842
@app.route("/<path:pagepath>")
39-
def show_page(pagepath):
40-
page = mongo.db.pages.find_one_or_404({"_id": pagepath})
43+
def show_page(pagepath: str) -> str:
44+
assert mongo.db is not None
45+
page: dict[str, Any] = mongo.db.pages.find_one_or_404({"_id": pagepath})
4146
return render_template("page.html", page=page, pagepath=pagepath)
4247

4348

4449
@app.route("/edit/<path:pagepath>", methods=["GET"])
45-
def edit_page(pagepath):
46-
page = mongo.db.pages.find_one_or_404({"_id": pagepath})
50+
def edit_page(pagepath: str) -> str:
51+
assert mongo.db is not None
52+
page: dict[str, Any] = mongo.db.pages.find_one_or_404({"_id": pagepath})
4753
return render_template("edit.html", page=page, pagepath=pagepath)
4854

4955

5056
@app.route("/edit/<path:pagepath>", methods=["POST"])
51-
def save_page(pagepath):
57+
def save_page(pagepath: str) -> Response:
5258
if "cancel" not in request.form:
59+
assert mongo.db is not None
5360
mongo.db.pages.update(
5461
{"_id": pagepath},
5562
{"$set": {"body": request.form["body"]}},
@@ -60,7 +67,7 @@ def save_page(pagepath):
6067

6168

6269
@app.errorhandler(404)
63-
def new_page(error):
70+
def new_page(error: Any) -> str:
6471
pagepath = request.path.lstrip("/")
6572
if pagepath.startswith("uploads"):
6673
filename = pagepath[len("uploads") :].lstrip("/")
@@ -69,12 +76,12 @@ def new_page(error):
6976

7077

7178
@app.route("/uploads/<path:filename>")
72-
def get_upload(filename):
79+
def get_upload(filename: str) -> Response:
7380
return mongo.send_file(filename)
7481

7582

7683
@app.route("/uploads/<path:filename>", methods=["POST"])
77-
def save_upload(filename):
84+
def save_upload(filename: str) -> str | Response:
7885
if request.files.get("file"):
7986
mongo.save_file(filename, request.files["file"])
8087
return redirect(url_for("get_upload", filename=filename))

flask_pymongo/__init__.py

+26-20
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,22 @@
2424
# POSSIBILITY OF SUCH DAMAGE.
2525
from __future__ import annotations
2626

27-
__all__ = ("PyMongo", "ASCENDING", "DESCENDING")
27+
__all__ = ("PyMongo", "ASCENDING", "DESCENDING", "BSONObjectIdConverter", "BSONProvider")
2828

2929
import hashlib
3030
from mimetypes import guess_type
31+
from typing import Any
3132

3233
import pymongo
33-
from flask import abort, current_app, request
34+
from flask import Flask, Response, abort, current_app, request
3435
from gridfs import GridFS, NoFile
3536
from pymongo import uri_parser
37+
from pymongo.driver_info import DriverInfo
3638
from werkzeug.wsgi import wrap_file
3739

38-
# DriverInfo was added in PyMongo 3.7
39-
try:
40-
from pymongo.driver_info import DriverInfo
41-
except ImportError:
42-
DriverInfo = None
43-
4440
from flask_pymongo._version import __version__
4541
from flask_pymongo.helpers import BSONObjectIdConverter, BSONProvider
46-
from flask_pymongo.wrappers import MongoClient
42+
from flask_pymongo.wrappers import Database, MongoClient
4743

4844
DESCENDING = pymongo.DESCENDING
4945
"""Descending sort order."""
@@ -65,15 +61,16 @@ class PyMongo:
6561
6662
"""
6763

68-
def __init__(self, app=None, uri=None, *args, **kwargs):
69-
self.cx = None
70-
self.db = None
71-
self._json_provider = BSONProvider(app)
64+
def __init__(
65+
self, app: Flask | None = None, uri: str | None = None, *args: Any, **kwargs: Any
66+
) -> None:
67+
self.cx: MongoClient | None = None
68+
self.db: Database | None = None
7269

7370
if app is not None:
7471
self.init_app(app, uri, *args, **kwargs)
7572

76-
def init_app(self, app, uri=None, *args, **kwargs):
73+
def init_app(self, app: Flask, uri: str | None = None, *args: Any, **kwargs: Any) -> None:
7774
"""Initialize this :class:`PyMongo` for use.
7875
7976
Configure a :class:`~pymongo.mongo_client.MongoClient`
@@ -122,10 +119,12 @@ def init_app(self, app, uri=None, *args, **kwargs):
122119
self.db = self.cx[database_name]
123120

124121
app.url_map.converters["ObjectId"] = BSONObjectIdConverter
125-
app.json = self._json_provider
122+
app.json = BSONProvider(app)
126123

127124
# view helpers
128-
def send_file(self, filename, base="fs", version=-1, cache_for=31536000):
125+
def send_file(
126+
self, filename: str, base: str = "fs", version: int = -1, cache_for: int = 31536000
127+
) -> Response:
129128
"""Respond with a file from GridFS.
130129
131130
Returns an instance of the :attr:`~flask.Flask.response_class`
@@ -153,6 +152,7 @@ def get_upload(filename):
153152
if not isinstance(cache_for, int):
154153
raise TypeError("'cache_for' must be an integer")
155154

155+
assert self.db is not None, "Please initialize the app before calling send_file!"
156156
storage = GridFS(self.db, base)
157157

158158
try:
@@ -183,7 +183,14 @@ def get_upload(filename):
183183
response.make_conditional(request)
184184
return response
185185

186-
def save_file(self, filename, fileobj, base="fs", content_type=None, **kwargs):
186+
def save_file(
187+
self,
188+
filename: str,
189+
fileobj: Any,
190+
base: str = "fs",
191+
content_type: str | None = None,
192+
**kwargs: Any,
193+
) -> Any:
187194
"""Save a file-like object to GridFS using the given filename.
188195
Return the "_id" of the created file.
189196
@@ -211,8 +218,7 @@ def save_upload(filename):
211218
if content_type is None:
212219
content_type, _ = guess_type(filename)
213220

221+
assert self.db is not None, "Please initialize the app before calling save_file!"
214222
storage = GridFS(self.db, base)
215-
id = storage.put(
216-
fileobj, filename=filename, content_type=content_type, **kwargs
217-
)
223+
id = storage.put(fileobj, filename=filename, content_type=content_type, **kwargs)
218224
return id

flask_pymongo/helpers.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
__all__ = ("BSONObjectIdConverter", "BSONProvider")
2828

29+
from typing import Any
30+
2931
from bson import json_util
3032
from bson.errors import InvalidId
3133
from bson.json_util import RELAXED_JSON_OPTIONS
@@ -35,7 +37,7 @@
3537
from werkzeug.routing import BaseConverter
3638

3739

38-
def _iteritems(obj):
40+
def _iteritems(obj: Any) -> Any:
3941
if hasattr(obj, "iteritems"):
4042
return obj.iteritems()
4143
if hasattr(obj, "items"):
@@ -65,13 +67,13 @@ def show_task(task_id):
6567
6668
"""
6769

68-
def to_python(self, value):
70+
def to_python(self, value: Any) -> ObjectId:
6971
try:
7072
return ObjectId(value)
7173
except InvalidId:
7274
raise abort(404) from None
7375

74-
def to_url(self, value):
76+
def to_url(self, value: Any) -> str:
7577
return str(value)
7678

7779

@@ -98,15 +100,15 @@ def json_route(cart_id):
98100
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`.
99101
"""
100102

101-
def __init__(self, app):
103+
def __init__(self, app: Any) -> None:
102104
self._default_kwargs = {"json_options": RELAXED_JSON_OPTIONS}
103105

104106
super().__init__(app)
105107

106-
def dumps(self, obj):
108+
def dumps(self, obj: Any, **kwargs: Any) -> str:
107109
"""Serialize MongoDB object types using :mod:`bson.json_util`."""
108110
return json_util.dumps(obj)
109111

110-
def loads(self, str_obj):
112+
def loads(self, s: str | bytes, **kwargs: Any) -> Any:
111113
"""Deserialize MongoDB object types using :mod:`bson.json_util`."""
112-
return json_util.loads(str_obj)
114+
return json_util.loads(s)
File renamed without changes.

flask_pymongo/wrappers.py

+20-18
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
# POSSIBILITY OF SUCH DAMAGE.
2525
from __future__ import annotations
2626

27+
from typing import Any
28+
2729
from flask import abort
2830
from pymongo import collection, database, mongo_client
2931

3032

31-
class MongoClient(mongo_client.MongoClient):
33+
class MongoClient(mongo_client.MongoClient[dict[str, Any]]):
3234
"""Wrapper for :class:`~pymongo.mongo_client.MongoClient`.
3335
3436
Returns instances of Flask-PyMongo
@@ -37,20 +39,20 @@ class MongoClient(mongo_client.MongoClient):
3739
3840
"""
3941

40-
def __getattr__(self, name):
42+
def __getattr__(self, name: str) -> Any:
4143
attr = super().__getattr__(name)
4244
if isinstance(attr, database.Database):
4345
return Database(self, name)
4446
return attr
4547

46-
def __getitem__(self, item):
47-
attr = super().__getitem__(item)
48+
def __getitem__(self, name: str) -> Any:
49+
attr = super().__getitem__(name)
4850
if isinstance(attr, database.Database):
49-
return Database(self, item)
51+
return Database(self, name)
5052
return attr
5153

5254

53-
class Database(database.Database):
55+
class Database(database.Database[dict[str, Any]]):
5456
"""Wrapper for :class:`~pymongo.database.Database`.
5557
5658
Returns instances of Flask-PyMongo
@@ -59,37 +61,37 @@ class Database(database.Database):
5961
6062
"""
6163

62-
def __getattr__(self, name):
64+
def __getattr__(self, name: str) -> Any:
6365
attr = super().__getattr__(name)
6466
if isinstance(attr, collection.Collection):
6567
return Collection(self, name)
6668
return attr
6769

68-
def __getitem__(self, item):
69-
item_ = super().__getitem__(item)
70+
def __getitem__(self, name: str) -> Any:
71+
item_ = super().__getitem__(name)
7072
if isinstance(item_, collection.Collection):
71-
return Collection(self, item)
73+
return Collection(self, name)
7274
return item_
7375

7476

75-
class Collection(collection.Collection):
77+
class Collection(collection.Collection[dict[str, Any]]):
7678
"""Sub-class of PyMongo :class:`~pymongo.collection.Collection` with helpers."""
7779

78-
def __getattr__(self, name):
80+
def __getattr__(self, name: str) -> Any:
7981
attr = super().__getattr__(name)
8082
if isinstance(attr, collection.Collection):
8183
db = self._Collection__database
8284
return Collection(db, attr.name)
8385
return attr
8486

85-
def __getitem__(self, item):
86-
item_ = super().__getitem__(item)
87-
if isinstance(item_, collection.Collection):
87+
def __getitem__(self, name: str) -> Any:
88+
item = super().__getitem__(name)
89+
if isinstance(item, collection.Collection):
8890
db = self._Collection__database
89-
return Collection(db, item_.name)
90-
return item_
91+
return Collection(db, item.name)
92+
return item
9193

92-
def find_one_or_404(self, *args, **kwargs):
94+
def find_one_or_404(self, *args: Any, **kwargs: Any) -> Any:
9395
"""Find a single document or raise a 404.
9496
9597
This is like :meth:`~pymongo.collection.Collection.find_one`, but

justfile

+3
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ lint:
1616

1717
docs:
1818
uv run sphinx-build -T -b html docs docs/_build
19+
20+
typing:
21+
uv run mypy --install-types --non-interactive .

0 commit comments

Comments
 (0)