Skip to content

Commit fe29aab

Browse files
authored
INTPYTHON-380 Update for flask 3.0 and add GitHub workflows (#170)
1 parent 39f0fd5 commit fe29aab

13 files changed

+139
-163
lines changed

.github/workflows/release-python.yml

-2
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,3 @@ jobs:
6262
path: dist/
6363
- name: Publish distribution 📦 to PyPI
6464
uses: pypa/gh-action-pypi-publish@release/v1
65-
with:
66-
repository-url: https://test.pypi.org/legacy/

.github/workflows/test-python.yml

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Python Tests
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
8+
concurrency:
9+
group: tests-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
defaults:
13+
run:
14+
shell: bash -eux {0}
15+
16+
env:
17+
MONGODB_VERSION: "7.0"
18+
19+
jobs:
20+
build:
21+
runs-on: ${{ matrix.os }}
22+
strategy:
23+
matrix:
24+
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
25+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
26+
fail-fast: false
27+
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
28+
steps:
29+
- uses: actions/checkout@v4
30+
with:
31+
persist-credentials: false
32+
fetch-depth: 0
33+
- name: Setup Python
34+
uses: actions/setup-python@v5
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
- name: Start MongoDB on Linux
38+
if: ${{ startsWith(runner.os, 'Linux') }}
39+
run: |
40+
docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${MONGODB_VERSION} mongod --replSet rs --setParameter transactionLifetimeLimitSeconds=5
41+
until docker exec --tty mongodb mongosh 127.0.0.1:27017 --eval "db.runCommand({ ping: 1 })"; do
42+
sleep 1
43+
done
44+
sudo docker exec --tty mongodb mongosh 127.0.0.1:27017 --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})"
45+
- name: Start MongoDB on MacOS
46+
if: ${{ startsWith(runner.os, 'macOS') }}
47+
run: |
48+
brew tap mongodb/brew
49+
brew install mongodb/brew/mongodb-community@${MONGODB_VERSION}
50+
brew services start mongodb-community@${MONGODB_VERSION}
51+
- name: Start MongoDB on Windows
52+
if: ${{ startsWith(runner.os, 'Windows') }}
53+
shell: powershell
54+
run: |
55+
mkdir data
56+
mongod --remove
57+
mongod --install --dbpath=$(pwd)/data --logpath=$PWD/mongo.log
58+
net start MongoDB
59+
- name: Install package and pytest
60+
run: |
61+
python -m pip install .
62+
python -m pip install pytest
63+
- name: Run the tests
64+
run: |
65+
pytest .

.github/workflows/zizmor.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: GitHub Actions Security Analysis with zizmor
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
zizmor:
11+
name: zizmor latest via Cargo
12+
runs-on: ubuntu-latest
13+
permissions:
14+
security-events: write
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
with:
19+
persist-credentials: false
20+
- name: Setup Rust
21+
uses: actions-rust-lang/setup-rust-toolchain@v1
22+
- name: Get zizmor
23+
run: cargo install zizmor
24+
- name: Run zizmor
25+
run: zizmor --format sarif . > results.sarif
26+
env:
27+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28+
- name: Upload SARIF file
29+
uses: github/codeql-action/upload-sarif@v3
30+
with:
31+
sarif_file: results.sarif
32+
category: zizmor

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Flask-PyMongo
22

3-
PyMongo support for Flask applications
3+
PyMongo support for Flask applications. Requires `flask>=3.0` and `pymongo>=4.0`
44

55
## Quickstart
66

azure-pipelines.yml

-40
This file was deleted.

flask_pymongo/__init__.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from functools import partial
3030
from mimetypes import guess_type
31+
import hashlib
3132

3233
from flask import abort, current_app, request
3334
from gridfs import GridFS, NoFile
@@ -41,7 +42,7 @@
4142
DriverInfo = None
4243

4344
from flask_pymongo._version import __version__
44-
from flask_pymongo.helpers import BSONObjectIdConverter, JSONEncoder
45+
from flask_pymongo.helpers import BSONObjectIdConverter, BSONProvider
4546
from flask_pymongo.wrappers import MongoClient
4647

