Skip to content

Commit

Permalink
Merge pull request #3288 from bdarnell/enable-312
Browse files Browse the repository at this point in the history
Updates for Python 3.12
  • Loading branch information
bdarnell committed Jun 22, 2023
2 parents 6f2d093 + 4d4d80c commit cb5db3b
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 78 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ jobs:
tox_env: py311-full
- python: '3.11.0'
tox_env: py311-full
# 3.12 is disabled until https://github.com/python/cpython/issues/105808 is fixed
#- python: '3.12.0-alpha - 3.12'
# tox_env: py312-full
- python: '3.12.0-beta.3 - 3.12'
tox_env: py312-full
- python: 'pypy-3.8'
# Pypy is a lot slower due to jit warmup costs, so don't run the
# "full" test config there.
Expand Down
21 changes: 14 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ black==22.12.0
# via -r requirements.in
build==0.10.0
# via pip-tools
cachetools==5.2.1
cachetools==5.3.1
# via tox
certifi==2022.12.7
# via requests
Expand All @@ -32,7 +32,7 @@ docutils==0.17.1
# via
# sphinx
# sphinx-rtd-theme
filelock==3.9.0
filelock==3.12.0
# via
# tox
# virtualenv
Expand All @@ -54,7 +54,7 @@ mypy-extensions==0.4.3
# via
# black
# mypy
packaging==23.0
packaging==23.1
# via
# build
# pyproject-api
Expand All @@ -64,7 +64,7 @@ pathspec==0.10.3
# via black
pip-tools==6.12.1
# via -r requirements.in
platformdirs==2.6.2
platformdirs==3.5.1
# via
# black
# tox
Expand All @@ -77,7 +77,7 @@ pyflakes==3.0.1
# via flake8
pygments==2.14.0
# via sphinx
pyproject-api==1.5.0
pyproject-api==1.5.1
# via tox
pyproject-hooks==1.0.0
# via build
Expand Down Expand Up @@ -108,15 +108,22 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
tox==4.3.5
tomli==2.0.1
# via
# black
# build
# mypy
# pyproject-api
# tox
tox==4.6.0
# via -r requirements.in
types-pycurl==7.45.2.0
# via -r requirements.in
typing-extensions==4.4.0
# via mypy
urllib3==1.26.14
# via requests
virtualenv==20.17.1
virtualenv==20.23.0
# via tox
wheel==0.38.4
# via pip-tools
Expand Down
3 changes: 2 additions & 1 deletion tornado/httputil.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,8 @@ def format_timestamp(
The argument may be a numeric timestamp as returned by `time.time`,
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)
'Sun, 27 Jan 2013 18:43:20 GMT'
Expand Down
12 changes: 9 additions & 3 deletions tornado/locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def format_date(
shorter: bool = False,
full_format: bool = False,
) -> 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
can return an absolute date string with ``relative=False``.
Expand All @@ -343,10 +343,16 @@ def format_date(
This method is primarily intended for dates in the past.
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)):
date = datetime.datetime.utcfromtimestamp(date)
now = datetime.datetime.utcnow()
date = datetime.datetime.fromtimestamp(date, datetime.timezone.utc)
if date.tzinfo is None:
date = date.replace(tzinfo=datetime.timezone.utc)
now = datetime.datetime.now(datetime.timezone.utc)
if date > now:
if relative and (date - now).seconds < 60:
# Due to click skew, things are some things slightly
Expand Down
12 changes: 10 additions & 2 deletions tornado/test/httpclient_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from tornado.log import gen_log, app_log
from tornado import netutil
from tornado.testing import AsyncHTTPTestCase, bind_unused_port, gen_test, ExpectLog
from tornado.test.util import skipOnTravis
from tornado.test.util import skipOnTravis, ignore_deprecation
from tornado.web import Application, RequestHandler, url
from tornado.httputil import format_timestamp, HTTPHeaders

Expand Down Expand Up @@ -887,7 +887,15 @@ def test_body_setter(self):
self.assertEqual(request.body, utf8("foo"))

