diff options
| author | David Lord <davidism@gmail.com> | 2021-02-10 18:57:36 -0800 |
|---|---|---|
| committer | David Lord <davidism@gmail.com> | 2021-02-10 19:04:31 -0800 |
| commit | 13c4f5b089aa0e600665cd751f0b7a2c77a13f57 (patch) | |
| tree | 61c3fc1bbded2d195f46589b730d2168b0447e5c | |
| parent | e6a3346efaf0f7f83b59e4eb7c80eeac40a73a03 (diff) | |
| download | werkzeug-parse_date-timezone.tar.gz | |
http.parse_date returns timzeone-aware valueparse_date-timezone
| -rw-r--r-- | CHANGES.rst | 14 | ||||
| -rw-r--r-- | docs/http.rst | 29 | ||||
| -rw-r--r-- | docs/quickstart.rst | 6 | ||||
| -rw-r--r-- | src/werkzeug/_internal.py | 36 | ||||
| -rw-r--r-- | src/werkzeug/http.py | 139 | ||||
| -rw-r--r-- | src/werkzeug/middleware/shared_data.py | 12 | ||||
| -rw-r--r-- | src/werkzeug/sansio/request.py | 25 | ||||
| -rw-r--r-- | src/werkzeug/sansio/response.py | 24 | ||||
| -rw-r--r-- | src/werkzeug/serving.py | 5 | ||||
| -rw-r--r-- | src/werkzeug/utils.py | 3 | ||||
| -rw-r--r-- | tests/test_http.py | 98 | ||||
| -rw-r--r-- | tests/test_internal.py | 9 | ||||
| -rw-r--r-- | tests/test_send_file.py | 2 | ||||
| -rw-r--r-- | tests/test_wrappers.py | 22 |
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", ), ] |
