Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-tornado6 for openSUSE:Factory
checked in at 2025-12-20 21:45:03
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-tornado6 (Old)
and /work/SRC/openSUSE:Factory/.python-tornado6.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tornado6"
Sat Dec 20 21:45:03 2025 rev:21 rq:1323582 version:6.5.4
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-tornado6/python-tornado6.changes
2025-05-23 14:27:30.792860059 +0200
+++
/work/SRC/openSUSE:Factory/.python-tornado6.new.1928/python-tornado6.changes
2025-12-20 21:45:08.635851013 +0100
@@ -1,0 +2,45 @@
+Tue Dec 16 13:42:10 UTC 2025 - Nico Krapp <[email protected]>
+
+- Update to 6.5.4
+ * The in operator for HTTPHeaders was incorrectly case-sensitive, causing
+ lookups to fail for headers with different casing than the original header
+ name. This was a regression in version 6.5.3 and has been fixed to restore
+ the intended case-insensitive behavior from version 6.5.2 and earlier.
+- Update to 6.5.3 (bsc#1254903, bsc#1254905, bsc#1254904)
+ * Fixed a denial-of-service vulnerability involving quadratic computation
+ when parsing multipart/form-data request bodies. CVE-2025-67726
+ Thanks to Finder16 for reporting this issue.
+ * Fixed a denial-of-service vulnerability involving quadratic computation
when
+ parsing repeated HTTP headers. CVE-2025-67725.
+ Thanks to Finder16 for reporting this issue.
+ * Fixed a header injection and XSS vulnerability involving the reason
argument
+ to .RequestHandler.set_status and tornado.web.HTTPError. CVE-2025-67724.
+ Thanks to Finder16 and Cheshire1225 for reporting this issue.
+ * Several demo applications bundled with the Tornado repo (blog, chat,
+ facebook) had an open redirect vulnerability which has been fixed. This is
+ not covered by a CVE or security advisory since the demo applications are
+ not included as a part of the Tornado package when installed, but
developers
+ who have copied code from these demos may which to review their own
+ applications for open redirects.
+ Thanks to J1vvoo for reporting this issue.
+ * he s3server demo application contained some path traversal vulnerabilities.
+ Since this demo application was not demonstrating any interesting aspects
of
+ Tornado, it has been deleted rather than being fixed.
+ Thanks to J1vvoo for reporting this issue.
+- Update to 6.5.2
+ * Fixed a bug that resulted in WebSocket pings not being sent at the
+ configured interval.
+ * Improved logging for invalid Host headers. This was previously logged as an
+ uncaught exception with a stack trace, now it is simply a 400 response
+ (logged as a warning in the access log).
+ * Restored the host argument to .HTTPServerRequest. This argument is
+ deprecated and will be removed in the future, but its removal with no
+ warning in 6.5.0 was a mistake.
+ * Removed a debugging print statement that was left in the code.
+ * Improved type hints for gen.multi.
+- Update to 6.5.1
+ * Fixed a bug in multipart/form-data parsing that could incorrectly reject
+ filenames containing characters above U+00FF (i.e. most characters outside
+ the Latin alphabet).
+
+-------------------------------------------------------------------
Old:
----
tornado-6.5.tar.gz
New:
----
tornado-6.5.4.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-tornado6.spec ++++++
--- /var/tmp/diff_new_pack.DTQR1G/_old 2025-12-20 21:45:10.427925160 +0100
+++ /var/tmp/diff_new_pack.DTQR1G/_new 2025-12-20 21:45:10.439925657 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-tornado6
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-tornado6
-Version: 6.5
+Version: 6.5.4
Release: 0
Summary: Open source version of scalable, non-blocking web server that
power FriendFeed
License: Apache-2.0
++++++ tornado-6.5.tar.gz -> tornado-6.5.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/PKG-INFO new/tornado-6.5.4/PKG-INFO
--- old/tornado-6.5/PKG-INFO 2025-05-15 22:19:11.890474600 +0200
+++ new/tornado-6.5.4/PKG-INFO 2025-12-15 19:43:01.912789000 +0100
@@ -1,13 +1,12 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: tornado
-Version: 6.5
+Version: 6.5.4
Summary: Tornado is a Python web framework and asynchronous networking
library, originally developed at FriendFeed.
Home-page: http://www.tornadoweb.org/
Author: Facebook
Author-email: [email protected]
License: Apache-2.0
Project-URL: Source, https://github.com/tornadoweb/tornado
-Platform: UNKNOWN
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
@@ -20,6 +19,17 @@
Requires-Python: >= 3.9
Description-Content-Type: text/x-rst
License-File: LICENSE
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: project-url
+Dynamic: requires-python
+Dynamic: summary
Tornado Web Server
==================
@@ -72,5 +82,3 @@
Documentation and links to additional resources are available at
https://www.tornadoweb.org
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/caresresolver.rst
new/tornado-6.5.4/docs/caresresolver.rst
--- old/tornado-6.5/docs/caresresolver.rst 2025-05-15 22:19:08.000000000
+0200
+++ new/tornado-6.5.4/docs/caresresolver.rst 2025-12-15 19:42:58.000000000
+0100
@@ -19,6 +19,9 @@
the default for ``tornado.simple_httpclient``, but other libraries
may default to ``AF_UNSPEC``.
+ This class requires ``pycares`` version 4. Since this class is deprecated,
it will not be
+ updated to support ``pycares`` version 5.
+
.. deprecated:: 6.2
This class is deprecated and will be removed in Tornado 7.0. Use the
default
thread-based resolver instead.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/releases/v6.5.1.rst
new/tornado-6.5.4/docs/releases/v6.5.1.rst
--- old/tornado-6.5/docs/releases/v6.5.1.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/docs/releases/v6.5.1.rst 2025-12-15 19:42:58.000000000
+0100
@@ -0,0 +1,11 @@
+What's new in Tornado 6.5.1
+===========================
+
+May 22, 2025
+------------
+
+Bug fixes
+~~~~~~~~~
+
+- Fixed a bug in ``multipart/form-data`` parsing that could incorrectly reject
filenames containing
+ characters above U+00FF (i.e. most characters outside the Latin alphabet).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/releases/v6.5.2.rst
new/tornado-6.5.4/docs/releases/v6.5.2.rst
--- old/tornado-6.5/docs/releases/v6.5.2.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/docs/releases/v6.5.2.rst 2025-12-15 19:42:58.000000000
+0100
@@ -0,0 +1,17 @@
+What's new in Tornado 6.5.2
+===========================
+
+Aug 8, 2025
+-----------
+
+Bug fixes
+~~~~~~~~~
+
+- Fixed a bug that resulted in WebSocket pings not being sent at the
configured interval.
+- Improved logging for invalid ``Host`` headers. This was previously logged as
an uncaught
+ exception with a stack trace, now it is simply a 400 response (logged as a
warning in the
+ access log).
+- Restored the ``host`` argument to `.HTTPServerRequest`. This argument is
deprecated
+ and will be removed in the future, but its removal with no warning in 6.5.0
was a mistake.
+- Removed a debugging print statement that was left in the code.
+- Improved type hints for ``gen.multi``.
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/releases/v6.5.3.rst
new/tornado-6.5.4/docs/releases/v6.5.3.rst
--- old/tornado-6.5/docs/releases/v6.5.3.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/docs/releases/v6.5.3.rst 2025-12-15 19:42:58.000000000
+0100
@@ -0,0 +1,33 @@
+What's new in Tornado 6.5.3
+===========================
+
+Dec 10, 2025
+------------
+
+Security fixes
+~~~~~~~~~~~~~~
+- Fixed a denial-of-service vulnerability involving quadratic computation when
parsing
+ ``multipart/form-data`` request bodies.
+ `CVE-2025-67726
<https://github.com/tornadoweb/tornado/security/advisories/GHSA-jhmp-mqwm-3gq8>`_
+ Thanks to `Finder16 <https://github.com/Finder16>`_ for reporting this issue.
+- Fixed a denial-of-service vulnerability involving quadratic computation when
parsing repeated HTTP
+ headers.
+ `CVE-2025-67725
<https://github.com/tornadoweb/tornado/security/advisories/GHSA-c98p-7wgm-6p64>`_.
+ Thanks to `Finder16 <https://github.com/Finder16>`_ for reporting this issue.
+- Fixed a header injection and XSS vulnerability involving the ``reason``
argument to
+ `.RequestHandler.set_status` and `tornado.web.HTTPError`.
+ `CVE-2025-67724
<https://github.com/tornadoweb/tornado/security/advisories/GHSA-pr2v-jx2c-wg9f>`_.
+ Thanks to `Finder16 <https://github.com/Finder16>`_ and
+ `Cheshire1225 <https://github.com/Cheshire1225>`_ for reporting this issue.
+
+Demo changes
+~~~~~~~~~~~~
+- Several demo applications bundled with the Tornado repo (``blog``, ``chat``,
``facebook``) had an
+ open redirect vulnerability which has been fixed. This is not covered by a
CVE or security
+ advisory since the demo applications are not included as a part of the
Tornado package when
+ installed, but developers who have copied code from these demos may which to
review their own
+ applications for open redirects. Thanks to `J1vvoo
<https://github.com/J1vvoo>`_ for reporting this
+ issue.
+- The ``s3server`` demo application contained some path traversal
vulnerabilities. Since this demo
+ application was not demonstrating any interesting aspects of Tornado, it has
been deleted rather
+ than being fixed. Thanks to `J1vvoo <https://github.com/J1vvoo>`_ for
reporting this issue.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/releases/v6.5.4.rst
new/tornado-6.5.4/docs/releases/v6.5.4.rst
--- old/tornado-6.5/docs/releases/v6.5.4.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/docs/releases/v6.5.4.rst 2025-12-15 19:42:58.000000000
+0100
@@ -0,0 +1,13 @@
+What's new in Tornado 6.5.4
+===========================
+
+Dec 15, 2025
+------------
+
+Bug fixes
+~~~~~~~~~
+
+- The ``in`` operator for ``HTTPHeaders`` was incorrectly case-sensitive,
causing
+ lookups to fail for headers with different casing than the original header
name.
+ This was a regression in version 6.5.3 and has been fixed to restore the
intended
+ case-insensitive behavior from version 6.5.2 and earlier.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/releases.rst
new/tornado-6.5.4/docs/releases.rst
--- old/tornado-6.5/docs/releases.rst 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/docs/releases.rst 2025-12-15 19:42:58.000000000 +0100
@@ -4,6 +4,10 @@
.. toctree::
:maxdepth: 2
+ releases/v6.5.4
+ releases/v6.5.3
+ releases/v6.5.2
+ releases/v6.5.1
releases/v6.5.0
releases/v6.4.2
releases/v6.4.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/docs/web.rst
new/tornado-6.5.4/docs/web.rst
--- old/tornado-6.5/docs/web.rst 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/docs/web.rst 2025-12-15 19:42:58.000000000 +0100
@@ -224,14 +224,22 @@
of `UIModule` or UI methods to be made available to templates.
May be set to a module, dictionary, or a list of modules
and/or dicts. See :ref:`ui-modules` for more details.
- * ``websocket_ping_interval``: If set to a number, all websockets will
- be pinged every n seconds. This can help keep the connection alive
- through certain proxy servers which close idle connections, and it
- can detect if the websocket has failed without being properly
closed.
- * ``websocket_ping_timeout``: If the ping interval is set, and the
- server doesn't receive a 'pong' in this many seconds, it will close
- the websocket. The default is three times the ping interval, with a
- minimum of 30 seconds. Ignored if the ping interval is not set.
+ * ``websocket_ping_interval``: If the ping interval has a non-zero
+ value, a ping will be sent periodically every
+ ``websocket_ping_interval`` seconds, and the connection will be
+ closed if a response is not received before the
+ ``websocket_ping_timeout``.
+ This can help keep the connection alive through certain proxy
+ servers which close idle connections, and it can detect if the
+ websocket has failed without being properly closed.
+ * ``websocket_ping_timeout``: For use with
``websocket_ping_interval``,
+ if the server does not receive a pong within this many seconds, it
+ will close the websocket_ping_timeout.
+ The default timeout is equal to the ping interval. The ping timeout
+ will be turned off if the ping interval is not set or if the
+ timeout is set to ``0``.
+ This can help to detect disconnected clients to avoid keeping
+ inactive connections open.
Authentication and security settings:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/__init__.py
new/tornado-6.5.4/tornado/__init__.py
--- old/tornado-6.5/tornado/__init__.py 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/__init__.py 2025-12-15 19:42:59.000000000
+0100
@@ -22,8 +22,8 @@
# is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version
# number has been incremented)
-version = "6.5"
-version_info = (6, 5, 0, 0)
+version = "6.5.4"
+version_info = (6, 5, 4, 0)
import importlib
import typing
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/__init__.pyi
new/tornado-6.5.4/tornado/__init__.pyi
--- old/tornado-6.5/tornado/__init__.pyi 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/tornado/__init__.pyi 2025-12-15 19:42:59.000000000
+0100
@@ -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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/gen.py
new/tornado-6.5.4/tornado/gen.py
--- old/tornado-6.5/tornado/gen.py 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/gen.py 2025-12-15 19:42:59.000000000 +0100
@@ -437,6 +437,20 @@
return self.next()
+@overload
+def multi(
+ children: Sequence[_Yieldable],
+ quiet_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (),
+) -> Future[List]: ...
+
+
+@overload
+def multi(
+ children: Mapping[Any, _Yieldable],
+ quiet_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (),
+) -> Future[Dict]: ...
+
+
def multi(
children: Union[Sequence[_Yieldable], Mapping[Any, _Yieldable]],
quiet_exceptions: "Union[Type[Exception], Tuple[Type[Exception], ...]]" =
(),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/http1connection.py
new/tornado-6.5.4/tornado/http1connection.py
--- old/tornado-6.5/tornado/http1connection.py 2025-05-15 22:19:08.000000000
+0200
+++ new/tornado-6.5.4/tornado/http1connection.py 2025-12-15
19:42:59.000000000 +0100
@@ -66,6 +66,9 @@
) -> None:
if value is not None:
assert typ is not None
+ # Let HTTPInputError pass through to higher-level handler
+ if isinstance(value, httputil.HTTPInputError):
+ return None
self.logger.error("Uncaught exception", exc_info=(typ, value, tb))
raise _QuietException
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/httputil.py
new/tornado-6.5.4/tornado/httputil.py
--- old/tornado-6.5/tornado/httputil.py 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/httputil.py 2025-12-15 19:42:59.000000000
+0100
@@ -70,6 +70,10 @@
# To be used with str.strip() and related methods.
HTTP_WHITESPACE = " \t"
+# Roughly the inverse of RequestHandler._VALID_HEADER_CHARS, but permits
+# chars greater than \xFF (which may appear after decoding utf8).
+_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]")
+
class _ABNF:
"""Class that holds a subset of ABNF rules from RFC 9110 and friends.
@@ -183,8 +187,14 @@
pass
def __init__(self, *args: typing.Any, **kwargs: str) -> None: # noqa: F811
- self._dict = {} # type: typing.Dict[str, str]
- self._as_list = {} # type: typing.Dict[str, typing.List[str]]
+ # Formally, HTTP headers are a mapping from a field name to a
"combined field value",
+ # which may be constructed from multiple field lines by joining them
with commas.
+ # In practice, however, some headers (notably Set-Cookie) do not
follow this convention,
+ # so we maintain a mapping from field name to a list of field lines in
self._as_list.
+ # self._combined_cache is a cache of the combined field values derived
from self._as_list
+ # on demand (and cleared whenever the list is modified).
+ self._as_list: dict[str, list[str]] = {}
+ self._combined_cache: dict[str, str] = {}
self._last_key = None # type: Optional[str]
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0],
HTTPHeaders):
# Copy constructor
@@ -196,20 +206,22 @@
# new public methods
- def add(self, name: str, value: str) -> None:
+ def add(self, name: str, value: str, *, _chars_are_bytes: bool = True) ->
None:
"""Adds a new value for the given key."""
if not _ABNF.field_name.fullmatch(name):
raise HTTPInputError("Invalid header name %r" % name)
- if not _ABNF.field_value.fullmatch(to_unicode(value)):
- # TODO: the fact we still support bytes here (contrary to type
annotations)
- # and still test for it should probably be changed.
- raise HTTPInputError("Invalid header value %r" % value)
+ if _chars_are_bytes:
+ if not _ABNF.field_value.fullmatch(to_unicode(value)):
+ # TODO: the fact we still support bytes here (contrary to type
annotations)
+ # and still test for it should probably be changed.
+ raise HTTPInputError("Invalid header value %r" % value)
+ else:
+ if _FORBIDDEN_HEADER_CHARS_RE.search(value):
+ raise HTTPInputError("Invalid header value %r" % value)
norm_name = _normalize_header(name)
self._last_key = norm_name
if norm_name in self:
- self._dict[norm_name] = (
- native_str(self[norm_name]) + "," + native_str(value)
- )
+ self._combined_cache.pop(norm_name, None)
self._as_list[norm_name].append(value)
else:
self[norm_name] = value
@@ -229,7 +241,7 @@
for value in values:
yield (name, value)
- def parse_line(self, line: str) -> None:
+ def parse_line(self, line: str, *, _chars_are_bytes: bool = True) -> None:
r"""Updates the dictionary with a single header line.
>>> h = HTTPHeaders()
@@ -263,19 +275,25 @@
if self._last_key is None:
raise HTTPInputError("first header line cannot start with
whitespace")
new_part = " " + line.strip(HTTP_WHITESPACE)
- if not _ABNF.field_value.fullmatch(new_part[1:]):
- raise HTTPInputError("Invalid header continuation %r" %
new_part)
+ if _chars_are_bytes:
+ if not _ABNF.field_value.fullmatch(new_part[1:]):
+ raise HTTPInputError("Invalid header continuation %r" %
new_part)
+ else:
+ if _FORBIDDEN_HEADER_CHARS_RE.search(new_part):
+ raise HTTPInputError("Invalid header value %r" % new_part)
self._as_list[self._last_key][-1] += new_part
- self._dict[self._last_key] += new_part
+ self._combined_cache.pop(self._last_key, None)
else:
try:
name, value = line.split(":", 1)
except ValueError:
raise HTTPInputError("no colon in header line")
- self.add(name, value.strip(HTTP_WHITESPACE))
+ self.add(
+ name, value.strip(HTTP_WHITESPACE),
_chars_are_bytes=_chars_are_bytes
+ )
@classmethod
- def parse(cls, headers: str) -> "HTTPHeaders":
+ def parse(cls, headers: str, *, _chars_are_bytes: bool = True) ->
"HTTPHeaders":
"""Returns a dictionary from HTTP header text.
>>> h = HTTPHeaders.parse("Content-Type:
text/html\\r\\nContent-Length: 42\\r\\n")
@@ -288,39 +306,64 @@
mix of `KeyError`, and `ValueError`.
"""
+ # _chars_are_bytes is a hack. This method is used in two places, HTTP
headers (in which
+ # non-ascii characters are to be interpreted as latin-1) and
multipart/form-data (in which
+ # they are to be interpreted as utf-8). For historical reasons, this
method handled this by
+ # expecting both callers to decode the headers to strings before
parsing them. This wasn't a
+ # problem until we started doing stricter validation of the characters
allowed in HTTP
+ # headers (using ABNF rules defined in terms of byte values), which
inadvertently started
+ # disallowing non-latin1 characters in multipart/form-data filenames.
+ #
+ # This method should have accepted bytes and a desired encoding, but
this change is being
+ # introduced in a patch release that shouldn't change the API.
Instead, the _chars_are_bytes
+ # flag decides whether to use HTTP-style ABNF validation (treating the
string as bytes
+ # smuggled through the latin1 encoding) or to accept any non-control
unicode characters
+ # as required by multipart/form-data. This method will change to
accept bytes in a future
+ # release.
h = cls()
start = 0
while True:
lf = headers.find("\n", start)
if lf == -1:
- h.parse_line(headers[start:])
+ h.parse_line(headers[start:],
_chars_are_bytes=_chars_are_bytes)
break
line = headers[start : lf + 1]
start = lf + 1
- h.parse_line(line)
+ h.parse_line(line, _chars_are_bytes=_chars_are_bytes)
return h
# MutableMapping abstract method implementations.
def __setitem__(self, name: str, value: str) -> None:
norm_name = _normalize_header(name)
- self._dict[norm_name] = value
+ self._combined_cache[norm_name] = value
self._as_list[norm_name] = [value]
+ def __contains__(self, name: object) -> bool:
+ # This is an important optimization to avoid the expensive
concatenation
+ # in __getitem__ when it's not needed.
+ if not isinstance(name, str):
+ return False
+ norm_name = _normalize_header(name)
+ return norm_name in self._as_list
+
def __getitem__(self, name: str) -> str:
- return self._dict[_normalize_header(name)]
+ header = _normalize_header(name)
+ if header not in self._combined_cache:
+ self._combined_cache[header] = ",".join(self._as_list[header])
+ return self._combined_cache[header]
def __delitem__(self, name: str) -> None:
norm_name = _normalize_header(name)
- del self._dict[norm_name]
+ del self._combined_cache[norm_name]
del self._as_list[norm_name]
def __len__(self) -> int:
- return len(self._dict)
+ return len(self._as_list)
def __iter__(self) -> Iterator[typing.Any]:
- return iter(self._dict)
+ return iter(self._as_list)
def copy(self) -> "HTTPHeaders":
# defined in dict but not in MutableMapping.
@@ -431,6 +474,11 @@
.. versionchanged:: 4.0
Moved from ``tornado.httpserver.HTTPRequest``.
+
+ .. deprecated:: 6.5.2
+ The ``host`` argument to the ``HTTPServerRequest`` constructor is
deprecated. Use
+ ``headers["Host"]`` instead. This argument was mistakenly removed in
Tornado 6.5.0 and
+ temporarily restored in 6.5.2.
"""
path = None # type: str
@@ -446,7 +494,7 @@
version: str = "HTTP/1.0",
headers: Optional[HTTPHeaders] = None,
body: Optional[bytes] = None,
- # host: Optional[str] = None,
+ host: Optional[str] = None,
files: Optional[Dict[str, List["HTTPFile"]]] = None,
connection: Optional["HTTPConnection"] = None,
start_line: Optional["RequestStartLine"] = None,
@@ -466,7 +514,7 @@
self.protocol = getattr(context, "protocol", "http")
try:
- self.host = self.headers["Host"]
+ self.host = host or self.headers["Host"]
except KeyError:
if version == "HTTP/1.0":
# HTTP/1.0 does not require the Host header.
@@ -474,7 +522,6 @@
else:
raise HTTPInputError("Missing Host header")
if not _ABNF.host.fullmatch(self.host):
- print(_ABNF.host.pattern)
raise HTTPInputError("Invalid Host header: %r" % self.host)
if "," in self.host:
# https://www.rfc-editor.org/rfc/rfc9112.html#name-request-target
@@ -946,7 +993,7 @@
eoh = part.find(b"\r\n\r\n")
if eoh == -1:
raise HTTPInputError("multipart/form-data missing headers")
- headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
+ headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"),
_chars_are_bytes=False)
disp_header = headers.get("Content-Disposition", "")
disposition, disp_params = _parse_header(disp_header)
if disposition != "form-data" or not part.endswith(b"\r\n"):
@@ -1048,19 +1095,34 @@
# It has also been modified to support valueless parameters as seen in
# websocket extension negotiations, and to support non-ascii values in
# RFC 2231/5987 format.
+#
+# _parseparam has been further modified with the logic from
+# https://github.com/python/cpython/pull/136072/files
+# to avoid quadratic behavior when parsing semicolons in quoted strings.
+#
+# TODO: See if we can switch to email.message.Message for this functionality.
+# This is the suggested replacement for the cgi.py module now that cgi has
+# been removed from recent versions of Python. We need to verify that
+# the email module is consistent with our existing behavior (and all relevant
+# RFCs for multipart/form-data) before making this change.
def _parseparam(s: str) -> Generator[str, None, None]:
- while s[:1] == ";":
- s = s[1:]
- end = s.find(";")
- while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
- end = s.find(";", end + 1)
+ start = 0
+ while s.find(";", start) == start:
+ start += 1
+ end = s.find(";", start)
+ ind, diff = start, 0
+ while end > 0:
+ diff += s.count('"', ind, end) - s.count('\\"', ind, end)
+ if diff % 2 == 0:
+ break
+ end, ind = ind, s.find(";", end + 1)
if end < 0:
end = len(s)
- f = s[:end]
+ f = s[start:end]
yield f.strip()
- s = s[end:]
+ start = end
def _parse_header(line: str) -> Tuple[str, Dict[str, str]]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/routing.py
new/tornado-6.5.4/tornado/routing.py
--- old/tornado-6.5/tornado/routing.py 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/routing.py 2025-12-15 19:42:59.000000000
+0100
@@ -279,8 +279,8 @@
self.delegate.finish()
def on_connection_close(self) -> None:
- assert self.delegate is not None
- self.delegate.on_connection_close()
+ if self.delegate is not None:
+ self.delegate.on_connection_close()
class _DefaultMessageDelegate(httputil.HTTPMessageDelegate):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/speedups.pyi
new/tornado-6.5.4/tornado/speedups.pyi
--- old/tornado-6.5/tornado/speedups.pyi 1970-01-01 01:00:00.000000000
+0100
+++ new/tornado-6.5.4/tornado/speedups.pyi 2025-12-15 19:42:59.000000000
+0100
@@ -0,0 +1 @@
+def websocket_mask(mask: bytes, data: bytes) -> bytes: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/httpclient_test.py
new/tornado-6.5.4/tornado/test/httpclient_test.py
--- old/tornado-6.5/tornado/test/httpclient_test.py 2025-05-15
22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/test/httpclient_test.py 2025-12-15
19:42:59.000000000 +0100
@@ -442,7 +442,7 @@
# test if client hangs on tricky invalid gzip
# curl/simple httpclient have different behavior (exception, logging)
with ExpectLog(
- app_log, "(Uncaught exception|Exception in callback)",
required=False
+ gen_log, ".*Malformed HTTP message.*unconsumed gzip data",
required=False
):
try:
response = self.fetch("/invalid_gzip")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/httpserver_test.py
new/tornado-6.5.4/tornado/test/httpserver_test.py
--- old/tornado-6.5/tornado/test/httpserver_test.py 2025-05-15
22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/test/httpserver_test.py 2025-12-15
19:42:59.000000000 +0100
@@ -462,6 +462,18 @@
self.io_loop.add_timeout(datetime.timedelta(seconds=0.05),
self.stop)
self.wait()
+ def test_invalid_host_header_with_whitespace(self):
+ with ExpectLog(
+ gen_log, ".*Malformed HTTP message.*Invalid Host header",
level=logging.INFO
+ ):
+ self.stream.write(b"GET / HTTP/1.0\r\nHost: foo bar\r\n\r\n")
+ start_line, headers, response = self.io_loop.run_sync(
+ lambda: read_stream_body(self.stream)
+ )
+ self.assertEqual("HTTP/1.1", start_line.version)
+ self.assertEqual(400, start_line.code)
+ self.assertEqual("Bad Request", start_line.reason)
+
def test_chunked_request_body(self):
# Chunked requests are not widely supported and we don't have a way
# to generate them in AsyncHTTPClient, but HTTPServer will read them.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/httputil_test.py
new/tornado-6.5.4/tornado/test/httputil_test.py
--- old/tornado-6.5/tornado/test/httputil_test.py 2025-05-15
22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/test/httputil_test.py 2025-12-15
19:42:59.000000000 +0100
@@ -155,7 +155,7 @@
self.assertEqual(file["filename"], filename)
self.assertEqual(file["body"], b"Foo")
- def test_non_ascii_filename(self):
+ def test_non_ascii_filename_rfc5987(self):
data = b"""\
--1234
Content-Disposition: form-data; name="files"; filename="ab.txt";
filename*=UTF-8''%C3%A1b.txt
@@ -170,6 +170,23 @@
self.assertEqual(file["filename"], "áb.txt")
self.assertEqual(file["body"], b"Foo")
+ def test_non_ascii_filename_raw(self):
+ data = """\
+--1234
+Content-Disposition: form-data; name="files"; filename="测试.txt"
+
+Foo
+--1234--""".encode(
+ "utf-8"
+ ).replace(
+ b"\n", b"\r\n"
+ )
+ args, files = form_data_args()
+ parse_multipart_form_data(b"1234", data, args, files)
+ file = files["files"][0]
+ self.assertEqual(file["filename"], "测试.txt")
+ self.assertEqual(file["body"], b"Foo")
+
def test_boundary_starts_and_ends_with_quotes(self):
data = b"""\
--1234
@@ -262,6 +279,29 @@
self.assertEqual(file["filename"], "ab.txt")
self.assertEqual(file["body"], b"Foo")
+ def test_disposition_param_linear_performance(self):
+ # This is a regression test for performance of parsing parameters
+ # to the content-disposition header, specifically for semicolons within
+ # quoted strings.
+ def f(n):
+ start = time.perf_counter()
+ message = (
+ b"--1234\r\nContent-Disposition: form-data; "
+ + b'x="'
+ + b";" * n
+ + b'"; '
+ + b'name="files"; filename="a.txt"\r\n\r\nFoo\r\n--1234--\r\n'
+ )
+ args: dict[str, list[bytes]] = {}
+ files: dict[str, list[HTTPFile]] = {}
+ parse_multipart_form_data(b"1234", message, args, files)
+ return time.perf_counter() - start
+
+ d1 = f(1_000)
+ d2 = f(10_000)
+ if d2 / d1 > 20:
+ self.fail(f"Disposition param parsing is not linear: {d1=} vs
{d2=}")
+
class HTTPHeadersTest(unittest.TestCase):
def test_multi_line(self):
@@ -289,6 +329,9 @@
sorted(list(headers.get_all())),
[("Asdf", "qwer zxcv"), ("Foo", "bar baz"), ("Foo", "even more
lines")],
)
+ # Verify case insensitivity in-operator
+ self.assertTrue("asdf" in headers)
+ self.assertTrue("Asdf" in headers)
def test_continuation(self):
data = "Foo: bar\r\n\tasdf"
@@ -454,6 +497,21 @@
with self.assertRaises(HTTPInputError):
headers.add(name, "bar")
+ def test_linear_performance(self):
+ def f(n):
+ start = time.perf_counter()
+ headers = HTTPHeaders()
+ for i in range(n):
+ headers.add("X-Foo", "bar")
+ return time.perf_counter() - start
+
+ # This runs under 50ms on my laptop as of 2025-12-09.
+ d1 = f(10_000)
+ d2 = f(100_000)
+ if d2 / d1 > 20:
+ # d2 should be about 10x d1 but allow a wide margin for
variability.
+ self.fail(f"HTTPHeaders.add() does not scale linearly: {d1=} vs
{d2=}")
+
class FormatTimestampTest(unittest.TestCase):
# Make sure that all the input types are supported.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/process_test.py
new/tornado-6.5.4/tornado/test/process_test.py
--- old/tornado-6.5/tornado/test/process_test.py 2025-05-15
22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/test/process_test.py 2025-12-15
19:42:59.000000000 +0100
@@ -141,7 +141,7 @@
@gen_test
def test_subprocess(self):
subproc = Subprocess(
- [sys.executable, "-u", "-i"],
+ [sys.executable, "-u", "-i", "-I"],
stdin=Subprocess.STREAM,
stdout=Subprocess.STREAM,
stderr=subprocess.STDOUT,
@@ -163,7 +163,7 @@
def test_close_stdin(self):
# Close the parent's stdin handle and see that the child recognizes it.
subproc = Subprocess(
- [sys.executable, "-u", "-i"],
+ [sys.executable, "-u", "-i", "-I"],
stdin=Subprocess.STREAM,
stdout=Subprocess.STREAM,
stderr=subprocess.STDOUT,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/web_test.py
new/tornado-6.5.4/tornado/test/web_test.py
--- old/tornado-6.5/tornado/test/web_test.py 2025-05-15 22:19:08.000000000
+0200
+++ new/tornado-6.5.4/tornado/test/web_test.py 2025-12-15 19:42:59.000000000
+0100
@@ -1746,7 +1746,7 @@
class Handler(RequestHandler):
def get(self):
reason = self.request.arguments.get("reason", [])
- self.set_status(
+ raise HTTPError(
int(self.get_argument("code")),
reason=to_unicode(reason[0]) if reason else None,
)
@@ -1769,6 +1769,19 @@
self.assertEqual(response.code, 682)
self.assertEqual(response.reason, "Unknown")
+ def test_header_injection(self):
+ response = self.fetch("/?code=200&reason=OK%0D%0AX-Injection:injected")
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.reason, "Unknown")
+ self.assertNotIn("X-Injection", response.headers)
+
+ def test_reason_xss(self):
+ response = self.fetch("/?code=400&reason=<script>alert(1)</script>")
+ self.assertEqual(response.code, 400)
+ self.assertEqual(response.reason, "Unknown")
+ self.assertNotIn(b"script", response.body)
+ self.assertIn(b"Unknown", response.body)
+
class DateHeaderTest(SimpleHandlerTestCase):
class Handler(RequestHandler):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/test/websocket_test.py
new/tornado-6.5.4/tornado/test/websocket_test.py
--- old/tornado-6.5/tornado/test/websocket_test.py 2025-05-15
22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/test/websocket_test.py 2025-12-15
19:42:59.000000000 +0100
@@ -1,5 +1,6 @@
import asyncio
import contextlib
+import datetime
import functools
import socket
import traceback
@@ -861,16 +862,21 @@
return app
@staticmethod
- def suppress_pong(ws):
- """Suppress the client's "pong" response."""
+ def install_hook(ws):
+ """Optionally suppress the client's "pong" response."""
+
+ ws.drop_pongs = False
+ ws.pongs_received = 0
def wrapper(fcn):
- def _inner(oppcode: int, data: bytes):
- if oppcode == 0xA: # NOTE: 0x9=ping, 0xA=pong
- # prevent pong responses
- return
+ def _inner(opcode: int, data: bytes):
+ if opcode == 0xA: # NOTE: 0x9=ping, 0xA=pong
+ ws.pongs_received += 1
+ if ws.drop_pongs:
+ # prevent pong responses
+ return
# leave all other responses unchanged
- return fcn(oppcode, data)
+ return fcn(opcode, data)
return _inner
@@ -883,13 +889,14 @@
ws = yield self.ws_connect(
"/", ping_interval=interval, ping_timeout=interval / 4
)
+ self.install_hook(ws)
# websocket handler (server side)
handler = self.handlers[0]
for _ in range(5):
# wait for the ping period
- yield gen.sleep(0.2)
+ yield gen.sleep(interval)
# connection should still be open from the server end
self.assertIsNone(handler.close_code)
@@ -898,8 +905,12 @@
# connection should still be open from the client end
assert ws.protocol.close_code is None
+ # Check that our hook is intercepting messages; allow for
+ # some variance in timing (due to e.g. cpu load)
+ self.assertGreaterEqual(ws.pongs_received, 4)
+
# suppress the pong response message
- self.suppress_pong(ws)
+ ws.drop_pongs = True
# give the server time to register this
yield gen.sleep(interval * 1.5)
@@ -912,6 +923,23 @@
self.assertEqual(ws.protocol.close_code, 1000)
+class PingCalculationTest(unittest.TestCase):
+ def test_ping_sleep_time(self):
+ from tornado.websocket import WebSocketProtocol13
+
+ now = datetime.datetime(2025, 1, 1, 12, 0, 0,
tzinfo=datetime.timezone.utc)
+ interval = 10 # seconds
+ last_ping_time = datetime.datetime(
+ 2025, 1, 1, 11, 59, 54, tzinfo=datetime.timezone.utc
+ )
+ sleep_time = WebSocketProtocol13.ping_sleep_time(
+ last_ping_time=last_ping_time.timestamp(),
+ interval=interval,
+ now=now.timestamp(),
+ )
+ self.assertEqual(sleep_time, 4)
+
+
class ManualPingTest(WebSocketBaseTestCase):
def get_app(self):
class PingHandler(TestWebSocketHandler):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/web.py
new/tornado-6.5.4/tornado/web.py
--- old/tornado-6.5/tornado/web.py 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tornado/web.py 2025-12-15 19:42:59.000000000 +0100
@@ -359,8 +359,10 @@
:arg int status_code: Response status code.
:arg str reason: Human-readable reason phrase describing the status
- code. If ``None``, it will be filled in from
- `http.client.responses` or "Unknown".
+ code (for example, the "Not Found" in ``HTTP/1.1 404 Not Found``).
+ Normally determined automatically from `http.client.responses`;
this
+ argument should only be used if you need to use a non-standard
+ status code.
.. versionchanged:: 5.0
@@ -369,6 +371,14 @@
"""
self._status_code = status_code
if reason is not None:
+ if "<" in reason or not
httputil._ABNF.reason_phrase.fullmatch(reason):
+ # Logically this would be better as an exception, but this
method
+ # is called on error-handling paths that would need some
refactoring
+ # to tolerate internal errors cleanly.
+ #
+ # The check for "<" is a defense-in-depth against XSS attacks
(we also
+ # escape the reason when rendering error pages).
+ reason = "Unknown"
self._reason = escape.native_str(reason)
else:
self._reason = httputil.responses.get(status_code, "Unknown")
@@ -1345,7 +1355,8 @@
reason = exception.reason
self.set_status(status_code, reason=reason)
try:
- self.write_error(status_code, **kwargs)
+ if status_code != 304:
+ self.write_error(status_code, **kwargs)
except Exception:
app_log.error("Uncaught exception in write_error", exc_info=True)
if not self._finished:
@@ -1373,7 +1384,7 @@
self.finish(
"<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>"
- % {"code": status_code, "message": self._reason}
+ % {"code": status_code, "message":
escape.xhtml_escape(self._reason)}
)
@property
@@ -2520,9 +2531,11 @@
mode). May contain ``%s``-style placeholders, which will be filled
in with remaining positional parameters.
:arg str reason: Keyword-only argument. The HTTP "reason" phrase
- to pass in the status line along with ``status_code``. Normally
+ to pass in the status line along with ``status_code`` (for example,
+ the "Not Found" in ``HTTP/1.1 404 Not Found``). Normally
determined automatically from ``status_code``, but can be used
- to use a non-standard numeric code.
+ to use a non-standard numeric code. This is not a general-purpose
+ error message.
"""
def __init__(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado/websocket.py
new/tornado-6.5.4/tornado/websocket.py
--- old/tornado-6.5/tornado/websocket.py 2025-05-15 22:19:08.000000000
+0200
+++ new/tornado-6.5.4/tornado/websocket.py 2025-12-15 19:42:59.000000000
+0100
@@ -1346,6 +1346,11 @@
):
self._ping_coroutine = asyncio.create_task(self.periodic_ping())
+ @staticmethod
+ def ping_sleep_time(*, last_ping_time: float, interval: float, now: float)
-> float:
+ """Calculate the sleep time until the next ping should be sent."""
+ return max(0, last_ping_time + interval - now)
+
async def periodic_ping(self) -> None:
"""Send a ping and wait for a pong if ping_timeout is configured.
@@ -1371,7 +1376,13 @@
return
# wait until the next scheduled ping
- await asyncio.sleep(IOLoop.current().time() - ping_time + interval)
+ await asyncio.sleep(
+ self.ping_sleep_time(
+ last_ping_time=ping_time,
+ interval=interval,
+ now=IOLoop.current().time(),
+ )
+ )
class WebSocketClientConnection(simple_httpclient._HTTPConnection):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado.egg-info/PKG-INFO
new/tornado-6.5.4/tornado.egg-info/PKG-INFO
--- old/tornado-6.5/tornado.egg-info/PKG-INFO 2025-05-15 22:19:11.000000000
+0200
+++ new/tornado-6.5.4/tornado.egg-info/PKG-INFO 2025-12-15 19:43:01.000000000
+0100
@@ -1,13 +1,12 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: tornado
-Version: 6.5
+Version: 6.5.4
Summary: Tornado is a Python web framework and asynchronous networking
library, originally developed at FriendFeed.
Home-page: http://www.tornadoweb.org/
Author: Facebook
Author-email: [email protected]
License: Apache-2.0
Project-URL: Source, https://github.com/tornadoweb/tornado
-Platform: UNKNOWN
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
@@ -20,6 +19,17 @@
Requires-Python: >= 3.9
Description-Content-Type: text/x-rst
License-File: LICENSE
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: project-url
+Dynamic: requires-python
+Dynamic: summary
Tornado Web Server
==================
@@ -72,5 +82,3 @@
Documentation and links to additional resources are available at
https://www.tornadoweb.org
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tornado.egg-info/SOURCES.txt
new/tornado-6.5.4/tornado.egg-info/SOURCES.txt
--- old/tornado-6.5/tornado.egg-info/SOURCES.txt 2025-05-15
22:19:11.000000000 +0200
+++ new/tornado-6.5.4/tornado.egg-info/SOURCES.txt 2025-12-15
19:43:01.000000000 +0100
@@ -117,7 +117,12 @@
docs/releases/v6.4.1.rst
docs/releases/v6.4.2.rst
docs/releases/v6.5.0.rst
+docs/releases/v6.5.1.rst
+docs/releases/v6.5.2.rst
+docs/releases/v6.5.3.rst
+docs/releases/v6.5.4.rst
tornado/__init__.py
+tornado/__init__.pyi
tornado/_locale_data.py
tornado/auth.py
tornado/autoreload.py
@@ -142,6 +147,7 @@
tornado/routing.py
tornado/simple_httpclient.py
tornado/speedups.c
+tornado/speedups.pyi
tornado/tcpclient.py
tornado/tcpserver.py
tornado/template.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/tornado-6.5/tox.ini new/tornado-6.5.4/tox.ini
--- old/tornado-6.5/tox.ini 2025-05-15 22:19:08.000000000 +0200
+++ new/tornado-6.5.4/tox.ini 2025-12-15 19:42:59.000000000 +0100
@@ -35,7 +35,10 @@
deps =
full: pycurl
full: twisted
- full: pycares
+ # Pycares 5 has some backwards-incompatible changes that we don't support.
+ # And since CaresResolver is deprecated, I do not expect to fix it, so
just
+ # pin the previous version. (This should really be in
requirements.{in,txt} instead)
+ full: pycares<5
docs: -r{toxinidir}/requirements.txt
lint: -r{toxinidir}/requirements.txt