Skip to content

Commit

Permalink
Do not auto-set timezone, allow date (#1886)
Browse files Browse the repository at this point in the history
  • Loading branch information
rly committed Apr 25, 2024
1 parent 001a9fd commit ff1a03c
Show file tree
Hide file tree
Showing 25 changed files with 265 additions and 172 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
- Support `stimulus_template` as optional predefined column in `IntracellularStimuliTable`. @stephprince [#1815](https://github.com/NeurodataWithoutBorders/pynwb/pull/1815)
- Support `NWBDataInterface` and `DynamicTable` in `NWBFile.stimulus`. @rly [#1842](https://github.com/NeurodataWithoutBorders/pynwb/pull/1842)
- Added support for python 3.12 and upgraded dependency versions. This also includes infrastructure updates for developers. @mavaylon1 [#1853](https://github.com/NeurodataWithoutBorders/pynwb/pull/1853)
- Added `mock_Units` for generating Units tables. @h-mayorquin [#1875](https://github.com/NeurodataWithoutBorders/pynwb/pull/1875) and [#1883](https://github.com/NeurodataWithoutBorders/pynwb/pull/1883)
- Added `mock_Units` for generating Units tables. @h-mayorquin [#1875](https://github.com/NeurodataWithoutBorders/pynwb/pull/1875) and [#1883](https://github.com/NeurodataWithoutBorders/pynwb/pull/1883)
- Allow datetimes without a timezone and without a time. @rly [#1886](https://github.com/NeurodataWithoutBorders/pynwb/pull/1886)
- No longer automatically set the timezone to the local timezone when not provided. [#1886](https://github.com/NeurodataWithoutBorders/pynwb/pull/1886)

### Bug fixes
- Fix bug with reading file with linked `TimeSeriesReferenceVectorData` @rly [#1865](https://github.com/NeurodataWithoutBorders/pynwb/pull/1865)
- Fix bug where extra keyword arguments could not be passed to `NWBFile.add_{x}_column`` for use in custom `VectorData`` classes. @rly [#1861](https://github.com/NeurodataWithoutBorders/pynwb/pull/1861)
- Fix bug where extra keyword arguments could not be passed to `NWBFile.add_{x}_column` for use in custom `VectorData` classes. @rly [#1861](https://github.com/NeurodataWithoutBorders/pynwb/pull/1861)

## PyNWB 2.6.0 (February 21, 2024)

Expand Down
6 changes: 1 addition & 5 deletions docs/gallery/advanced_io/h5dataio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,16 @@
#

from datetime import datetime

from dateutil.tz import tzlocal

from pynwb import NWBFile

start_time = datetime(2017, 4, 3, 11, tzinfo=tzlocal())
start_time = datetime(2017, 4, 3, hour=11, minute=0)

nwbfile = NWBFile(
session_description="demonstrate advanced HDF5 I/O features",
identifier="NWB123",
session_start_time=start_time,
)


####################
# Normally if we create a :py:class:`~pynwb.base.TimeSeries` we would do

Expand Down
7 changes: 2 additions & 5 deletions docs/gallery/advanced_io/linking_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,12 @@
# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_linking_data.png'

from datetime import datetime
from uuid import uuid4

import numpy as np
from dateutil.tz import tzlocal

from pynwb import NWBHDF5IO, NWBFile, TimeSeries
from uuid import uuid4

# Create the base data
start_time = datetime(2017, 4, 3, 11, tzinfo=tzlocal())
start_time = datetime(2017, 4, 3, hour=11, minute=0)
data = np.arange(1000).reshape((100, 10))
timestamps = np.arange(100)
filename1 = "external1_example.nwb"
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/advanced_io/parallelio.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
# from datetime import datetime
# from hdmf.backends.hdf5.h5_utils import H5DataIO
#
# start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific"))
# start_time = datetime(2018, 4, 25, hour=2, minute=30, second=3)
# fname = "test_parallel_pynwb.nwb"
# rank = MPI.COMM_WORLD.rank # The process ID (integer 0-3 for 4-process run)
#
Expand Down
8 changes: 2 additions & 6 deletions docs/gallery/advanced_io/plot_iterative_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,8 @@

# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_iterative_write.png'
from datetime import datetime
from uuid import uuid4

from dateutil.tz import tzlocal

from pynwb import NWBHDF5IO, NWBFile, TimeSeries

from uuid import uuid4

def write_test_file(filename, data, close_io=True):
"""
Expand All @@ -129,7 +125,7 @@ def write_test_file(filename, data, close_io=True):
"""

# Create a test NWBfile
start_time = datetime(2017, 4, 3, 11, tzinfo=tzlocal())
start_time = datetime(2017, 4, 3, hour=11, minute=30)
nwbfile = NWBFile(
session_description="demonstrate iterative write",
identifier=str(uuid4()),
Expand Down
21 changes: 8 additions & 13 deletions docs/gallery/domain/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,18 @@
The following examples will reference variables that may not be defined within the block they are used in. For
clarity, we define them here:
"""
# Define file paths used in the tutorial

import os

# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_image_data.png'
from datetime import datetime
from uuid import uuid4

import numpy as np
from dateutil import tz
from dateutil.tz import tzlocal
import os
from PIL import Image

from pynwb import NWBHDF5IO, NWBFile
from pynwb.base import Images
from pynwb.image import GrayscaleImage, ImageSeries, OpticalSeries, RGBAImage, RGBImage
from uuid import uuid4

# Define file paths used in the tutorial
nwbfile_path = os.path.abspath("images_tutorial.nwb")
moviefiles_path = [
os.path.abspath("image/file_1.tiff"),
Expand All @@ -50,12 +45,12 @@
# Create an :py:class:`~pynwb.file.NWBFile` object with the required fields
# (``session_description``, ``identifier``, ``session_start_time``) and additional metadata.

session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific"))
session_start_time = datetime(2018, 4, 25, hour=2, minute=30)

nwbfile = NWBFile(
session_description="my first synthetic recording",
identifier=str(uuid4()),
session_start_time=datetime.now(tzlocal()),
session_start_time=session_start_time,
experimenter=[
"Baggins, Bilbo",
],
Expand Down Expand Up @@ -138,21 +133,21 @@
# ^^^^^^^^^^^^^^
#
# External files (e.g. video files of the behaving animal) can be added to the :py:class:`~pynwb.file.NWBFile`
# by creating an :py:class:`~pynwb.image.ImageSeries` object using the
# by creating an :py:class:`~pynwb.image.ImageSeries` object using the
# :py:attr:`~pynwb.image.ImageSeries.external_file` attribute that specifies
# the path to the external file(s) on disk.
# The file(s) path must be relative to the path of the NWB file.
# Either ``external_file`` or ``data`` must be specified, but not both.
#
# If the sampling rate is constant, use :py:attr:`~pynwb.base.TimeSeries.rate` and
# If the sampling rate is constant, use :py:attr:`~pynwb.base.TimeSeries.rate` and
# :py:attr:`~pynwb.base.TimeSeries.starting_time` to specify time.
# For irregularly sampled recordings, use :py:attr:`~pynwb.base.TimeSeries.timestamps` to specify time for each sample
# image.
#
# Each external image may contain one or more consecutive frames of the full :py:class:`~pynwb.image.ImageSeries`.
# The :py:attr:`~pynwb.image.ImageSeries.starting_frame` attribute serves as an index to indicate which frame
# each file contains.
# For example, if the ``external_file`` dataset has three paths to files and the first and the second file have 2
# For example, if the ``external_file`` dataset has three paths to files and the first and the second file have 2
# frames, and the third file has 3 frames, then this attribute will have values `[0, 2, 4]`.

external_file = [
Expand Down
4 changes: 2 additions & 2 deletions docs/gallery/general/add_remove_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
nwbfile = NWBFile(
session_description="demonstrate adding to an NWB file",
identifier="NWB123",
session_start_time=datetime.datetime.now(datetime.timezone.utc),
session_start_time=datetime.datetime.now(),
)

filename = "nwbfile.nwb"
Expand Down Expand Up @@ -91,7 +91,7 @@
nwbfile = NWBFile(
session_description="demonstrate export of an NWB file",
identifier="NWB123",
session_start_time=datetime.datetime.now(datetime.timezone.utc),
session_start_time=datetime.datetime.now(),
)
data1 = list(range(100, 200, 10))
timestamps1 = np.arange(10, dtype=float)
Expand Down
19 changes: 9 additions & 10 deletions docs/gallery/general/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,15 @@ def __init__(self, **kwargs):
# To demonstrate this, first we will make some simulated data using our extensions.

from datetime import datetime

from dateutil.tz import tzlocal

from pynwb import NWBFile
from uuid import uuid4

start_time = datetime(2017, 4, 3, 11, tzinfo=tzlocal())
create_date = datetime(2017, 4, 15, 12, tzinfo=tzlocal())
session_start_time = datetime(2017, 4, 3, hour=11, minute=0)

nwbfile = NWBFile(
"demonstrate caching", "NWB456", start_time, file_create_date=create_date
session_description="demonstrate caching",
identifier=str(uuid4()),
session_start_time=session_start_time,
)

device = nwbfile.create_device(name="trodes_rig123")
Expand Down Expand Up @@ -333,18 +332,18 @@ class PotatoSack(MultiContainerInterface):
# Then use the objects (again, this would often be done in a different file).

from datetime import datetime

from dateutil.tz import tzlocal

from pynwb import NWBHDF5IO, NWBFile

# You can add potatoes to a potato sack in different ways
potato_sack = PotatoSack(potatos=Potato(name="potato1", age=2.3, weight=3.0))
potato_sack.add_potato(Potato("potato2", 3.0, 4.0))
potato_sack.create_potato("big_potato", 10.0, 20.0)

session_start_time = datetime(2017, 4, 3, hour=12, minute=0)
nwbfile = NWBFile(
"a file with metadata", "NB123A", datetime(2018, 6, 1, tzinfo=tzlocal())
session_description="a file with metadata",
identifier=str(uuid4()),
session_start_time = session_start_time,
)

pmod = nwbfile.create_processing_module("module_name", "desc")
Expand Down
8 changes: 3 additions & 5 deletions docs/gallery/general/object_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@
"""

from datetime import datetime
# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_objectid.png'

from datetime import datetime
import numpy as np
from dateutil.tz import tzlocal

# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_objectid.png'
from pynwb import NWBFile, TimeSeries

# set up the NWBFile
start_time = datetime(2019, 4, 3, 11, tzinfo=tzlocal())
start_time = datetime(2019, 4, 3, hour=11, minute=0)
nwbfile = NWBFile(
session_description="demonstrate NWB object IDs",
identifier="NWB456",
Expand Down
2 changes: 1 addition & 1 deletion docs/gallery/general/plot_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
# Use keyword arguments when constructing :py:class:`~pynwb.file.NWBFile` objects.
#

session_start_time = datetime(2018, 4, 25, 2, 30, 3, tzinfo=tz.gettz("US/Pacific"))
session_start_time = datetime(2018, 4, 25, hour=2, minute=30, second=3, tzinfo=tz.gettz("US/Pacific"))

nwbfile = NWBFile(
session_description="Mouse exploring an open field", # required
Expand Down
7 changes: 2 additions & 5 deletions docs/gallery/general/plot_timeintervals.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,15 @@

# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_timeintervals.png'
from datetime import datetime
from uuid import uuid4

import numpy as np
from dateutil.tz import tzlocal

from pynwb import NWBFile, TimeSeries
from uuid import uuid4

# create the NWBFile
nwbfile = NWBFile(
session_description="my first synthetic recording", # required
identifier=str(uuid4()), # required
session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), # required
session_start_time=datetime(2017, 4, 3, hour=11), # required
experimenter="Baggins, Bilbo", # optional
lab="Bag End Laboratory", # optional
institution="University of Middle Earth at the Shire", # optional
Expand Down
12 changes: 4 additions & 8 deletions docs/gallery/general/scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,20 @@
# To demonstrate linking and scratch space, lets assume we are starting with some acquired data.
#

from datetime import datetime
# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_scratch.png'

from datetime import datetime
import numpy as np
from dateutil.tz import tzlocal

# sphinx_gallery_thumbnail_path = 'figures/gallery_thumbnails_scratch.png'
from pynwb import NWBHDF5IO, NWBFile, TimeSeries

# set up the NWBFile
start_time = datetime(2019, 4, 3, 11, tzinfo=tzlocal())
create_date = datetime(2019, 4, 15, 12, tzinfo=tzlocal())
start_time = datetime(2019, 4, 3, hour=11, minute=0)

nwb = NWBFile(
session_description="demonstrate NWBFile scratch", # required
identifier="NWB456", # required
session_start_time=start_time, # required
file_create_date=create_date,
) # optional
)

# make some fake data
timestamps = np.linspace(0, 100, 1024)
Expand Down
41 changes: 9 additions & 32 deletions src/pynwb/file.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, date, timedelta
from dateutil.tz import tzlocal
from collections.abc import Iterable
from warnings import warn
Expand Down Expand Up @@ -104,8 +104,8 @@ class Subject(NWBContainer):
'doc': ('The weight of the subject, including units. Using kilograms is recommended. e.g., "0.02 kg". '
'If a float is provided, then the weight will be stored as "[value] kg".'),
'default': None},
{'name': 'date_of_birth', 'type': datetime, 'default': None,
'doc': 'The datetime of the date of birth. May be supplied instead of age.'},
{'name': 'date_of_birth', 'type': (datetime, date), 'default': None,
'doc': 'The date of birth, which may include time and timezone. May be supplied instead of age.'},
{'name': 'strain', 'type': str, 'doc': 'The strain of the subject, e.g., "C57BL/6J"', 'default': None},
)
def __init__(self, **kwargs):
Expand Down Expand Up @@ -141,10 +141,6 @@ def __init__(self, **kwargs):
if isinstance(args_to_set["age"], timedelta):
args_to_set["age"] = pd.Timedelta(args_to_set["age"]).isoformat()

date_of_birth = args_to_set['date_of_birth']
if date_of_birth and date_of_birth.tzinfo is None:
args_to_set['date_of_birth'] = _add_missing_timezone(date_of_birth)

for key, val in args_to_set.items():
setattr(self, key, val)

Expand Down Expand Up @@ -308,10 +304,11 @@ class NWBFile(MultiContainerInterface, HERDManager):
@docval({'name': 'session_description', 'type': str,
'doc': 'a description of the session where this data was generated'},
{'name': 'identifier', 'type': str, 'doc': 'a unique text identifier for the file'},
{'name': 'session_start_time', 'type': datetime, 'doc': 'the start date and time of the recording session'},
{'name': 'file_create_date', 'type': ('array_data', datetime),
{'name': 'session_start_time', 'type': (datetime, date),
'doc': 'the start date and time of the recording session'},
{'name': 'file_create_date', 'type': ('array_data', datetime, date),
'doc': 'the date and time the file was created and subsequent modifications made', 'default': None},
{'name': 'timestamps_reference_time', 'type': datetime,
{'name': 'timestamps_reference_time', 'type': (datetime, date),
'doc': 'date and time corresponding to time zero of all timestamps; defaults to value '
'of session_start_time', 'default': None},
{'name': 'experimenter', 'type': (tuple, list, str),
Expand Down Expand Up @@ -466,26 +463,18 @@ def __init__(self, **kwargs):
kwargs['name'] = 'root'
super().__init__(**kwargs)

# add timezone to session_start_time if missing
session_start_time = args_to_set['session_start_time']
if session_start_time.tzinfo is None:
args_to_set['session_start_time'] = _add_missing_timezone(session_start_time)

# set timestamps_reference_time to session_start_time if not provided
# if provided, ensure that it has a timezone
timestamps_reference_time = args_to_set['timestamps_reference_time']
if timestamps_reference_time is None:
args_to_set['timestamps_reference_time'] = args_to_set['session_start_time']
elif timestamps_reference_time.tzinfo is None:
raise ValueError("'timestamps_reference_time' must be a timezone-aware datetime object.")

# convert file_create_date to list and add timezone if missing
file_create_date = args_to_set['file_create_date']
if file_create_date is None:
file_create_date = datetime.now(tzlocal())
if isinstance(file_create_date, datetime):
if isinstance(file_create_date, (datetime, date)):
file_create_date = [file_create_date]
args_to_set['file_create_date'] = list(map(_add_missing_timezone, file_create_date))
args_to_set['file_create_date'] = file_create_date

# backwards-compatibility code for ic_electrodes / icephys_electrodes
icephys_electrodes = args_to_set['icephys_electrodes']
Expand Down Expand Up @@ -1155,18 +1144,6 @@ def copy(self):
return NWBFile(**kwargs)


def _add_missing_timezone(date):
"""
Add local timezone information on a datetime object if it is missing.
"""
if not isinstance(date, datetime):
raise ValueError("require datetime object")
if date.tzinfo is None:
warn("Date is missing timezone information. Updating to local timezone.", stacklevel=2)
return date.replace(tzinfo=tzlocal())
return date


def _tablefunc(table_name, description, columns):
t = DynamicTable(name=table_name, description=description)
for c in columns:
Expand Down

0 comments on commit ff1a03c

Please sign in to comment.