diff --git a/docs/changes/4.0.0.rst b/docs/changes/4.0.0.rst index 3b3202f66..c3e9d426a 100644 --- a/docs/changes/4.0.0.rst +++ b/docs/changes/4.0.0.rst @@ -4,9 +4,10 @@ Changelog for Falcon 4.0.0 Summary ------- -The third beta release of Falcon 4.0.0 is here! +Falcon ``4.0.0b4`` is hopefully the final beta release before moving forward to +a release candidate. -Falcon 4.0 is now feature-complete, and we would really be thankful if you +As Falcon 4.0 is now feature-complete, we would really be thankful if you could test this beta release with your apps, and :ref:`let us know if you run into any issues `! Please also check the list of **breaking changes** below. @@ -16,9 +17,9 @@ checker of choice without any *typeshed* extensions for Falcon, and :ref:`report back to us ` how it went! As always, you can grab the new release -`from PyPI `__:: +`from PyPI `__:: - pip install falcon==4.0.0b3 + pip install falcon==4.0.0b4 (Alternatively, continue reading these docs for more :ref:`installation options `.) @@ -232,6 +233,11 @@ Breaking Changes :attr:`~falcon.Request.if_modified_since`, :attr:`~falcon.Request.if_unmodified_since`, and :attr:`falcon.testing.Cookie.expires`) has also changed to timezone-aware. + + Furthermore, the default value of the `format_string` parameter in + :meth:`falcon.Request.get_param_as_datetime` and + :class:`falcon.routing.DateTimeConverter` has also been updated to use a + timezone-aware form. (`#2182 `__) - ``setup.cfg`` was dropped in favor of consolidating all static project configuration in ``pyproject.toml`` (``setup.py`` is still needed for diff --git a/falcon/request.py b/falcon/request.py index c95c743d5..b6f3406f3 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -2134,7 +2134,7 @@ def get_param_as_datetime( def get_param_as_datetime( self, name: str, - format_string: str = '%Y-%m-%dT%H:%M:%SZ', + format_string: str = '%Y-%m-%dT%H:%M:%S%z', required: bool = False, store: StoreArg = None, default: Optional[datetime] = None, @@ -2147,7 +2147,7 @@ def get_param_as_datetime( Keyword Args: format_string (str): String used to parse the param value into a ``datetime``. Any format recognized by strptime() is - supported (default ``'%Y-%m-%dT%H:%M:%SZ'``). + supported (default ``'%Y-%m-%dT%H:%M:%S%z'``). required (bool): Set to ``True`` to raise ``HTTPBadRequest`` instead of returning ``None`` when the parameter is not found (default ``False``). @@ -2165,6 +2165,14 @@ def get_param_as_datetime( Raises: HTTPBadRequest: A required param is missing from the request, or the value could not be converted to a ``datetime``. + + .. versionchanged:: 4.0 + The default value of `format_string` was changed from + ``'%Y-%m-%dT%H:%M:%SZ'`` to ``'%Y-%m-%dT%H:%M:%S%z'``. + + The new format is a superset of the old one parsing-wise, however, + the converted :class:`~datetime.datetime` object is now + timezone-aware. """ param_value = self.get_param(name, required=required) diff --git a/falcon/routing/converters.py b/falcon/routing/converters.py index d012a412d..9706674a5 100644 --- a/falcon/routing/converters.py +++ b/falcon/routing/converters.py @@ -182,12 +182,19 @@ class DateTimeConverter(BaseConverter): Keyword Args: format_string (str): String used to parse the field value into a datetime. Any format recognized by strptime() is - supported (default ``'%Y-%m-%dT%H:%M:%SZ'``). + supported (default ``'%Y-%m-%dT%H:%M:%S%z'``). + + .. versionchanged:: 4.0 + The default value of `format_string` was changed from + ``'%Y-%m-%dT%H:%M:%SZ'`` to ``'%Y-%m-%dT%H:%M:%S%z'``. + + The new format is a superset of the old one parsing-wise, however, the + converted :class:`~datetime.datetime` object is now timezone-aware. """ __slots__ = ('_format_string',) - def __init__(self, format_string: str = '%Y-%m-%dT%H:%M:%SZ') -> None: + def __init__(self, format_string: str = '%Y-%m-%dT%H:%M:%S%z') -> None: self._format_string = format_string def convert(self, value: str) -> Optional[datetime]: diff --git a/falcon/version.py b/falcon/version.py index 594cffc92..a10f53d7a 100644 --- a/falcon/version.py +++ b/falcon/version.py @@ -14,5 +14,5 @@ """Falcon version.""" -__version__ = '4.0.0b3' +__version__ = '4.0.0b4' """Current version of Falcon.""" diff --git a/tests/test_query_params.py b/tests/test_query_params.py index 93acbddf6..eeac5d9cb 100644 --- a/tests/test_query_params.py +++ b/tests/test_query_params.py @@ -1,5 +1,6 @@ from datetime import date from datetime import datetime +from datetime import timezone import json from uuid import UUID @@ -848,7 +849,9 @@ def test_get_datetime_valid(self, simulate_request, client, resource): query_string = 'thedate={}'.format(date_value) simulate_request(client=client, path='/', query_string=query_string) req = resource.captured_req - assert req.get_param_as_datetime('thedate') == datetime(2015, 4, 20, 10, 10, 10) + assert req.get_param_as_datetime('thedate') == datetime( + 2015, 4, 20, 10, 10, 10, tzinfo=timezone.utc + ) def test_get_datetime_missing_param(self, simulate_request, client, resource): client.app.add_route('/', resource) @@ -857,16 +860,36 @@ def test_get_datetime_missing_param(self, simulate_request, client, resource): req = resource.captured_req assert req.get_param_as_datetime('thedate') is None - def test_get_datetime_valid_with_format(self, simulate_request, client, resource): + @pytest.mark.parametrize( + 'format_string, date_value, expected', + [ + ('%Y%m%d %H:%M:%S', '20150420 10:10:10', datetime(2015, 4, 20, 10, 10, 10)), + ( + '%Y-%m-%dT%H:%M:%SZ', + '2015-04-20T10:10:10Z', + datetime(2015, 4, 20, 10, 10, 10), + ), + ( + '%Y%m%dT%H:%M:%S.%fZ', + '20150420T10:10:10.133701Z', + datetime(2015, 4, 20, 10, 10, 10, 133701), + ), + ], + ) + def test_get_datetime_valid_with_format( + self, simulate_request, client, resource, format_string, date_value, expected + ): client.app.add_route('/', resource) - date_value = '20150420 10:10:10' query_string = 'thedate={}'.format(date_value) - format_string = '%Y%m%d %H:%M:%S' simulate_request(client=client, path='/', query_string=query_string) req = resource.captured_req - assert req.get_param_as_datetime( - 'thedate', format_string=format_string - ) == datetime(2015, 4, 20, 10, 10, 10) + assert ( + req.get_param_as_datetime( + 'thedate', + format_string=format_string, + ) + == expected + ) def test_get_datetime_store(self, simulate_request, client, resource): client.app.add_route('/', resource) @@ -877,7 +900,9 @@ def test_get_datetime_store(self, simulate_request, client, resource): store = {} req.get_param_as_datetime('thedate', store=store) assert len(store) != 0 - assert store.get('thedate') == datetime(2015, 4, 20, 10, 10, 10) + assert store.get('thedate') == datetime( + 2015, 4, 20, 10, 10, 10, tzinfo=timezone.utc + ) def test_get_datetime_invalid(self, simulate_request, client, resource): client.app.add_route('/', resource) diff --git a/tests/test_uri_converters.py b/tests/test_uri_converters.py index 14919fd88..c8cc80829 100644 --- a/tests/test_uri_converters.py +++ b/tests/test_uri_converters.py @@ -1,4 +1,5 @@ from datetime import datetime +from datetime import timezone import math import string import uuid @@ -153,7 +154,9 @@ def test_datetime_converter(value, format_string, expected): def test_datetime_converter_default_format(): c = converters.DateTimeConverter() - assert c.convert('2017-07-03T14:30:01Z') == datetime(2017, 7, 3, 14, 30, 1) + assert c.convert('2017-07-03T14:30:01Z') == datetime( + 2017, 7, 3, 14, 30, 1, tzinfo=timezone.utc + ) @pytest.mark.parametrize( diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py index b596177c2..4f10b6a0e 100644 --- a/tests/test_uri_templates.py +++ b/tests/test_uri_templates.py @@ -6,6 +6,7 @@ """ from datetime import datetime +from datetime import timezone import math import uuid @@ -288,7 +289,7 @@ def test_int_converter_rejections(client, uri_template): ( '/{start_year:int}-to-{timestamp:dt}', '/1961-to-1969-07-21T02:56:00Z', - datetime(1969, 7, 21, 2, 56, 0), + datetime(1969, 7, 21, 2, 56, 0, tzinfo=timezone.utc), ), ( '/{start_year:int}-to-{timestamp:dt("%Y-%m-%d")}',