Skip to content

Commit

Permalink
Remove dependence on pkg_resources (replace with importlib_resources). (
Browse files Browse the repository at this point in the history
#780)

pkg_resources now throws a warning that we really really shouldn't be using it.

To replace pkg_resources version parsing:
  - remove old Werkzeug version compatibility check
  - change flask json compatibility to use checks on attributes rather than version numbers. This should
    also fix the #777 (Quart compatibility) and allowed us to remove the upper bound Flask version check (which is considered bad form).

To replace pkg_resources 'files' API which we use to find Babel translations - change SECURITY_I18N_DIRNAME to default to 'builtin' which is checked and initialized as part of configuring our Flask-Babel Domain instance.

This also allows to remove the (recently added) dependence on setuptools.
  • Loading branch information
jwag956 committed Apr 9, 2023
1 parent 3d746bd commit 4f5eefd
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 38 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Expand Up @@ -19,7 +19,7 @@ repos:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 23.1.0
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
Expand All @@ -30,7 +30,7 @@ repos:
- flake8-bugbear
- flake8-implicit-str-concat
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.19.16
rev: v1.19.17
hooks:
- id: djlint-jinja
files: "\\.html"
Expand Down
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -3,6 +3,18 @@ Flask-Security Changelog

Here you can see the full list of changes between each Flask-Security release.

Version Next
------------

Released TBD

Fixes
+++++

- (:issue:`764`) Remove old Werkzeug compatibility check.
- (:issue:`777`) Compatibility with Quart.
- (:pr:`xx`) Remove dependence on pkg_resources / setuptools (use importlib_resources package)

Version 5.1.2
-------------

Expand Down
5 changes: 3 additions & 2 deletions docs/configuration.rst
Expand Up @@ -62,9 +62,10 @@ These configuration keys are used globally across all features.
Specifies the directory containing the ``MO`` files used for translations.
When using flask-babel this can also be a list of directory names - this
enables application to override a subset of messages if desired.
enables application to override a subset of messages if desired. The
default ``builtin`` uses translations shipped with Flask-Security.

Default: ``[PATH_LIB]/flask_security/translations``.
Default: ``builtin``.

.. py:data:: SECURITY_PASSWORD_HASH
Expand Down
4 changes: 2 additions & 2 deletions docs/customizing.rst
Expand Up @@ -319,7 +319,7 @@ and/or a specific translation). Adding the following to your app::

app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error")

Will change the default message in english.
will change the default message in english.

.. tip::
The string messages themselves are a 'key' into the translation .po/.mo files.
Expand All @@ -344,7 +344,7 @@ Then compile it with::

Finally add your translations directory to your configuration::

app.config["SECURITY_I18N_DIRNAME"] = [pkg_resources.resource_filename("flask_security", "translations"), "translations"]
app.config["SECURITY_I18N_DIRNAME"] = ["builtin", "translations"]

.. note::
This only works when using Flask-Babel since Flask-BabelEx doesn't support a list of translation directories.
Expand Down
27 changes: 25 additions & 2 deletions flask_security/babel.py
Expand Up @@ -4,7 +4,7 @@
I18N support for Flask-Security.
:copyright: (c) 2019-2021 by J. Christopher Wagner (jwag).
:copyright: (c) 2019-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
As of Flask-Babel 2.0.0 - it supports the Flask-BabelEx Domain extension - and it
Expand All @@ -15,6 +15,11 @@

# flake8: noqa: F811

from collections.abc import Iterable
import atexit
from contextlib import ExitStack
from importlib_resources import files, as_file

import typing as t

from flask import current_app
Expand Down Expand Up @@ -71,10 +76,28 @@ def make_lazy_string(__func, msg):

