summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
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,