diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e8ffe6b4..8dc3b5ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,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) +- 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) diff --git a/docs/gallery/advanced_io/h5dataio.py b/docs/gallery/advanced_io/h5dataio.py index 3b4391655..5b5f73bc2 100644 --- a/docs/gallery/advanced_io/h5dataio.py +++ b/docs/gallery/advanced_io/h5dataio.py @@ -19,12 +19,9 @@ # 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", @@ -32,7 +29,6 @@ session_start_time=start_time, ) - #################### # Normally if we create a :py:class:`~pynwb.base.TimeSeries` we would do diff --git a/docs/gallery/advanced_io/linking_data.py b/docs/gallery/advanced_io/linking_data.py index 2f79d1488..93f93c825 100644 --- a/docs/gallery/advanced_io/linking_data.py +++ b/docs/gallery/advanced_io/linking_data.py @@ -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" diff --git a/docs/gallery/advanced_io/parallelio.py b/docs/gallery/advanced_io/parallelio.py index 53abdf239..f04ef4a87 100644 --- a/docs/gallery/advanced_io/parallelio.py +++ b/docs/gallery/advanced_io/parallelio.py @@ -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) # diff --git a/docs/gallery/advanced_io/plot_iterative_write.py b/docs/gallery/advanced_io/plot_iterative_write.py index 958981a0b..36b8bc0be 100644 --- a/docs/gallery/advanced_io/plot_iterative_write.py +++ b/docs/gallery/advanced_io/plot_iterative_write.py @@ -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): """ @@ -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()), diff --git a/docs/gallery/domain/images.py b/docs/gallery/domain/images.py index d6eef24b3..b4511e3c5 100644 --- a/docs/gallery/domain/images.py +++ b/docs/gallery/domain/images.py @@ -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"), @@ -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", ], @@ -138,13 +133,13 @@ # ^^^^^^^^^^^^^^ # # 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. @@ -152,7 +147,7 @@ # 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 = [ diff --git a/docs/gallery/general/add_remove_containers.py b/docs/gallery/general/add_remove_containers.py index 86aa373b2..fcb74e72a 100644 --- a/docs/gallery/general/add_remove_containers.py +++ b/docs/gallery/general/add_remove_containers.py @@ -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" @@ -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) diff --git a/docs/gallery/general/extensions.py b/docs/gallery/general/extensions.py index ddf9159c7..7e232f168 100644 --- a/docs/gallery/general/extensions.py +++ b/docs/gallery/general/extensions.py @@ -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") @@ -333,9 +332,6 @@ 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 @@ -343,8 +339,11 @@ class PotatoSack(MultiContainerInterface): 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") diff --git a/docs/gallery/general/object_id.py b/docs/gallery/general/object_id.py index a4de45625..25f125805 100644 --- a/docs/gallery/general/object_id.py +++ b/docs/gallery/general/object_id.py @@ -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", diff --git a/docs/gallery/general/plot_file.py b/docs/gallery/general/plot_file.py index 5c59abf8d..5bfa837ee 100644 --- a/docs/gallery/general/plot_file.py +++ b/docs/gallery/general/plot_file.py @@ -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 diff --git a/docs/gallery/general/plot_timeintervals.py b/docs/gallery/general/plot_timeintervals.py index 4069fd4a4..3a4ad306a 100644 --- a/docs/gallery/general/plot_timeintervals.py +++ b/docs/gallery/general/plot_timeintervals.py @@ -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 diff --git a/docs/gallery/general/scratch.py b/docs/gallery/general/scratch.py index 0e00c5e96..9083e4081 100644 --- a/docs/gallery/general/scratch.py +++ b/docs/gallery/general/scratch.py @@ -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) diff --git a/src/pynwb/file.py b/src/pynwb/file.py index 0b294e873..06b7fbe07 100644 --- a/src/pynwb/file.py +++ b/src/pynwb/file.py @@ -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 @@ -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): @@ -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) @@ -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), @@ -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'] @@ -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: diff --git a/src/pynwb/io/file.py b/src/pynwb/io/file.py index 1908c6b31..15c2c1c06 100644 --- a/src/pynwb/io/file.py +++ b/src/pynwb/io/file.py @@ -1,4 +1,4 @@ -from dateutil.parser import parse as dateutil_parse +import datetime from hdmf.build import ObjectMapper @@ -8,6 +8,22 @@ from .utils import get_nwb_version +def parse_datetime(datestr): + """Parse an ISO 8601 date string into a datetime object or a date object. + + If the date string does not contain a time component, then parse into a date object. + + :param datestr: str + :return: datetime.datetime or datetime.date + """ + if isinstance(datestr, bytes): + datestr = datestr.decode("utf-8") + dt = datetime.datetime.fromisoformat(datestr) + if "T" not in datestr: + dt = dt.date() + return dt + + @register_map(NWBFile) class NWBFileMap(ObjectMapper): @@ -157,19 +173,19 @@ def scratch(self, builder, manager): @ObjectMapper.constructor_arg('session_start_time') def dateconversion(self, builder, manager): datestr = builder.get('session_start_time').data - date = dateutil_parse(datestr) - return date + dt = parse_datetime(datestr) + return dt @ObjectMapper.constructor_arg('timestamps_reference_time') def dateconversion_trt(self, builder, manager): datestr = builder.get('timestamps_reference_time').data - date = dateutil_parse(datestr) - return date + dt = parse_datetime(datestr) + return dt @ObjectMapper.constructor_arg('file_create_date') def dateconversion_list(self, builder, manager): datestr = builder.get('file_create_date').data - dates = list(map(dateutil_parse, datestr)) + dates = list(map(parse_datetime, datestr)) return dates @ObjectMapper.constructor_arg('file_name') @@ -223,8 +239,8 @@ def dateconversion(self, builder, manager): return else: datestr = dob_builder.data - date = dateutil_parse(datestr) - return date + dt = parse_datetime(datestr) + return dt @ObjectMapper.constructor_arg("age__reference") def age_reference_none(self, builder, manager): diff --git a/src/pynwb/testing/mock/ecephys.py b/src/pynwb/testing/mock/ecephys.py index 97831c3de..36796c267 100644 --- a/src/pynwb/testing/mock/ecephys.py +++ b/src/pynwb/testing/mock/ecephys.py @@ -129,7 +129,7 @@ def mock_Units( nwbfile: Optional[NWBFile] = None, ) -> Units: - units_table = Units() + units_table = Units(name="units") # This is for nwbfile.units= mock_Units() to work units_table.add_column(name="unit_name", description="a readable identifier for the unit") rng = np.random.default_rng(seed=seed) diff --git a/src/pynwb/testing/mock/file.py b/src/pynwb/testing/mock/file.py index 943f86dcb..50369e7e1 100644 --- a/src/pynwb/testing/mock/file.py +++ b/src/pynwb/testing/mock/file.py @@ -1,7 +1,6 @@ from typing import Optional from uuid import uuid4 from datetime import datetime -from dateutil.tz import tzlocal from ...file import NWBFile, Subject from .utils import name_generator @@ -10,7 +9,7 @@ def mock_NWBFile( session_description: str = 'session_description', identifier: Optional[str] = None, - session_start_time: datetime = datetime(1970, 1, 1, tzinfo=tzlocal()), + session_start_time: datetime = datetime(1970, 1, 1), **kwargs ): return NWBFile( diff --git a/src/pynwb/testing/testh5io.py b/src/pynwb/testing/testh5io.py index 7234e79f5..45ae8cebe 100644 --- a/src/pynwb/testing/testh5io.py +++ b/src/pynwb/testing/testh5io.py @@ -1,5 +1,4 @@ from datetime import datetime -from dateutil.tz import tzlocal, tzutc import os from abc import ABCMeta, abstractmethod import warnings @@ -33,8 +32,8 @@ def getContainer(self, nwbfile): def setUp(self): self.container = self.setUpContainer() - self.start_time = datetime(1971, 1, 1, 12, tzinfo=tzutc()) - self.create_date = datetime(2018, 4, 15, 12, tzinfo=tzlocal()) + self.start_time = datetime(1971, 1, 1, 12) + self.create_date = datetime(2018, 4, 15, 12) self.container_type = self.container.__class__.__name__ self.filename = 'test_%s.nwb' % self.container_type self.export_filename = 'test_export_%s.nwb' % self.container_type @@ -226,7 +225,7 @@ def setUp(self): container_type = self.getContainerType().replace(" ", "_") session_description = 'A file to test writing and reading a %s' % container_type identifier = 'TEST_%s' % container_type - session_start_time = datetime(1971, 1, 1, 12, tzinfo=tzutc()) + session_start_time = datetime(1971, 1, 1, 12) self.nwbfile = NWBFile( session_description=session_description, identifier=identifier, diff --git a/tests/integration/hdf5/test_base.py b/tests/integration/hdf5/test_base.py index 60f8510ff..75c346012 100644 --- a/tests/integration/hdf5/test_base.py +++ b/tests/integration/hdf5/test_base.py @@ -1,6 +1,5 @@ import numpy as np from datetime import datetime -from dateutil.tz import tzlocal from pynwb import TimeSeries, NWBFile, NWBHDF5IO from pynwb.base import Images, Image, ImageReferences @@ -34,7 +33,7 @@ def test_timestamps_linking(self): tsa = TimeSeries(name='a', data=np.linspace(0, 1, 1000), timestamps=np.arange(1000.), unit='m') tsb = TimeSeries(name='b', data=np.linspace(0, 1, 1000), timestamps=tsa, unit='m') nwbfile = NWBFile(identifier='foo', - session_start_time=datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal()), + session_start_time=datetime(2017, 5, 1, 12, 0, 0), session_description='bar') nwbfile.add_acquisition(tsa) nwbfile.add_acquisition(tsb) @@ -52,7 +51,7 @@ def test_data_linking(self): tsb = TimeSeries(name='b', data=tsa, timestamps=np.arange(1000.), unit='m') tsc = TimeSeries(name='c', data=tsb, timestamps=np.arange(1000.), unit='m') nwbfile = NWBFile(identifier='foo', - session_start_time=datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal()), + session_start_time=datetime(2017, 5, 1, 12, 0, 0), session_description='bar') nwbfile.add_acquisition(tsa) nwbfile.add_acquisition(tsb) diff --git a/tests/integration/hdf5/test_io.py b/tests/integration/hdf5/test_io.py index d68334c89..da22a0651 100644 --- a/tests/integration/hdf5/test_io.py +++ b/tests/integration/hdf5/test_io.py @@ -245,7 +245,7 @@ class TestAppend(TestCase): def setUp(self): self.nwbfile = NWBFile(session_description='hi', identifier='hi', - session_start_time=datetime(1970, 1, 1, 12, tzinfo=tzutc())) + session_start_time=datetime(1970, 1, 1, 12)) self.path = "test_append.nwb" def tearDown(self): @@ -312,7 +312,7 @@ class TestH5DataIO(TestCase): def setUp(self): self.nwbfile = NWBFile(session_description='a', identifier='b', - session_start_time=datetime(1970, 1, 1, 12, tzinfo=tzutc())) + session_start_time=datetime(1970, 1, 1, 12)) self.path = "test_pynwb_io_hdf5_h5dataIO.h5" def tearDown(self): diff --git a/tests/integration/hdf5/test_modular_storage.py b/tests/integration/hdf5/test_modular_storage.py index fba5d02db..09c916bfb 100644 --- a/tests/integration/hdf5/test_modular_storage.py +++ b/tests/integration/hdf5/test_modular_storage.py @@ -1,7 +1,6 @@ import os import gc from datetime import datetime -from dateutil.tz import tzutc import numpy as np from hdmf.backends.hdf5 import HDF5IO @@ -19,7 +18,7 @@ def setUp(self): self.link_filename = os.path.join(os.getcwd(), 'test_time_series_modular_link.nwb') # Make the data container file write - self.start_time = datetime(1971, 1, 1, 12, tzinfo=tzutc()) + self.start_time = datetime(1971, 1, 1, 12) self.data = np.arange(2000).reshape((1000, 2)) self.timestamps = np.linspace(0, 1, 1000) # The container before roundtrip diff --git a/tests/integration/hdf5/test_nwbfile.py b/tests/integration/hdf5/test_nwbfile.py index e164ec649..641928598 100644 --- a/tests/integration/hdf5/test_nwbfile.py +++ b/tests/integration/hdf5/test_nwbfile.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, date from dateutil.tz import tzlocal, tzutc import pandas as pd import numpy as np @@ -21,7 +21,12 @@ def setUp(self): """ Set up an NWBFile object with an acquisition TimeSeries, analysis TimeSeries, and a processing module """ self.start_time = datetime(1970, 1, 1, 12, tzinfo=tzutc()) self.ref_time = datetime(1979, 1, 1, 0, tzinfo=tzutc()) - self.create_date = datetime(2017, 4, 15, 12, tzinfo=tzlocal()) + # try some dates with/without timezone and time + self.create_date = [ + datetime(2017, 5, 1, 12, tzinfo=tzlocal()), + datetime(2017, 5, 2, 13), + datetime(2017, 5, 2), + ] self.manager = get_manager() self.filename = 'test_nwbfileio.h5' self.nwbfile = NWBFile(session_description='a test NWB File', @@ -152,16 +157,61 @@ def getContainer(self, nwbfile): return nwbfile +class TestNWBFileNoTimezoneRoundTrip(TestNWBFileIO): + """ Test that an NWBFile with no timezone information can be written to and read from file """ + + def build_nwbfile(self): + description = 'test nwbfile no time' + identifier = 'TEST_no_time' + start_time = datetime(2024, 4, 10, 0, 21) + + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=start_time + ) + + def roundtripContainer(self, cache_spec=False): + self.read_nwbfile = super().roundtripContainer(cache_spec=cache_spec) + self.assertEqual(self.read_nwbfile.session_start_time, self.container.session_start_time) + self.assertIsNone(self.read_nwbfile.session_start_time.tzinfo) + return self.read_nwbfile + + +class TestNWBFileNoTimeRoundTrip(TestNWBFileIO): + """ Test that an NWBFile with no time information can be written to and read from file """ + + def build_nwbfile(self): + description = 'test nwbfile no time' + identifier = 'TEST_no_time' + start_time = date(2024, 4, 9) + + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=start_time + ) + + def roundtripContainer(self, cache_spec=False): + self.read_nwbfile = super().roundtripContainer(cache_spec=cache_spec) + self.assertEqual(self.read_nwbfile.session_start_time, self.container.session_start_time) + self.assertIsInstance(self.read_nwbfile.session_start_time, date) + self.assertNotIsInstance(self.read_nwbfile.session_start_time, datetime) + return self.read_nwbfile + + class TestExperimentersConstructorRoundtrip(TestNWBFileIO): """ Test that a list of multiple experimenters in a constructor is written to and read from file """ def build_nwbfile(self): description = 'test nwbfile experimenter' identifier = 'TEST_experimenter' - self.nwbfile = NWBFile(session_description=description, - identifier=identifier, - session_start_time=self.start_time, - experimenter=('experimenter1', 'experimenter2')) + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=self.start_time, + experimenter=('experimenter1', 'experimenter2') + ) class TestExperimentersSetterRoundtrip(TestNWBFileIO): @@ -170,10 +220,12 @@ class TestExperimentersSetterRoundtrip(TestNWBFileIO): def build_nwbfile(self): description = 'test nwbfile experimenter' identifier = 'TEST_experimenter' - self.nwbfile = NWBFile(session_description=description, - identifier=identifier, - session_start_time=self.start_time) - self.nwbfile.experimenter = ('experimenter1', 'experimenter2') + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=self.start_time + ) + self.container.experimenter = ('experimenter1', 'experimenter2') class TestPublicationsConstructorRoundtrip(TestNWBFileIO): @@ -182,10 +234,12 @@ class TestPublicationsConstructorRoundtrip(TestNWBFileIO): def build_nwbfile(self): description = 'test nwbfile publications' identifier = 'TEST_publications' - self.nwbfile = NWBFile(session_description=description, - identifier=identifier, - session_start_time=self.start_time, - related_publications=('pub1', 'pub2')) + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=self.start_time, + related_publications=('pub1', 'pub2') + ) class TestPublicationsSetterRoundtrip(TestNWBFileIO): @@ -194,10 +248,12 @@ class TestPublicationsSetterRoundtrip(TestNWBFileIO): def build_nwbfile(self): description = 'test nwbfile publications' identifier = 'TEST_publications' - self.nwbfile = NWBFile(session_description=description, - identifier=identifier, - session_start_time=self.start_time) - self.nwbfile.related_publications = ('pub1', 'pub2') + self.container = NWBFile( + session_description=description, + identifier=identifier, + session_start_time=self.start_time + ) + self.container.related_publications = ('pub1', 'pub2') class TestSubjectIO(NWBH5IOMixin, TestCase): @@ -251,6 +307,55 @@ def getContainer(self, nwbfile): return nwbfile.subject +class TestSubjectDOBNoDateSetIO(NWBH5IOMixin, TestCase): + + def setUpContainer(self): + """ Return the test Subject """ + return Subject( + age="P90D", + description="An unfortunate rat", + genotype="WT", + sex="M", + species="Rattus norvegicus", + subject_id="RAT123", + weight="2 kg", + date_of_birth=date(2024, 4, 9), + strain="my_strain", + ) + + def addContainer(self, nwbfile): + """ Add the test Subject to the given NWBFile """ + nwbfile.subject = self.container + + def getContainer(self, nwbfile): + """ Return the test Subject from the given NWBFile """ + return nwbfile.subject + + +class TestSubjectMinimalSetIO(NWBH5IOMixin, TestCase): + + def setUpContainer(self): + """ Return the test Subject """ + return Subject( + age="P90D", + description="An unfortunate rat", + genotype="WT", + sex="M", + species="Rattus norvegicus", + subject_id="RAT123", + weight="2 kg", + strain="my_strain", + ) + + def addContainer(self, nwbfile): + """ Add the test Subject to the given NWBFile """ + nwbfile.subject = self.container + + def getContainer(self, nwbfile): + """ Return the test Subject from the given NWBFile """ + return nwbfile.subject + + class TestEmptySubjectIO(TestSubjectIO): def setUpContainer(self): diff --git a/tests/unit/test_epoch.py b/tests/unit/test_epoch.py index 318ad3943..4bf2637df 100644 --- a/tests/unit/test_epoch.py +++ b/tests/unit/test_epoch.py @@ -1,7 +1,6 @@ import numpy as np import pandas as pd from datetime import datetime -from dateutil import tz from pynwb.epoch import TimeIntervals from pynwb import TimeSeries, NWBFile @@ -67,7 +66,7 @@ def test_dataframe_roundtrip_drop_ts(self): self.assertEqual(obtained.loc[2, 'foo'], df.loc[2, 'foo']) def test_no_tags(self): - nwbfile = NWBFile("a file with header data", "NB123A", datetime(1970, 1, 1, tzinfo=tz.tzutc())) + nwbfile = NWBFile("a file with header data", "NB123A", datetime(1970, 1, 1)) df = self.get_dataframe() for i, row in df.iterrows(): nwbfile.add_epoch(start_time=row['start_time'], stop_time=row['stop_time']) diff --git a/tests/unit/test_extension.py b/tests/unit/test_extension.py index 7664bbf22..abbb6511a 100644 --- a/tests/unit/test_extension.py +++ b/tests/unit/test_extension.py @@ -2,7 +2,6 @@ import random import string from datetime import datetime -from dateutil.tz import tzlocal from tempfile import gettempdir from hdmf.spec import RefSpec @@ -108,7 +107,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.test_attr = test_attr - nwbfile = NWBFile("a file with header data", "NB123A", datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal())) + nwbfile = NWBFile("a file with header data", "NB123A", datetime(2017, 5, 1, 12, 0, 0)) nwbfile.add_lab_meta_data(MyTestMetaData(name='test_name', test_attr=5.)) @@ -128,7 +127,7 @@ def test_lab_meta_auto(self): MyTestMetaData = get_class('MyTestMetaData', self.prefix) - nwbfile = NWBFile("a file with header data", "NB123A", datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal())) + nwbfile = NWBFile("a file with header data", "NB123A", datetime(2017, 5, 1, 12, 0, 0)) nwbfile.add_lab_meta_data(MyTestMetaData(name='test_name', test_attr=5.)) diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 98446fa46..fc44dfdd8 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd -from datetime import datetime, timedelta +from datetime import datetime, date, timedelta from dateutil.tz import tzlocal, tzutc from hdmf.common import DynamicTable @@ -9,7 +9,7 @@ from hdmf.utils import docval, get_docval, popargs from pynwb import NWBFile, TimeSeries, NWBHDF5IO from pynwb.base import Image, Images -from pynwb.file import Subject, ElectrodeTable, _add_missing_timezone +from pynwb.file import Subject, ElectrodeTable from pynwb.epoch import TimeIntervals from pynwb.ecephys import ElectricalSeries from pynwb.testing import TestCase, remove_test_file @@ -19,9 +19,10 @@ class NWBFileTest(TestCase): def setUp(self): self.start = datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal()) self.ref_time = datetime(1979, 1, 1, 0, tzinfo=tzutc()) + # try some dates with/without timezone and time self.create = [datetime(2017, 5, 1, 12, tzinfo=tzlocal()), - datetime(2017, 5, 2, 13, 0, 0, 1, tzinfo=tzutc()), - datetime(2017, 5, 2, 14, tzinfo=tzutc())] + datetime(2017, 5, 2, 13), + datetime(2017, 5, 2)] self.path = 'nwbfile_test.h5' self.nwbfile = NWBFile(session_description='a test session description for a test NWBFile', identifier='FILE123', @@ -502,6 +503,22 @@ def test_multi_publications(self): related_publications=('pub1', 'pub2')) self.assertTupleEqual(self.nwbfile.related_publications, ('pub1', 'pub2')) + def test_session_start_time_no_timezone(self): + self.nwbfile = NWBFile( + session_description='a test session description for a test NWBFile', + identifier='FILE123', + session_start_time=datetime(2024, 4, 10, 0, 21), + ) + self.assertIsNone(self.nwbfile.session_start_time.tzinfo) + + def test_session_start_time_no_time(self): + self.nwbfile = NWBFile( + session_description='a test session description for a test NWBFile', + identifier='FILE123', + session_start_time=date(2024, 4, 10), + ) + self.assertEqual(self.nwbfile.session_start_time, date(2024, 4, 10)) + class SubjectTest(TestCase): def setUp(self): @@ -517,7 +534,7 @@ def setUp(self): date_of_birth=datetime(2017, 5, 1, 12, tzinfo=tzlocal()), strain='my_strain', ) - self.start = datetime(2017, 5, 1, 12, tzinfo=tzlocal()) + self.start = datetime(2017, 5, 1, 12) self.path = 'nwbfile_test.h5' self.nwbfile = NWBFile( 'a test session description for a test NWBFile', @@ -586,6 +603,35 @@ def test_subject_age_duration(self): self.assertEqual(subject.age, "P1DT3H46M39S") + def test_dob_no_timezone(self): + self.subject = Subject( + age='P90D', + age__reference="birth", + description='An unfortunate rat', + genotype='WT', + sex='M', + species='Rattus norvegicus', + subject_id='RAT123', + weight='2 kg', + date_of_birth=datetime(2024, 4, 10, 0, 21), + strain='my_strain', + ) + + def test_dob_no_time(self): + self.subject = Subject( + age='P90D', + age__reference="birth", + description='An unfortunate rat', + genotype='WT', + sex='M', + species='Rattus norvegicus', + subject_id='RAT123', + weight='2 kg', + date_of_birth=date(2024, 4, 10), + strain='my_strain', + ) + + class TestCacheSpec(TestCase): """Test whether the file can be written and read when caching the spec.""" @@ -643,22 +689,3 @@ def test_reftime_default(self): # 'timestamps_reference_time' should default to 'session_start_time' self.assertEqual(self.nwbfile.timestamps_reference_time, self.start_time) - -class TestTimestampsRefAware(TestCase): - def setUp(self): - self.start_time = datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal()) - self.ref_time_notz = datetime(1979, 1, 1, 0, 0, 0) - - def test_reftime_tzaware(self): - with self.assertRaises(ValueError): - # 'timestamps_reference_time' must be a timezone-aware datetime - NWBFile('test session description', - 'TEST124', - self.start_time, - timestamps_reference_time=self.ref_time_notz) - - -class TestTimezone(TestCase): - def test_raise_warning__add_missing_timezone(self): - with self.assertWarnsWith(UserWarning, "Date is missing timezone information. Updating to local timezone."): - _add_missing_timezone(datetime(2017, 5, 1, 12)) diff --git a/tests/unit/test_icephys.py b/tests/unit/test_icephys.py index e0e8332f9..8ff1a67ba 100644 --- a/tests/unit/test_icephys.py +++ b/tests/unit/test_icephys.py @@ -14,7 +14,6 @@ from pynwb.testing import TestCase from pynwb.file import NWBFile # Needed to test icephys functionality defined on NWBFile from datetime import datetime -from dateutil.tz import tzlocal def GetElectrode(): @@ -46,7 +45,7 @@ def test_sweep_table_depractation_warn(self): _ = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), + session_start_time=datetime(2017, 4, 3, 11), ic_electrodes=[self.icephys_electrode, ], sweep_table=SweepTable()) @@ -57,14 +56,14 @@ def test_ic_electrodes_parameter_deprecation(self): _ = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), + session_start_time=datetime(2017, 4, 3, 11), ic_electrodes=[self.icephys_electrode, ]) def test_icephys_electrodes_parameter(self): nwbfile = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), + session_start_time=datetime(2017, 4, 3, 11), icephys_electrodes=[self.icephys_electrode, ]) self.assertEqual(nwbfile.get_icephys_electrode('test_iS'), self.icephys_electrode) @@ -73,7 +72,7 @@ def test_add_ic_electrode_deprecation(self): nwbfile = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal())) + session_start_time=datetime(2017, 4, 3, 11)) msg = "NWBFile.add_ic_electrode has been replaced by NWBFile.add_icephys_electrode." with self.assertWarnsWith(DeprecationWarning, msg): @@ -83,7 +82,7 @@ def test_ic_electrodes_attribute_deprecation(self): nwbfile = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal()), + session_start_time=datetime(2017, 4, 3, 11), icephys_electrodes=[self.icephys_electrode, ]) # make sure NWBFile.ic_electrodes property warns @@ -100,7 +99,7 @@ def test_create_ic_electrode_deprecation(self): nwbfile = NWBFile( session_description='NWBFile icephys test', identifier='NWB123', # required - session_start_time=datetime(2017, 4, 3, 11, tzinfo=tzlocal())) + session_start_time=datetime(2017, 4, 3, 11)) device = Device(name='device_name') msg = "NWBFile.create_ic_electrode has been replaced by NWBFile.create_icephys_electrode." with self.assertWarnsWith(DeprecationWarning, msg): diff --git a/tests/unit/test_scratch.py b/tests/unit/test_scratch.py index 398ae2f78..c04a00e99 100644 --- a/tests/unit/test_scratch.py +++ b/tests/unit/test_scratch.py @@ -1,5 +1,4 @@ from datetime import datetime -from dateutil.tz import tzlocal import numpy as np from numpy.testing import assert_array_equal import pandas as pd @@ -15,7 +14,7 @@ def setUp(self): self.nwbfile = NWBFile( session_description='a file to test writing and reading scratch data', identifier='TEST_scratch', - session_start_time=datetime(2017, 5, 1, 12, 0, 0, tzinfo=tzlocal()) + session_start_time=datetime(2017, 5, 1, 12, 0, 0) ) def test_constructor_list(self):