diff --git a/CHANGES.rst b/CHANGES.rst index e712ba27..424fe876 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/docs/cli.rst b/docs/cli.rst index 759c315a..abcfb7c6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -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 diff --git a/src/flask/cli.py b/src/flask/cli.py index dbf7f37b..fd9baa67 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -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: diff --git a/tests/test_cli.py b/tests/test_cli.py index 14bfc4c9..635daabe 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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"),