Skip to content

Commit

Permalink
Update master for release v0.16.0 (#153)
Browse files Browse the repository at this point in the history
* @ #135 | update for the next version iteration

* @ #134 | allow releases/vX.Y.Z and releases/X.Y.Z to be published as docs

* Compatibility with werkzeug 2.2 (#145)

Compatibility with Flask>=2.2


* Initialize base_args as class field

Only include actual rule args in base_args

* Fix compatibility with Werkzeug 2.2

werkzeug.routing.parse_rule was considered an internal detail and has
been removed during a refactor. We can now rely on werkzeug.routing.Rule
which has a `arguments` field that contains the information we need.
However, this field is not populated until the Rule has been added to a
Map and this Map has been bound. So, we use a dummy map and bind it to
the empty string just to get the arguments field ready for our use.

* Rework test matrix, pass more exhaustive requirements to pip

- EOL python versions and alpha flask have been removed from test matrix
- Add python 3.10
- Only test "branches" of flask and not all bugfix releases

* Fix test_base_args

The tested view has a route_base with no declared arguments, so we
shouldn't expect the base_args to have a nonzero length.

* Rewrite test_rule_options

This test file mostly tests the same behavior over and over:
that the options given to `FlaskView.register`, such as
`strict_slashes`, are passed along to `Flask.add_url_rule`.

It did this by testing the behavior of the view when calling
its endpoints with or without the trailing slash, expecting it
to return the same response.

However, werkzeug never guaranteed this behavior. The only
documented behavior tunable by this option is:

"If strict_slashes is enabled (the default), visiting a branch URL
without a trailing slash will redirect to the URL with a slash
appended."

So, with the reimplementation of the routing in werkzeug 2.2, this
behavior is lost.

Instead of testing the behavior of the view, let's mock `add_url_rule`
and check that it's called with `strict_slashes` as `False`.

* Fix test_blueprints

Flask >=2.2 raises an error when trying to apply changes to an app
which has already serviced requests. For this specific test, we can
redefine the app.

* Pass rule_options to add_url_rules when adding a @route-ed rule

* Remove nose which is deprecated, use pytest

* docs: add changelog entry and bump version (#151)

Signed-off-by: F.N. Claessen <[email protected]>

---------

Signed-off-by: F.N. Claessen <[email protected]>
Co-authored-by: hoatle <[email protected]>
Co-authored-by: Jérôme Tamba <[email protected]>
  • Loading branch information
3 people authored Sep 7, 2023
1 parent 167e093 commit ba31582
Show file tree
Hide file tree
Showing 33 changed files with 414 additions and 520 deletions.
36 changes: 9 additions & 27 deletions .github/workflows/lint-test-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,14 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"]
flask: ["0.12.5", "1.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.1.0", "1.1.1",
"1.1.2", "1.1.3", "1.1.4", "2.0.0", "2.0.1", "2.0.2"]
python: ["3.7", "3.8", "3.9", "3.10"]
requirements:
- "flask>=1.0,<1.1 werkzeug<2.1 jinja2<3 markupsafe<2.1 itsdangerous<2.1"
- "flask>=1.1,<1.2 markupsafe==2.0.1"
- "flask>=2.0,<2.1"
- "flask>=2.1,<2.2"
- "flask>=2.2,<2.3"
experimental: [false]
# include:
# - python: "3.10"
# # known issue: https://github.com/nose-devs/nose/issues/1099
# experimental: true
exclude:
# excludes flask versions from unsupported python versions
- python: "2.7"
flask: "1.1.3"
- python: "2.7"
flask: "2.0.0"
- python: "2.7"
flask: "2.0.1"
- python: "2.7"
flask: "2.0.2"
- python: "3.5"
flask: "1.1.3"
- python: "3.5"
flask: "2.0.0"
- python: "3.5"
flask: "2.0.1"
- python: "3.5"
flask: "2.0.2"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
Expand All @@ -63,9 +45,9 @@ jobs:
run: |
docker-compose run --rm dev
env:
REQUIREMENTS: ${{ matrix.requirements }}
CHECK_STYLE: no
DEV_IMAGE: python:${{ matrix.python }}
FLASK: ${{ matrix.flask }}

# build and publish docs
docs:
Expand All @@ -92,7 +74,7 @@ jobs:
TARGET_FOLDER=develop
elif [[ $GIT_BRANCH =~ ^v[0-9.]+[0-9.a-z\-]*$ ]]; then
TARGET_FOLDER=${GIT_BRANCH:1}
elif [[ $GIT_BRANCH =~ ^[0-9.]+[0-9.a-z\-]*$ || $GIT_BRANCH =~ ^releases\/[0-9.]+[0-9.a-z\-]*$ ]]; then
elif [[ $GIT_BRANCH =~ ^[0-9.]+[0-9.a-z\-]*$ || $GIT_BRANCH =~ ^releases\/[v0-9.]+[0-9.a-z\-]*$ ]]; then
TARGET_FOLDER=$GIT_BRANCH
fi
echo "\$TARGET_FOLDER: $TARGET_FOLDER"
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
2023-08-28 F.N. Claessen <[email protected]>
-----------------------------------------

Version: 0.16.0

- Tasks:
* Compatibility with werkzeug 2.2 #145


2021-12-25 Hoat Le <[email protected]>
--------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test-clean:
coverage erase

test-intg:
coverage run --branch --source=. `which nosetests` -v --exe
coverage run --branch --source=. -m pytest

test: | test-clean test-intg

Expand Down
2 changes: 1 addition & 1 deletion dev-setup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Stop the watching files by using `Ctrl + c`.
## How to develop
- `$ docker-compose up` will check code style and run tests with defaul Python image.
- `$ docker-compose up` will check code style and run tests with default Python image.
There are environment variables to run and check with different versions of Python:
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
working_dir: /opt/app
command: sh run-dev.sh
environment:
FLASK: ${FLASK:-1.1.1}
REQUIREMENTS: ${REQUIREMENTS:-flask==2.0.0}
CHECK_STYLE: ${CHECK_STYLE:-yes}
RUN_TEST: ${RUN_TEST:-yes}
volumes:
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
# built documents.
#
# The short X.Y version.
version = '0.15'
version = '0.16'
# The full version, including alpha/beta/rc tags.
release = '0.15.0-b1'
release = '0.16.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
26 changes: 13 additions & 13 deletions flask_classful.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
import functools
import inspect
from uuid import UUID
from werkzeug.routing import parse_rule
from werkzeug.routing import Rule, Map
from flask import request, make_response
from flask.wrappers import ResponseBase
import re

_py2 = sys.version_info[0] == 2

__version__ = "0.15.0-b1"
__version__ = "0.16.0"


def route(rule, **options):
Expand Down Expand Up @@ -66,6 +66,7 @@ class FlaskView(object):
route_base = None
route_prefix = None
trailing_slash = True
base_args = []
excluded_methods = [] # specify the class methods to be explicitly excluded from routing creation
# TODO(hoatle): make method_dashified=True as default instead,
# this is not a compatible change
Expand Down Expand Up @@ -165,6 +166,7 @@ def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
if hasattr(value, "_rule_cache") and name in value._rule_cache:
for idx, cached_rule in enumerate(value._rule_cache[name]):
rule, options = cached_rule
options.update(rule_options)
rule = cls.build_rule(rule)
sub, ep, options = cls.parse_options(options)

Expand Down Expand Up @@ -370,9 +372,8 @@ def build_rule(cls, rule, method=None):
rule_parts.append(route_base)
if len(rule) > 0: # the case of rule='' empty string
rule_parts.append(rule)
ignored_rule_args = ['self']
if hasattr(cls, 'base_args'):
ignored_rule_args += cls.base_args

ignored_rule_args = ['self'] + cls.base_args

if method and getattr(cls, 'inspect_args', True):
argspec = get_true_argspec(method)
Expand Down Expand Up @@ -400,13 +401,13 @@ def get_route_base(cls):

if cls.route_base is not None:
route_base = cls.route_base
base_rule = parse_rule(route_base)
# see: https://github.com/teracyhq/flask-classful/issues/50
if hasattr(cls, 'base_args'):
# thanks to: https://github.com/teracyhq/flask-classful/pull/56#issuecomment-328985183
cls.base_args = list(set(cls.base_args).union(r[2] for r in base_rule))
else:
cls.base_args = [r[2] for r in base_rule]
if not route_base.startswith('/'):
route_base = '/' + route_base
base_rule = Rule(route_base)
# Add rule to a dummy map and bind that map so that
# the Rule's arguments field is populated
Map(rules=[base_rule]).bind('')
cls.base_args.extend(base_rule.arguments)
else:
route_base = cls.default_route_base()

Expand Down Expand Up @@ -524,4 +525,3 @@ def unpack(value):

class DecoratorCompatibilityError(Exception):
pass

3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# test
nose
tox
pytest

# code style, coverage
coverage
Expand Down
4 changes: 2 additions & 2 deletions run-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
set -eu pipefail

make resolve;
pip install Flask==$FLASK;
pip install webargs!=5.0.0;
pip install $REQUIREMENTS;
python setup.py install;

if [ "$CHECK_STYLE" = "yes" ] || [ "$CHECK_STYLE" = "1" ]; then
make check-style;
fi

if [ "$RUN_TEST" = "yes" ] || [ "$RUN_TEST" = "1" ]; then
pip install webargs
make test;
make report-coverage;
fi
3 changes: 0 additions & 3 deletions setup.cfg

This file was deleted.

9 changes: 0 additions & 9 deletions test_classful/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +0,0 @@
import nose


if __name__ == "__main__":
# nose.run()
nose.run(env={
'NOSE_INCLUDE_EXE': True,
'NOSE_VERBOSE': 2
})
38 changes: 18 additions & 20 deletions test_classful/test_base_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from marshmallow import Schema, fields
from webargs.flaskparser import use_args
from webargs import fields
from nose.tools import eq_

# we'll make a list to hold some quotes for our app
quotes = [
Expand Down Expand Up @@ -138,69 +137,68 @@ def put(self, args, id):

def test_users_post():
resp = client.post('users/', headers=input_headers, data=json.dumps({'email':'[email protected]'}))
eq_(resp.status_code, 200)
eq_("[email protected]", resp.data.decode('ascii'))
assert resp.status_code == 200
assert "[email protected]" == resp.data.decode('ascii')

def test_users_put():
resp = client.put('users/1/', headers=input_headers, data=json.dumps({'email':'[email protected]'}))
eq_(resp.status_code, 200)
eq_("[email protected]", resp.data.decode('ascii'))
assert resp.status_code == 200
assert "[email protected]" == resp.data.decode('ascii')

def test_users_patch():
resp = client.patch('users/1/', headers=input_headers, data=json.dumps({'email':'[email protected]'}))
eq_(resp.status_code, 200)
eq_("[email protected]", resp.data.decode('ascii'))
assert resp.status_code == 200
assert "[email protected]" == resp.data.decode('ascii')

def test_quotes_index():
resp = client.get("/quotes/")
num = len(str(resp.data).split("<br>"))
eq_(3, num)
assert 3 == num
resp = client.get("/quotes")
eq_(resp.status_code, 308)
assert resp.status_code == 308


def test_quotes_get():
resp = client.get("/quotes/0/")
eq_(quotes[0], resp.data.decode('ascii'))
assert quotes[0] == resp.data.decode('ascii')


def test_quotes_put():
resp = client.put("/quotes/1/",
headers=input_headers,
data=json.dumps(input_data))
eq_(input_data["text"], resp.data.decode('ascii'))
assert input_data["text"] == resp.data.decode('ascii')

def test_quotes_factory():
resp = client.patch("/quotes/1/",
headers=input_headers,
data=json.dumps(input_data))
eq_(input_data["text"], resp.data.decode('ascii'))
assert input_data["text"] == resp.data.decode('ascii')

def test_quotes2_index():
resp = client.get("/quotes-2/")
num = len(str(resp.data).split("<br>"))
eq_(3, num)
assert 3 == num
resp = client.get("/quotes-2")
eq_(resp.status_code, 308)
assert resp.status_code == 308


def test_quotes2_get():
resp = client.get("/quotes-2/0/")
eq_(quotes[0], resp.data.decode('ascii'))
eq_(UglyNameView.base_args.count(UglyNameView.route_base), 1)
assert quotes[0] == resp.data.decode('ascii')
assert UglyNameView.base_args.count(UglyNameView.route_base) == 0


def test_quotes2_put():
resp = client.put("/quotes-2/1/",
headers=input_headers,
data=json.dumps(input_data))
eq_(input_data["text"], resp.data.decode('ascii'))
eq_(UglyNameView.base_args.count(UglyNameView.route_base), 1)
assert input_data["text"] == resp.data.decode('ascii')
assert UglyNameView.base_args.count(UglyNameView.route_base) == 0

# see: https://github.com/teracyhq/flask-classful/pull/56#issuecomment-328985183
def test_unique_elements():
client.put("/quotes-2/1/",
headers=input_headers,
data=json.dumps(input_data))
eq_(UglyNameView.base_args.count(UglyNameView.route_base), 1)

assert UglyNameView.base_args.count(UglyNameView.route_base) == 0
Loading

0 comments on commit ba31582

Please sign in to comment.