Merge branch 'UpdateTornado' into dev

This commit is contained in:
JackDandy 2023-12-10 05:31:41 +00:00
commit 1715eaf21b
20 changed files with 540 additions and 389 deletions

View file

@ -11,6 +11,7 @@
* Update Rarfile 4.1a1 (8a72967) to 4.1 (c9140d8) * Update Rarfile 4.1a1 (8a72967) to 4.1 (c9140d8)
* Update soupsieve 2.4.1 (2e66beb) to 2.5.0 (dc71495) * Update soupsieve 2.4.1 (2e66beb) to 2.5.0 (dc71495)
* Update thefuzz 0.19.0 (c2cd4f4) to 0.21.0 (0b49e4a) * Update thefuzz 0.19.0 (c2cd4f4) to 0.21.0 (0b49e4a)
* Update Tornado Web Server 6.3.3 (e4d6984) to 6.4 (b3f2a4b)
* Update urllib3 2.0.5 (d9f85a7) to 2.0.7 (56f01e0) * Update urllib3 2.0.5 (d9f85a7) to 2.0.7 (56f01e0)
* Add support for Brotli compression * Add support for Brotli compression
* Add ignore Plex extras * Add ignore Plex extras

View file

@ -22,8 +22,8 @@
# is zero for an official release, positive for a development branch, # is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
version = "6.3.3" version = "6.4"
version_info = (6, 3, 3, 0) version_info = (6, 4, 0, 0)
import importlib import importlib
import typing import typing

33
lib/tornado/__init__.pyi Normal file
View file

@ -0,0 +1,33 @@
import typing
version: str
version_info: typing.Tuple[int, int, int, int]
from . import auth
from . import autoreload
from . import concurrent
from . import curl_httpclient
from . import escape
from . import gen
from . import http1connection
from . import httpclient
from . import httpserver
from . import httputil
from . import ioloop
from . import iostream
from . import locale
from . import locks
from . import log
from . import netutil
from . import options
from . import platform
from . import process
from . import queues
from . import routing
from . import simple_httpclient
from . import tcpclient
from . import tcpserver
from . import template
from . import testing
from . import util
from . import web

View file

