diff options
Diffstat (limited to 'src')
| -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 |
7 files changed, 128 insertions, 116 deletions
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, |
