Skip to content

Commit

Permalink
Merge branch 'master' into fix-env-chdir
Browse files Browse the repository at this point in the history
  • Loading branch information
greyli authored Apr 14, 2020
2 parents 2cb004c + 2062d98 commit cdb429c
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 395 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Version 2.0.0
Unreleased

- Drop support for Python 2 and 3.5.
- JSON support no longer uses simplejson. To use another JSON module,
override ``app.json_encoder`` and ``json_decoder``. :issue:`3555`
- The ``encoding`` option to JSON functions is deprecated. :pr:`3562`
- Passing ``script_info`` to app factory functions is deprecated. This
was not portable outside the ``flask`` command. Use
``click.get_current_context().obj`` if it's needed. :issue:`3552`
- Add :meth:`sessions.SessionInterface.get_cookie_name` to allow
setting the session cookie name dynamically. :pr:`3369`
- Add :meth:`Config.from_file` to load config using arbitrary file
Expand All @@ -19,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`
- When loading a ``.env`` or ``.flaskenv`` file on top level directory,
Flask will not change current work directory to the location of dotenv
files, in order to prevent potential confusion. :pr:`3560`
Expand Down
89 changes: 16 additions & 73 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,6 @@ Incoming Request Data
:inherited-members:
:exclude-members: json_module

.. attribute:: environ

The underlying WSGI environment.

.. attribute:: path
.. attribute:: full_path
.. attribute:: script_root
.. attribute:: url
.. attribute:: base_url
.. attribute:: url_root

Provides different ways to look at the current :rfc:`3987`.
Imagine your application is listening on the following application
root::

http://www.example.com/myapplication

And a user requests the following URI::

http://www.example.com/myapplication/%CF%80/page.html?x=y

In this case the values of the above mentioned attributes would be
the following:

============= ======================================================
`path` ``'/π/page.html'``
`full_path` ``'/π/page.html?x=y'``
`script_root` ``'/myapplication'``
`base_url` ``'http://www.example.com/myapplication/π/page.html'``
`url` ``'http://www.example.com/myapplication/π/page.html?x=y'``
`url_root` ``'http://www.example.com/myapplication/'``
============= ======================================================


.. attribute:: request

To access incoming request data, you can use the global `request`
Expand Down Expand Up @@ -279,58 +245,34 @@ Message Flashing

.. autofunction:: get_flashed_messages


JSON Support
------------

.. module:: flask.json

Flask uses ``simplejson`` for the JSON implementation. Since simplejson
is provided by both the standard library as well as extension, Flask will
try simplejson first and then fall back to the stdlib json module. On top
of that it will delegate access to the current application's JSON encoders
and decoders for easier customization.

So for starters instead of doing::

try:
import simplejson as json
except ImportError:
import json

You can instead just do this::

from flask import json
Flask uses the built-in :mod:`json` module for handling JSON. It will
use the current blueprint's or application's JSON encoder and decoder
for easier customization. By default it handles some extra data types:

For usage examples, read the :mod:`json` documentation in the standard
library. The following extensions are by default applied to the stdlib's
JSON module:
- :class:`datetime.datetime` and :class:`datetime.date` are serialized
to :rfc:`822` strings. This is the same as the HTTP date format.
- :class:`uuid.UUID` is serialized to a string.
- :class:`dataclasses.dataclass` is passed to
:func:`dataclasses.asdict`.
- :class:`~markupsafe.Markup` (or any object with a ``__html__``
method) will call the ``__html__`` method to get a string.

1. ``datetime`` objects are serialized as :rfc:`822` strings.
2. Any object with an ``__html__`` method (like :class:`~flask.Markup`)
will have that method called and then the return value is serialized
as string.

The :func:`~htmlsafe_dumps` function of this json module is also available
as a filter called ``|tojson`` in Jinja2. Note that in versions of Flask prior
to Flask 0.10, you must disable escaping with ``|safe`` if you intend to use
``|tojson`` output inside ``script`` tags. In Flask 0.10 and above, this
happens automatically (but it's harmless to include ``|safe`` anyway).
:func:`~htmlsafe_dumps` is also available as the ``|tojson`` template
filter. The filter marks the output with ``|safe`` so it can be used
inside ``script`` tags.

.. sourcecode:: html+jinja

<script type=text/javascript>
doSomethingWith({{ user.username|tojson|safe }});
renderChart({{ axis_data|tojson }});
</script>

.. admonition:: Auto-Sort JSON Keys

The configuration variable :data:`JSON_SORT_KEYS` can be set to
``False`` to stop Flask from auto-sorting keys. By default sorting
is enabled and outside of the app context sorting is turned on.

Notice that disabling key sorting can cause issues when using
content based HTTP caches and Python's hash randomization feature.

.. autofunction:: jsonify

.. autofunction:: dumps
Expand All @@ -349,6 +291,7 @@ happens automatically (but it's harmless to include ``|safe`` anyway).

.. automodule:: flask.json.tag


Template Rendering
------------------

Expand Down
8 changes: 2 additions & 6 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,8 @@ Within the given import, the command looks for an application instance named
found, the command looks for a factory function named ``create_app`` or
``make_app`` that returns an instance.

When calling an application factory, if the factory takes an argument named
``script_info``, then the :class:`~cli.ScriptInfo` instance is passed as a
keyword argument. If the application factory takes only one argument and no
parentheses follow the factory name, the :class:`~cli.ScriptInfo` instance
is passed as a positional argument. If parentheses follow the factory name,
their contents are parsed as Python literals and passes as arguments to the
If parentheses follow the factory name, their contents are parsed as
Python literals and passed as arguments and keyword arguments to the
function. This means that strings must still be in quotes.


Expand Down
4 changes: 0 additions & 4 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,12 @@ These distributions will not be installed automatically. Flask will detect and
use them if you install them.

* `Blinker`_ provides support for :doc:`signals`.
* `SimpleJSON`_ is a fast JSON implementation that is compatible with
Python's ``json`` module. It is preferred for JSON operations if it is
installed.
* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask``
commands.
* `Watchdog`_ provides a faster, more efficient reloader for the development
server.

.. _Blinker: https://pythonhosted.org/blinker/
.. _SimpleJSON: https://simplejson.readthedocs.io/
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
.. _watchdog: https://pythonhosted.org/watchdog/

Expand Down
6 changes: 3 additions & 3 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Sphinx~=2.2.0
Pallets-Sphinx-Themes~=1.2.2
Sphinx~=3.0.0
Pallets-Sphinx-Themes~=1.2.3
sphinxcontrib-log-cabinet~=1.0.1
sphinx-issues~=1.2.0
packaging~=19.2
packaging~=20.3
117 changes: 75 additions & 42 deletions src/flask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import sys
import traceback
import warnings
from functools import update_wrapper
from operator import attrgetter
from threading import Lock
Expand Down Expand Up @@ -85,90 +86,124 @@ 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)
arg_names = args_spec.args
arg_defaults = args_spec.defaults
sig = inspect.signature(app_factory)
args = [] if args is None else args
kwargs = {} if kwargs is None else kwargs

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,
)
kwargs["script_info"] = script_info

if "script_info" in arg_names:
return app_factory(*arguments, script_info=script_info)
elif arguments:
return app_factory(*arguments)
elif not arguments and len(arg_names) == 1 and arg_defaults is None:
return app_factory(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"
" single argument to the app factory function in 2.1.",
DeprecationWarning,
)
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


def find_app_by_string(script_info, module, app_name):
"""Checks if the given string is a variable name or a function. If it is a
function, it checks for specified arguments and whether it takes a
``script_info`` argument and calls the function with the appropriate
arguments.
"""Check if the given string is a variable name or a function. Call
a function to get the app instance, or return the variable directly.
"""
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."
)
Expand Down Expand Up @@ -355,8 +390,6 @@ def load_app(self):
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:
Expand Down
Loading

0 comments on commit cdb429c

Please sign in to comment.