@ -33,20 +33,36 @@ See the individual service classes below for complete documentation.
Example usage for Google OAuth: Example usage for Google OAuth:
.. testsetup::
import urllib
.. testcode:: .. testcode::
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
tornado.auth.GoogleOAuth2Mixin): tornado.auth.GoogleOAuth2Mixin):
async def get(self):
# Google requires an exact match for redirect_uri, so it's
# best to get it from your app configuration instead of from
# self.request.full_uri().
redirect_uri = urllib.parse.urljoin(self.application.settings['redirect_base_uri'],
self.reverse_url('google_oauth'))
async def get(self): async def get(self):
if self.get_argument('code', False): if self.get_argument('code', False):
user = await self.get_authenticated_user( access = await self.get_authenticated_user(
redirect_uri='http://your.site.com/auth/google', redirect_uri=redirect_uri,
code=self.get_argument('code')) code=self.get_argument('code'))
# Save the user with e.g. set_signed_cookie user = await self.oauth2_request(
"https://www.googleapis.com/oauth2/v1/userinfo",
access_token=access["access_token"])
# Save the user and access token. For example:
user_cookie = dict(id=user["id"], access_token=access["access_token"])
self.set_signed_cookie("user", json.dumps(user_cookie))
self.redirect("/")
else: else:
self.authorize_redirect( self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google', redirect_uri=redirect_uri,
client_id=self.settings['google_oauth']['key'], client_id=self.get_google_oauth_settings()['key'],
scope=['profile', 'email'], scope=['profile', 'email'],
response_type='code', response_type='code',
extra_params={'approval_prompt': 'auto'}) extra_params={'approval_prompt': 'auto'})
@ -63,6 +79,7 @@ import hmac
import time import time
import urllib.parse import urllib.parse
import uuid import uuid
import warnings
from tornado import httpclient from tornado import httpclient
from tornado import escape from tornado import escape
@ -571,7 +588,13 @@ class OAuth2Mixin(object):
The ``callback`` argument and returned awaitable were removed; The ``callback`` argument and returned awaitable were removed;
this is now an ordinary synchronous function. this is now an ordinary synchronous function.
.. deprecated:: 6.4
The ``client_secret`` argument (which has never had any effect)
is deprecated and will be removed in Tornado 7.0.
""" """
if client_secret is not None:
warnings.warn("client_secret argument is deprecated", DeprecationWarning)
handler = cast(RequestHandler, self) handler = cast(RequestHandler, self)
args = {"response_type": response_type} args = {"response_type": response_type}
if redirect_uri is not None: if redirect_uri is not None:
@ -705,6 +728,12 @@ class TwitterMixin(OAuthMixin):
includes the attributes ``username``, ``name``, ``access_token``, includes the attributes ``username``, ``name``, ``access_token``,
and all of the custom Twitter user attributes described at and all of the custom Twitter user attributes described at
https://dev.twitter.com/docs/api/1.1/get/users/show https://dev.twitter.com/docs/api/1.1/get/users/show
.. deprecated:: 6.3
This class refers to version 1.1 of the Twitter API, which has been
deprecated by Twitter. Since Twitter has begun to limit access to its
API, this class will no longer be updated and will be removed in the
future.
""" """
_OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token" _OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"
@ -839,12 +868,18 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
* Go to the Google Dev Console at http://console.developers.google.com * Go to the Google Dev Console at http://console.developers.google.com
* Select a project, or create a new one. * Select a project, or create a new one.
* Depending on permissions required, you may need to set your app to
"testing" mode and add your account as a test user, or go through
a verfication process. You may also need to use the "Enable
APIs and Services" command to enable specific services.
* In the sidebar on the left, select Credentials. * In the sidebar on the left, select Credentials.
* Click CREATE CREDENTIALS and click OAuth client ID. * Click CREATE CREDENTIALS and click OAuth client ID.
* Under Application type, select Web application. * Under Application type, select Web application.
* Name OAuth 2.0 client and click Create. * Name OAuth 2.0 client and click Create.
* Copy the "Client secret" and "Client ID" to the application settings as * Copy the "Client secret" and "Client ID" to the application settings as
``{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}`` ``{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}``
* You must register the ``redirect_uri`` you plan to use with this class
on the Credentials page.
.. versionadded:: 3.2 .. versionadded:: 3.2
""" """
@ -890,23 +925,35 @@ class GoogleOAuth2Mixin(OAuth2Mixin):
Example usage: Example usage:
.. testsetup::
import urllib
.. testcode:: .. testcode::
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler, class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
tornado.auth.GoogleOAuth2Mixin): tornado.auth.GoogleOAuth2Mixin):
async def get(self):
# Google requires an exact match for redirect_uri, so it's
# best to get it from your app configuration instead of from
# self.request.full_uri().
redirect_uri = urllib.parse.urljoin(self.application.settings['redirect_base_uri'],
self.reverse_url('google_oauth'))
async def get(self): async def get(self):
if self.get_argument('code', False): if self.get_argument('code', False):
access = await self.get_authenticated_user( access = await self.get_authenticated_user(
redirect_uri='http://your.site.com/auth/google', redirect_uri=redirect_uri,
code=self.get_argument('code')) code=self.get_argument('code'))
user = await self.oauth2_request( user = await self.oauth2_request(
"https://www.googleapis.com/oauth2/v1/userinfo", "https://www.googleapis.com/oauth2/v1/userinfo",
access_token=access["access_token"]) access_token=access["access_token"])
# Save the user and access token with # Save the user and access token. For example:
# e.g. set_signed_cookie. user_cookie = dict(id=user["id"], access_token=access["access_token"])
self.set_signed_cookie("user", json.dumps(user_cookie))
self.redirect("/")
else: else:
self.authorize_redirect( self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google', redirect_uri=redirect_uri,
client_id=self.get_google_oauth_settings()['key'], client_id=self.get_google_oauth_settings()['key'],
scope=['profile', 'email'], scope=['profile', 'email'],
response_type='code', response_type='code',
@ -971,18 +1018,21 @@ class FacebookGraphMixin(OAuth2Mixin):
class FacebookGraphLoginHandler(tornado.web.RequestHandler, class FacebookGraphLoginHandler(tornado.web.RequestHandler,
tornado.auth.FacebookGraphMixin): tornado.auth.FacebookGraphMixin):
async def get(self): async def get(self):
redirect_uri = urllib.parse.urljoin(
self.application.settings['redirect_base_uri'],
self.reverse_url('facebook_oauth'))
if self.get_argument("code", False): if self.get_argument("code", False):
user = await self.get_authenticated_user( user = await self.get_authenticated_user(
redirect_uri='/auth/facebookgraph/', redirect_uri=redirect_uri,
client_id=self.settings["facebook_api_key"], client_id=self.settings["facebook_api_key"],
client_secret=self.settings["facebook_secret"], client_secret=self.settings["facebook_secret"],
code=self.get_argument("code")) code=self.get_argument("code"))
# Save the user with e.g. set_signed_cookie # Save the user with e.g. set_signed_cookie
else: else:
self.authorize_redirect( self.authorize_redirect(
redirect_uri='/auth/facebookgraph/', redirect_uri=redirect_uri,
client_id=self.settings["facebook_api_key"], client_id=self.settings["facebook_api_key"],
extra_params={"scope": "read_stream,offline_access"}) extra_params={"scope": "user_posts"})
.. testoutput:: .. testoutput::
:hide: :hide:

View file

@ -60,8 +60,7 @@ import sys
# may become relative in spite of the future import. # may become relative in spite of the future import.
# #
# We address the former problem by reconstructing the original command # We address the former problem by reconstructing the original command
# line (Python >= 3.4) or by setting the $PYTHONPATH environment # line before re-execution so the new process will
# variable (Python < 3.4) before re-execution so the new process will
# see the correct path. We attempt to address the latter problem when # see the correct path. We attempt to address the latter problem when
# tornado.autoreload is run as __main__. # tornado.autoreload is run as __main__.
@ -76,8 +75,9 @@ if __name__ == "__main__":
del sys.path[0] del sys.path[0]
import functools import functools
import importlib.abc
import os import os
import pkgutil # type: ignore import pkgutil
import sys import sys
import traceback import traceback
import types import types
@ -87,18 +87,13 @@ import weakref
from tornado import ioloop from tornado import ioloop
from tornado.log import gen_log from tornado.log import gen_log
from tornado import process from tornado import process
from tornado.util import exec_in
try: try:
import signal import signal
except ImportError: except ImportError:
signal = None # type: ignore signal = None # type: ignore
import typing from typing import Callable, Dict, Optional, List, Union
from typing import Callable, Dict
if typing.TYPE_CHECKING:
from typing import List, Optional, Union # noqa: F401
# os.execv is broken on Windows and can't properly parse command line # os.execv is broken on Windows and can't properly parse command line
# arguments and executable name if they contain whitespaces. subprocess # arguments and executable name if they contain whitespaces. subprocess
@ -108,9 +103,11 @@ _has_execv = sys.platform != "win32"
_watched_files = set() _watched_files = set()
_reload_hooks = [] _reload_hooks = []
_reload_attempted = False _reload_attempted = False
_io_loops = weakref.WeakKeyDictionary() # type: ignore _io_loops: "weakref.WeakKeyDictionary[ioloop.IOLoop, bool]" = (
weakref.WeakKeyDictionary()
)
_autoreload_is_main = False _autoreload_is_main = False
_original_argv = None # type: Optional[List[str]] _original_argv: Optional[List[str]] = None
_original_spec = None _original_spec = None
@ -126,7 +123,7 @@ def start(check_time: int = 500) -> None:
_io_loops[io_loop] = True _io_loops[io_loop] = True
if len(_io_loops) > 1: if len(_io_loops) > 1:
gen_log.warning("tornado.autoreload started more than once in the same process") gen_log.warning("tornado.autoreload started more than once in the same process")
modify_times = {} # type: Dict[str, float] modify_times: Dict[str, float] = {}
callback = functools.partial(_reload_on_update, modify_times) callback = functools.partial(_reload_on_update, modify_times)
scheduler = ioloop.PeriodicCallback(callback, check_time) scheduler = ioloop.PeriodicCallback(callback, check_time)
scheduler.start() scheduler.start()
@ -214,10 +211,7 @@ def _reload() -> None:
# sys.path fixes: see comments at top of file. If __main__.__spec__ # sys.path fixes: see comments at top of file. If __main__.__spec__
# exists, we were invoked with -m and the effective path is about to # exists, we were invoked with -m and the effective path is about to
# change on re-exec. Reconstruct the original command line to # change on re-exec. Reconstruct the original command line to
# ensure that the new process sees the same path we did. If # ensure that the new process sees the same path we did.
# __spec__ is not available (Python < 3.4), check instead if
# sys.path[0] is an empty string and add the current directory to
# $PYTHONPATH.
if _autoreload_is_main: if _autoreload_is_main:
assert _original_argv is not None assert _original_argv is not None
spec = _original_spec spec = _original_spec
@ -225,43 +219,25 @@ def _reload() -> None:
else: else:
spec = getattr(sys.modules["__main__"], "__spec__", None) spec = getattr(sys.modules["__main__"], "__spec__", None)
argv = sys.argv argv = sys.argv
if spec: if spec and spec.name != "__main__":
# __spec__ is set in two cases: when running a module, and when running a directory. (when
# running a file, there is no spec). In the former case, we must pass -m to maintain the
# module-style behavior (setting sys.path), even though python stripped -m from its argv at
# startup. If sys.path is exactly __main__, we're running a directory and should fall
# through to the non-module behavior.
#
# Some of this, including the use of exactly __main__ as a spec for directory mode,
# is documented at https://docs.python.org/3/library/runpy.html#runpy.run_path
argv = ["-m", spec.name] + argv[1:] argv = ["-m", spec.name] + argv[1:]
else:
path_prefix = "." + os.pathsep
if sys.path[0] == "" and not os.environ.get("PYTHONPATH", "").startswith(
path_prefix
):
os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "")
if not _has_execv: if not _has_execv:
subprocess.Popen([sys.executable] + argv) subprocess.Popen([sys.executable] + argv)
os._exit(0) os._exit(0)
else: else:
try:
os.execv(sys.executable, [sys.executable] + argv) os.execv(sys.executable, [sys.executable] + argv)
except OSError:
# Mac OS X versions prior to 10.6 do not support execv in
# a process that contains multiple threads. Instead of
# re-executing in the current process, start a new one
# and cause the current process to exit. This isn't
# ideal since the new process is detached from the parent
# terminal and thus cannot easily be killed with ctrl-C,
# but it's better than not being able to autoreload at
# all.
# Unfortunately the errno returned in this case does not
# appear to be consistent, so we can't easily check for
# this error specifically.
os.spawnv(
os.P_NOWAIT, sys.executable, [sys.executable] + argv # type: ignore
)
# At this point the IOLoop has been closed and finally
# blocks will experience errors if we allow the stack to
# unwind, so just exit uncleanly.
os._exit(0)
_USAGE = """\ _USAGE = """
Usage:
python -m tornado.autoreload -m module.to.run [args...] python -m tornado.autoreload -m module.to.run [args...]
python -m tornado.autoreload path/to/script.py [args...] python -m tornado.autoreload path/to/script.py [args...]
""" """
@ -283,6 +259,12 @@ def main() -> None:
# Remember that we were launched with autoreload as main. # Remember that we were launched with autoreload as main.
# The main module can be tricky; set the variables both in our globals # The main module can be tricky; set the variables both in our globals
# (which may be __main__) and the real importable version. # (which may be __main__) and the real importable version.
#
# We use optparse instead of the newer argparse because we want to
# mimic the python command-line interface which requires stopping
# parsing at the first positional argument. optparse supports
# this but as far as I can tell argparse does not.
import optparse
import tornado.autoreload import tornado.autoreload
global _autoreload_is_main global _autoreload_is_main
@ -292,38 +274,43 @@ def main() -> None:
tornado.autoreload._original_argv = _original_argv = original_argv tornado.autoreload._original_argv = _original_argv = original_argv
original_spec = getattr(sys.modules["__main__"], "__spec__", None) original_spec = getattr(sys.modules["__main__"], "__spec__", None)
tornado.autoreload._original_spec = _original_spec = original_spec tornado.autoreload._original_spec = _original_spec = original_spec
sys.argv = sys.argv[:]
if len(sys.argv) >= 3 and sys.argv[1] == "-m":
mode = "module"
module = sys.argv[2]
del sys.argv[1:3]
elif len(sys.argv) >= 2:
mode = "script"
script = sys.argv[1]
sys.argv = sys.argv[1:]
else:
print(_USAGE, file=sys.stderr)
sys.exit(1)
parser = optparse.OptionParser(
prog="python -m tornado.autoreload",
usage=_USAGE,
epilog="Either -m or a path must be specified, but not both",
)
parser.disable_interspersed_args()
parser.add_option("-m", dest="module", metavar="module", help="module to run")
parser.add_option(
"--until-success",
action="store_true",
help="stop reloading after the program exist successfully (status code 0)",
)
opts, rest = parser.parse_args()
if opts.module is None:
if not rest:
print("Either -m or a path must be specified", file=sys.stderr)
sys.exit(1)
path = rest[0]
sys.argv = rest[:]
else:
path = None
sys.argv = [sys.argv[0]] + rest
# SystemExit.code is typed funny: https://github.com/python/typeshed/issues/8513
# All we care about is truthiness
exit_status: Union[int, str, None] = 1
try: try:
if mode == "module":
import runpy import runpy
runpy.run_module(module, run_name="__main__", alter_sys=True) if opts.module is not None:
elif mode == "script": runpy.run_module(opts.module, run_name="__main__", alter_sys=True)
with open(script) as f: else:
# Execute the script in our namespace instead of creating assert path is not None
# a new one so that something that tries to import __main__ runpy.run_path(path, run_name="__main__")
# (e.g. the unittest module) will see names defined in the
# script instead of just those defined in this module.
global __file__
__file__ = script
# If __package__ is defined, imports may be incorrectly
# interpreted as relative to this module.
global __package__
del __package__
exec_in(f.read(), globals(), globals())
except SystemExit as e: except SystemExit as e:
exit_status = e.code
gen_log.info("Script exited with status %s", e.code) gen_log.info("Script exited with status %s", e.code)
except Exception as e: except Exception as e:
gen_log.warning("Script exited with uncaught exception", exc_info=True) gen_log.warning("Script exited with uncaught exception", exc_info=True)
@ -331,7 +318,7 @@ def main() -> None:
# never made it into sys.modules and so we won't know to watch it. # never made it into sys.modules and so we won't know to watch it.
# Just to make sure we've covered everything, walk the stack trace # Just to make sure we've covered everything, walk the stack trace
# from the exception and watch every file. # from the exception and watch every file.
for (filename, lineno, name, line) in traceback.extract_tb(sys.exc_info()[2]): for filename, lineno, name, line in traceback.extract_tb(sys.exc_info()[2]):
watch(filename) watch(filename)
if isinstance(e, SyntaxError): if isinstance(e, SyntaxError):
# SyntaxErrors are special: their innermost stack frame is fake # SyntaxErrors are special: their innermost stack frame is fake
@ -340,17 +327,20 @@ def main() -> None:
if e.filename is not None: if e.filename is not None:
watch(e.filename) watch(e.filename)
else: else:
exit_status = 0
gen_log.info("Script exited normally") gen_log.info("Script exited normally")
# restore sys.argv so subsequent executions will include autoreload # restore sys.argv so subsequent executions will include autoreload
sys.argv = original_argv sys.argv = original_argv
if mode == "module": if opts.module is not None:
assert opts.module is not None
# runpy did a fake import of the module as __main__, but now it's # runpy did a fake import of the module as __main__, but now it's
# no longer in sys.modules. Figure out where it is and watch it. # no longer in sys.modules. Figure out where it is and watch it.
loader = pkgutil.get_loader(module) loader = pkgutil.get_loader(opts.module)
if loader is not None: if loader is not None and isinstance(loader, importlib.abc.FileLoader):
watch(loader.get_filename()) # type: ignore watch(loader.get_filename())
if opts.until_success and not exit_status:
return
wait() wait()

