Skip to content

Commit 8cec473

Browse files
authored
Fix the parsing of the ISO 8601 Z UTC designator (#448)
* Fix parsing UTC designator * Test the pure Python version first
1 parent 205a86a commit 8cec473

File tree

5 files changed

+43
-18
lines changed

5 files changed

+43
-18
lines changed

Diff for: .github/workflows/tests.yml

+12-11
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,16 @@ jobs:
4848
run: |
4949
source $HOME/.poetry/env
5050
poetry install
51+
- name: Test Pure Python
52+
run: |
53+
source $HOME/.poetry/env
54+
PENDULUM_EXTENSIONS=0 poetry run pytest -q tests
5155
- name: Test
5256
run: |
5357
source $HOME/.poetry/env
5458
poetry run pytest -q tests
5559
poetry install
56-
- name: Test Pure Python
57-
run: |
58-
source $HOME/.poetry/env
59-
PENDULUM_EXTENSIONS=0 poetry run pytest -q tests
60+
6061
MacOS:
6162
needs: Linting
6263
runs-on: macos-latest
@@ -89,14 +90,14 @@ jobs:
8990
run: |
9091
source $HOME/.poetry/env
9192
poetry install
92-
- name: Test
93-
run: |
94-
source $HOME/.poetry/env
95-
poetry run pytest -q tests
9693
- name: Test Pure Python
9794
run: |
9895
source $HOME/.poetry/env
9996
PENDULUM_EXTENSIONS=0 poetry run pytest -q tests
97+
- name: Test
98+
run: |
99+
source $HOME/.poetry/env
100+
poetry run pytest -q tests
100101
Windows:
101102
needs: Linting
102103
runs-on: windows-latest
@@ -130,12 +131,12 @@ jobs:
130131
run: |
131132
$env:Path += ";$env:Userprofile\.poetry\bin"
132133
poetry install
133-
- name: Test
134+
- name: Test Pure Python
134135
run: |
135136
$env:Path += ";$env:Userprofile\.poetry\bin"
137+
$env:PENDULUM_EXTENSIONS = "0"
136138
poetry run pytest -q tests
137-
- name: Test Pure Python
139+
- name: Test
138140
run: |
139141
$env:Path += ";$env:Userprofile\.poetry\bin"
140-
$env:PENDULUM_EXTENSIONS = "0"
141142
poetry run pytest -q tests

Diff for: pendulum/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def _safe_timezone(obj):
7979
# pytz
8080
if hasattr(obj, "localize"):
8181
obj = obj.zone
82+
elif obj.tzname(None) == "UTC":
83+
return UTC
8284
else:
8385
offset = obj.utcoffset(None)
8486

Diff for: pendulum/parsing/_iso8601.c

+20-5
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ int is_long_year(int year) {
178178
typedef struct {
179179
PyObject_HEAD
180180
int offset;
181+
char *tzname;
181182
} FixedOffset;
182183

183184
/*
@@ -186,10 +187,16 @@ typedef struct {
186187
*/
187188
static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) {
188189
int offset;
189-
if (!PyArg_ParseTuple(args, "i", &offset))
190+
char *tzname = NULL;
191+
192+
static char *kwlist[] = {"offset", "tzname", NULL};
193+
194+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname))
190195
return -1;
191196

192197
self->offset = offset;
198+
self->tzname = tzname;
199+
193200
return 0;
194201
}
195202

@@ -217,6 +224,10 @@ static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) {
217224
* return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60)
218225
*/
219226
static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) {
227+
if (self->tzname != NULL) {
228+
return PyUnicode_FromString(self->tzname);
229+
}
230+
220231
char tzname_[7] = {0};
221232
char sign = '+';
222233
int offset = self->offset;
@@ -292,16 +303,17 @@ static PyTypeObject FixedOffset_type = {
292303
* Skip overhead of calling PyObject_New and PyObject_Init.
293304
* Directly allocate object.
294305
*/
295-
static PyObject *new_fixed_offset_ex(int offset, PyTypeObject *type) {
306+
static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) {
296307
FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0));
297308

298309
if (self != NULL)
299310
self->offset = offset;
311+
self->tzname = name;
300312

301313
return (PyObject *) self;
302314
}
303315

304-
#define new_fixed_offset(offset) new_fixed_offset_ex(offset, &FixedOffset_type)
316+
#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type)
305317

306318

307319
/*
@@ -455,6 +467,7 @@ typedef struct {
455467
int microsecond;
456468
int offset;
457469
int has_offset;
470+
char *tzname;
458471
int years;
459472
int months;
460473
int weeks;
@@ -487,6 +500,7 @@ Parsed* new_parsed() {
487500
parsed->microsecond = 0;
488501
parsed->offset = 0;
489502
parsed->has_offset = 0;
503+
parsed->tzname = NULL;
490504

491505
parsed->years = 0;
492506
parsed->months = 0;
@@ -585,7 +599,7 @@ Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
585599
}
586600

587601
// Checks
588-
if (week > 53 || week > 52 && !is_long_year(parsed->year)) {
602+
if (week > 53 || (week > 52 && !is_long_year(parsed->year))) {
589603
parsed->error = PARSER_INVALID_WEEK_NUMBER;
590604

591605
return NULL;
@@ -850,6 +864,7 @@ Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) {
850864
// Timezone
851865
if (*c == 'Z') {
852866
parsed->has_offset = 1;
867+
parsed->tzname = "UTC";
853868
c++;
854869
} else if (*c == '+' || *c == '-') {
855870
tz_sign = 1;
@@ -1258,7 +1273,7 @@ PyObject* parse_iso8601(PyObject *self, PyObject *args) {
12581273
if (!parsed->has_offset) {
12591274
tzinfo = Py_BuildValue("");
12601275
} else {
1261-
tzinfo = new_fixed_offset(parsed->offset);
1276+
tzinfo = new_fixed_offset(parsed->offset, parsed->tzname);
12621277
}
12631278

12641279
obj = PyDateTimeAPI->DateTime_FromDateAndTime(

Diff for: pendulum/parsing/iso8601.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..helpers import is_leap
1313
from ..helpers import is_long_year
1414
from ..helpers import week_day
15+
from ..tz.timezone import UTC
1516
from ..tz.timezone import FixedTimezone
1617
from .exceptions import ParserError
1718

@@ -230,7 +231,7 @@ def parse_iso8601(text):
230231
tz = m.group("tz")
231232
if tz:
232233
if tz == "Z":
233-
offset = 0
234+
tzinfo = UTC
234235
else:
235236
negative = True if tz.startswith("-") else False
236237
tz = tz[1:]
@@ -248,7 +249,7 @@ def parse_iso8601(text):
248249
if negative:
249250
offset = -1 * offset
250251

251-
tzinfo = FixedTimezone(offset)
252+
tzinfo = FixedTimezone(offset)
252253

253254
if is_time:
254255
return datetime.time(hour, minute, second, microsecond)

Diff for: tests/test_parsing.py

+6
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,9 @@ def test_parse_now():
131131

132132
with pendulum.test(mock_now):
133133
assert pendulum.parse("now") == mock_now
134+
135+
136+
def test_parse_with_utc_timezone():
137+
dt = pendulum.parse("2020-02-05T20:05:37.364951Z")
138+
139+
assert "2020-02-05T20:05:37.364951Z" == dt.to_iso8601_string()

0 commit comments

Comments
 (0)