class FsDomain(_domain_cls):
def __init__(self, app):
# By default, we use our packaged translations. However, we have to allow
# for app to add translation directories or completely override ours.
# Grabbing the packaged translations is a bit complex - so we use
# the keyword 'builtin' to mean ours.
cfdir = cv("I18N_DIRNAME", app=app)
if cfdir == "builtin" or (
isinstance(cfdir, Iterable) and "builtin" in cfdir
):
fm = ExitStack()
atexit.register(fm.close)
ref = files("flask_security") / "translations"
path = fm.enter_context(as_file(ref))
if cfdir == "builtin":
dirs = [str(path)]
else:
dirs = [d if d != "builtin" else str(path) for d in cfdir]
else:
dirs = cfdir
super().__init__(
**{
"domain": cv("I18N_DOMAIN", app=app),
_dir_keyword: cv("I18N_DIRNAME", app=app),
_dir_keyword: dirs,
}
)

Expand Down
6 changes: 2 additions & 4 deletions flask_security/core.py
Expand Up @@ -18,7 +18,6 @@
import typing as t
import warnings

import pkg_resources
from flask import current_app, g
from flask_login import AnonymousUserMixin, LoginManager
from flask_login import UserMixin as BaseUserMixin
Expand Down Expand Up @@ -130,7 +129,7 @@
"FLASH_MESSAGES": True,
"RETURN_GENERIC_RESPONSES": False,
"I18N_DOMAIN": "flask_security",
"I18N_DIRNAME": pkg_resources.resource_filename("flask_security", "translations"),
"I18N_DIRNAME": "builtin",
"EMAIL_VALIDATOR_ARGS": None,
"PASSWORD_HASH": "bcrypt",
"PASSWORD_SALT": None,
Expand Down Expand Up @@ -1404,8 +1403,7 @@ def init_app(

# set all (SECURITY) config items as attributes (minus the SECURITY_ prefix)
for key, value in get_config(app).items():
# need to start getting rid of this - especially things like *_URL which
# should never be referenced
# need to start getting rid of this - very confusing.
if not key.endswith("_URL"):
setattr(self, key.lower(), value)

Expand Down
15 changes: 4 additions & 11 deletions flask_security/json.py
Expand Up @@ -6,13 +6,6 @@
Pieces of this code liberally copied from flask-mongoengine.
"""
from flask import __version__ as flask_version
from pkg_resources import parse_version


def use_json_provider() -> bool:
"""Split Flask before 2.2.0 and after, to use/not use JSON provider approach."""
return parse_version(flask_version) >= parse_version("2.2.0")


def _use_encoder(superclass): # pragma: no cover
Expand Down Expand Up @@ -51,7 +44,8 @@ def default(obj):

def setup_json(app, bp=None):
# Called at init_app time.
if use_json_provider():
if hasattr(app, "json_provider_class"):
# Flask >= 2.2
app.json_provider_class = _use_provider(app.json_provider_class)
app.json = app.json_provider_class(app)
# a bit if a hack - if a package (e.g. flask-mongoengine) hasn't
Expand All @@ -61,9 +55,8 @@ def setup_json(app, bp=None):
# (app.json_encoder is always set)
# (If they do, then Flask 2.2.x won't use json_provider at all)
# Of course if they do this AFTER we're initialized all bets are off.
if parse_version(flask_version) >= parse_version("2.2.0"):
if getattr(app, "_json_encoder", None):
app.json_encoder = _use_encoder(app.json_encoder)
if getattr(app, "_json_encoder", None):
app.json_encoder = _use_encoder(app.json_encoder)

elif bp: # pragma: no cover
bp.json_encoder = _use_encoder(app.json_encoder)
7 changes: 1 addition & 6 deletions flask_security/utils.py
Expand Up @@ -5,7 +5,7 @@
Flask-Security utils module
:copyright: (c) 2012-2019 by Matt Wright.
:copyright: (c) 2019-2022 by J. Christopher Wagner (jwag).
:copyright: (c) 2019-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
import abc
Expand All @@ -14,7 +14,6 @@
from functools import partial
import hashlib
import hmac
from pkg_resources import parse_version
import time
import typing as t
from urllib.parse import parse_qsl, parse_qs, urlsplit, urlunsplit, urlencode
Expand All @@ -40,7 +39,6 @@
from flask_wtf import csrf
from wtforms import ValidationError
from itsdangerous import BadSignature, SignatureExpired
from werkzeug import __version__ as werkzeug_version
from werkzeug.local import LocalProxy

from .quart_compat import best, get_quart_status
Expand Down Expand Up @@ -926,10 +924,7 @@ def csrf_cookie_handler(response: "Response") -> "Response":

if op == "clear":
# Alas delete_cookie only accepts some of the keywords set_cookie does
# and Werkzeug didn't accept samesite, secure, httponly until 2.0
allowed = ["path", "domain", "secure", "httponly", "samesite"]
if parse_version(werkzeug_version) < parse_version("2.0.0"): # pragma: no cover
allowed = ["path", "domain"]
args = {k: csrf_cookie.get(k) for k in allowed if k in csrf_cookie}
response.delete_cookie(csrf_cookie_name, **args)
session.pop("fs_cc")
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Expand Up @@ -20,9 +20,11 @@ filterwarnings =
ignore:.*'app.json_encoder' is deprecated.*:DeprecationWarning:flask:0
ignore:.*Setting 'json_encoder'.*:DeprecationWarning:flask:0
ignore:.*'JSONEncoder'.*:DeprecationWarning:flask:0
ignore:.*'locked_cached_property'.*:DeprecationWarning:flask:0
ignore::DeprecationWarning:mongoengine:
ignore:.*passwordless feature.*:DeprecationWarning:flask_security:0
ignore:.*passing settings to bcrypt.*:DeprecationWarning:passlib:0
ignore:.*'crypt' is deprecated.*:DeprecationWarning:passlib:0
ignore:.*pkg_resources is deprecated.*:DeprecationWarning:pkg_resources:0
ignore:.*'sms' was enabled in SECURITY_US_ENABLED_METHODS;.*:UserWarning:flask_security:0
ignore:.*'get_token_status' is deprecated.*:DeprecationWarning:flask_security:0
4 changes: 2 additions & 2 deletions setup.py
Expand Up @@ -10,7 +10,7 @@
version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)

install_requires = [
"Flask>=2.1.0,<2.3",
"Flask>=2.1.0",
"Flask-Login>=0.6.0",
"Flask-Principal>=0.4.0",
"Flask-WTF>=1.0.0",
Expand All @@ -19,7 +19,7 @@
"passlib>=1.7.4",
"blinker>=1.4",
"wtforms>=3.0.0", # for form-level errors
"setuptools", # for pkg_resources
"importlib_resources>=5.10.0",
]

packages = find_packages(exclude=["tests"])
Expand Down
29 changes: 22 additions & 7 deletions tests/test_misc.py
Expand Up @@ -14,7 +14,6 @@
from unittest import mock
import re
import os.path
import pkg_resources
import sys
import time
import typing as t
Expand Down Expand Up @@ -635,15 +634,31 @@ def test_xlation(app, client):
def test_myxlation(app, sqlalchemy_datastore, pytestconfig):
# Test changing a single MSG and having an additional translation dir
# Flask-BabelEx doesn't support lists of directories..
try:
import flask_babelex # noqa: F401
pytest.importorskip("flask_babel")

pytest.skip("Flask-BabelEx doesn't support lists of translations")
except ImportError:
pass
i18n_dirname = [
"builtin",
os.path.join(pytestconfig.rootdir, "tests/translations"),
]
init_app_with_options(
app, sqlalchemy_datastore, **{"SECURITY_I18N_DIRNAME": i18n_dirname}
)

assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog"

app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error")

client = app.test_client()
response = client.post("/login", data=dict(email="matt@lp.com", password="forgot"))
assert b"Passe - no-worky" in response.data


@pytest.mark.babel()
@pytest.mark.app_settings(babel_default_locale="fr_FR")
def test_myxlation_complete(app, sqlalchemy_datastore, pytestconfig):
# Test having own translations and not using builtin.
pytest.importorskip("flask_babel")
i18n_dirname = [
pkg_resources.resource_filename("flask_security", "translations"),
os.path.join(pytestconfig.rootdir, "tests/translations"),
]
init_app_with_options(
Expand Down

0 comments on commit 4f5eefd

Please sign in to comment.