View file

@ -54,7 +54,7 @@ def is_future(x: Any) -> bool:
class DummyExecutor(futures.Executor): class DummyExecutor(futures.Executor):
def submit( def submit( # type: ignore[override]
self, fn: Callable[..., _T], *args: Any, **kwargs: Any self, fn: Callable[..., _T], *args: Any, **kwargs: Any
) -> "futures.Future[_T]": ) -> "futures.Future[_T]":
future = futures.Future() # type: futures.Future[_T] future = futures.Future() # type: futures.Future[_T]
@ -64,6 +64,13 @@ class DummyExecutor(futures.Executor):
future_set_exc_info(future, sys.exc_info()) future_set_exc_info(future, sys.exc_info())
return future return future
if sys.version_info >= (3, 9):
def shutdown(self, wait: bool = True, cancel_futures: bool = False) -> None:
pass
else:
def shutdown(self, wait: bool = True) -> None: def shutdown(self, wait: bool = True) -> None:
pass pass
@ -150,8 +157,7 @@ def chain_future(a: "Future[_T]", b: "Future[_T]") -> None:
""" """
def copy(future: "Future[_T]") -> None: def copy(a: "Future[_T]") -> None:
assert future is a
if b.done(): if b.done():
return return
if hasattr(a, "exc_info") and a.exc_info() is not None: # type: ignore if hasattr(a, "exc_info") and a.exc_info() is not None: # type: ignore

View file

