Skip to content

Commit

Permalink
Merge pull request #185 from level12/deprecation-updates
Browse files Browse the repository at this point in the history
resolve flask deprecations
  • Loading branch information
guruofgentoo committed Oct 25, 2022
2 parents 6f148a9 + 32e36fd commit edde099
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 15 deletions.
28 changes: 25 additions & 3 deletions keg/db/__init__.py
Expand Up @@ -17,10 +17,11 @@


class KegSQLAlchemy(fsa.SQLAlchemy):

def apply_driver_hacks(self, app, info, options):
def _apply_driver_defaults(self, options, app):
"""Override some driver specific settings"""
super_return_value = super(KegSQLAlchemy, self).apply_driver_hacks(app, info, options)
super_return_value = None
if hasattr(super(), '_apply_driver_defaults'):
super_return_value = super()._apply_driver_defaults(options, app)

# Turn on SA pessimistic disconnect handling by default:
# http://docs.sqlalchemy.org/en/latest/core/pooling.html#disconnect-handling-pessimistic
Expand All @@ -33,6 +34,27 @@ def apply_driver_hacks(self, app, info, options):

return super_return_value

def apply_driver_hacks(self, app, info, options):
"""This method is renamed to _apply_driver_defaults in flask-sqlalchemy 3.0"""
super_return_value = super().apply_driver_hacks(app, info, options)

# follow the logic to set some defaults, but the super won't exist there
self._apply_driver_defaults(options, app)

return super_return_value

def get_engine(self, app=None, bind=None):
if not hasattr(self, '_app_engines'):
# older version of flask-sqlalchemy, we can just call super
return super().get_engine(app=app, bind=bind)

# More recent flask-sqlalchemy, use the cached engines directly.
# Note: we don't necessarily have an app context active here, depending
# on if this is being called during app init. But if we attempt to access
# the underlying cache directly, we get a weak ref error.
with app.app_context():
return self.engines[bind]

def get_engines(self, app):
# the default engine doesn't have a bind
retval = [(None, self.get_engine(app))]
Expand Down
2 changes: 1 addition & 1 deletion keg/db/dialect_ops.py
Expand Up @@ -32,7 +32,7 @@ def execute_sql(self, statements):

def create_all(self):
self.create_schemas()
db.create_all(bind=self.bind_name)
db.create_all(self.bind_name)

def create_schemas(self):
pass
Expand Down
22 changes: 18 additions & 4 deletions keg/templating.py
@@ -1,14 +1,28 @@
from __future__ import absolute_import

from flask.globals import _request_ctx_stack
# flask.globals.request_ctx is only available in Flask >= 2.2.0
try:
from flask.globals import request_ctx
except ImportError:
from flask.globals import _request_ctx_stack
request_ctx = None

from keg.extensions import lazy_gettext as _


def _get_bc_request_context():
"""Flask 2.2 changed the method of fetching the request context
from globals. Flask 2.3 will remove the old way of doing it.
Support both here."""
if request_ctx is None:
return _request_ctx_stack.top
return request_ctx


def _keg_default_template_ctx_processor():
"""Default template context processor. Injects `assets`.
"""
reqctx = _request_ctx_stack.top
reqctx = _get_bc_request_context()
rv = {}
if reqctx is not None:
rv['assets'] = reqctx.assets
Expand Down Expand Up @@ -53,7 +67,7 @@ def parse_include(self, parser, stream, lineno):

def _include_support(self, template_name, caller):
"""Helper callback."""
ctx = _request_ctx_stack.top
ctx = _get_bc_request_context()
ctx.assets.load_related(template_name)

# have to return empty string to avoid exception about None not being iterable.
Expand All @@ -74,5 +88,5 @@ def parse_content(self, parser, stream, lineno):

def _content_support(self, asset_type, caller):
"""Helper callback."""
ctx = _request_ctx_stack.top
ctx = _get_bc_request_context()
return ctx.assets.combine_content(asset_type)
18 changes: 18 additions & 0 deletions keg/testing.py
Expand Up @@ -170,6 +170,20 @@ def invoke_command(app_cls, *args, **kwargs):
return result


def cleanup_app_contexts():
while flask.current_app:
cm = ContextManager.get_for(flask.current_app.__class__)
if cm.ctx:
cm.cleanup()
else:
break

# support older flask as well
if flask.current_app and getattr(flask, '_app_ctx_stack'):
while flask._app_ctx_stack.pop():
pass


