From 8cec473f5f5101e17f7d51d073f753516c4b8bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Sat, 7 Mar 2020 00:45:30 +0100 Subject: [PATCH] Fix the parsing of the ISO 8601 Z UTC designator (#448) * Fix parsing UTC designator * Test the pure Python version first --- .github/workflows/tests.yml | 23 ++++++++++++----------- pendulum/__init__.py | 2 ++ pendulum/parsing/_iso8601.c | 25 ++++++++++++++++++++----- pendulum/parsing/iso8601.py | 5 +++-- tests/test_parsing.py | 6 ++++++ 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa743c1e..3d3b56d4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,15 +48,16 @@ jobs: run: | source $HOME/.poetry/env poetry install + - name: Test Pure Python + run: | + source $HOME/.poetry/env + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests - name: Test run: | source $HOME/.poetry/env poetry run pytest -q tests poetry install - - name: Test Pure Python - run: | - source $HOME/.poetry/env - PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + MacOS: needs: Linting runs-on: macos-latest @@ -89,14 +90,14 @@ jobs: run: | source $HOME/.poetry/env poetry install - - name: Test - run: | - source $HOME/.poetry/env - poetry run pytest -q tests - name: Test Pure Python run: | source $HOME/.poetry/env PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + - name: Test + run: | + source $HOME/.poetry/env + poetry run pytest -q tests Windows: needs: Linting runs-on: windows-latest @@ -130,12 +131,12 @@ jobs: run: | $env:Path += ";$env:Userprofile\.poetry\bin" poetry install - - name: Test + - name: Test Pure Python run: | $env:Path += ";$env:Userprofile\.poetry\bin" + $env:PENDULUM_EXTENSIONS = "0" poetry run pytest -q tests - - name: Test Pure Python + - name: Test run: | $env:Path += ";$env:Userprofile\.poetry\bin" - $env:PENDULUM_EXTENSIONS = "0" poetry run pytest -q tests diff --git a/pendulum/__init__.py b/pendulum/__init__.py index bc248a23..78524b2c 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -79,6 +79,8 @@ def _safe_timezone(obj): # pytz if hasattr(obj, "localize"): obj = obj.zone + elif obj.tzname(None) == "UTC": + return UTC else: offset = obj.utcoffset(None) diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c index c44f5100..41c66fae 100644 --- a/pendulum/parsing/_iso8601.c +++ b/pendulum/parsing/_iso8601.c @@ -178,6 +178,7 @@ int is_long_year(int year) { typedef struct { PyObject_HEAD int offset; + char *tzname; } FixedOffset; /* @@ -186,10 +187,16 @@ typedef struct { */ static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) { int offset; - if (!PyArg_ParseTuple(args, "i", &offset)) + char *tzname = NULL; + + static char *kwlist[] = {"offset", "tzname", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname)) return -1; self->offset = offset; + self->tzname = tzname; + return 0; } @@ -217,6 +224,10 @@ static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) { * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60) */ static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) { + if (self->tzname != NULL) { + return PyUnicode_FromString(self->tzname); + } + char tzname_[7] = {0}; char sign = '+'; int offset = self->offset; @@ -292,16 +303,17 @@ static PyTypeObject FixedOffset_type = { * Skip overhead of calling PyObject_New and PyObject_Init. * Directly allocate object. */ -static PyObject *new_fixed_offset_ex(int offset, PyTypeObject *type) { +static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) { FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0)); if (self != NULL) self->offset = offset; + self->tzname = name; return (PyObject *) self; } -#define new_fixed_offset(offset) new_fixed_offset_ex(offset, &FixedOffset_type) +#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type) /* @@ -455,6 +467,7 @@ typedef struct { int microsecond; int offset; int has_offset; + char *tzname; int years; int months; int weeks; @@ -487,6 +500,7 @@ Parsed* new_parsed() { parsed->microsecond = 0; parsed->offset = 0; parsed->has_offset = 0; + parsed->tzname = NULL; parsed->years = 0; parsed->months = 0; @@ -585,7 +599,7 @@ Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { } // Checks - if (week > 53 || week > 52 && !is_long_year(parsed->year)) { + if (week > 53 || (week > 52 && !is_long_year(parsed->year))) { parsed->error = PARSER_INVALID_WEEK_NUMBER; return NULL; @@ -850,6 +864,7 @@ Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { // Timezone if (*c == 'Z') { parsed->has_offset = 1; + parsed->tzname = "UTC"; c++; } else if (*c == '+' || *c == '-') { tz_sign = 1; @@ -1258,7 +1273,7 @@ PyObject* parse_iso8601(PyObject *self, PyObject *args) { if (!parsed->has_offset) { tzinfo = Py_BuildValue(""); } else { - tzinfo = new_fixed_offset(parsed->offset); + tzinfo = new_fixed_offset(parsed->offset, parsed->tzname); } obj = PyDateTimeAPI->DateTime_FromDateAndTime( diff --git a/pendulum/parsing/iso8601.py b/pendulum/parsing/iso8601.py index ad14cc94..2d8f222a 100644 --- a/pendulum/parsing/iso8601.py +++ b/pendulum/parsing/iso8601.py @@ -12,6 +12,7 @@ from ..helpers import is_leap from ..helpers import is_long_year from ..helpers import week_day +from ..tz.timezone import UTC from ..tz.timezone import FixedTimezone from .exceptions import ParserError @@ -230,7 +231,7 @@ def parse_iso8601(text): tz = m.group("tz") if tz: if tz == "Z": - offset = 0 + tzinfo = UTC else: negative = True if tz.startswith("-") else False tz = tz[1:] @@ -248,7 +249,7 @@ def parse_iso8601(text): if negative: offset = -1 * offset - tzinfo = FixedTimezone(offset) + tzinfo = FixedTimezone(offset) if is_time: return datetime.time(hour, minute, second, microsecond) diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 1d943657..3dcf050b 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -131,3 +131,9 @@ def test_parse_now(): with pendulum.test(mock_now): assert pendulum.parse("now") == mock_now + + +def test_parse_with_utc_timezone(): + dt = pendulum.parse("2020-02-05T20:05:37.364951Z") + + assert "2020-02-05T20:05:37.364951Z" == dt.to_iso8601_string()