Skip to content

Commit 17b3bb5

Browse files
committed
Fix timezone handling in datetime_from_np_time
Closes #176
1 parent 2b34544 commit 17b3bb5

File tree

8 files changed

+42
-4
lines changed

8 files changed

+42
-4
lines changed

continuous-integration/requirements-3.11.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ flake8==7.3.0
5151
# via emsarray (pyproject.toml)
5252
fonttools==4.59.1
5353
# via matplotlib
54+
freezegun==1.5.5
55+
# via emsarray (pyproject.toml)
5456
fsspec==2025.7.0
5557
# via dask
5658
geojson==3.2.0
@@ -188,6 +190,7 @@ pytest-mpl==0.17.0
188190
# via emsarray (pyproject.toml)
189191
python-dateutil==2.9.0.post0
190192
# via
193+
# freezegun
191194
# matplotlib
192195
# pandas
193196
pytz==2025.2

continuous-integration/requirements-3.12.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ flake8==7.3.0
5151
# via emsarray (pyproject.toml)
5252
fonttools==4.59.1
5353
# via matplotlib
54+
freezegun==1.5.5
55+
# via emsarray (pyproject.toml)
5456
fsspec==2025.7.0
5557
# via dask
5658
geojson==3.2.0
@@ -186,6 +188,7 @@ pytest-mpl==0.17.0
186188
# via emsarray (pyproject.toml)
187189
python-dateutil==2.9.0.post0
188190
# via
191+
# freezegun
189192
# matplotlib
190193
# pandas
191194
pytz==2025.2

continuous-integration/requirements-3.13.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ flake8==7.3.0
5151
# via emsarray (pyproject.toml)
5252
fonttools==4.59.1
5353
# via matplotlib
54+
freezegun==1.5.5
55+
# via emsarray (pyproject.toml)
5456
fsspec==2025.7.0
5557
# via dask
5658
geojson==3.2.0
@@ -186,6 +188,7 @@ pytest-mpl==0.17.0
186188
# via emsarray (pyproject.toml)
187189
python-dateutil==2.9.0.post0
188190
# via
191+
# freezegun
189192
# matplotlib
190193
# pandas
191194
pytz==2025.2

continuous-integration/requirements-minimum.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,6 @@ zipp~=3.20.2
194194
# emsarray
195195
# pytz
196196
# tzdata
197-
pytz
198197
tzdata
199198
certifi
199+
pytz

docs/releases/development.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ Next release (in development)
99
:ref:`how to set the clim parameter in plots <example-plot-with-clim>`
1010
(:pr:`179`).
1111
* Bumped pinned dependencies (:pr:`183`).
12+
* Fixed :func:`emsarray.utils.datetime_from_np_time`
13+
when the system timezone is not UTC and a specific timezone is requested
14+
(:issue:`176`, :pr:`183`).

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ testing = [
6464
"types-pytz",
6565
"flake8",
6666
"isort",
67+
"freezegun",
6768
]
6869

6970
[project.scripts]

src/emsarray/utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,15 +695,17 @@ def datetime_from_np_time(
695695
The timezone that the numpy datetime is in.
696696
Defaults to UTC, as xarray will convert all time variables to UTC when
697697
opening files.
698+
The returned Python datetime will be in this timezone.
698699
699700
Returns
700701
=======
701702
datetime.datetime
702703
A timezone aware Python datetime.datetime instance.
703704
"""
704-
epoc = numpy.datetime64('1970-01-01')
705-
timestamp = (np_time - epoc).astype('timedelta64[ns]')
706-
return datetime.datetime.fromtimestamp(timestamp.astype(float) / 1e9, tz=tz)
705+
np_epoc = numpy.datetime64('1970-01-01')
706+
ns_since_epoc = (np_time - np_epoc).astype('timedelta64[ns]')
707+
py_epoc = datetime.datetime(1970, 1, 1, tzinfo=tz)
708+
return py_epoc + datetime.timedelta(seconds=ns_since_epoc.astype(float) / 1e9)
707709

708710

709711
class RequiresExtraException(Exception):

tests/test_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pathlib
44
from importlib.metadata import version
55

6+
import freezegun
67
import netCDF4
78
import numpy
89
import numpy.testing
@@ -569,3 +570,25 @@ def test_wind_dimension_renamed():
569570
)
570571
wound = utils.wind_dimension(data_array, ['y', 'x'], [5, 4], linear_dimension='ix')
571572
xarray.testing.assert_equal(wound, expected)
573+
574+
575+
@pytest.mark.parametrize('tz_offset', [0, 10, -4])
576+
def test_datetime_from_np_time(tz_offset: int):
577+
# Change the system timezone to `tz_offset`
578+
with freezegun.freeze_time(tz_offset=tz_offset):
579+
np_time = numpy.datetime64('2025-08-18T12:05:00.123456')
580+
581+
# Test that converting works using the UTC default timezone,
582+
# regardless of system timezone
583+
py_time_utc = utils.datetime_from_np_time(np_time)
584+
assert py_time_utc == datetime.datetime(2025, 8, 18, 12, 5, 0, 123456, tzinfo=datetime.UTC)
585+
586+
# Test that converting works when interpreted in the system timezone.
587+
py_tz_system = datetime.timezone(datetime.timedelta(hours=tz_offset))
588+
py_time_local = utils.datetime_from_np_time(np_time, tz=py_tz_system)
589+
assert py_time_local == datetime.datetime(2025, 8, 18, 12, 5, 0, 123456, tzinfo=py_tz_system)
590+
591+
# Test that converting works when using some other arbitrary timezone.
592+
py_tz_eucla = datetime.timezone(datetime.timedelta(hours=8, minutes=45))
593+
py_time_eucla = utils.datetime_from_np_time(np_time, tz=py_tz_eucla)
594+
assert py_time_eucla == datetime.datetime(2025, 8, 18, 12, 5, 0, 123456, tzinfo=py_tz_eucla)

0 commit comments

Comments
 (0)