def test_if_modified_since(self):
http_date = datetime.datetime.utcnow()
http_date = datetime.datetime.now(datetime.timezone.utc)
request = HTTPRequest("http://example.com", if_modified_since=http_date)
self.assertEqual(
request.headers, {"If-Modified-Since": format_timestamp(http_date)}
)

def test_if_modified_since_naive_deprecated(self):
with ignore_deprecation():
http_date = datetime.datetime.utcnow()
request = HTTPRequest("http://example.com", if_modified_since=http_date)
self.assertEqual(
request.headers, {"If-Modified-Since": format_timestamp(http_date)}
Expand Down
26 changes: 24 additions & 2 deletions tornado/test/httputil_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from tornado.escape import utf8, native_str
from tornado.log import gen_log
from tornado.testing import ExpectLog
from tornado.test.util import ignore_deprecation

import copy
import datetime
Expand Down Expand Up @@ -412,8 +413,29 @@ def test_time_tuple(self):
self.assertEqual(9, len(tup))
self.check(tup)

def test_datetime(self):
self.check(datetime.datetime.utcfromtimestamp(self.TIMESTAMP))
def test_utc_naive_datetime(self):
self.check(
datetime.datetime.fromtimestamp(
self.TIMESTAMP, datetime.timezone.utc
).replace(tzinfo=None)
)

def test_utc_naive_datetime_deprecated(self):
with ignore_deprecation():
self.check(datetime.datetime.utcfromtimestamp(self.TIMESTAMP))

def test_utc_aware_datetime(self):
self.check(
datetime.datetime.fromtimestamp(self.TIMESTAMP, datetime.timezone.utc)
)

def test_other_aware_datetime(self):
# Other timezones are ignored; the timezone is always printed as GMT
self.check(
datetime.datetime.fromtimestamp(
self.TIMESTAMP, datetime.timezone(datetime.timedelta(hours=-4))
)
)


# HTTPServerRequest is mainly tested incidentally to the server itself,
Expand Down
88 changes: 49 additions & 39 deletions tornado/test/locale_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,45 +91,55 @@ def test_format_date(self):
locale.format_date(date, full_format=True), "April 28, 2013 at 6:35 pm"
)

now = datetime.datetime.utcnow()

self.assertEqual(
locale.format_date(now - datetime.timedelta(seconds=2), full_format=False),
"2 seconds ago",
)
self.assertEqual(
locale.format_date(now - datetime.timedelta(minutes=2), full_format=False),
"2 minutes ago",
)
self.assertEqual(
locale.format_date(now - datetime.timedelta(hours=2), full_format=False),
"2 hours ago",
)

self.assertEqual(
locale.format_date(
now - datetime.timedelta(days=1), full_format=False, shorter=True
),
"yesterday",
)

date = now - datetime.timedelta(days=2)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
locale._weekdays[date.weekday()],
)

date = now - datetime.timedelta(days=300)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
"%s %d" % (locale._months[date.month - 1], date.day),
)

date = now - datetime.timedelta(days=500)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
"%s %d, %d" % (locale._months[date.month - 1], date.day, date.year),
)
aware_dt = datetime.datetime.now(datetime.timezone.utc)
naive_dt = aware_dt.replace(tzinfo=None)
for name, now in {"aware": aware_dt, "naive": naive_dt}.items():
with self.subTest(dt=name):
self.assertEqual(
locale.format_date(
now - datetime.timedelta(seconds=2), full_format=False
),
"2 seconds ago",
)
self.assertEqual(
locale.format_date(
now - datetime.timedelta(minutes=2), full_format=False
),
"2 minutes ago",
)
self.assertEqual(
locale.format_date(
now - datetime.timedelta(hours=2), full_format=False
),
"2 hours ago",
)

self.assertEqual(
locale.format_date(
now - datetime.timedelta(days=1),
full_format=False,
shorter=True,
),
"yesterday",
)

date = now - datetime.timedelta(days=2)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
locale._weekdays[date.weekday()],
)