@ -17,9 +17,15 @@
Also includes a few other miscellaneous string manipulation functions that Also includes a few other miscellaneous string manipulation functions that
have crept in over time. have crept in over time.
Many functions in this module have near-equivalents in the standard library
(the differences mainly relate to handling of bytes and unicode strings,
and were more relevant in Python 2). In new code, the standard library
functions are encouraged instead of this module where applicable. See the
docstrings on each function for details.
""" """
import html.entities import html
import json import json
import re import re
import urllib.parse import urllib.parse
@ -30,16 +36,6 @@ import typing
from typing import Union, Any, Optional, Dict, List, Callable from typing import Union, Any, Optional, Dict, List, Callable
_XHTML_ESCAPE_RE = re.compile("[&<>\"']")
_XHTML_ESCAPE_DICT = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
}
def xhtml_escape(value: Union[str, bytes]) -> str: def xhtml_escape(value: Union[str, bytes]) -> str:
"""Escapes a string so it is valid within HTML or XML. """Escapes a string so it is valid within HTML or XML.
@ -47,25 +43,50 @@ def xhtml_escape(value: Union[str, bytes]) -> str:
When used in attribute values the escaped strings must be enclosed When used in attribute values the escaped strings must be enclosed
in quotes. in quotes.
Equivalent to `html.escape` except that this function always returns
type `str` while `html.escape` returns `bytes` if its input is `bytes`.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
Added the single quote to the list of escaped characters. Added the single quote to the list of escaped characters.
.. versionchanged:: 6.4
Now simply wraps `html.escape`. This is equivalent to the old behavior
except that single quotes are now escaped as ``&#x27;`` instead of
``&#39;`` and performance may be different.
""" """
return _XHTML_ESCAPE_RE.sub( return html.escape(to_unicode(value))
lambda match: _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value)
)
def xhtml_unescape(value: Union[str, bytes]) -> str: def xhtml_unescape(value: Union[str, bytes]) -> str:
"""Un-escapes an XML-escaped string.""" """Un-escapes an XML-escaped string.
return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
Equivalent to `html.unescape` except that this function always returns
type `str` while `html.unescape` returns `bytes` if its input is `bytes`.
.. versionchanged:: 6.4
Now simply wraps `html.unescape`. This changes behavior for some inputs
as required by the HTML 5 specification
https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
Some invalid inputs such as surrogates now raise an error, and numeric
references to certain ISO-8859-1 characters are now handled correctly.
"""
return html.unescape(to_unicode(value))
# The fact that json_encode wraps json.dumps is an implementation detail. # The fact that json_encode wraps json.dumps is an implementation detail.
# Please see https://github.com/tornadoweb/tornado/pull/706 # Please see https://github.com/tornadoweb/tornado/pull/706
# before sending a pull request that adds **kwargs to this function. # before sending a pull request that adds **kwargs to this function.
def json_encode(value: Any) -> str: def json_encode(value: Any) -> str:
"""JSON-encodes the given Python object.""" """JSON-encodes the given Python object.
Equivalent to `json.dumps` with the additional guarantee that the output
will never contain the character sequence ``</`` which can be problematic
when JSON is embedded in an HTML ``<script>`` tag.
"""
# JSON permits but does not require forward slashes to be escaped. # JSON permits but does not require forward slashes to be escaped.
# This is useful when json data is emitted in a <script> tag # This is useful when json data is emitted in a <script> tag
# in HTML, as it prevents </script> tags from prematurely terminating # in HTML, as it prevents </script> tags from prematurely terminating
@ -78,9 +99,9 @@ def json_encode(value: Any) -> str:
def json_decode(value: Union[str, bytes]) -> Any: def json_decode(value: Union[str, bytes]) -> Any:
"""Returns Python objects for the given JSON string. """Returns Python objects for the given JSON string.
Supports both `str` and `bytes` inputs. Supports both `str` and `bytes` inputs. Equvalent to `json.loads`.
""" """
return json.loads(to_basestring(value)) return json.loads(value)
def squeeze(value: str) -> str: def squeeze(value: str) -> str:
@ -91,16 +112,20 @@ def squeeze(value: str) -> str:
def url_escape(value: Union[str, bytes], plus: bool = True) -> str: def url_escape(value: Union[str, bytes], plus: bool = True) -> str:
"""Returns a URL-encoded version of the given value. """Returns a URL-encoded version of the given value.
If ``plus`` is true (the default), spaces will be represented Equivalent to either `urllib.parse.quote_plus` or `urllib.parse.quote` depending on the ``plus``
as "+" instead of "%20". This is appropriate for query strings argument.
but not for the path component of a URL. Note that this default
is the reverse of Python's urllib module. If ``plus`` is true (the default), spaces will be represented as ``+`` and slashes will be
represented as ``%2F``. This is appropriate for query strings. If ``plus`` is false, spaces
will be represented as ``%20`` and slashes are left as-is. This is appropriate for the path
component of a URL. Note that the default of ``plus=True`` is effectively the
reverse of Python's urllib module.
.. versionadded:: 3.1 .. versionadded:: 3.1
The ``plus`` argument The ``plus`` argument
""" """
quote = urllib.parse.quote_plus if plus else urllib.parse.quote quote = urllib.parse.quote_plus if plus else urllib.parse.quote
return quote(utf8(value)) return quote(value)
@typing.overload @typing.overload
@ -108,28 +133,29 @@ def url_unescape(value: Union[str, bytes], encoding: None, plus: bool = True) ->
pass pass
@typing.overload # noqa: F811 @typing.overload
def url_unescape( def url_unescape(
value: Union[str, bytes], encoding: str = "utf-8", plus: bool = True value: Union[str, bytes], encoding: str = "utf-8", plus: bool = True
) -> str: ) -> str:
pass pass
def url_unescape( # noqa: F811 def url_unescape(
value: Union[str, bytes], encoding: Optional[str] = "utf-8", plus: bool = True value: Union[str, bytes], encoding: Optional[str] = "utf-8", plus: bool = True
) -> Union[str, bytes]: ) -> Union[str, bytes]:
"""Decodes the given value from a URL. """Decodes the given value from a URL.
The argument may be either a byte or unicode string. The argument may be either a byte or unicode string.
If encoding is None, the result will be a byte string. Otherwise, If encoding is None, the result will be a byte string and this function is equivalent to
the result is a unicode string in the specified encoding. `urllib.parse.unquote_to_bytes` if ``plus=False``. Otherwise, the result is a unicode string in
the specified encoding and this function is equivalent to either `urllib.parse.unquote_plus` or
`urllib.parse.unquote` except that this function also accepts `bytes` as input.
If ``plus`` is true (the default), plus signs will be interpreted If ``plus`` is true (the default), plus signs will be interpreted as spaces (literal plus signs
as spaces (literal plus signs must be represented as "%2B"). This must be represented as "%2B"). This is appropriate for query strings and form-encoded values
is appropriate for query strings and form-encoded values but not but not for the path component of a URL. Note that this default is the reverse of Python's
for the path component of a URL. Note that this default is the urllib module.
reverse of Python's urllib module.
.. versionadded:: 3.1 .. versionadded:: 3.1
The ``plus`` argument The ``plus`` argument
@ -175,17 +201,17 @@ def utf8(value: bytes) -> bytes:
pass pass
@typing.overload # noqa: F811 @typing.overload
def utf8(value: str) -> bytes: def utf8(value: str) -> bytes:
pass pass
@typing.overload # noqa: F811 @typing.overload
def utf8(value: None) -> None: def utf8(value: None) -> None:
pass pass
def utf8(value: Union[None, str, bytes]) -> Optional[bytes]: # noqa: F811 def utf8(value: Union[None, str, bytes]) -> Optional[bytes]:
"""Converts a string argument to a byte string. """Converts a string argument to a byte string.
If the argument is already a byte string or None, it is returned unchanged. If the argument is already a byte string or None, it is returned unchanged.
@ -206,17 +232,17 @@ def to_unicode(value: str) -> str:
pass pass
@typing.overload # noqa: F811 @typing.overload
def to_unicode(value: bytes) -> str: def to_unicode(value: bytes) -> str:
pass pass
@typing.overload # noqa: F811 @typing.overload
def to_unicode(value: None) -> None: def to_unicode(value: None) -> None:
pass pass
def to_unicode(value: Union[None, str, bytes]) -> Optional[str]: # noqa: F811 def to_unicode(value: Union[None, str, bytes]) -> Optional[str]:
"""Converts a string argument to a unicode string. """Converts a string argument to a unicode string.
If the argument is already a unicode string or None, it is returned If the argument is already a unicode string or None, it is returned
@ -375,28 +401,3 @@ def linkify(
# that we won't pick up &quot;, etc. # that we won't pick up &quot;, etc.
text = _unicode(xhtml_escape(text)) text = _unicode(xhtml_escape(text))
return _URL_RE.sub(make_link, text) return _URL_RE.sub(make_link, text)
def _convert_entity(m: typing.Match) -> str:
if m.group(1) == "#":
try:
if m.group(2)[:1].lower() == "x":
return chr(int(m.group(2)[1:], 16))
else:
return chr(int(m.group(2)))
except ValueError:
return "&#%s;" % m.group(2)
try:
return _HTML_UNICODE_MAP[m.group(2)]
except KeyError:
return "&%s;" % m.group(2)
def _build_unicode_map() -> Dict[str, str]:
unicode_map = {}
for name, value in html.entities.name2codepoint.items():
unicode_map[name] = chr(value)
return unicode_map
_HTML_UNICODE_MAP = _build_unicode_map()

View file

@ -840,13 +840,17 @@ class Runner(object):
return False return False
def _wrap_awaitable(awaitable: Awaitable) -> Future:
# Convert Awaitables into Futures. # Convert Awaitables into Futures.
try: # Note that we use ensure_future, which handles both awaitables
_wrap_awaitable = asyncio.ensure_future # and coroutines, rather than create_task, which only accepts
except AttributeError: # coroutines. (ensure_future calls create_task if given a coroutine)
# asyncio.ensure_future was introduced in Python 3.4.4, but fut = asyncio.ensure_future(awaitable)
# Debian jessie still ships with 3.4.2 so try the old name. # See comments on IOLoop._pending_tasks.
_wrap_awaitable = getattr(asyncio, "async") loop = IOLoop.current()
loop._register_task(fut)
fut.add_done_callback(lambda f: loop._unregister_task(f))
return fut
def convert_yielded(yielded: _Yieldable) -> Future: def convert_yielded(yielded: _Yieldable) -> Future:

View file

@ -74,7 +74,7 @@ class HTTPServer(TCPServer, Configurable, httputil.HTTPServerConnectionDelegate)
To make this server serve SSL traffic, send the ``ssl_options`` keyword To make this server serve SSL traffic, send the ``ssl_options`` keyword
argument with an `ssl.SSLContext` object. For compatibility with older argument with an `ssl.SSLContext` object. For compatibility with older
versions of Python ``ssl_options`` may also be a dictionary of keyword versions of Python ``ssl_options`` may also be a dictionary of keyword
arguments for the `ssl.wrap_socket` method.:: arguments for the `ssl.SSLContext.wrap_socket` method.::
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"), ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"),

View file

