use ast to parse FLASK_APP

enables keyword arguments to factory functions
This commit is contained in:
David Lord 2020-04-07 17:54:51 -07:00
parent 0d04b40d13
commit ff2f71379b
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
4 changed files with 67 additions and 40 deletions

View File

@ -25,6 +25,8 @@ Unreleased
200 OK and an empty file. :issue:`3358`
- When using ad-hoc certificates, check for the cryptography library
instead of PyOpenSSL. :pr:`3492`
- When specifying a factory function with ``FLASK_APP``, keyword
argument can be passed. :issue:`3553`
Version 1.1.2

View File

@ -76,8 +76,8 @@ found, the command looks for a factory function named ``create_app`` or
``make_app`` that returns an instance.
If parentheses follow the factory name, their contents are parsed as
Python literals and passed as arguments to the function. This means that
strings must still be in quotes.
Python literals and passed as arguments and keyword arguments to the
function. This means that strings must still be in quotes.
Run the Development Server

View File

@ -86,55 +86,60 @@ def find_best_app(script_info, module):
)
def call_factory(script_info, app_factory, arguments=()):
def call_factory(script_info, app_factory, args=None, kwargs=None):
"""Takes an app factory, a ``script_info` object and optionally a tuple
of arguments. Checks for the existence of a script_info argument and calls
the app_factory depending on that and the arguments provided.
"""
args_spec = inspect.getfullargspec(app_factory)
sig = inspect.signature(app_factory)
args = [] if args is None else args
kwargs = {} if kwargs is None else kwargs
if "script_info" in args_spec.args:
if "script_info" in sig.parameters:
warnings.warn(
"The 'script_info' argument is deprecated and will not be"
" passed to the app factory function in 2.1.",
DeprecationWarning,
)
return app_factory(*arguments, script_info=script_info)
elif arguments:
return app_factory(*arguments)
elif not arguments and len(args_spec.args) == 1 and args_spec.defaults is None:
kwargs["script_info"] = script_info
if (
not args
and len(sig.parameters) == 1
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty
):
warnings.warn(
"Script info is deprecated and will not be passed as the"
" first argument to the app factory function in 2.1.",
" single argument to the app factory function in 2.1.",
DeprecationWarning,
)
return app_factory(script_info)
args.append(script_info)
return app_factory()
return app_factory(*args, **kwargs)
def _called_with_wrong_args(factory):
def _called_with_wrong_args(f):
"""Check whether calling a function raised a ``TypeError`` because
the call failed or because something in the factory raised the
error.
:param factory: the factory function that was called
:return: true if the call failed
:param f: The function that was called.
:return: ``True`` if the call failed.
"""
tb = sys.exc_info()[2]
try:
while tb is not None:
if tb.tb_frame.f_code is factory.__code__:
# in the factory, it was called successfully
if tb.tb_frame.f_code is f.__code__:
# In the function, it was called successfully.
return False
tb = tb.tb_next
# didn't reach the factory
# Didn't reach the function.
return True
finally:
# explicitly delete tb as it is circular referenced
# Delete tb to break a circular reference.
# https://docs.python.org/2/library/sys.html#sys.exc_info
del tb
@ -145,37 +150,60 @@ def find_app_by_string(script_info, module, app_name):
"""
from . import Flask
match = re.match(r"^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$", app_name)
if not match:
# Parse app_name as a single expression to determine if it's a valid
# attribute name or function call.
try:
expr = ast.parse(app_name.strip(), mode="eval").body
except SyntaxError:
raise NoAppException(
f"{app_name!r} is not a valid variable name or function expression."
f"Failed to parse {app_name!r} as an attribute name or function call."
)
name, args = match.groups()
if isinstance(expr, ast.Name):
name = expr.id
args = kwargs = None
elif isinstance(expr, ast.Call):
# Ensure the function name is an attribute name only.
if not isinstance(expr.func, ast.Name):
raise NoAppException(
f"Function reference must be a simple name: {app_name!r}."
)
name = expr.func.id
# Parse the positional and keyword arguments as literals.
try:
args = [ast.literal_eval(arg) for arg in expr.args]
kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords}
except ValueError:
# literal_eval gives cryptic error messages, show a generic
# message with the full expression instead.
raise NoAppException(
f"Failed to parse arguments as literal values: {app_name!r}."
)
else:
raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call."
)
try:
attr = getattr(module, name)
except AttributeError as e:
raise NoAppException(e.args[0])
except AttributeError:
raise NoAppException(
f"Failed to find attribute {name!r} in {module.__name__!r}."
)
# If the attribute is a function, call it with any args and kwargs
# to get the real application.
if inspect.isfunction(attr):
if args:
try:
args = ast.literal_eval(f"({args},)")
except (ValueError, SyntaxError):
raise NoAppException(f"Could not parse the arguments in {app_name!r}.")
else:
args = ()
try:
app = call_factory(script_info, attr, args)
except TypeError as e:
app = call_factory(script_info, attr, args, kwargs)
except TypeError:
if not _called_with_wrong_args(attr):
raise
raise NoAppException(
f"{e}\nThe factory {app_name!r} in module"
f"The factory {app_name!r} in module"
f" {module.__name__!r} could not be called with the"
" specified arguments."
)
@ -362,8 +390,6 @@ class ScriptInfo:
if self._loaded_app is not None:
return self._loaded_app
app = None
if self.create_app is not None:
app = call_factory(self, self.create_app)
else:

View File

@ -203,7 +203,6 @@ def test_prepare_import(request, value, path, result):
("cliapp.factory", None, "app"),
("cliapp.factory", "create_app", "app"),
("cliapp.factory", "create_app()", "app"),
# no script_info
("cliapp.factory", 'create_app2("foo", "bar")', "app2_foo_bar"),
# trailing comma space
("cliapp.factory", 'create_app2("foo", "bar", )', "app2_foo_bar"),