diff --git a/.github/workflows/run_all_tests.yml b/.github/workflows/run_all_tests.yml index 1935c7392..dab896025 100644 --- a/.github/workflows/run_all_tests.yml +++ b/.github/workflows/run_all_tests.yml @@ -36,8 +36,8 @@ jobs: - { name: windows-python3.12 , test-tox-env: py312 , build-tox-env: build-py312 , python-ver: "3.12", os: windows-latest } - { name: windows-python3.12-upgraded , test-tox-env: py312-upgraded , build-tox-env: build-py312-upgraded , python-ver: "3.12", os: windows-latest } - { name: windows-python3.12-prerelease, test-tox-env: py312-prerelease, build-tox-env: build-py312-prerelease, python-ver: "3.11", os: windows-latest } - - { name: macos-python3.8-minimum , test-tox-env: py38-minimum , build-tox-env: build-py38-minimum , python-ver: "3.8" , os: macos-latest } - - { name: macos-python3.9 , test-tox-env: py39 , build-tox-env: build-py39 , python-ver: "3.9" , os: macos-latest } + - { name: macos-python3.8-minimum , test-tox-env: py38-minimum , build-tox-env: build-py38-minimum , python-ver: "3.8" , os: macos-13 } + - { name: macos-python3.9 , test-tox-env: py39 , build-tox-env: build-py39 , python-ver: "3.9" , os: macos-13 } - { name: macos-python3.10 , test-tox-env: py310 , build-tox-env: build-py310 , python-ver: "3.10", os: macos-latest } - { name: macos-python3.11 , test-tox-env: py311 , build-tox-env: build-py311 , python-ver: "3.11", os: macos-latest } - { name: macos-python3.12 , test-tox-env: py312 , build-tox-env: build-py312 , python-ver: "3.12", os: macos-latest } @@ -95,7 +95,7 @@ jobs: - { name: windows-gallery-python3.8-minimum , test-tox-env: gallery-py38-minimum , python-ver: "3.8" , os: windows-latest } - { name: windows-gallery-python3.12-upgraded , test-tox-env: gallery-py312-upgraded , python-ver: "3.12", os: windows-latest } - { name: windows-gallery-python3.12-prerelease, test-tox-env: gallery-py312-prerelease, python-ver: "3.12", os: windows-latest } - - { name: macos-gallery-python3.8-minimum , test-tox-env: gallery-py38-minimum , python-ver: "3.8" , os: macos-latest } + - { name: macos-gallery-python3.8-minimum , test-tox-env: gallery-py38-minimum , python-ver: "3.8" , os: macos-13 } - { name: macos-gallery-python3.12-upgraded , test-tox-env: gallery-py312-upgraded , python-ver: "3.12", os: macos-latest } - { name: macos-gallery-python3.12-prerelease , test-tox-env: gallery-py312-prerelease, python-ver: "3.12", os: macos-latest } steps: @@ -198,7 +198,7 @@ jobs: include: - { name: conda-linux-python3.12-ros3 , python-ver: "3.12", os: ubuntu-latest } - { name: conda-windows-python3.12-ros3, python-ver: "3.12", os: windows-latest } - - { name: conda-macos-python3.12-ros3 , python-ver: "3.12", os: macos-latest } + - { name: conda-macos-python3.12-ros3 , python-ver: "3.12", os: macos-13 } # This is due to DANDI not supporting osx-arm64. Will support macos-latest when this changes. steps: - name: Cancel non-latest runs uses: styfle/cancel-workflow-action@0.11.0 @@ -224,7 +224,7 @@ jobs: - name: Install run dependencies run: | pip install -r requirements-dev.txt - pip install -e . + pip install . conda info conda list pip list @@ -245,7 +245,7 @@ jobs: include: - { name: conda-linux-gallery-python3.12-ros3 , python-ver: "3.12", os: ubuntu-latest } - { name: conda-windows-gallery-python3.12-ros3, python-ver: "3.12", os: windows-latest } - - { name: conda-macos-gallery-python3.12-ros3 , python-ver: "3.12", os: macos-latest } + - { name: conda-macos-gallery-python3.12-ros3 , python-ver: "3.12", os: macos-13 } # This is due to DANDI not supporting osx-arm64. Will support macos-latest when this changes. steps: - name: Cancel non-latest runs uses: styfle/cancel-workflow-action@0.11.0 @@ -271,7 +271,7 @@ jobs: - name: Install run dependencies run: | pip install matplotlib - pip install -e . + pip install . pip list - name: Conda reporting diff --git a/.github/workflows/run_coverage.yml b/.github/workflows/run_coverage.yml index 400de00f3..5f060abbf 100644 --- a/.github/workflows/run_coverage.yml +++ b/.github/workflows/run_coverage.yml @@ -53,20 +53,20 @@ jobs: - name: Install package run: | - python -m pip install -e . # must install in editable mode for coverage to find sources + python -m pip install . python -m pip list - name: Run unit tests and generate coverage report run: | python -m coverage run test.py --pynwb python -m coverage xml # codecov uploader requires xml format - python -m coverage report -m + python -m coverage report - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: flags: unit - files: coverage.xml + file: coverage.xml fail_ci_if_error: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -77,13 +77,13 @@ jobs: # validation CLI tests generate separate .coverage files that need to be merged python -m coverage combine python -m coverage xml # codecov uploader requires xml format - python -m coverage report -m + python -m coverage report - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: flags: integration - files: coverage.xml + file: coverage.xml fail_ci_if_error: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/run_dandi_read_tests.yml b/.github/workflows/run_dandi_read_tests.yml index df9ef4613..cdde371c6 100644 --- a/.github/workflows/run_dandi_read_tests.yml +++ b/.github/workflows/run_dandi_read_tests.yml @@ -31,7 +31,7 @@ jobs: run: | python -m pip install dandi fsspec requests aiohttp pytest python -m pip uninstall -y pynwb # uninstall pynwb - python -m pip install -e . + python -m pip install . python -m pip list - name: Conda reporting diff --git a/.github/workflows/run_inspector_tests.yml b/.github/workflows/run_inspector_tests.yml index cb8d57458..120e30a79 100644 --- a/.github/workflows/run_inspector_tests.yml +++ b/.github/workflows/run_inspector_tests.yml @@ -34,8 +34,7 @@ jobs: git clone https://github.com/NeurodataWithoutBorders/nwbinspector.git cd nwbinspector python -m pip install -r requirements.txt pytest - # must install in editable mode for coverage to find sources - python -m pip install -e . # this might install a pinned version of pynwb instead of the current one + python -m pip install . # this might install a pinned version of pynwb instead of the current one cd .. python -m pip uninstall -y pynwb # uninstall the pinned version of pynwb python -m pip install . # reinstall current branch of pynwb diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index d2b3d169a..b512b2de0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -24,7 +24,7 @@ jobs: - { name: linux-python3.12-upgraded , test-tox-env: py312-upgraded , build-tox-env: build-py312-upgraded , python-ver: "3.12", os: ubuntu-latest , upload-wheels: true } - { name: windows-python3.8-minimum , test-tox-env: py38-minimum , build-tox-env: build-py38-minimum , python-ver: "3.8" , os: windows-latest } - { name: windows-python3.12-upgraded , test-tox-env: py312-upgraded , build-tox-env: build-py312-upgraded , python-ver: "3.12", os: windows-latest } - - { name: macos-python3.8-minimum , test-tox-env: py38-minimum , build-tox-env: build-py38-minimum , python-ver: "3.8" , os: macos-latest } + - { name: macos-python3.8-minimum , test-tox-env: py38-minimum , build-tox-env: build-py38-minimum , python-ver: "3.8" , os: macos-13 } steps: - name: Cancel non-latest runs uses: styfle/cancel-workflow-action@0.11.0 @@ -200,7 +200,7 @@ jobs: - name: Install run dependencies run: | pip install -r requirements-dev.txt - pip install -e . + pip install . conda info conda list pip list @@ -245,7 +245,7 @@ jobs: - name: Install run dependencies run: | pip install matplotlib - pip install -e . + pip install . pip list - name: Conda reporting diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1949712..4f61a0587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,29 @@ # PyNWB Changelog -## PyNWB 2.7.0 (Upcoming) +## PyNWB 2.8.0 (Upcoming) ### Enhancements and minor changes +- Set rate default value inside `mock_ElectricalSeries` to avoid having to set `rate=None` explicitly when passing timestamps. @h-mayorquin [#1894](https://github.com/NeurodataWithoutBorders/pynwb/pull/1894) + +## PyNWB 2.7.0 (May 2, 2024) + +### Enhancements and minor changes +- Added `bounds` field to `SpatialSeries` to set optional boundary range (min, max) for each dimension of data. @mavaylon1 [#1869](https://github.com/NeurodataWithoutBorders/pynwb/pull/1869/files) - Added support for NWB schema 2.7.0. See [2.7.0 release notes](https://nwb-schema.readthedocs.io/en/latest/format_release_notes.html) for details - - Deprecated `ImagingRetinotopy` neurodata type. @rly [#1813](https://github.com/NeurodataWithoutBorders/pynwb/pull/1813) - - Modified `OptogeneticSeries` to allow 2D data, primarily in extensions of `OptogeneticSeries`. @rly [#1812](https://github.com/NeurodataWithoutBorders/pynwb/pull/1812) - - 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) +- Deprecated `ImagingRetinotopy` neurodata type. @rly [#1813](https://github.com/NeurodataWithoutBorders/pynwb/pull/1813) +- Modified `OptogeneticSeries` to allow 2D data, primarily in extensions of `OptogeneticSeries`. @rly [#1812](https://github.com/NeurodataWithoutBorders/pynwb/pull/1812) +- 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 `grid_spacing`, `grid_spacing_unit`, `origin_coords`, `origin_coords_unit` to `ImagingPlane` fields. @h-mayorquin [#1892](https://github.com/NeurodataWithoutBorders/pynwb/pull/1892) +- 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) +- Updated testing to not install in editable mode and not run `coverage` by default. [#1897](https://github.com/NeurodataWithoutBorders/pynwb/pull/1897) ### 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/domain/plot_behavior.py b/docs/gallery/domain/plot_behavior.py index 8f341bea1..35fbae81f 100644 --- a/docs/gallery/domain/plot_behavior.py +++ b/docs/gallery/domain/plot_behavior.py @@ -105,6 +105,9 @@ # # For position data ``reference_frame`` indicates the zero-position, e.g. # the 0,0 point might be the bottom-left corner of an enclosure, as viewed from the tracking camera. +# In :py:class:`~pynwb.behavior.SpatialSeries`, the ``bounds`` field allows the user to set +# the boundary range, i.e., (min, max), for each dimension of ``data``. The units are the same as in ``data``. +# This field does not enforce a boundary on the dataset itself. timestamps = np.linspace(0, 50) / 200 @@ -112,6 +115,7 @@ name="SpatialSeries", description="Position (x, y) in an open field.", data=position_data, + bounds=[(0,50), (0,50)], timestamps=timestamps, reference_frame="(0,0) is bottom left corner", ) 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/docs/source/conf.py b/docs/source/conf.py index 6f69fc4c1..4eaf1a19b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -168,6 +168,8 @@ def __call__(self, filename): nitpick_ignore = [('py:class', 'Intracomm'), ('py:class', 'BaseStorageSpec')] +suppress_warnings = ["config.cache"] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/environment-ros3.yml b/environment-ros3.yml index 155d2a938..8c93623bc 100644 --- a/environment-ros3.yml +++ b/environment-ros3.yml @@ -5,11 +5,11 @@ channels: - defaults dependencies: - python==3.12 - - h5py==3.10.0 - - hdmf==3.12.2 + - h5py==3.11.0 + - hdmf==3.13.0 - matplotlib==3.8.0 - - numpy==1.26 - - pandas==2.1.2 + - numpy==1.26.4 + - pandas==2.2.1 - python-dateutil==2.8.2 - setuptools - pytest==7.4.3 # This is for the upcoming pytest update diff --git a/pyproject.toml b/pyproject.toml index 1bae035af..ab2fceb33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ classifiers = [ dependencies = [ "h5py>=2.10", "hdmf>=3.12.2", - "numpy>=1.18", # match the version used in hdmf + "numpy>=1.18, <2.0", # pin below 2.0 until HDMF supports numpy 2.0 "pandas>=1.1.5", "python-dateutil>=2.7.3", ] @@ -58,28 +58,24 @@ version-file = "src/pynwb/_version.py" [tool.hatch.build.targets.wheel] packages = ["src/pynwb"] -# [tool.pytest.ini_options] # TODO: uncomment when pytest is integrated -# # Addopts creates a shortcut for pytest. For example below, running `pytest` will actually run `pytest --cov --cov-report html`. -# addopts = "--cov --cov-report html" # generates coverage report in html format without showing anything on the terminal. - [tool.codespell] skip = "htmlcov,.git,.mypy_cache,.pytest_cache,.coverage,*.pdf,*.svg,venvs,.tox,nwb-schema,./docs/_build/*,*.ipynb" ignore-words-list = "optin,potatos" [tool.coverage.run] branch = true -source = ["src/"] -omit = [ - "src/pynwb/_due.py", - "src/pynwb/testing/*", - "src/pynwb/legacy/*" -] +source = ["pynwb"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@abstract" ] +omit = [ + "*/pynwb/_due.py", + "*/pynwb/testing/*", + "*/pynwb/legacy/*" +] [tool.ruff] select = ["E", "F", "T100", "T201", "T203"] diff --git a/requirements.txt b/requirements.txt index 836052317..dd152ad16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # pinned dependencies to reproduce an entire development environment to use PyNWB -h5py==3.10.0 -hdmf==3.12.2 -numpy==1.26.1 -pandas==2.1.2 -python-dateutil==2.8.2 +h5py==3.11.0 +hdmf==3.13.0 +numpy==1.26.4 +pandas==2.2.1 +python-dateutil==2.9.0.post0 diff --git a/src/pynwb/behavior.py b/src/pynwb/behavior.py index b9388e8df..1b3078535 100644 --- a/src/pynwb/behavior.py +++ b/src/pynwb/behavior.py @@ -23,9 +23,11 @@ class SpatialSeries(TimeSeries): __nwbfields__ = ('reference_frame',) @docval(*get_docval(TimeSeries.__init__, 'name'), # required - {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ((None, ), (None, None)), # required + {'name': 'data', 'type': ('array_data', 'data', TimeSeries), 'shape': ((None, ), (None, None)), # required 'doc': ('The data values. Can be 1D or 2D. The first dimension must be time. If 2D, there can be 1, 2, ' 'or 3 columns, which represent x, y, and z.')}, + {'name': 'bounds', 'type': list, 'shape': ((1, 2), (2, 2), (3, 2)), 'default': None, + 'doc': 'The boundary range (min, max) for each dimension of data.'}, {'name': 'reference_frame', 'type': str, # required 'doc': 'description defining what the zero-position is'}, {'name': 'unit', 'type': str, 'doc': 'The base unit of measurement (should be SI unit)', @@ -36,7 +38,7 @@ def __init__(self, **kwargs): """ Create a SpatialSeries TimeSeries dataset """ - name, data, reference_frame, unit = popargs('name', 'data', 'reference_frame', 'unit', kwargs) + name, data, bounds, reference_frame, unit = popargs('name', 'data', 'bounds', 'reference_frame', 'unit', kwargs) super().__init__(name, data, unit, **kwargs) # NWB 2.5 restricts length of second dimension to be <= 3 @@ -47,6 +49,7 @@ def __init__(self, **kwargs): "The second dimension should have length <= 3 to represent at most x, y, z." % (name, str(data_shape))) + self.bounds = bounds self.reference_frame = reference_frame @staticmethod 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/ophys.py b/src/pynwb/ophys.py index bdff47592..6f1483079 100644 --- a/src/pynwb/ophys.py +++ b/src/pynwb/ophys.py @@ -43,7 +43,12 @@ class ImagingPlane(NWBContainer): 'manifold', 'conversion', 'unit', - 'reference_frame') + 'reference_frame', + 'grid_spacing', + 'grid_spacing_unit', + 'origin_coords', + 'origin_coords_unit' + ) @docval(*get_docval(NWBContainer.__init__, 'name'), # required {'name': 'optical_channel', 'type': (list, OpticalChannel), # required diff --git a/src/pynwb/testing/mock/ecephys.py b/src/pynwb/testing/mock/ecephys.py index 97831c3de..0669e7493 100644 --- a/src/pynwb/testing/mock/ecephys.py +++ b/src/pynwb/testing/mock/ecephys.py @@ -70,7 +70,7 @@ def mock_ElectricalSeries( name: Optional[str] = None, description: str = "description", data=None, - rate: float = 30000.0, + rate: Optional[float] = None, timestamps=None, starting_time: Optional[float] = None, electrodes: Optional[DynamicTableRegion] = None, @@ -80,6 +80,10 @@ def mock_ElectricalSeries( conversion: float = 1.0, offset: float = 0., ) -> ElectricalSeries: + + # Set a default rate if timestamps are not provided + rate = 30_000.0 if (timestamps is None and rate is None) else rate + electrical_series = ElectricalSeries( name=name or name_generator("ElectricalSeries"), description=description, @@ -129,7 +133,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_behavior.py b/tests/integration/hdf5/test_behavior.py new file mode 100644 index 000000000..d4b230bcc --- /dev/null +++ b/tests/integration/hdf5/test_behavior.py @@ -0,0 +1,17 @@ +import numpy as np + +from pynwb.behavior import SpatialSeries +from pynwb.testing import AcquisitionH5IOMixin, TestCase + + +class TestSpatialSeriesIO(AcquisitionH5IOMixin, TestCase): + + def setUpContainer(self): + """ Return the test TimeSeries to read/write """ + return SpatialSeries( + name='test_sS', + data=np.ones((3, 2)), + bounds=[(-1,1),(-1,1),(-1,1)], + reference_frame='reference_frame', + timestamps=[1., 2., 3.] + ) 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_behavior.py b/tests/unit/test_behavior.py index 0b7173da0..6bcf1a9eb 100644 --- a/tests/unit/test_behavior.py +++ b/tests/unit/test_behavior.py @@ -12,11 +12,13 @@ def test_init(self): sS = SpatialSeries( name='test_sS', data=np.ones((3, 2)), + bounds=[(-1,1),(-1,1),(-1,1)], reference_frame='reference_frame', timestamps=[1., 2., 3.] ) self.assertEqual(sS.name, 'test_sS') self.assertEqual(sS.unit, 'meters') + self.assertEqual(sS.bounds, [(-1,1),(-1,1),(-1,1)]) self.assertEqual(sS.reference_frame, 'reference_frame') def test_set_unit(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): diff --git a/tox.ini b/tox.ini index 798da6fe6..8aecbb544 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ requires = pip >= 22.0 [testenv] download = True -usedevelop = True setenv = PYTHONDONTWRITEBYTECODE = 1 VIRTUALENV_PIP = 23.3.1 @@ -112,7 +111,7 @@ install_command = deps = -rrequirements.txt commands = - python -m pip install -e . + python -m pip install . python -m pip install -r requirements-doc.txt # NOTE: allensdk (requirements-doc.txt) requires pynwb python -m pip check python -m pip list @@ -144,7 +143,7 @@ basepython = python3.12 deps = -rrequirements-dev.txt commands = - python -m pip install -U -e . + python -m pip install -U . python -m pip install -r requirements-doc.txt # NOTE: allensdk (requirements-doc.txt) requires pynwb python -m pip check python -m pip list @@ -156,7 +155,7 @@ basepython = python3.12 deps = -rrequirements-dev.txt commands = - python -m pip install -U --pre -e . + python -m pip install -U --pre . python -m pip install -r requirements-doc.txt # NOTE: allensdk (requirements-doc.txt) requires pynwb python -m pip check python -m pip list