@ -856,7 +856,8 @@ def format_timestamp(
The argument may be a numeric timestamp as returned by `time.time`, The argument may be a numeric timestamp as returned by `time.time`,
a time tuple as returned by `time.gmtime`, or a `datetime.datetime` a time tuple as returned by `time.gmtime`, or a `datetime.datetime`
object. object. Naive `datetime.datetime` objects are assumed to represent
UTC; aware objects are converted to UTC before formatting.
>>> format_timestamp(1359312200) >>> format_timestamp(1359312200)
'Sun, 27 Jan 2013 18:43:20 GMT' 'Sun, 27 Jan 2013 18:43:20 GMT'

View file

@ -50,7 +50,7 @@ import typing
from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from typing import Dict, List # noqa: F401 from typing import Dict, List, Set # noqa: F401
from typing_extensions import Protocol from typing_extensions import Protocol
else: else:
@ -159,6 +159,18 @@ class IOLoop(Configurable):
# In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops. # In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops.
_ioloop_for_asyncio = dict() # type: Dict[asyncio.AbstractEventLoop, IOLoop] _ioloop_for_asyncio = dict() # type: Dict[asyncio.AbstractEventLoop, IOLoop]
# Maintain a set of all pending tasks to follow the warning in the docs
# of asyncio.create_tasks:
# https://docs.python.org/3.11/library/asyncio-task.html#asyncio.create_task
# This ensures that all pending tasks have a strong reference so they
# will not be garbage collected before they are finished.
# (Thus avoiding "task was destroyed but it is pending" warnings)
# An analogous change has been proposed in cpython for 3.13:
# https://github.com/python/cpython/issues/91887
# If that change is accepted, this can eventually be removed.
# If it is not, we will consider the rationale and may remove this.
_pending_tasks = set() # type: Set[Future]
@classmethod @classmethod
def configure( def configure(
cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any
@ -632,9 +644,6 @@ class IOLoop(Configurable):
other interaction with the `IOLoop` must be done from that other interaction with the `IOLoop` must be done from that
`IOLoop`'s thread. `add_callback()` may be used to transfer `IOLoop`'s thread. `add_callback()` may be used to transfer
control from other threads to the `IOLoop`'s thread. control from other threads to the `IOLoop`'s thread.
To add a callback from a signal handler, see
`add_callback_from_signal`.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -643,8 +652,13 @@ class IOLoop(Configurable):
) -> None: ) -> None:
"""Calls the given callback on the next I/O loop iteration. """Calls the given callback on the next I/O loop iteration.
Safe for use from a Python signal handler; should not be used Intended to be afe for use from a Python signal handler; should not be
otherwise. used otherwise.
.. deprecated:: 6.4
Use ``asyncio.AbstractEventLoop.add_signal_handler`` instead.
This method is suspected to have been broken since Tornado 5.0 and
will be removed in version 7.0.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -682,22 +696,20 @@ class IOLoop(Configurable):
# the error logging (i.e. it goes to tornado.log.app_log # the error logging (i.e. it goes to tornado.log.app_log
# instead of asyncio's log). # instead of asyncio's log).
future.add_done_callback( future.add_done_callback(
lambda f: self._run_callback(functools.partial(callback, future)) lambda f: self._run_callback(functools.partial(callback, f))
) )
else: else:
assert is_future(future) assert is_future(future)
# For concurrent futures, we use self.add_callback, so # For concurrent futures, we use self.add_callback, so
# it's fine if future_add_done_callback inlines that call. # it's fine if future_add_done_callback inlines that call.
future_add_done_callback( future_add_done_callback(future, lambda f: self.add_callback(callback, f))
future, lambda f: self.add_callback(callback, future)
)
def run_in_executor( def run_in_executor(
self, self,
executor: Optional[concurrent.futures.Executor], executor: Optional[concurrent.futures.Executor],
func: Callable[..., _T], func: Callable[..., _T],
*args: Any *args: Any
) -> Awaitable[_T]: ) -> "Future[_T]":
"""Runs a function in a ``concurrent.futures.Executor``. If """Runs a function in a ``concurrent.futures.Executor``. If
``executor`` is ``None``, the IO loop's default executor will be used. ``executor`` is ``None``, the IO loop's default executor will be used.
@ -803,6 +815,12 @@ class IOLoop(Configurable):
except OSError: except OSError:
pass pass
def _register_task(self, f: Future) -> None:
self._pending_tasks.add(f)
def _unregister_task(self, f: Future) -> None:
self._pending_tasks.discard(f)
class _Timeout(object): class _Timeout(object):
"""An IOLoop timeout, a UNIX timestamp and a callback""" """An IOLoop timeout, a UNIX timestamp and a callback"""

View file

@ -1219,7 +1219,7 @@ class IOStream(BaseIOStream):
The ``ssl_options`` argument may be either an `ssl.SSLContext` The ``ssl_options`` argument may be either an `ssl.SSLContext`
object or a dictionary of keyword arguments for the object or a dictionary of keyword arguments for the
`ssl.wrap_socket` function. The ``server_hostname`` argument `ssl.SSLContext.wrap_socket` function. The ``server_hostname`` argument
will be used for certificate validation unless disabled will be used for certificate validation unless disabled
in the ``ssl_options``. in the ``ssl_options``.
@ -1324,7 +1324,7 @@ class SSLIOStream(IOStream):
If the socket passed to the constructor is already connected, If the socket passed to the constructor is already connected,
it should be wrapped with:: it should be wrapped with::
ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs) ssl.SSLContext(...).wrap_socket(sock, do_handshake_on_connect=False, **kwargs)
before constructing the `SSLIOStream`. Unconnected sockets will be before constructing the `SSLIOStream`. Unconnected sockets will be
wrapped when `IOStream.connect` is finished. wrapped when `IOStream.connect` is finished.
@ -1335,7 +1335,7 @@ class SSLIOStream(IOStream):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
"""The ``ssl_options`` keyword argument may either be an """The ``ssl_options`` keyword argument may either be an
`ssl.SSLContext` object or a dictionary of keywords arguments `ssl.SSLContext` object or a dictionary of keywords arguments
for `ssl.wrap_socket` for `ssl.SSLContext.wrap_socket`
""" """
self._ssl_options = kwargs.pop("ssl_options", _client_ssl_defaults) self._ssl_options = kwargs.pop("ssl_options", _client_ssl_defaults)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -1413,9 +1413,9 @@ class SSLIOStream(IOStream):
return self.close(exc_info=err) return self.close(exc_info=err)
else: else:
self._ssl_accepting = False self._ssl_accepting = False
if not self._verify_cert(self.socket.getpeercert()): # Prior to the introduction of SNI, this is where we would check
self.close() # the server's claimed hostname.
return assert ssl.HAS_SNI
self._finish_ssl_connect() self._finish_ssl_connect()
def _finish_ssl_connect(self) -> None: def _finish_ssl_connect(self) -> None:
@ -1424,33 +1424,6 @@ class SSLIOStream(IOStream):
self._ssl_connect_future = None self._ssl_connect_future = None
future_set_result_unless_cancelled(future, self) future_set_result_unless_cancelled(future, self)
def _verify_cert(self, peercert: Any) -> bool:
"""Returns ``True`` if peercert is valid according to the configured
validation mode and hostname.
The ssl handshake already tested the certificate for a valid
CA signature; the only thing that remains is to check
the hostname.
"""
if isinstance(self._ssl_options, dict):
verify_mode = self._ssl_options.get("cert_reqs", ssl.CERT_NONE)
elif isinstance(self._ssl_options, ssl.SSLContext):
verify_mode = self._ssl_options.verify_mode
assert verify_mode in (ssl.CERT_NONE, ssl.CERT_REQUIRED, ssl.CERT_OPTIONAL)
if verify_mode == ssl.CERT_NONE or self._server_hostname is None:
return True
cert = self.socket.getpeercert()
if cert is None and verify_mode == ssl.CERT_REQUIRED:
gen_log.warning("No SSL certificate given")
return False
try:
ssl.match_hostname(peercert, self._server_hostname)
except ssl.CertificateError as e:
gen_log.warning("Invalid SSL certificate: %s" % e)
return False
else:
return True
def _handle_read(self) -> None: def _handle_read(self) -> None:
if self._ssl_accepting: if self._ssl_accepting:
self._do_ssl_handshake() self._do_ssl_handshake()

View file

@ -333,7 +333,7 @@ class Locale(object):
shorter: bool = False, shorter: bool = False,
full_format: bool = False, full_format: bool = False,
) -> str: ) -> str:
"""Formats the given date (which should be GMT). """Formats the given date.
By default, we return a relative time (e.g., "2 minutes ago"). You By default, we return a relative time (e.g., "2 minutes ago"). You
can return an absolute date string with ``relative=False``. can return an absolute date string with ``relative=False``.
@ -343,10 +343,16 @@ class Locale(object):
This method is primarily intended for dates in the past. This method is primarily intended for dates in the past.
For dates in the future, we fall back to full format. For dates in the future, we fall back to full format.
.. versionchanged:: 6.4
Aware `datetime.datetime` objects are now supported (naive
datetimes are still assumed to be UTC).
""" """
if isinstance(date, (int, float)): if isinstance(date, (int, float)):
date = datetime.datetime.utcfromtimestamp(date) date = datetime.datetime.fromtimestamp(date, datetime.timezone.utc)
now = datetime.datetime.utcnow() if date.tzinfo is None:
date = date.replace(tzinfo=datetime.timezone.utc)
now = datetime.datetime.now(datetime.timezone.utc)
if date > now: if date > now:
if relative and (date - now).seconds < 60: if relative and (date - now).seconds < 60:
# Due to click skew, things are some things slightly # Due to click skew, things are some things slightly

View file