class CLIBase(object):
"""Test class base for testing Keg click commands.
Expand All @@ -186,6 +200,10 @@ class CLIBase(object):

@classmethod
def setup_class(cls):
# If a current app context is set, it may complicate what click is doing to
# set up and run a specific app.
cleanup_app_contexts()

cls.runner = click.testing.CliRunner()

def invoke(self, *args, **kwargs):
Expand Down
10 changes: 8 additions & 2 deletions keg/tests/test_cli.py
Expand Up @@ -40,9 +40,12 @@ def test_missing_command(self):
@need_dotenv
def test_dotenv(self):
test_dir = os.path.dirname(__file__)
# ensure flask looks in the expected working directory
working_dir = os.path.abspath(os.path.join(test_dir, '..', '..'))
os.chdir(working_dir)

# Place dotenv file in search path of python-dotenv
flaskenv = os.path.abspath(os.path.join(test_dir, '..', '..', '.flaskenv'))
flaskenv = os.path.join(working_dir, '.flaskenv')
try:
with open(flaskenv, 'w') as f:
f.write('FOO=bar')
Expand All @@ -57,9 +60,12 @@ def test_dotenv(self):
@mock.patch.dict(os.environ, {'FLASK_SKIP_DOTENV': '1'})
def test_disable_dotenv_from_env(self):
test_dir = os.path.dirname(__file__)
# ensure flask looks in the expected working directory
working_dir = os.path.abspath(os.path.join(test_dir, '..', '..'))
os.chdir(working_dir)

# Place dotenv file in search path of python-dotenv
flaskenv = os.path.abspath(os.path.join(test_dir, '..', '..', '.flaskenv'))
flaskenv = os.path.join(working_dir, '.flaskenv')
try:
with open(flaskenv, 'w') as f:
f.write('FOO=bar')
Expand Down
4 changes: 3 additions & 1 deletion keg/tests/test_config.py
Expand Up @@ -6,7 +6,7 @@

from keg.app import Keg
from keg.config import Config
from keg.testing import invoke_command
from keg.testing import cleanup_app_contexts, invoke_command
from keg_apps.profile.cli import ProfileApp


Expand Down Expand Up @@ -137,13 +137,15 @@ def test_invoke_command_for_testing(self):
"""
Using testing.invoke_command() should use a testing profile by default.
"""
cleanup_app_contexts()
resp = invoke_command(ProfileApp, 'show-profile')
assert 'testing-default' in resp.output

def test_invoke_command_with_environment(self):
"""
Environement overrides should still take priority for invoke_command() usage.
"""
cleanup_app_contexts()
resp = invoke_command(ProfileApp, 'show-profile',
env={'KEG_APPS_PROFILE_CONFIG_PROFILE': 'EnvironmentProfile'})
assert 'environment' in resp.output
5 changes: 4 additions & 1 deletion keg/tests/test_db.py
Expand Up @@ -61,7 +61,10 @@ def test_init_without_db_binds(self):
# Make sure we don't get an error initializing the app when the SQLALCHEMY_BINDS config
# option is None
app = DB2App.testing_prep()
assert app.config.get('SQLALCHEMY_BINDS') is None
value = app.config.get('SQLALCHEMY_BINDS')
# flask-sqlalchemy < 3.0: None
# flask-sqlalchemy 3.0+: {}
assert value is None or value == {}


class TestDatabaseManager(object):
Expand Down
20 changes: 18 additions & 2 deletions keg/web.py
Expand Up @@ -6,7 +6,11 @@
from blazeutils.strings import case_cw2us, case_cw2dash
import flask
from flask import request
from flask.views import MethodView, MethodViewType, http_method_funcs
from flask.views import MethodView, http_method_funcs
try:
from flask.views import MethodViewType
except ImportError:
MethodViewType = None
import six
from werkzeug.datastructures import MultiDict

Expand Down Expand Up @@ -82,7 +86,7 @@ def _call_with_expected_args(view, calling_args, method, method_is_bound=True):
return method(*args, **kwargs)


class _ViewMeta(MethodViewType):
class _OldViewMeta(MethodViewType or object):
def __init__(cls, name, bases, d):
MethodViewType.__init__(cls, name, bases, d)

Expand All @@ -94,6 +98,9 @@ def __init__(cls, name, bases, d):
cls.assign_blueprint(cls.blueprint)


_ViewMeta = _OldViewMeta if MethodViewType is not None else type


class BaseView(MethodView, metaclass=_ViewMeta):
"""
Base class for all Keg views to inherit from. `BaseView` automatically calculates and installs
Expand All @@ -111,6 +118,15 @@ class BaseView(MethodView, metaclass=_ViewMeta):
# names of qs arguments that should be merged w/ URL arguments and passed to view methods
expected_qs_args = []

def __init_subclass__(cls, **kwargs):
"""Flask before 2.2.0 used a metaclass to perform view setup, but this
changed to using `init_subclass`. If the old way is enabled, no need to
do anything but call the super here."""
super().__init_subclass__(**kwargs)

if MethodViewType is None and cls.blueprint is not None:
cls.assign_blueprint(cls.blueprint)

def __init__(self, responding_method=None):
self.responding_method = responding_method

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -16,7 +16,7 @@ skip_install = true
recreate=True
commands =
pip --version
lowest: pip install flask<2
lowest: pip install flask<2 markupsafe~=2.0.0
pip install --progress-bar off .[tests]
i18n: pip install --progress-bar off .[i18n]
# Output installed versions to compare with previous test runs in case a dependency's change
Expand Down

0 comments on commit edde099

Please sign in to comment.