4748
DESCENDING = pymongo.DESCENDING
@@ -65,10 +66,10 @@ class PyMongo(object):
6566
6667
"""
6768

68-
def __init__(self, app=None, uri=None, json_options=None, *args, **kwargs):
69+
def __init__(self, app=None, uri=None, *args, **kwargs):
6970
self.cx = None
7071
self.db = None
71-
self._json_encoder = partial(JSONEncoder, json_options=json_options)
72+
self._json_provider = BSONProvider(app)
7273

7374
if app is not None:
7475
self.init_app(app, uri, *args, **kwargs)
@@ -122,7 +123,7 @@ def init_app(self, app, uri=None, *args, **kwargs):
122123
self.db = self.cx[database_name]
123124

124125
app.url_map.converters["ObjectId"] = BSONObjectIdConverter
125-
app.json_encoder = self._json_encoder
126+
app.json = self._json_provider
126127

127128
# view helpers
128129
def send_file(self, filename, base="fs", version=-1, cache_for=31536000):
@@ -163,14 +164,20 @@ def get_upload(filename):
163164
# mostly copied from flask/helpers.py, with
164165
# modifications for GridFS
165166
data = wrap_file(request.environ, fileobj, buffer_size=1024 * 255)
167+
content_type, _ = guess_type(filename)
166168
response = current_app.response_class(
167169
data,
168-
mimetype=fileobj.content_type,
170+
mimetype=content_type,
169171
direct_passthrough=True,
170172
)
171173
response.content_length = fileobj.length
172174
response.last_modified = fileobj.upload_date
173-
response.set_etag(fileobj.md5)
175+
# Compute the sha1 sum of the file for the etag.
176+
pos = fileobj.tell()
177+
raw_data = fileobj.read()
178+
fileobj.seek(pos)
179+
digest = hashlib.sha1(raw_data).hexdigest()
180+
response.set_etag(digest)
174181
response.cache_control.max_age = cache_for
175182
response.cache_control.public = True
176183
response.make_conditional(request)

flask_pymongo/helpers.py

+17-53
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,16 @@
2424
# POSSIBILITY OF SUCH DAMAGE.
2525

2626

27-
__all__ = ("BSONObjectIdConverter", "JSONEncoder")
27+
__all__ = ("BSONObjectIdConverter", "BSONProvider")
2828

29-
from bson import json_util, SON
29+
from bson import json_util
3030
from bson.errors import InvalidId
3131
from bson.objectid import ObjectId
32-
from flask import abort, json as flask_json
33-
from six import iteritems, string_types
32+
from flask import abort
33+
from flask.json.provider import JSONProvider
3434
from werkzeug.routing import BaseConverter
3535
import pymongo
36-
37-
if pymongo.version_tuple >= (3, 5, 0):
38-
from bson.json_util import RELAXED_JSON_OPTIONS
39-
DEFAULT_JSON_OPTIONS = RELAXED_JSON_OPTIONS
40-
else:
41-
DEFAULT_JSON_OPTIONS = None
36+
from bson.json_util import RELAXED_JSON_OPTIONS
4237

4338

4439
def _iteritems(obj):
@@ -83,7 +78,7 @@ def to_url(self, value):
8378
return str(value)
8479

8580

86-
class JSONEncoder(flask_json.JSONEncoder):
81+
class BSONProvider(JSONProvider):
8782

8883
"""A JSON encoder that uses :mod:`bson.json_util` for MongoDB documents.
8984
@@ -101,54 +96,23 @@ def json_route(cart_id):
10196
differently than you expect. See :class:`~bson.json_util.JSONOptions`
10297
for details on the particular serialization that will be used.
10398
104-
A :class:`~flask_pymongo.helpers.JSONEncoder` is automatically
99+
A :class:`~flask_pymongo.helpers.JSONProvider` is automatically
105100
automatically installed on the :class:`~flask_pymongo.PyMongo`
106101
instance at creation time, using
107-
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`. You can change the
108-
:class:`~bson.json_util.JSONOptions` in use by passing
109-
``json_options`` to the :class:`~flask_pymongo.PyMongo`
110-
constructor.
111-
112-
.. note::
113-
114-
:class:`~bson.json_util.JSONOptions` is only supported as of
115-
PyMongo version 3.4. For older versions of PyMongo, you will
116-
have less control over the JSON format that results from calls
117-
to :func:`~flask.json.jsonify`.
118-
119-
.. versionadded:: 2.4.0
120-
102+
:const:`~bson.json_util.RELAXED_JSON_OPTIONS`.
121103
"""
122104

123-
def __init__(self, json_options, *args, **kwargs):
124-
if json_options is None:
125-
json_options = DEFAULT_JSON_OPTIONS
126-
if json_options is not None:
127-
self._default_kwargs = {"json_options": json_options}
128-
else:
129-
self._default_kwargs = {}
105+
def __init__(self, app):
106+
self._default_kwargs = {"json_options": RELAXED_JSON_OPTIONS}
130107

131-
super(JSONEncoder, self).__init__(*args, **kwargs)
108+
super().__init__(app)
132109

133-
def default(self, obj):
110+
def dumps(self, obj):
134111
"""Serialize MongoDB object types using :mod:`bson.json_util`.
112+
"""
113+
return json_util.dumps(obj)
135114

136-
Falls back to Flask's default JSON serialization for all other types.
137-
138-
This may raise ``TypeError`` for object types not recognized.
139-
140-
.. versionadded:: 2.4.0
141-
115+
def loads(self, str_obj):
116+
"""Deserialize MongoDB object types using :mod:`bson.json_util`.
142117
"""
143-
if hasattr(obj, "iteritems") or hasattr(obj, "items"):
144-
return SON((k, self.default(v)) for k, v in iteritems(obj))
145-
elif hasattr(obj, "__iter__") and not isinstance(obj, string_types):
146-
return [self.default(v) for v in obj]
147-
else:
148-
try:
149-
return json_util.default(obj, **self._default_kwargs)
150-
except TypeError:
151-
# PyMongo couldn't convert into a serializable object, and
152-
# the Flask default JSONEncoder won't; so we return the
153-
# object itself and let stdlib json handle it if possible
154-
return obj
118+
return json_util.loads(str_obj)

flask_pymongo/tests/test_config.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def test_config_with_uri_in_flask_conf_var(self):
4444

4545
_wait_until_connected(mongo)
4646
assert mongo.db.name == self.dbname
47-
assert ("localhost", self.port) == mongo.cx.address
47+
assert ("localhost", self.port) == mongo.cx.address or ("127.0.0.1", self.port) == mongo.cx.address
4848

4949
def test_config_with_uri_passed_directly(self):
5050
uri = "mongodb://localhost:{}/{}".format(self.port, self.dbname)
@@ -53,7 +53,7 @@ def test_config_with_uri_passed_directly(self):
5353

5454
_wait_until_connected(mongo)
5555
assert mongo.db.name == self.dbname
56-
assert ("localhost", self.port) == mongo.cx.address
56+
assert ("localhost", self.port) == mongo.cx.address or ("127.0.0.1", self.port) == mongo.cx.address
5757

5858
def test_it_fails_with_no_uri(self):
5959
self.app.config.pop("MONGO_URI", None)

flask_pymongo/tests/test_gridfs.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from hashlib import md5
1+
from hashlib import sha1
22
from io import BytesIO
33

44
from bson.objectid import ObjectId
@@ -30,15 +30,6 @@ def test_it_saves_files(self):
3030
gridfs = GridFS(self.mongo.db)
3131
assert gridfs.exists({"filename": "my-file"})
3232

33-
def test_it_guesses_type_from_filename(self):
34-
fileobj = BytesIO(b"these are the bytes")
35-
36-
self.mongo.save_file("my-file.txt", fileobj)
37-
38-
gridfs = GridFS(self.mongo.db)
39-
gridfile = gridfs.find_one({"filename": "my-file.txt"})
40-
assert gridfile.content_type == "text/plain"
41-
4233
def test_it_saves_files_with_props(self):
4334
fileobj = BytesIO(b"these are the bytes")
4435

@@ -82,7 +73,7 @@ def test_it_sets_supports_conditional_gets(self):
8273
environ_args = {
8374
"method": "GET",
8475
"headers": {
85-
"If-None-Match": md5(self.myfile.getvalue()).hexdigest(),
76+
"If-None-Match": sha1(self.myfile.getvalue()).hexdigest(),
8677
},
8778
}
8879

flask_pymongo/tests/test_json.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from bson import ObjectId
44
from flask import jsonify
5-
from six import ensure_str
65

76
from flask_pymongo.tests.util import FlaskPyMongoTest
87

@@ -11,12 +10,12 @@ class JSONTest(FlaskPyMongoTest):
1110

1211
def test_it_encodes_json(self):
1312
resp = jsonify({"foo": "bar"})
14-
dumped = json.loads(ensure_str(resp.get_data()))
13+
dumped = json.loads(resp.get_data().decode('utf-8'))
1514
self.assertEqual(dumped, {"foo": "bar"})
1615

1716
def test_it_handles_pymongo_types(self):
1817
resp = jsonify({"id": ObjectId("5cf29abb5167a14c9e6e12c4")})
19-
dumped = json.loads(ensure_str(resp.get_data()))
18+
dumped = json.loads(resp.get_data().decode('utf-8'))
2019
self.assertEqual(dumped, {"id": {"$oid": "5cf29abb5167a14c9e6e12c4"}})
2120

2221
def test_it_jsonifies_a_cursor(self):
@@ -25,5 +24,5 @@ def test_it_jsonifies_a_cursor(self):
2524
curs = self.mongo.db.rows.find(projection={"_id": False}).sort("foo")
2625

2726
resp = jsonify(curs)
28-
dumped = json.loads(ensure_str(resp.get_data()))
27+
dumped = json.loads(resp.get_data().decode('utf-8'))
2928
self.assertEqual([{"foo": "bar"}, {"foo": "baz"}], dumped)

0 commit comments

Comments
 (0)