Merge pull request #3564 from pallets/cli-ast-parse

use ast to parse FLASK_APP
This commit is contained in:
David Lord 2020-04-07 18:01:55 -07:00 committed by GitHub
commit 2062d984ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 40 deletions

View file

@ -25,6 +25,8 @@ Unreleased
200 OK and an empty file. :issue:`3358` 200 OK and an empty file. :issue:`3358`
- When using ad-hoc certificates, check for the cryptography library - When using ad-hoc certificates, check for the cryptography library
instead of PyOpenSSL. :pr:`3492` instead of PyOpenSSL. :pr:`3492`
- When specifying a factory function with ``FLASK_APP``, keyword
argument can be passed. :issue:`3553`
Version 1.1.2 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. ``make_app`` that returns an instance.
If parentheses follow the factory name, their contents are parsed as If parentheses follow the factory name, their contents are parsed as
Python literals and passed as arguments to the function. This means that Python literals and passed as arguments and keyword arguments to the
strings must still be in quotes. function. This means that strings must still be in quotes.
Run the Development Server 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 """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 of arguments. Checks for the existence of a script_info argument and calls
the app_factory depending on that and the arguments provided. 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( warnings.warn(
"The 'script_info' argument is deprecated and will not be" "The 'script_info' argument is deprecated and will not be"
" passed to the app factory function in 2.1.", " passed to the app factory function in 2.1.",
DeprecationWarning, DeprecationWarning,
) )
return app_factory(*arguments, script_info=script_info) kwargs["script_info"] = script_info
elif arguments:
return app_factory(*arguments) if (
elif not arguments and len(args_spec.args) == 1 and args_spec.defaults is None: not args
and len(sig.parameters) == 1
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty
):
warnings.warn( warnings.warn(
"Script info is deprecated and will not be passed as the" "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, 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 """Check whether calling a function raised a ``TypeError`` because
the call failed or because something in the factory raised the the call failed or because something in the factory raised the
error. error.
:param factory: the factory function that was called :param f: The function that was called.
:return: true if the call failed :return: ``True`` if the call failed.
""" """
tb = sys.exc_info()[2] tb = sys.exc_info()[2]
try: try:
while tb is not None: while tb is not None:
if tb.tb_frame.f_code is factory.__code__: if tb.tb_frame.f_code is f.__code__:
# in the factory, it was called successfully # In the function, it was called successfully.
return False return False
tb = tb.tb_next tb = tb.tb_next
# didn't reach the factory # Didn't reach the function.
return True return True
finally: 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 # https://docs.python.org/2/library/sys.html#sys.exc_info
del tb del tb
@ -145,37 +150,60 @@ def find_app_by_string(script_info, module, app_name):
""" """
from . import Flask from . import Flask
match = re.match(r"^ *([^ ()]+) *(?:\((.*?) *,? *\))? *$", app_name) # Parse app_name as a single expression to determine if it's a valid
# attribute name or function call.
if not match: try:
expr = ast.parse(app_name.strip(), mode="eval").body
except SyntaxError:
raise NoAppException( 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: try:
attr = getattr(module, name) attr = getattr(module, name)
except AttributeError as e: except AttributeError:
raise NoAppException(e.args[0]) 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 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: try:
app = call_factory(script_info, attr, args) app = call_factory(script_info, attr, args, kwargs)
except TypeError as e: except TypeError:
if not _called_with_wrong_args(attr): if not _called_with_wrong_args(attr):
raise raise
raise NoAppException( 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" f" {module.__name__!r} could not be called with the"
" specified arguments." " specified arguments."
) )
@ -362,8 +390,6 @@ class ScriptInfo:
if self._loaded_app is not None: if self._loaded_app is not None:
return self._loaded_app return self._loaded_app
app = None
if self.create_app is not None: if self.create_app is not None:
app = call_factory(self, self.create_app) app = call_factory(self, self.create_app)
else: else:

View file

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