summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst14
-rw-r--r--docs/http.rst29
-rw-r--r--docs/quickstart.rst6
-rw-r--r--src/werkzeug/_internal.py36
-rw-r--r--src/werkzeug/http.py139
-rw-r--r--src/werkzeug/middleware/shared_data.py12
-rw-r--r--src/werkzeug/sansio/request.py25
-rw-r--r--src/werkzeug/sansio/response.py24
-rw-r--r--src/werkzeug/serving.py5
-rw-r--r--src/werkzeug/utils.py3
-rw-r--r--tests/test_http.py98
-rw-r--r--tests/test_internal.py9
-rw-r--r--tests/test_send_file.py2
-rw-r--r--tests/test_wrappers.py22
14 files changed, 236 insertions, 188 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 35aa037c..ce39938e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -19,6 +19,15 @@ Unreleased
- Deprecate the ``environ["werkzeug.server.shutdown"]`` function
that is available when running the development server. :issue:`1752`
- Remove the unused, internal ``posixemulation`` module. :issue:`1759`
+- All ``datetime`` values are timezone-aware with
+ ``tzinfo=timezone.utc``. This applies to anything using
+ ``http.parse_date``: ``Request.date``, ``.if_modified_since``,
+ ``.if_unmodified_since``; ``Response.date``, ``.expires``,
+ ``.last_modified``, ``.retry_after``; ``parse_if_range_header``, and
+ ``IfRange.date``. When comparing values, the other values must also
+ be aware, or these values must be made naive. When passing
+ parameters or setting attributes, naive values are still assumed to
+ be in UTC. :pr:`2040`
- Merge all request and response wrapper mixin code into single
``Request`` and ``Response`` classes. Using the mixin classes is no
longer necessary and will show a deprecation warning. Checking
@@ -86,6 +95,11 @@ Unreleased
:pr:`1915`
- Add arguments to ``delete_cookie`` to match ``set_cookie`` and the
attributes modern browsers expect. :pr:`1889`
+- ``utils.cookie_date`` is deprecated, use ``utils.http_date``
+ instead. The value for ``Set-Cookie expires`` is no longer "-"
+ delimited. :pr:`2040`
+- ``utils.http_date``, and attributes and values that use it, no
+ longer accept ``time.struct_time`` tuples. :pr:`2040`
- Use ``request.headers`` instead of ``request.environ`` to look up
header attributes. :pr:`1808`
- The test ``Client`` request methods (``client.get``, etc.) always
diff --git a/docs/http.rst b/docs/http.rst
index 0f16cc8f..3db53506 100644
--- a/docs/http.rst
+++ b/docs/http.rst
@@ -9,21 +9,32 @@ that are useful when implementing WSGI middlewares or whenever you are
operating on a lower level layer. All this functionality is also exposed
from request and response objects.
-Date Functions
-==============
-The following functions simplify working with times in an HTTP context.
-Werkzeug uses offset-naive :class:`~datetime.datetime` objects internally
-that store the time in UTC. If you're working with timezones in your
-application make sure to replace the tzinfo attribute with a UTC timezone
-information before processing the values.
+Datetime Functions
+==================
-.. autofunction:: cookie_date
+These functions simplify working with times in an HTTP context. Werkzeug
+produces timezone-aware :class:`~datetime.datetime` objects in UTC. When
+passing datetime objects to Werkzeug, it assumes any naive datetime is
+in UTC.
-.. autofunction:: http_date
+When comparing datetime values from Werkzeug, your own datetime objects
+must also be timezone-aware, or you must make the values from Werkzeug
+naive.
+
+* ``dt = datetime.now(timezone.utc)`` gets the current time in UTC.
+* ``dt = datetime(..., tzinfo=timezone.utc)`` creates a time in UTC.
+* ``dt = dt.replace(tzinfo=timezone.utc)`` makes a naive object aware
+ by assuming it's in UTC.
+* ``dt = dt.replace(tzinfo=None)`` makes an aware object naive.
.. autofunction:: parse_date
+.. autofunction:: http_date
+
+.. autofunction:: cookie_date
+
+
Header Parsing
==============
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index abe5c798..be4574f2 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -185,7 +185,7 @@ True
E-tags and other conditional headers are available in parsed form as well:
>>> request.if_modified_since
-datetime.datetime(2009, 2, 20, 10, 10, 25)
+datetime.datetime(2009, 2, 20, 10, 10, 25, tzinfo=datetime.timezone.utc)
>>> request.if_none_match
<ETags '"e51c9-1e5d-46356dc86c640"'>
>>> request.cache_control
@@ -253,8 +253,8 @@ retrieve them:
>>> response.content_length
12
->>> from datetime import datetime
->>> response.date = datetime(2009, 2, 20, 17, 42, 51)
+>>> from datetime import datetime, timezone
+>>> response.date = datetime(2009, 2, 20, 17, 42, 51, tzinfo=timezone.utc)
>>> response.headers['Date']
'Fri, 20 Feb 2009 17:42:51 GMT'
diff --git a/src/werkzeug/_internal.py b/src/werkzeug/_internal.py
index afb7f3f3..8a0c17b6 100644
--- a/src/werkzeug/_internal.py
+++ b/src/werkzeug/_internal.py
@@ -8,8 +8,8 @@ import typing
import typing as t
from datetime import date
from datetime import datetime
+from datetime import timezone
from itertools import chain
-from time import struct_time
from weakref import WeakKeyDictionary
if t.TYPE_CHECKING:
@@ -295,20 +295,26 @@ def _parse_signature(func):
return parse
-def _date_to_unix(arg: t.Union[datetime, int, float, struct_time]) -> int:
- """Converts a timetuple, integer or datetime object into the seconds from
- epoch in utc.
- """
- if isinstance(arg, datetime):
- arg = arg.utctimetuple()
- elif isinstance(arg, (int, float)):
- return int(arg)
- year, month, day, hour, minute, second = arg[:6]
- days = date(year, month, 1).toordinal() - _epoch_ord + day - 1
- hours = days * 24 + hour
- minutes = hours * 60 + minute
- seconds = minutes * 60 + second
- return seconds
+@typing.overload
+def _dt_as_utc(dt: None) -> None:
+ ...
+
+
+@typing.overload
+def _dt_as_utc(dt: datetime) -> datetime:
+ ...
+
+
+def _dt_as_utc(dt):
+ if dt is None:
+ return dt
+
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=timezone.utc)
+ elif dt.tzinfo != timezone.utc:
+ return dt.astimezone(timezone.utc)
+
+ return dt
_TAccessorValue = t.TypeVar("_TAccessorValue")
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
index 4b7a0489..9e6e8273 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -1,15 +1,14 @@
import base64
+import email.utils
import re
import typing
import typing as t
import warnings
from datetime import datetime
from datetime import timedelta
-from email.utils import parsedate_tz
+from datetime import timezone
from enum import Enum
from hashlib import sha1
-from time import gmtime
-from time import struct_time
from time import time
from urllib.parse import unquote_to_bytes as _unquote
from urllib.request import parse_http_list as _parse_list_header
@@ -20,6 +19,7 @@ from ._internal import _make_cookie_domain
from ._internal import _to_bytes
from ._internal import _to_str
from ._internal import _wsgi_decoding_dance
+from werkzeug._internal import _dt_as_utc
if t.TYPE_CHECKING:
from wsgiref.types import WSGIEnvironment
@@ -699,6 +699,9 @@ def parse_if_range_header(value: t.Optional[str]) -> "ds.IfRange":
"""Parses an if-range header which can be an etag or a date. Returns
a :class:`~werkzeug.datastructures.IfRange` object.
+ .. versionchanged:: 2.0.0
+ If the value represents a datetime, it is timezone-aware.
+
.. versionadded:: 0.7
"""
if not value:
@@ -892,97 +895,69 @@ def generate_etag(data: bytes) -> str:
def parse_date(value: t.Optional[str]) -> t.Optional[datetime]:
- """Parse one of the following date formats into a datetime object:
-
- .. sourcecode:: text
+ """Parse an :rfc:`2822` date into a timezone-aware
+ :class:`datetime.datetime` object, or ``None`` if parsing fails.
- Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
- Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
- Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
+ This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It
+ returns ``None`` if parsing fails instead of raising an exception,
+ and always returns a timezone-aware datetime object. If the string
+ doesn't have timezone information, it is assumed to be UTC.
- If parsing fails the return value is `None`.
+ :param value: A string with a supported date format.
- :param value: a string with a supported date format.
- :return: a :class:`datetime.datetime` object.
+ .. versionchanged:: 2.0.0
+ Return a timezone-aware datetime object. Use
+ ``email.utils.parsedate_to_datetime``.
"""
- if value:
- t = parsedate_tz(value.strip())
- if t is not None:
- try:
- year = t[0]
- # unfortunately that function does not tell us if two digit
- # years were part of the string, or if they were prefixed
- # with two zeroes. So what we do is to assume that 69-99
- # refer to 1900, and everything below to 2000
- if 0 <= year <= 68:
- year += 2000
- elif 69 <= year <= 99:
- year += 1900
- return datetime(*((year,) + t[1:7])) - timedelta(seconds=t[-1] or 0)
- except (ValueError, OverflowError):
- return None
- return None
-
+ if value is None:
+ return None
-_t_date_input = t.Optional[t.Union[datetime, int, float, struct_time]]
-
-
-def _dump_date(d: _t_date_input, delim: str) -> str:
- """Used for `http_date` and `cookie_date`."""
- if d is None:
- d = gmtime()
- elif isinstance(d, datetime):
- d = d.utctimetuple()
- elif isinstance(d, (int, float)):
- d = gmtime(d)
- weekday = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")[d.tm_wday]
- month = (
- "Jan",
- "Feb",
- "Mar",
- "Apr",
- "May",
- "Jun",
- "Jul",
- "Aug",
- "Sep",
- "Oct",
- "Nov",
- "Dec",
- )[d.tm_mon - 1]
- return (
- f"{weekday}, {d.tm_mday:02d}{delim}{month}{delim}{d.tm_year:04d}"
- f" {d.tm_hour:02d}:{d.tm_min:02d}:{d.tm_sec:02d} GMT"
- )
+ try:
+ dt = email.utils.parsedate_to_datetime(value)
+ except TypeError:
+ return None
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=timezone.utc)
-def cookie_date(expires: _t_date_input = None) -> str:
- """Formats the time to ensure compatibility with Netscape's cookie
- standard.
+ return dt
- Accepts a floating point number expressed in seconds since the epoch in, a
- datetime object or a timetuple. All times in UTC. The :func:`parse_date`
- function can be used to parse such a date.
- Outputs a string in the format ``Wdy, DD-Mon-YYYY HH:MM:SS GMT``.
+def cookie_date(expires: t.Optional[t.Union[datetime, int, float]] = None) -> str:
+ """Format a datetime object or timestamp into an :rfc:`2822` date
+ string for ``Set-Cookie expires``.
- :param expires: If provided that date is used, otherwise the current.
+ .. deprecated:: 2.0.0
+ Use :func:`http_date` instead. Will be removed in version 2.1.
"""
- return _dump_date(expires, "-")
+ warnings.warn(
+ "'cookie_date' is deprecated and will be removed in Werkzeug"
+ " version 2.1. Use 'http_date' instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return http_date(expires)
-def http_date(timestamp: _t_date_input = None) -> str:
- """Formats the time to match the RFC1123 date format.
+def http_date(timestamp: t.Optional[t.Union[datetime, int, float]] = None) -> str:
+ """Format a datetime object or timestamp into an :rfc:`2822` date
+ string.
- Accepts a floating point number expressed in seconds since the epoch in, a
- datetime object or a timetuple. All times in UTC. The :func:`parse_date`
- function can be used to parse such a date.
+ This is a wrapper for :func:`email.utils.format_datetime` and
+ ``.formatdate``. It assumes naive datetime objects are in UTC
+ instead of raising an exception.
- Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``.
+ :param timestamp: The datetime or timestamp to format. Defaults to
+ the current time.
- :param timestamp: If provided that date is used, otherwise the current.
+ .. versionchanged:: 2.0.0
+ Use ``email.utils.format_datetime``.
"""
- return _dump_date(timestamp, " ")
+ if isinstance(timestamp, datetime):
+ timestamp = _dt_as_utc(timestamp)
+ return email.utils.format_datetime(timestamp, usegmt=True)
+
+ return email.utils.formatdate(timestamp, usegmt=True)
def parse_age(value: t.Optional[str] = None) -> t.Optional[timedelta]:
@@ -1061,10 +1036,10 @@ def is_resource_modified(
if isinstance(last_modified, str):
last_modified = parse_date(last_modified)
- # ensure that microsecond is zero because the HTTP spec does not transmit
- # that either and we might have some false positives. See issue #39
+ # HTTP doesn't use microsecond, remove it to avoid false positive
+ # comparisons. Mark naive datetimes as UTC.
if last_modified is not None:
- last_modified = last_modified.replace(microsecond=0)
+ last_modified = _dt_as_utc(last_modified.replace(microsecond=0))
if_range = None
if not ignore_if_range and "HTTP_RANGE" in environ:
@@ -1285,9 +1260,9 @@ def dump_cookie(
max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
if expires is not None:
if not isinstance(expires, str):
- expires = cookie_date(expires)
+ expires = http_date(expires)
elif max_age is not None and sync_expires:
- expires = cookie_date(time() + max_age)
+ expires = http_date(time() + max_age)
if samesite is not None:
samesite = samesite.title()
diff --git a/src/werkzeug/middleware/shared_data.py b/src/werkzeug/middleware/shared_data.py
index 776498ee..2578f7ae 100644
--- a/src/werkzeug/middleware/shared_data.py
+++ b/src/werkzeug/middleware/shared_data.py
@@ -14,8 +14,8 @@ import pkgutil
import posixpath
import typing as t
from datetime import datetime
+from datetime import timezone
from io import BytesIO
-from time import mktime
from time import time
from zlib import adler32
@@ -148,7 +148,7 @@ class SharedDataMiddleware:
def _opener(self, filename: str) -> _TOpener:
return lambda: (
open(filename, "rb"),
- datetime.utcfromtimestamp(os.path.getmtime(filename)),
+ datetime.fromtimestamp(os.path.getmtime(filename), tz=timezone.utc),
int(os.path.getsize(filename)),
)
@@ -156,7 +156,7 @@ class SharedDataMiddleware:
return lambda x: (os.path.basename(filename), self._opener(filename))
def get_package_loader(self, package: str, package_path: str) -> _TLoader:
- load_time = datetime.utcnow()
+ load_time = datetime.now(timezone.utc)
provider = pkgutil.get_loader(package)
if hasattr(provider, "get_resource_reader"):
@@ -185,7 +185,9 @@ class SharedDataMiddleware:
basename,
lambda: (
resource,
- datetime.utcfromtimestamp(os.path.getmtime(resource.name)),
+ datetime.fromtimestamp(
+ os.path.getmtime(resource.name), tz=timezone.utc
+ ),
os.path.getsize(resource.name),
),
)
@@ -238,7 +240,7 @@ class SharedDataMiddleware:
get_filesystem_encoding()
)
- timestamp = mktime(mtime.timetuple())
+ timestamp = mtime.timestamp()
checksum = adler32(real_filename) & 0xFFFFFFFF # type: ignore
return f"wzsdm-{timestamp}-{file_size}-{checksum}"
diff --git a/src/werkzeug/sansio/request.py b/src/werkzeug/sansio/request.py
index 3aec37c1..de0b7edc 100644
--- a/src/werkzeug/sansio/request.py
+++ b/src/werkzeug/sansio/request.py
@@ -229,7 +229,11 @@ class Request:
parse_date,
doc="""The Date general-header field represents the date and
time at which the message was originated, having the same
- semantics as orig-date in RFC 822.""",
+ semantics as orig-date in RFC 822.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """,
read_only=True,
)
max_forwards = header_property(
@@ -341,21 +345,30 @@ class Request:
@cached_property
def if_modified_since(self) -> t.Optional[datetime]:
- """The parsed `If-Modified-Since` header as datetime object."""
+ """The parsed `If-Modified-Since` header as a datetime object.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """
return parse_date(self.headers.get("If-Modified-Since"))
@cached_property
def if_unmodified_since(self) -> t.Optional[datetime]:
- """The parsed `If-Unmodified-Since` header as datetime object."""
+ """The parsed `If-Unmodified-Since` header as a datetime object.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """
return parse_date(self.headers.get("If-Unmodified-Since"))
@cached_property
def if_range(self) -> IfRange:
- """The parsed `If-Range` header.
+ """The parsed ``If-Range`` header.
- .. versionadded:: 0.7
+ .. versionchanged:: 2.0.0
+ ``IfRange.date`` is timezone-aware.
- :rtype: :class:`~werkzeug.datastructures.IfRange`
+ .. versionadded:: 0.7
"""
return parse_if_range_header(self.headers.get("If-Range"))
diff --git a/src/werkzeug/sansio/response.py b/src/werkzeug/sansio/response.py
index b7b5a4a1..f0c5586e 100644
--- a/src/werkzeug/sansio/response.py
+++ b/src/werkzeug/sansio/response.py
@@ -2,6 +2,7 @@ import typing
import typing as t
from datetime import datetime
from datetime import timedelta
+from datetime import timezone
from .._internal import _to_str
from ..datastructures import Headers
@@ -382,7 +383,11 @@ class Response:
http_date,
doc="""The Date general-header field represents the date and
time at which the message was originated, having the same
- semantics as orig-date in RFC 822.""",
+ semantics as orig-date in RFC 822.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """,
)
expires = header_property(
"Expires",
@@ -391,7 +396,11 @@ class Response:
http_date,
doc="""The Expires entity-header field gives the date/time after
which the response is considered stale. A stale cache entry may
- not normally be returned by a cache.""",
+ not normally be returned by a cache.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """,
)
last_modified = header_property(
"Last-Modified",
@@ -400,7 +409,11 @@ class Response:
http_date,
doc="""The Last-Modified entity-header field indicates the date
and time at which the origin server believes the variant was
- last modified.""",
+ last modified.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
+ """,
)
@property
@@ -410,12 +423,15 @@ class Response:
service is expected to be unavailable to the requesting client.
Time in seconds until expiration or date.
+
+ .. versionchanged:: 2.0.0
+ The datetime object is timezone-aware.
"""
value = self.headers.get("retry-after")
if value is None:
return None
elif value.isdigit():
- return datetime.utcnow() + timedelta(seconds=int(value))
+ return datetime.now(timezone.utc) + timedelta(seconds=int(value))
return parse_date(value)
@retry_after.setter
diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
index dd6922a6..a5526d03 100644
--- a/src/werkzeug/serving.py
+++ b/src/werkzeug/serving.py
@@ -22,6 +22,7 @@ import typing as t
import warnings
from datetime import datetime as dt
from datetime import timedelta
+from datetime import timezone
from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer
@@ -475,8 +476,8 @@ def generate_adhoc_ssl_pair(
.issuer_name(subject)
.public_key(pkey.public_key())
.serial_number(x509.random_serial_number())
- .not_valid_before(dt.utcnow())
- .not_valid_after(dt.utcnow() + timedelta(days=365))
+ .not_valid_before(dt.now(timezone.utc))
+ .not_valid_after(dt.now(timezone.utc) + timedelta(days=365))
.add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False)
.add_extension(x509.SubjectAlternativeName([x509.DNSName("*")]), critical=False)
.sign(pkey, hashes.SHA256(), default_backend())
diff --git a/src/werkzeug/utils.py b/src/werkzeug/utils.py
index fafba99e..f834138d 100644
--- a/src/werkzeug/utils.py
+++ b/src/werkzeug/utils.py
@@ -11,7 +11,6 @@ import unicodedata
import warnings
from datetime import datetime
from html.entities import name2codepoint
-from time import struct_time
from time import time
from zlib import adler32
@@ -564,7 +563,7 @@ def send_file(
download_name: t.Optional[str] = None,
conditional: bool = True,
etag: t.Union[bool, str] = True,
- last_modified: t.Optional[t.Union[datetime, int, float, struct_time]] = None,
+ last_modified: t.Optional[t.Union[datetime, int, float]] = None,
max_age: t.Optional[
t.Union[int, t.Callable[[t.Optional[t.Union[os.PathLike, str]]], int]]
] = None,
diff --git a/tests/test_http.py b/tests/test_http.py
index b0231192..dcb62ddb 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -1,5 +1,7 @@
import base64
from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
import pytest
@@ -239,27 +241,6 @@ class TestHTTPUtility:
assert bool(etags)
assert etags.contains_raw('W/"foo"')
- def test_parse_date(self):
- assert http.parse_date("Sun, 06 Nov 1994 08:49:37 GMT ") == datetime(
- 1994, 11, 6, 8, 49, 37
- )
- assert http.parse_date("Sunday, 06-Nov-94 08:49:37 GMT") == datetime(
- 1994, 11, 6, 8, 49, 37
- )
- assert http.parse_date(" Sun Nov 6 08:49:37 1994") == datetime(
- 1994, 11, 6, 8, 49, 37
- )
- assert http.parse_date("foo") is None
-
- def test_parse_date_overflows(self):
- assert http.parse_date(" Sun 02 Feb 1343 08:49:37 GMT") == datetime(
- 1343, 2, 2, 8, 49, 37
- )
- assert http.parse_date("Thu, 01 Jan 1970 00:00:00 GMT") == datetime(
- 1970, 1, 1, 0, 0
- )
- assert http.parse_date("Thu, 33 Jan 1970 00:00:00 GMT") is None
-
def test_remove_entity_headers(self):
now = http.http_date()
headers1 = [
@@ -430,12 +411,6 @@ class TestHTTPUtility:
env, last_modified=datetime(2008, 1, 1, 13, 30), ignore_if_range=True
)
- def test_date_formatting(self):
- assert http.cookie_date(0) == "Thu, 01-Jan-1970 00:00:00 GMT"
- assert http.cookie_date(datetime(1970, 1, 1)) == "Thu, 01-Jan-1970 00:00:00 GMT"
- assert http.http_date(0) == "Thu, 01 Jan 1970 00:00:00 GMT"
- assert http.http_date(datetime(1970, 1, 1)) == "Thu, 01 Jan 1970 00:00:00 GMT"
-
def test_parse_cookie(self):
cookies = http.parse_cookie(
"dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed;"
@@ -583,7 +558,7 @@ class TestRange:
rv = http.parse_if_range_header("Thu, 01 Jan 1970 00:00:00 GMT")
assert rv.etag is None
- assert rv.date == datetime(1970, 1, 1)
+ assert rv.date == datetime(1970, 1, 1, tzinfo=timezone.utc)
assert rv.to_header() == "Thu, 01 Jan 1970 00:00:00 GMT"
for x in "", None:
@@ -660,18 +635,6 @@ class TestRange:
assert rv.length == 100
assert rv.units == "bytes"
- @pytest.mark.parametrize(
- ("args", "expected"),
- (
- ((1, 1, 1), "Mon, 01 Jan 0001 00:00:00 GMT"),
- ((999, 1, 1), "Tue, 01 Jan 0999 00:00:00 GMT"),
- ((1000, 1, 1), "Wed, 01 Jan 1000 00:00:00 GMT"),
- ((2020, 1, 1), "Wed, 01 Jan 2020 00:00:00 GMT"),
- ),
- )
- def test_http_date_lt_1000(self, args, expected):
- assert http.http_date(datetime(*args)) == expected
-
class TestRegression:
def test_best_match_works(self):
@@ -699,3 +662,58 @@ def test_authorization_to_header(value: str) -> None:
parsed = http.parse_authorization_header(value)
assert parsed is not None
assert parsed.to_header() == value
+
+
+@pytest.mark.parametrize(
+ ("value", "expect"),
+ [
+ (
+ "Sun, 06 Nov 1994 08:49:37 GMT ",
+ datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc),
+ ),
+ (
+ "Sunday, 06-Nov-94 08:49:37 GMT",
+ datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc),
+ ),
+ (
+ " Sun Nov 6 08:49:37 1994",
+ datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc),
+ ),
+ ("foo", None),
+ (
+ " Sun 02 Feb 1343 08:49:37 GMT",
+ datetime(1343, 2, 2, 8, 49, 37, tzinfo=timezone.utc),
+ ),
+ (
+ "Thu, 01 Jan 1970 00:00:00 GMT",
+ datetime(1970, 1, 1, tzinfo=timezone.utc),
+ ),
+ ("Thu, 33 Jan 1970 00:00:00 GMT", None),
+ ],
+)
+def test_parse_date(value, expect):
+ assert http.parse_date(value) == expect
+
+
+@pytest.mark.parametrize(
+ ("value", "expect"),
+ [
+ (
+ datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc),
+ "Sun, 06 Nov 1994 08:49:37 GMT",
+ ),
+ (
+ datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone(timedelta(hours=-8))),
+ "Sun, 06 Nov 1994 16:49:37 GMT",
+ ),
+ (datetime(1994, 11, 6, 8, 49, 37), "Sun, 06 Nov 1994 08:49:37 GMT"),
+ (0, "Thu, 01 Jan 1970 00:00:00 GMT"),
+ (datetime(1970, 1, 1), "Thu, 01 Jan 1970 00:00:00 GMT"),
+ (datetime(1, 1, 1), "Mon, 01 Jan 0001 00:00:00 GMT"),
+ (datetime(999, 1, 1), "Tue, 01 Jan 0999 00:00:00 GMT"),
+ (datetime(1000, 1, 1), "Wed, 01 Jan 1000 00:00:00 GMT"),
+ (datetime(2020, 1, 1), "Wed, 01 Jan 2020 00:00:00 GMT"),
+ ],
+)
+def test_http_date(value, expect):
+ assert http.http_date(value) == expect
diff --git a/tests/test_internal.py b/tests/test_internal.py
index 6506beff..6e673fdc 100644
--- a/tests/test_internal.py
+++ b/tests/test_internal.py
@@ -1,4 +1,3 @@
-from datetime import datetime
from warnings import filterwarnings
from warnings import resetwarnings
@@ -10,14 +9,6 @@ from werkzeug.wrappers import Request
from werkzeug.wrappers import Response
-def test_date_to_unix():
- assert internal._date_to_unix(datetime(1970, 1, 1)) == 0
- assert internal._date_to_unix(datetime(1970, 1, 1, 1, 0, 0)) == 3600
- assert internal._date_to_unix(datetime(1970, 1, 1, 1, 1, 1)) == 3661
- x = datetime(2010, 2, 15, 16, 15, 39)
- assert internal._date_to_unix(x) == 1266250539
-
-
def test_easteregg():
req = Request.from_values("/?macgybarchakku")
resp = Response.force_type(internal._easteregg(None), req)
diff --git a/tests/test_send_file.py b/tests/test_send_file.py
index 424fe602..159aeb5f 100644
--- a/tests/test_send_file.py
+++ b/tests/test_send_file.py
@@ -35,7 +35,7 @@ def test_x_sendfile():
def test_last_modified():
- last_modified = datetime.datetime(1999, 1, 1)
+ last_modified = datetime.datetime(1999, 1, 1, tzinfo=datetime.timezone.utc)
rv = send_file(txt_path, environ, last_modified=last_modified)
assert rv.last_modified == last_modified
rv.close()
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
index 13214302..dfb9a75c 100644
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -3,6 +3,7 @@ import json
import os
from datetime import datetime
from datetime import timedelta
+from datetime import timezone
from io import BytesIO
import pytest
@@ -258,7 +259,7 @@ def test_base_response():
(
"Set-Cookie",
"foo=bar; Domain=example.org;"
- " Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=60;"
+ " Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60;"
" Path=/blub; SameSite=Strict",
),
]
@@ -270,7 +271,7 @@ def test_base_response():
("Content-Type", "text/plain; charset=utf-8"),
(
"Set-Cookie",
- "foo=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/",
+ "foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/",
),
]
@@ -434,8 +435,9 @@ def test_etag_request():
assert etags.contains_weak("foo")
assert not etags.contains("foo")
- assert request.if_modified_since == datetime(2008, 1, 22, 11, 18, 44)
- assert request.if_unmodified_since == datetime(2008, 1, 22, 11, 18, 44)
+ dt = datetime(2008, 1, 22, 11, 18, 44, tzinfo=timezone.utc)
+ assert request.if_modified_since == dt
+ assert request.if_unmodified_since == dt
def test_user_agent():
@@ -910,7 +912,7 @@ def test_common_response_descriptors():
del response.mimetype_params["charset"]
assert response.content_type == "text/html; x-foo=yep"
- now = datetime.utcnow().replace(microsecond=0)
+ now = datetime.now(timezone.utc).replace(microsecond=0)
assert response.content_length is None
response.content_length = "42"
@@ -967,7 +969,7 @@ def test_common_request_descriptors():
assert request.mimetype_params == {"charset": "utf-8"}
assert request.content_length == 23
assert request.referrer == "http://www.example.com/"
- assert request.date == datetime(2009, 2, 28, 19, 4, 35)
+ assert request.date == datetime(2009, 2, 28, 19, 4, 35, tzinfo=timezone.utc)
assert request.max_forwards == 10
assert "no-cache" in request.pragma
assert request.content_encoding == "gzip"
@@ -1419,7 +1421,7 @@ class TestSetCookie:
(
"Set-Cookie",
"foo=bar; Domain=example.org;"
- " Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=60;"
+ " Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60;"
" Secure; Path=/blub",
),
]
@@ -1442,7 +1444,7 @@ class TestSetCookie:
(
"Set-Cookie",
"foo=bar; Domain=example.org;"
- " Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=60;"
+ " Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60;"
" HttpOnly; Path=/blub",
),
]
@@ -1465,7 +1467,7 @@ class TestSetCookie:
(
"Set-Cookie",
"foo=bar; Domain=example.org;"
- " Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=60;"
+ " Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60;"
" Secure; HttpOnly; Path=/blub",
),
]
@@ -1487,7 +1489,7 @@ class TestSetCookie:
(
"Set-Cookie",
"foo=bar; Domain=example.org;"
- " Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=60;"
+ " Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60;"
" Path=/blub; SameSite=Strict",
),
]