date = now - datetime.timedelta(days=300)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
"%s %d" % (locale._months[date.month - 1], date.day),
)

date = now - datetime.timedelta(days=500)
self.assertEqual(
locale.format_date(date, full_format=False, shorter=True),
"%s %d, %d" % (locale._months[date.month - 1], date.day, date.year),
)

def test_friendly_number(self):
locale = tornado.locale.get("en_US")
Expand Down
25 changes: 13 additions & 12 deletions tornado/test/web_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,10 +404,10 @@ def test_set_cookie_expires_days(self):
match = re.match("foo=bar; expires=(?P<expires>.+); Path=/", header)
assert match is not None

expires = datetime.datetime.utcnow() + datetime.timedelta(days=10)
parsed = email.utils.parsedate(match.groupdict()["expires"])
assert parsed is not None
header_expires = datetime.datetime(*parsed[:6])
expires = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=10
)
header_expires = email.utils.parsedate_to_datetime(match.groupdict()["expires"])
self.assertTrue(abs((expires - header_expires).total_seconds()) < 10)

def test_set_cookie_false_flags(self):
Expand Down Expand Up @@ -1697,11 +1697,10 @@ def get(self):

def test_date_header(self):
response = self.fetch("/")
parsed = email.utils.parsedate(response.headers["Date"])
assert parsed is not None
header_date = datetime.datetime(*parsed[:6])
header_date = email.utils.parsedate_to_datetime(response.headers["Date"])
self.assertTrue(
header_date - datetime.datetime.utcnow() < datetime.timedelta(seconds=2)
header_date - datetime.datetime.now(datetime.timezone.utc)
< datetime.timedelta(seconds=2)
)


Expand Down Expand Up @@ -3010,10 +3009,12 @@ def test_xsrf_httponly(self):
match = re.match(".*; expires=(?P<expires>.+);.*", header)
assert match is not None

expires = datetime.datetime.utcnow() + datetime.timedelta(days=2)
parsed = email.utils.parsedate(match.groupdict()["expires"])
assert parsed is not None
header_expires = datetime.datetime(*parsed[:6])
expires = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
days=2
)
header_expires = email.utils.parsedate_to_datetime(match.groupdict()["expires"])
if header_expires.tzinfo is None:
header_expires = header_expires.replace(tzinfo=datetime.timezone.utc)
self.assertTrue(abs((expires - header_expires).total_seconds()) < 10)


Expand Down
28 changes: 19 additions & 9 deletions tornado/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,9 @@ def set_cookie(
if domain:
morsel["domain"] = domain
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:
morsel["expires"] = httputil.format_timestamp(expires)
if path:
Expand Down Expand Up @@ -698,7 +700,9 @@ def clear_cookie(self, name: str, **kwargs: Any) -> None:
raise TypeError(
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)

def clear_all_cookies(self, **kwargs: Any) -> None:
Expand Down Expand Up @@ -2812,12 +2816,12 @@ def should_return_304(self) -> bool:
# content has not been modified
ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value)
if date_tuple is not None:
if_since = datetime.datetime(*date_tuple[:6])
assert self.modified is not None
if if_since >= self.modified:
return True
if_since = email.utils.parsedate_to_datetime(ims_value)
if if_since.tzinfo is None:
if_since = if_since.replace(tzinfo=datetime.timezone.utc)
assert self.modified is not None
if if_since >= self.modified:
return True

return False

Expand Down Expand Up @@ -2981,6 +2985,10 @@ def get_modified_time(self) -> Optional[datetime.datetime]:
object or None.
.. 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()
# NOTE: Historically, this used stat_result[stat.ST_MTIME],
Expand All @@ -2991,7 +2999,9 @@ def get_modified_time(self) -> Optional[datetime.datetime]:
# consistency with the past (and because we have a unit test
# that relies on this), we truncate the float here, although
# 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

def get_content_type(self) -> str:
Expand Down

0 comments on commit cb5db3b

Please sign in to comment.