@ -594,7 +594,7 @@ def ssl_options_to_context(
`~ssl.SSLContext` object. `~ssl.SSLContext` object.
The ``ssl_options`` dictionary contains keywords to be passed to The ``ssl_options`` dictionary contains keywords to be passed to
`ssl.wrap_socket`. In Python 2.7.9+, `ssl.SSLContext` objects can ``ssl.SSLContext.wrap_socket``. In Python 2.7.9+, `ssl.SSLContext` objects can
be used instead. This function converts the dict form to its be used instead. This function converts the dict form to its
`~ssl.SSLContext` equivalent, and may be used when a component which `~ssl.SSLContext` equivalent, and may be used when a component which
accepts both forms needs to upgrade to the `~ssl.SSLContext` version accepts both forms needs to upgrade to the `~ssl.SSLContext` version
@ -652,9 +652,7 @@ def ssl_wrap_socket(
``ssl_options`` may be either an `ssl.SSLContext` object or a ``ssl_options`` may be either an `ssl.SSLContext` object or a
dictionary (as accepted by `ssl_options_to_context`). Additional dictionary (as accepted by `ssl_options_to_context`). Additional
keyword arguments are passed to ``wrap_socket`` (either the keyword arguments are passed to `ssl.SSLContext.wrap_socket`.
`~ssl.SSLContext` method or the `ssl` module function as
appropriate).
.. versionchanged:: 6.2 .. versionchanged:: 6.2
@ -664,14 +662,10 @@ def ssl_wrap_socket(
context = ssl_options_to_context(ssl_options, server_side=server_side) context = ssl_options_to_context(ssl_options, server_side=server_side)
if server_side is None: if server_side is None:
server_side = False server_side = False
if ssl.HAS_SNI: assert ssl.HAS_SNI
# In python 3.4, wrap_socket only accepts the server_hostname # TODO: add a unittest for hostname validation (python added server-side SNI support in 3.4)
# argument if HAS_SNI is true.
# TODO: add a unittest (python added server-side SNI support in 3.4)
# In the meantime it can be manually tested with # In the meantime it can be manually tested with
# python3 -m tornado.httpclient https://sni.velox.ch # python3 -m tornado.httpclient https://sni.velox.ch
return context.wrap_socket( return context.wrap_socket(
socket, server_hostname=server_hostname, server_side=server_side, **kwargs socket, server_hostname=server_hostname, server_side=server_side, **kwargs
) )
else:
return context.wrap_socket(socket, server_side=server_side, **kwargs)

View file

@ -36,23 +36,32 @@ import warnings
from tornado.gen import convert_yielded from tornado.gen import convert_yielded
from tornado.ioloop import IOLoop, _Selectable from tornado.ioloop import IOLoop, _Selectable
from typing import Any, TypeVar, Awaitable, Callable, Union, Optional, List, Dict from typing import (
Any,
Callable,
Dict,
List,
Optional,
Protocol,
Set,
Tuple,
TypeVar,
Union,
)
if typing.TYPE_CHECKING:
from typing import Set, Tuple # noqa: F401
from typing_extensions import Protocol
class _HasFileno(Protocol): class _HasFileno(Protocol):
def fileno(self) -> int: def fileno(self) -> int:
pass pass
_FileDescriptorLike = Union[int, _HasFileno] _FileDescriptorLike = Union[int, _HasFileno]
_T = TypeVar("_T") _T = TypeVar("_T")
# Collection of selector thread event loops to shut down on exit. # Collection of selector thread event loops to shut down on exit.
_selector_loops = set() # type: Set[AddThreadSelectorEventLoop] _selector_loops: Set["SelectorThread"] = set()
def _atexit_callback() -> None: def _atexit_callback() -> None:
@ -64,6 +73,7 @@ def _atexit_callback() -> None:
loop._waker_w.send(b"a") loop._waker_w.send(b"a")
except BlockingIOError: except BlockingIOError:
pass pass
if loop._thread is not None:
# If we don't join our (daemon) thread here, we may get a deadlock # If we don't join our (daemon) thread here, we may get a deadlock
# during interpreter shutdown. I don't really understand why. This # during interpreter shutdown. I don't really understand why. This
# deadlock happens every time in CI (both travis and appveyor) but # deadlock happens every time in CI (both travis and appveyor) but
@ -87,16 +97,16 @@ class BaseAsyncIOLoop(IOLoop):
# as windows where the default event loop does not implement these methods. # as windows where the default event loop does not implement these methods.
self.selector_loop = asyncio_loop self.selector_loop = asyncio_loop
if hasattr(asyncio, "ProactorEventLoop") and isinstance( if hasattr(asyncio, "ProactorEventLoop") and isinstance(
asyncio_loop, asyncio.ProactorEventLoop # type: ignore asyncio_loop, asyncio.ProactorEventLoop
): ):
# Ignore this line for mypy because the abstract method checker # Ignore this line for mypy because the abstract method checker
# doesn't understand dynamic proxies. # doesn't understand dynamic proxies.
self.selector_loop = AddThreadSelectorEventLoop(asyncio_loop) # type: ignore self.selector_loop = AddThreadSelectorEventLoop(asyncio_loop) # type: ignore
# Maps fd to (fileobj, handler function) pair (as in IOLoop.add_handler) # Maps fd to (fileobj, handler function) pair (as in IOLoop.add_handler)
self.handlers = {} # type: Dict[int, Tuple[Union[int, _Selectable], Callable]] self.handlers: Dict[int, Tuple[Union[int, _Selectable], Callable]] = {}
# Set of fds listening for reads/writes # Set of fds listening for reads/writes
self.readers = set() # type: Set[int] self.readers: Set[int] = set()
self.writers = set() # type: Set[int] self.writers: Set[int] = set()
self.closing = False self.closing = False
# If an asyncio loop was closed through an asyncio interface # If an asyncio loop was closed through an asyncio interface
# instead of IOLoop.close(), we'd never hear about it and may # instead of IOLoop.close(), we'd never hear about it and may
@ -239,6 +249,7 @@ class BaseAsyncIOLoop(IOLoop):
def add_callback_from_signal( def add_callback_from_signal(
self, callback: Callable, *args: Any, **kwargs: Any self, callback: Callable, *args: Any, **kwargs: Any
) -> None: ) -> None:
warnings.warn("add_callback_from_signal is deprecated", DeprecationWarning)
try: try:
self.asyncio_loop.call_soon_threadsafe( self.asyncio_loop.call_soon_threadsafe(
self._run_callback, functools.partial(callback, *args, **kwargs) self._run_callback, functools.partial(callback, *args, **kwargs)
@ -251,7 +262,7 @@ class BaseAsyncIOLoop(IOLoop):
executor: Optional[concurrent.futures.Executor], executor: Optional[concurrent.futures.Executor],
func: Callable[..., _T], func: Callable[..., _T],
*args: Any, *args: Any,
) -> Awaitable[_T]: ) -> "asyncio.Future[_T]":
return self.asyncio_loop.run_in_executor(executor, func, *args) return self.asyncio_loop.run_in_executor(executor, func, *args)
def set_default_executor(self, executor: concurrent.futures.Executor) -> None: def set_default_executor(self, executor: concurrent.futures.Executor) -> None:
@ -417,87 +428,51 @@ class AnyThreadEventLoopPolicy(_BasePolicy): # type: ignore
def get_event_loop(self) -> asyncio.AbstractEventLoop: def get_event_loop(self) -> asyncio.AbstractEventLoop:
try: try:
return super().get_event_loop() return super().get_event_loop()
except (RuntimeError, AssertionError): except RuntimeError:
# This was an AssertionError in Python 3.4.2 (which ships with Debian Jessie)
# and changed to a RuntimeError in 3.4.3.
# "There is no current event loop in thread %r" # "There is no current event loop in thread %r"
loop = self.new_event_loop() loop = self.new_event_loop()
self.set_event_loop(loop) self.set_event_loop(loop)
return loop return loop
class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop): class SelectorThread:
"""Wrap an event loop to add implementations of the ``add_reader`` method family. """Define ``add_reader`` methods to be called in a background select thread.
Instances of this class start a second thread to run a selector. Instances of this class start a second thread to run a selector.
This thread is completely hidden from the user; all callbacks are This thread is completely hidden from the user;
run on the wrapped event loop's thread. all callbacks are run on the wrapped event loop's thread.
This class is used automatically by Tornado; applications should not need
to refer to it directly.
It is safe to wrap any event loop with this class, although it only makes sense
for event loops that do not implement the ``add_reader`` family of methods
themselves (i.e. ``WindowsProactorEventLoop``)
Closing the ``AddThreadSelectorEventLoop`` also closes the wrapped event loop.
Typically used via ``AddThreadSelectorEventLoop``,
but can be attached to a running asyncio loop.
""" """
# This class is a __getattribute__-based proxy. All attributes other than those _closed = False
# in this set are proxied through to the underlying loop.
MY_ATTRIBUTES = {
"_consume_waker",
"_select_cond",
"_select_args",
"_closing_selector",
"_thread",
"_handle_event",
"_readers",
"_real_loop",
"_start_select",
"_run_select",
"_handle_select",
"_wake_selector",
"_waker_r",
"_waker_w",
"_writers",
"add_reader",
"add_writer",
"close",
"remove_reader",
"remove_writer",
}
def __getattribute__(self, name: str) -> Any:
if name in AddThreadSelectorEventLoop.MY_ATTRIBUTES:
return super().__getattribute__(name)
return getattr(self._real_loop, name)
def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None: def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
self._real_loop = real_loop self._real_loop = real_loop
# Create a thread to run the select system call. We manage this thread
# manually so we can trigger a clean shutdown from an atexit hook. Note
# that due to the order of operations at shutdown, only daemon threads
# can be shut down in this way (non-daemon threads would require the
# introduction of a new hook: https://bugs.python.org/issue41962)
self._select_cond = threading.Condition() self._select_cond = threading.Condition()
self._select_args = ( self._select_args: Optional[
None Tuple[List[_FileDescriptorLike], List[_FileDescriptorLike]]
) # type: Optional[Tuple[List[_FileDescriptorLike], List[_FileDescriptorLike]]] ] = None
self._closing_selector = False self._closing_selector = False
self._thread = threading.Thread( self._thread: Optional[threading.Thread] = None
name="Tornado selector", self._thread_manager_handle = self._thread_manager()
daemon=True,
target=self._run_select,
)
self._thread.start()
# Start the select loop once the loop is started.
self._real_loop.call_soon(self._start_select)
self._readers = {} # type: Dict[_FileDescriptorLike, Callable] async def thread_manager_anext() -> None:
self._writers = {} # type: Dict[_FileDescriptorLike, Callable] # the anext builtin wasn't added until 3.10. We just need to iterate
# this generator one step.
await self._thread_manager_handle.__anext__()
# When the loop starts, start the thread. Not too soon because we can't
# clean up if we get to this point but the event loop is closed without
# starting.
self._real_loop.call_soon(
lambda: self._real_loop.create_task(thread_manager_anext())
)
self._readers: Dict[_FileDescriptorLike, Callable] = {}
self._writers: Dict[_FileDescriptorLike, Callable] = {}
# Writing to _waker_w will wake up the selector thread, which # Writing to _waker_w will wake up the selector thread, which
# watches for _waker_r to be readable. # watches for _waker_r to be readable.
@ -507,28 +482,49 @@ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
_selector_loops.add(self) _selector_loops.add(self)
self.add_reader(self._waker_r, self._consume_waker) self.add_reader(self._waker_r, self._consume_waker)
def __del__(self) -> None:
# If the top-level application code uses asyncio interfaces to
# start and stop the event loop, no objects created in Tornado
# can get a clean shutdown notification. If we're just left to
# be GC'd, we must explicitly close our sockets to avoid
# logging warnings.
_selector_loops.discard(self)
self._waker_r.close()
self._waker_w.close()
def close(self) -> None: def close(self) -> None:
if self._closed:
return
with self._select_cond: with self._select_cond:
self._closing_selector = True self._closing_selector = True
self._select_cond.notify() self._select_cond.notify()
self._wake_selector() self._wake_selector()
if self._thread is not None:
self._thread.join() self._thread.join()
_selector_loops.discard(self) _selector_loops.discard(self)
self.remove_reader(self._waker_r)
self._waker_r.close() self._waker_r.close()
self._waker_w.close() self._waker_w.close()
self._real_loop.close() self._closed = True
async def _thread_manager(self) -> typing.AsyncGenerator[None, None]:
# Create a thread to run the select system call. We manage this thread
# manually so we can trigger a clean shutdown from an atexit hook. Note
# that due to the order of operations at shutdown, only daemon threads
# can be shut down in this way (non-daemon threads would require the
# introduction of a new hook: https://bugs.python.org/issue41962)
self._thread = threading.Thread(
name="Tornado selector",
daemon=True,
target=self._run_select,
)
self._thread.start()
self._start_select()
try:
# The presense of this yield statement means that this coroutine
# is actually an asynchronous generator, which has a special
# shutdown protocol. We wait at this yield point until the
# event loop's shutdown_asyncgens method is called, at which point
# we will get a GeneratorExit exception and can shut down the
# selector thread.
yield
except GeneratorExit:
self.close()
raise
def _wake_selector(self) -> None: def _wake_selector(self) -> None:
if self._closed:
return
try: try:
self._waker_w.send(b"a") self._waker_w.send(b"a")
except BlockingIOError: except BlockingIOError:
@ -614,7 +610,7 @@ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
pass pass
def _handle_select( def _handle_select(
self, rs: List["_FileDescriptorLike"], ws: List["_FileDescriptorLike"] self, rs: List[_FileDescriptorLike], ws: List[_FileDescriptorLike]
) -> None: ) -> None:
for r in rs: for r in rs:
self._handle_event(r, self._readers) self._handle_event(r, self._readers)
@ -624,8 +620,8 @@ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
def _handle_event( def _handle_event(
self, self,
fd: "_FileDescriptorLike", fd: _FileDescriptorLike,
cb_map: Dict["_FileDescriptorLike", Callable], cb_map: Dict[_FileDescriptorLike, Callable],
) -> None: ) -> None:
try: try:
callback = cb_map[fd] callback = cb_map[fd]
@ -634,18 +630,18 @@ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
callback() callback()
def add_reader( def add_reader(
self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any self, fd: _FileDescriptorLike, callback: Callable[..., None], *args: Any
) -> None: ) -> None:
self._readers[fd] = functools.partial(callback, *args) self._readers[fd] = functools.partial(callback, *args)
self._wake_selector() self._wake_selector()
def add_writer( def add_writer(
self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any self, fd: _FileDescriptorLike, callback: Callable[..., None], *args: Any
) -> None: ) -> None:
self._writers[fd] = functools.partial(callback, *args) self._writers[fd] = functools.partial(callback, *args)
self._wake_selector() self._wake_selector()
def remove_reader(self, fd: "_FileDescriptorLike") -> bool: def remove_reader(self, fd: _FileDescriptorLike) -> bool:
try: try:
del self._readers[fd] del self._readers[fd]
except KeyError: except KeyError:
@ -653,10 +649,70 @@ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
self._wake_selector() self._wake_selector()
return True return True
def remove_writer(self, fd: "_FileDescriptorLike") -> bool: def remove_writer(self, fd: _FileDescriptorLike) -> bool:
try: try:
del self._writers[fd] del self._writers[fd]
except KeyError: except KeyError:
return False return False
self._wake_selector() self._wake_selector()
return True return True
class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
"""Wrap an event loop to add implementations of the ``add_reader`` method family.
Instances of this class start a second thread to run a selector.
This thread is completely hidden from the user; all callbacks are
run on the wrapped event loop's thread.
This class is used automatically by Tornado; applications should not need
to refer to it directly.
It is safe to wrap any event loop with this class, although it only makes sense
for event loops that do not implement the ``add_reader`` family of methods
themselves (i.e. ``WindowsProactorEventLoop``)
Closing the ``AddThreadSelectorEventLoop`` also closes the wrapped event loop.
"""
# This class is a __getattribute__-based proxy. All attributes other than those
# in this set are proxied through to the underlying loop.
MY_ATTRIBUTES = {
"_real_loop",
"_selector",
"add_reader",
"add_writer",
"close",
"remove_reader",
"remove_writer",
}
def __getattribute__(self, name: str) -> Any:
if name in AddThreadSelectorEventLoop.MY_ATTRIBUTES:
return super().__getattribute__(name)
return getattr(self._real_loop, name)
def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
self._real_loop = real_loop
self._selector = SelectorThread(real_loop)
def close(self) -> None:
self._selector.close()
self._real_loop.close()
def add_reader(
self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any
) -> None:
return self._selector.add_reader(fd, callback, *args)
def add_writer(
self, fd: "_FileDescriptorLike", callback: Callable[..., None], *args: Any
) -> None:
return self._selector.add_writer(fd, callback, *args)
def remove_reader(self, fd: "_FileDescriptorLike") -> bool:
return self._selector.remove_reader(fd)
def remove_writer(self, fd: "_FileDescriptorLike") -> bool:
return self._selector.remove_writer(fd)

View file

@ -17,6 +17,7 @@
the server into multiple processes and managing subprocesses. the server into multiple processes and managing subprocesses.
""" """
import asyncio
import os import os
import multiprocessing import multiprocessing
import signal import signal
@ -210,7 +211,6 @@ class Subprocess(object):
_initialized = False _initialized = False
_waiting = {} # type: ignore _waiting = {} # type: ignore
_old_sigchld = None
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self.io_loop = ioloop.IOLoop.current() self.io_loop = ioloop.IOLoop.current()
@ -322,11 +322,8 @@ class Subprocess(object):
""" """
if cls._initialized: if cls._initialized:
return return
io_loop = ioloop.IOLoop.current() loop = asyncio.get_event_loop()
cls._old_sigchld = signal.signal( loop.add_signal_handler(signal.SIGCHLD, cls._cleanup)
signal.SIGCHLD,
lambda sig, frame: io_loop.add_callback_from_signal(cls._cleanup),
)
cls._initialized = True cls._initialized = True
@classmethod @classmethod
@ -334,7 +331,8 @@ class Subprocess(object):
"""Removes the ``SIGCHLD`` handler.""" """Removes the ``SIGCHLD`` handler."""
if not cls._initialized: if not cls._initialized:
return return
signal.signal(signal.SIGCHLD, cls._old_sigchld) loop = asyncio.get_event_loop()
loop.remove_signal_handler(signal.SIGCHLD)
cls._initialized = False cls._initialized = False
@classmethod @classmethod
@ -352,7 +350,7 @@ class Subprocess(object):
return return
assert ret_pid == pid assert ret_pid == pid
subproc = cls._waiting.pop(pid) subproc = cls._waiting.pop(pid)
subproc.io_loop.add_callback_from_signal(subproc._set_returncode, status) subproc.io_loop.add_callback(subproc._set_returncode, status)
def _set_returncode(self, status: int) -> None: def _set_returncode(self, status: int) -> None:
if sys.platform == "win32": if sys.platform == "win32":

View file

@ -61,7 +61,7 @@ class TCPServer(object):
To make this server serve SSL traffic, send the ``ssl_options`` keyword To make this server serve SSL traffic, send the ``ssl_options`` keyword
argument with an `ssl.SSLContext` object. For compatibility with older argument with an `ssl.SSLContext` object. For compatibility with older
versions of Python ``ssl_options`` may also be a dictionary of keyword versions of Python ``ssl_options`` may also be a dictionary of keyword
arguments for the `ssl.wrap_socket` method.:: arguments for the `ssl.SSLContext.wrap_socket` method.::
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"), ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"),

View file

@ -206,10 +206,7 @@ class AsyncTestCase(unittest.TestCase):
# this always happens in tests, so cancel any tasks that are # this always happens in tests, so cancel any tasks that are
# still pending by the time we get here. # still pending by the time we get here.
asyncio_loop = self.io_loop.asyncio_loop # type: ignore asyncio_loop = self.io_loop.asyncio_loop # type: ignore
if hasattr(asyncio, "all_tasks"): # py37 tasks = asyncio.all_tasks(asyncio_loop)
tasks = asyncio.all_tasks(asyncio_loop) # type: ignore
else:
tasks = asyncio.Task.all_tasks(asyncio_loop)
# Tasks that are done may still appear here and may contain # Tasks that are done may still appear here and may contain
# non-cancellation exceptions, so filter them out. # non-cancellation exceptions, so filter them out.
tasks = [t for t in tasks if not t.done()] # type: ignore tasks = [t for t in tasks if not t.done()] # type: ignore
@ -520,7 +517,9 @@ class AsyncHTTPSTestCase(AsyncHTTPTestCase):
def default_ssl_options() -> Dict[str, Any]: def default_ssl_options() -> Dict[str, Any]:
# Testing keys were generated with: # Testing keys were generated with:
# openssl req -new -keyout tornado/test/test.key \ # openssl req -new -keyout tornado/test/test.key \
# -out tornado/test/test.crt -nodes -days 3650 -x509 # -out tornado/test/test.crt \
# -nodes -days 3650 -x509 \
# -subj "/CN=foo.example.com" -addext "subjectAltName = DNS:foo.example.com"
module_dir = os.path.dirname(__file__) module_dir = os.path.dirname(__file__)
return dict( return dict(
certfile=os.path.join(module_dir, "test", "test.crt"), certfile=os.path.join(module_dir, "test", "test.crt"),

View file

@ -647,7 +647,9 @@ class RequestHandler(object):
if domain: if domain:
morsel["domain"] = domain morsel["domain"] = domain
if expires_days is not None and not expires: if expires_days is not None and not expires:
expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days) expires = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=expires_days
)
if expires: if expires:
morsel["expires"] = httputil.format_timestamp(expires) morsel["expires"] = httputil.format_timestamp(expires)
if path: if path:
@ -698,7 +700,9 @@ class RequestHandler(object):
raise TypeError( raise TypeError(
f"clear_cookie() got an unexpected keyword argument '{excluded_arg}'" f"clear_cookie() got an unexpected keyword argument '{excluded_arg}'"
) )
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) expires = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
days=365
)
self.set_cookie(name, value="", expires=expires, **kwargs) self.set_cookie(name, value="", expires=expires, **kwargs)
def clear_all_cookies(self, **kwargs: Any) -> None: def clear_all_cookies(self, **kwargs: Any) -> None:
@ -2793,7 +2797,8 @@ class StaticFileHandler(RequestHandler):
if cache_time > 0: if cache_time > 0:
self.set_header( self.set_header(
"Expires", "Expires",
datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time), datetime.datetime.now(datetime.timezone.utc)
+ datetime.timedelta(seconds=cache_time),
) )
self.set_header("Cache-Control", "max-age=" + str(cache_time)) self.set_header("Cache-Control", "max-age=" + str(cache_time))
@ -2812,9 +2817,9 @@ class StaticFileHandler(RequestHandler):
# content has not been modified # content has not been modified
ims_value = self.request.headers.get("If-Modified-Since") ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None: if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value) if_since = email.utils.parsedate_to_datetime(ims_value)
if date_tuple is not None: if if_since.tzinfo is None:
if_since = datetime.datetime(*date_tuple[:6]) if_since = if_since.replace(tzinfo=datetime.timezone.utc)
assert self.modified is not None assert self.modified is not None
if if_since >= self.modified: if if_since >= self.modified:
return True return True
@ -2981,6 +2986,10 @@ class StaticFileHandler(RequestHandler):
object or None. object or None.
.. versionadded:: 3.1 .. versionadded:: 3.1
.. versionchanged:: 6.4
Now returns an aware datetime object instead of a naive one.
Subclasses that override this method may return either kind.
""" """
stat_result = self._stat() stat_result = self._stat()
# NOTE: Historically, this used stat_result[stat.ST_MTIME], # NOTE: Historically, this used stat_result[stat.ST_MTIME],
@ -2991,7 +3000,9 @@ class StaticFileHandler(RequestHandler):
# consistency with the past (and because we have a unit test # consistency with the past (and because we have a unit test
# that relies on this), we truncate the float here, although # that relies on this), we truncate the float here, although
# I'm not sure that's the right thing to do. # I'm not sure that's the right thing to do.
modified = datetime.datetime.utcfromtimestamp(int(stat_result.st_mtime)) modified = datetime.datetime.fromtimestamp(
int(stat_result.st_mtime), datetime.timezone.utc
)
return modified return modified
def get_content_type(self) -> str: def get_content_type(self) -> str:
@ -3125,7 +3136,7 @@ class FallbackHandler(RequestHandler):
django.core.handlers.wsgi.WSGIHandler()) django.core.handlers.wsgi.WSGIHandler())
application = tornado.web.Application([ application = tornado.web.Application([
(r"/foo", FooHandler), (r"/foo", FooHandler),
(r".*", FallbackHandler, dict(fallback=wsgi_app), (r".*", FallbackHandler, dict(fallback=wsgi_app)),
]) ])
""" """

View file

@ -20,6 +20,7 @@ import sys
import struct import struct
import tornado import tornado
from urllib.parse import urlparse from urllib.parse import urlparse
import warnings
import zlib import zlib
from tornado.concurrent import Future, future_set_result_unless_cancelled from tornado.concurrent import Future, future_set_result_unless_cancelled
@ -1356,7 +1357,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
ping_interval: Optional[float] = None, ping_interval: Optional[float] = None,
ping_timeout: Optional[float] = None, ping_timeout: Optional[float] = None,
max_message_size: int = _default_max_message_size, max_message_size: int = _default_max_message_size,
subprotocols: Optional[List[str]] = [], subprotocols: Optional[List[str]] = None,
resolver: Optional[Resolver] = None, resolver: Optional[Resolver] = None,
) -> None: ) -> None:
self.connect_future = Future() # type: Future[WebSocketClientConnection] self.connect_future = Future() # type: Future[WebSocketClientConnection]
@ -1410,6 +1411,15 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
104857600, 104857600,
) )
def __del__(self) -> None:
if self.protocol is not None:
# Unclosed client connections can sometimes log "task was destroyed but
# was pending" warnings if shutdown strikes at the wrong time (such as
# while a ping is being processed due to ping_interval). Log our own
# warning to make it a little more deterministic (although it's still
# dependent on GC timing).
warnings.warn("Unclosed WebSocketClientConnection", ResourceWarning)
def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None: def close(self, code: Optional[int] = None, reason: Optional[str] = None) -> None:
"""Closes the websocket connection. """Closes the websocket connection.