Repository: h5py/h5py
Pull request #2485 (2024-08-23 09:03:58)

DOC: unlimited maxshape for 1D Datasets
I ran into a minor lack of documentation, which might actually be pretty obvious for advanced users, and I also figured it out quite fast, but I anyway wanted to ask if it is of interest, to extend the documentation on this level. Just to illustrate the problem, here a MWE:

```py
N_tsvd_iterations = 10
with h5py.File("foobar.h5", 'w-') as f:
    dset = f.create_dataset('test_data', (N_tsvd_iterations,), maxshape=None, 
                      chunks=True, dtype=np.float64)
    print(dset.shape)
    print(dset.maxshape)
    dset = f.create_dataset('test_data_2', (N_tsvd_iterations,), maxshape=(None,), 
                      chunks=True, dtype=np.float64)
    print(dset.shape)
    print(dset.maxshape)
```
The output reads as follows:
```
(10,)
(10,)
(10,)
(None,)
```

While it is somewhat obvious, since passing `maxshape=None` is identical to not passing it at all, one may anyways run into the issue when extending a finite `maxshape` to an unlimited one, i. e. naively replacing the given finite integer with `None`.

Therefore, I mentioned this case both in the docstring of `create_dataset` and in the main documentation of `Dataset`. Maybe there are better ways of handling this special case, but for the moment, I thought it would be good to just document it.

What do you think about this case?


PS: I did not run any tests as asked in the template as I didn't change any python code, only docs. Should I anyway do it? Probably it would make more sense to test build the documentation, wouldn't it?



Patch:
diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
index aa60bc95..599b70e1 100644
--- a/.github/workflows/build_wheels.yml
+++ b/.github/workflows/build_wheels.yml
@@ -56,7 +56,7 @@ jobs:
           - "3.10"
           - "3.11"
           - "3.12"
-          - "3.13.0rc1"
+          - "3.13"
         include:
           - os: ubuntu-latest
             arch: x86_64
@@ -108,7 +108,7 @@ jobs:
         id: triage
 
       # Now actually build the wheels
-      - uses: pypa/cibuildwheel@v2.20.0
+      - uses: pypa/cibuildwheel@v2.21.3
         env:
           # Note that tool.cibuildwheel "skip" in pyproject.toml also limits what gets built
           CIBW_ARCHS: ${{ matrix.arch }}
@@ -121,6 +121,9 @@ jobs:
           CIBW_BEFORE_TEST: ${{ env.CIBW_BEFORE_TEST }}
         if: steps.triage.outputs.skip != '1'
 
+      - run: python ./ci/bundle_hdf5_whl.py wheelhouse
+        if: steps.triage.outputs.skip != '1' && runner.os == 'Windows'
+
       # And upload the results
       - uses: actions/upload-artifact@v4
         with:
@@ -145,7 +148,7 @@ jobs:
       # If it's a PR, do nothing else.
       # If it's a scheduled run, it was built with nightly NumPy, push to scientific-python:
       - name: Upload wheels to Anaconda Cloud as nightlies
-        uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
+        uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1
         with:
           artifacts_path: dist
           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
diff --git a/.travis.yml b/.travis.yml
index 6c7c862d..e5446cbd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,7 +23,7 @@ env:
     - HDF5_CACHE_DIR=$HOME/.cache/hdf5
     - TERM=dumb
     - TOXENV=py311-test-deps
-    - HDF5_VERSION=1.10.5
+    - HDF5_VERSION=1.10.6
     - HDF5_DIR=$HDF5_CACHE_DIR/$HDF5_VERSION
     - H5PY_ENFORCE_COVERAGE=yes
 
diff --git a/api_gen.py b/api_gen.py
index bfe43757..983ccfb3 100644
--- a/api_gen.py
+++ b/api_gen.py
@@ -185,6 +185,9 @@ from ._errors cimport set_exception, set_default_error_handler
 
 class LineProcessor:
 
+    def __init__(self, config) -> None:
+        self.config = config
+
     def run(self):
 
         # Function definitions file
@@ -232,51 +235,42 @@ class LineProcessor:
         replace_or_remove(path_cython_defs)
         replace_or_remove(path_raw_defs)
 
-    def add_cython_if(self, block):
-        """ Wrap a block of code in the required "IF" checks """
-
-        def wrapif(condition, code):
-            code = code.replace('\n', '\n    ', code.count('\n') - 1)  # Yes, -1.
-            code = "IF {0}:\n    {1}".format(condition, code)
-            return code
-
-        if self.line.mpi:
-            block = wrapif('MPI', block)
-
-        if self.line.ros3:
-            block = wrapif('ROS3', block)
-
-        if self.line.direct_vfd:
-            block = wrapif('DIRECT_VFD', block)
-
-        if self.line.min_version is not None and self.line.max_version is not None:
-            block = wrapif('HDF5_VERSION >= {0.min_version} and HDF5_VERSION <= {0.max_version}'.format(self.line), block)
-        elif self.line.min_version is not None:
-            block = wrapif('HDF5_VERSION >= {0.min_version}'.format(self.line), block)
-        elif self.line.max_version is not None:
-            block = wrapif('HDF5_VERSION <= {0.max_version}'.format(self.line), block)
-
-        return block
+    def check_settings(self) -> bool:
+        """ Return True if and only if the code block should be compiled within given settings """
+        if (
+            (self.line.mpi and not self.config.mpi)
+            or (self.line.ros3 and not self.config.ros3)
+            or (self.line.direct_vfd and not self.config.direct_vfd)
+            or (self.line.min_version is not None and self.config.hdf5_version < self.line.min_version)
+            or (self.line.max_version is not None and self.config.hdf5_version > self.line.max_version)
+        ):
+            return False
+        else:
+            return True
 
     def write_raw_sig(self):
         """ Write out "cdef extern"-style definition for an HDF5 function """
+        if not self.check_settings():
+            return
         raw_sig = "{0.code} {0.fname}({0.sig}) {0.nogil}\n".format(self.line)
-        raw_sig = self.add_cython_if(raw_sig)
         raw_sig = "\n".join(("    " + x if x.strip() else x) for x in raw_sig.split("\n"))
         self.raw_defs.write(raw_sig)
 
     def write_cython_sig(self):
         """ Write out Cython signature for wrapper function """
+        if not self.check_settings():
+            return
         if self.line.fname == 'H5Dget_storage_size':
             # Special case: https://github.com/h5py/h5py/issues/1475
             cython_sig = "cdef {0.code} {0.fname}({0.sig}) except? {0.err_value}\n".format(self.line)
         else:
             cython_sig = "cdef {0.code} {0.fname}({0.sig}) except {0.err_value}\n".format(self.line)
-        cython_sig = self.add_cython_if(cython_sig)
         self.cython_defs.write(cython_sig)
 
     def write_cython_imp(self):
         """ Write out Cython wrapper implementation """
+        if not self.check_settings():
+            return
         if self.line.nogil:
             imp = """\
 cdef {0.code} {0.fname}({0.sig}) except {0.err_value}:
@@ -321,12 +315,11 @@ cdef {0.code} {0.fname}({0.sig}) except {0.err_value}:
 
 """
         imp = imp.format(self.line)
-        imp = self.add_cython_if(imp)
         self.cython_imp.write(imp)
 
 
-def run():
-    lp = LineProcessor()
+def run(config):
+    lp = LineProcessor(config)
     lp.run()
 
 
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index f7009fc7..e6365593 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -8,7 +8,7 @@ variables:
 jobs:
 - job: 'static_checks'
   pool:
-    vmImage: ubuntu-20.04
+    vmImage: ubuntu-22.04
   variables:
     TOXENV: docs,checkreadme,rever
   steps:
@@ -25,7 +25,7 @@ jobs:
 
 - job: 'ubuntu'
   pool:
-    vmImage: ubuntu-20.04
+    vmImage: ubuntu-22.04
   strategy:
     matrix:
       # -deps : test with default (latest) versions of dependencies
@@ -67,7 +67,7 @@ jobs:
         HDF5_DIR: $(HDF5_CACHE_DIR)/$(HDF5_VERSION)
         H5PY_ENFORCE_COVERAGE: yes
     # do mpi tests
-      py39-deps-hdf51105-mpi:
+      py39-deps-hdf51122-mpi:
         python.version: '3.9'
         TOXENV: py39-test-mindeps-mpi4py
         HDF5_VERSION: 1.12.2
diff --git a/ci/triage_build.sh b/ci/triage_build.sh
index 382db64f..a5b97cdb 100755
--- a/ci/triage_build.sh
+++ b/ci/triage_build.sh
@@ -23,7 +23,7 @@ if [[ "$ARCH" == "aarch64" ]]; then
     echo "TOX_TEST_LIMITED=1" | tee -a $GITHUB_ENV
     if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && [[ "$MSG" != *'[aarch64]'* ]]; then
         echo "Building limited $KIND wheels for PR with commit message: $MSG"
-        if [[ "$PYTHON" != "3.12" ]]; then
+        if [[ "$PYTHON" != "3.13" ]]; then
             echo "skip=1" >> $GITHUB_OUTPUT
         fi
         REPORTED=1
@@ -33,21 +33,18 @@ if [[ "$REPORTED" != "1" ]]; then
     echo "Building full $KIND wheels on event_name=$GITHUB_EVENT_NAME with commit message: $MSG"
 fi
 CIBW_SKIP="pp* *musllinux*"
-CIBW_PRERELEASE_PYTHONS=false
 # If it's a scheduled build or [pip-pre] in commit message, use pip-pre
 if [[ "$GITHUB_EVENT_NAME" == "schedule" ]] || [[ "$MSG" = *'[pip-pre]'* ]]; then
-    echo "Using NumPy pip-pre wheel and (on Linux) enabling Python 3.13 for wheel building, setting CIBW_BEFORE_BUILD, CIBW_BUILD_FRONTEND, and CIBW_PRERELEASE_PYTHONS"
-    # No Python 3.9 on scientific-python-nightly-wheels
-    CIBW_SKIP="$CIBW_SKIP cp39-*"
-    echo "CIBW_BEFORE_BUILD=pip install --pre --only-binary numpy --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \"numpy>=2.1.0.dev0\" \"Cython>=0.29.31,<4\" pkgconfig \"setuptools>=61\" wheel" | tee -a $GITHUB_ENV
+    echo "Using NumPy pip-pre wheel and (on Linux), setting CIBW_BEFORE_BUILD, CIBW_BUILD_FRONTEND and CIBW_BEFORE_TEST"
+    echo "CIBW_BEFORE_BUILD=pip install --pre --only-binary numpy --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \"numpy>=2.0.0.dev0\" \"Cython>=0.29.31,<4\" pkgconfig \"setuptools>=61\" wheel" | tee -a $GITHUB_ENV
     echo "CIBW_BUILD_FRONTEND=pip; args: --no-build-isolation" | tee -a $GITHUB_ENV
     # This is harder on other architectures, so only do it on Linux for now
-    if [[ "$RUNNER_OS" == "Linux" ]] && [[ "$ARCH" == "x86_64" ]]; then
-        CIBW_PRERELEASE_PYTHONS=true
-    fi
-    echo "CIBW_BEFORE_TEST=pip install --pre --only-binary numpy --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \"numpy>=2.1.0.dev0\"" | tee -a $GITHUB_ENV
+    echo "CIBW_BEFORE_TEST=pip install --pre --only-binary numpy --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \"numpy>=2.0.0.dev0\"" | tee -a $GITHUB_ENV
 fi
 
+# strip '-dev' suffix for pre-releases Pythons
+PYTHON="${PYTHON%-dev*}"
+
 # replace dots in PYTHON with nothing, e.g., 3.8->38
 CIBW_BUILD="cp${PYTHON//./}-*_$ARCH"
 echo "CIBW_BUILD=$CIBW_BUILD" | tee -a $GITHUB_ENV
diff --git a/docs/conf.py b/docs/conf.py
index c92ce049..bb4f1354 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -62,7 +62,7 @@ copyright = '2014, Andrew Collette and contributors'
 # built documents.
 #
 # The full version, including alpha/beta/rc tags.
-release = '3.11.0'
+release = '3.12.1'
 # The short X.Y version.
 version = '.'.join(release.split('.')[:2])
 
diff --git a/docs/contributing.rst b/docs/contributing.rst
index ef747255..9434eea3 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -231,7 +231,7 @@ opening the various files as we work through the example.
 
 First, get ahold of
 the function signature; the easiest place for this is at the `online
-HDF5 Reference Manual <https://support.hdfgroup.org/HDF5/doc/RM/RM_H5Front.html>`_.
+HDF5 Reference Manual <https://support.hdfgroup.org/documentation/hdf5/latest/_r_m.htmll>`_.
 Then, add the function's C signature to the file ``api_functions.txt``::
 
   hsize_t   H5Dget_storage_size(hid_t dset_id)
diff --git a/docs/high/dataset.rst b/docs/high/dataset.rst
index 40cc151f..debc45ba 100644
--- a/docs/high/dataset.rst
+++ b/docs/high/dataset.rst
@@ -214,6 +214,10 @@ axes using ``None``::
 
     >>> dset = f.create_dataset("unlimited", (10, 10), maxshape=(None, 10))
 
+For a 1D dataset, ``maxshape`` can be an integer instead of a tuple. But to make an
+unlimited 1D dataset, ``maxshape`` must be a tuple ``(None,)``. Passing ``None`` gives
+the default behaviour, where the initial size is also the maximum.
+
 .. note:: Resizing an array with existing data works differently than in NumPy; if
     any axis shrinks, the data in the missing region is discarded.  Data does
     not "rearrange" itself as it does when resizing a NumPy array.
@@ -272,10 +276,10 @@ The ``compression_opts`` parameter will then be passed to this filter.
      A Python package of several popular filters, including Blosc, LZ4 and ZFP,
      for convenient use with h5py
 
-   `HDF5 Filter Plugins <https://portal.hdfgroup.org/display/support/HDF5+Filter+Plugins>`_
+   `HDF5 Filter Plugins <https://github.com/HDFGroup/hdf5_plugins/releases>`_
      A collection of filters as a single download from The HDF Group
 
-   `Registered filter plugins <https://portal.hdfgroup.org/display/support/Filters>`_
+   `Registered filter plugins <https://github.com/HDFGroup/hdf5_plugins/blob/master/docs/RegisteredFilterPlugins.md>`_
      The index of publicly announced filter plugins
 
 .. note:: The underlying implementation of the compression filter will have the
@@ -345,7 +349,7 @@ A MultiBlockSlice can be used in place of a slice to select a number of (count)
 blocks of multiple elements separated by a stride, rather than a set of single
 elements separated by a step.
 
-For an explanation of how this slicing works, see the `HDF5 documentation <https://support.hdfgroup.org/HDF5/Tutor/selectsimple.html>`_.
+For an explanation of how this slicing works, see the `HDF5 documentation <https://support.hdfgroup.org/documentation/hdf5/latest/_l_b_dset_sub_r_w.html>`_.
 
 For example::
 
diff --git a/docs/high/dims.rst b/docs/high/dims.rst
index 8fe913ad..77719ea6 100644
--- a/docs/high/dims.rst
+++ b/docs/high/dims.rst
@@ -95,7 +95,7 @@ returned. There is no guarantee that the name of the dimension scale is unique.
 
 Nested dimension scales are not permitted: if a dataset has a dimension scale
 attached to it, converting the dataset to a dimension scale will fail, since the
-`HDF5 specification doesn't allow this <https://confluence.hdfgroup.org/display/HDF5/H5DS_SET_SCALE>`_. ::
+`HDF5 specification doesn't allow this <https://support.hdfgroup.org/documentation/hdf5/latest/group___h5_d_s.html#ga508a06962e9fc11dff32ed356e0a71fa>`_. ::
 
    >>> f['data'].make_scale()
    RuntimeError: Unspecified error in H5DSset_scale (return value <0)
diff --git a/docs/high/file.rst b/docs/high/file.rst
index e757fe1a..a9f3c266 100644
--- a/docs/high/file.rst
+++ b/docs/high/file.rst
@@ -382,7 +382,7 @@ chunk cache*. They apply to all datasets unless specifically changed for each on
   approximately 100 times that number of chunks. The default value is 521.
 
 Chunks and caching are described in greater detail in the `HDF5 documentation
-<https://portal.hdfgroup.org/display/HDF5/Chunking+in+HDF5>`_.
+<https://support.hdfgroup.org/documentation/hdf5-docs/advanced_topics/chunking_in_hdf5.html>`_.
 
 .. _file_alignment:
 
@@ -401,7 +401,7 @@ takes effect and the alignment in bytes within the file. The alignment is
 measured from the end of the user block.
 
 For more information, see the official HDF5 documentation `H5P_SET_ALIGNMENT
-<https://portal.hdfgroup.org/display/HDF5/H5P_SET_ALIGNMENT>`_.
+<https://support.hdfgroup.org/documentation/hdf5/latest/group___f_a_p_l.html#gab99d5af749aeb3896fd9e3ceb273677a>`_.
 
 .. _file_meta_block_size:
 
@@ -416,7 +416,7 @@ especially in combination with the ``libver`` option. This controls how the
 overall data and metadata are laid out within the file.
 
 For more information, see the official HDF5 documentation `H5P_SET_META_BLOCK_SIZE
-<https://portal.hdfgroup.org/display/HDF5/H5P_SET_META_BLOCK_SIZE>`_.
+<https://support.hdfgroup.org/documentation/hdf5/latest/group___f_a_p_l.html#ga8822e3dedc8e1414f20871a87d533cb1>`_.
 
 Reference
 ---------
diff --git a/docs/high/group.rst b/docs/high/group.rst
index e6d8885f..7b222a06 100644
--- a/docs/high/group.rst
+++ b/docs/high/group.rst
@@ -382,6 +382,15 @@ Reference
         :keyword fillvalue: This value will be used when reading
                             uninitialized parts of the dataset.
 
+        :keyword fill_time: Control when to write the fill value. One of the
+            following choices: `alloc`, write fill value before writing
+            application data values or when the dataset is created; `never`,
+            never write fill value; `ifset`, write fill value if it is defined.
+            Default to `ifset`, which is the default of HDF5 library. If the
+            whole dataset is going to be written by the application, setting
+            this to `never` can avoid unnecessary writing of fill value and
+            potentially improve performance.
+
         :keyword track_times: Enable dataset creation timestamps (**T**/F).
 
         :keyword track_order: Track attribute creation order if
diff --git a/docs/high/lowlevel.rst b/docs/high/lowlevel.rst
index 3b35fc38..c6c92821 100644
--- a/docs/high/lowlevel.rst
+++ b/docs/high/lowlevel.rst
@@ -8,7 +8,7 @@ h5py also provides a low-level API, which more closely follows the HDF5 C API.
 .. seealso::
 
    - `h5py Low-Level API Reference <https://api.h5py.org/>`_
-   - `HDF5 C/Fortran Reference Manual <https://confluence.hdfgroup.org/display/HDF5/Core+Library>`_
+   - `HDF5 C/Fortran Reference Manual <https://support.hdfgroup.org/documentation/hdf5/latest/_r_m.html>`_
 
 You can easily switch between the two levels in your code:
 
diff --git a/docs/index.rst b/docs/index.rst
index a1d579ad..d3aaecb6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,7 +3,7 @@ HDF5 for Python
 
 The h5py package is a Pythonic interface to the HDF5 binary data format.
 
-`HDF5 <https://hdfgroup.org>`_ lets you store huge amounts of numerical
+`HDF5 <https://www.hdfgroup.org/solutions/hdf5/>`_ lets you store huge amounts of numerical
 data, and easily manipulate that data from NumPy. For example, you can slice
 into multi-terabyte datasets stored on disk, as if they were real NumPy
 arrays. Thousands of datasets can be stored in a single file, categorized and
diff --git a/docs/related_projects.rst b/docs/related_projects.rst
index 55654a29..e886cba9 100644
--- a/docs/related_projects.rst
+++ b/docs/related_projects.rst
@@ -14,11 +14,6 @@ take advantage of some of HDF5's features. h5py provides a comparison between
 the two projects (see :ref:`h5py_pytable_cmp`), as does the
 `PyTables project <https://www.pytables.org/FAQ.html#how-does-pytables-compare-with-the-h5py-project>`_.
 
-.. seealso::
-
-   `HDF Group's list of HDF5 tools
-   <https://portal.hdfgroup.org/display/HDF5/HDF5+Tools+by+Category>`_
-
 .. contents::
    :local:
 
@@ -66,7 +61,7 @@ Exploring and Visualising HDF5 files
 h5py does not contain a tool for exploring or visualising HDF5 files, but tools
 that can display the structure of h5py include:
 
-* `HDFView <https://confluence.hdfgroup.org/display/HDFVIEW/HDFView>`_ is a
+* `HDFView <https://www.hdfgroup.org/download-hdfview/>`_ is a
   visual tool for browsing and editing HDF5 files.
 * `ViTables <https://vitables.org/>`_ is a GUI for browsing and editing files
   in both PyTables and HDF5 formats, and is built on top of PyTables.
diff --git a/docs/swmr.rst b/docs/swmr.rst
index 89580a62..f5c6bcef 100644
--- a/docs/swmr.rst
+++ b/docs/swmr.rst
@@ -37,7 +37,7 @@ creating the file.
 
 
 The HDF Group has documented the SWMR features in details on the website:
-`Single-Writer/Multiple-Reader (SWMR) Documentation <https://support.hdfgroup.org/HDF5/docNewFeatures/NewFeaturesSwmrDocs.html>`_.
+`Single-Writer/Multiple-Reader (SWMR) Documentation <https://support.hdfgroup.org/documentation/hdf5/latest/_s_w_m_r.html>`_.
 This is highly recommended reading for anyone intending to use the SWMR feature
 even through h5py. For production systems in particular pay attention to the
 file system requirements regarding POSIX I/O semantics.
diff --git a/docs/vds.rst b/docs/vds.rst
index bd47ad1c..a26d862a 100644
--- a/docs/vds.rst
+++ b/docs/vds.rst
@@ -25,11 +25,11 @@ HDF5 dataset.
 
 .. Warning::
 
-   Virtual dataset files cannot be opened with versions of the hdf5 library
+   Virtual dataset files cannot be opened with versions of the HDF5 library
    older than 1.10.
 
-The HDF Group has documented the VDS features in detail on the website:
-`Virtual Datasets (VDS) Documentation <https://support.hdfgroup.org/HDF5/docNewFeatures/NewFeaturesVirtualDatasetDocs.html>`_.
+The HDF Group has overview of the VDS features on their website:
+`Virtual Datasets (VDS) Documentation <https://support.hdfgroup.org/documentation/hdf5/latest/_v_d_s.html>`_.
 
 .. _creating_vds:
 
@@ -89,7 +89,7 @@ found in the examples folder:
 - `dataset_concatenation.py <https://github.com/h5py/h5py/blob/master/examples/dataset_concatenation.py>`_
   illustrates virtually stacking datasets together along a new axis.
 - A number of examples are based on the sample use cases presented in the
-  `virtual datasets RFC <https://support.hdfgroup.org/HDF5/docNewFeatures/VDS/HDF5-VDS-requirements-use-cases-2014-12-10.pdf>`__:
+  `virtual datasets RFC <https://support.hdfgroup.org/releases/hdf5/documentation/rfc/HDF5-VDS-requirements-use-cases-2014-12-10.pdf>`__:
 
   - `excalibur_detector_modules.py <https://github.com/h5py/h5py/blob/master/examples/excalibur_detector_modules.py>`_
   - `dual_pco_edge.py <https://github.com/h5py/h5py/blob/master/examples/dual_pco_edge.py>`_
@@ -125,7 +125,7 @@ Reference
 
    When `creating a virtual dataset <creating_vds_>`_, paths to sources present
    in the same file are changed to a ".", referring to the current file (see
-   `H5Pset_virtual <https://portal.hdfgroup.org/display/HDF5/H5P_SET_VIRTUAL>`_).
+   `H5Pset_virtual <https://support.hdfgroup.org/documentation/hdf5/latest/group___d_c_p_l.html#gadec895092dbbedb94f85d9cacf8924f5>`_).
    This will keep such sources valid in case the file is renamed.
 
    :param path_or_dataset:
diff --git a/docs/whatsnew/2.7.rst b/docs/whatsnew/2.7.rst
index 7798c0ac..06b2f58d 100644
--- a/docs/whatsnew/2.7.rst
+++ b/docs/whatsnew/2.7.rst
@@ -41,8 +41,8 @@ Other changes
 * Avoid errors when the Python GC runs during ``nonlocal_close()`` (:pr:`776` by Antoine Pitrou)
 * Move from ``six.PY3`` to ``six.PY2`` (:pr:`686` by James Tocknell)
 
-.. _`HDF5 Direct Chunk Write` : https://support.hdfgroup.org/HDF5/doc/Advanced/DirectChunkWrite/
-.. _`HDF5 File Image Operations` : https://support.hdfgroup.org/HDF5/doc/Advanced/FileImageOperations/HDF5FileImageOperations.pdf
+.. _`HDF5 Direct Chunk Write` : https://support.hdfgroup.org/releases/hdf5/documentation/rfc/DECTRIS%20Integration%20RFC%202012-11-29.pdf
+.. _`HDF5 File Image Operations` : https://support.hdfgroup.org/documentation/hdf5/latest/_f_i_l_e_i_m_g_o_p_s.html
 
 Acknowledgements
 ----------------
diff --git a/docs/whatsnew/3.12.rst b/docs/whatsnew/3.12.rst
new file mode 100644
index 00000000..6d40cb96
--- /dev/null
+++ b/docs/whatsnew/3.12.rst
@@ -0,0 +1,56 @@
+What's new in h5py 3.12
+=======================
+
+New features
+------------
+
+* h5py now has pre-built packages for Python 3.13.
+
+  * However, h5py is not yet compatible with the new free-threading mode;
+    we're tracking work on that in issue :issue:`2475`.
+
+Breaking changes
+----------------
+
+* Support for Python 3.8 was dropped (:pr:`2471`). Python 3.9 or newer is
+  required to build or install h5py 3.12.
+* The minimum supported version of HDF5 was increased to 1.10.6 (:pr:`2486`).
+  If you need h5py on HDF5 1.10.4 or .5, please use h5py 3.11.
+* The fill time for chunked storage was previously set to ``h5d.FILL_TIME_ALLOC``.
+  Now this the default comes from HDF5, which uses ``h5d.FILL_TIME_IFSET``
+  (equivalent to ``fill_time='ifset'``) (:pr:`2463`). Please use
+  ``fill_time='alloc'`` if the change is a problem for you.
+
+Exposing HDF5 functions
+-----------------------
+
+* Expose fill time option in dataset creation property list via the
+  ``fill_time`` parameter in :meth:`~.Group.create_dataset` (:pr:`2463`).
+
+Bug fixes
+---------
+
+* Fix an error where native float16 support is not available (:pr:`2422`).
+* Fixed values of ``H5F_close_degree_t`` enum (:pr:`2433`).
+* External links are now accessed with libhdf5's default access properties
+  (:pr:`2433`).
+* Fix the iteration order for the root group in a file with creation order
+  tracked (:pr:`2410`).
+* Fixed some deprecation warnings from NumPy (:pr:`2416`).
+
+Building h5py
+-------------
+
+* Require a newer version of mpi4py for Python 3.12 (:pr:`2418`).
+* The test suite is now configured to fail on unexpected warnings (:pr:`2428`).
+* The generated Cython wrapper code (``defs.*`` & ``_hdf5.pxd``) is now specific
+  to the version of HDF5 it's building for. If the version of HDF5 has changed,
+  ``api_gen.py`` should be run automatically to recreate this (:pr:`2479`,
+  :pr:`2480`).
+* Various PRs modernising & cleaning up old Cython code, see the `3.12 milestone
+  on Github <https://github.com/h5py/h5py/milestone/31?closed=1>`_ for details.
+
+3.12.1 bug fix release
+----------------------
+
+* Fix bundling HDF5 DLLs in pre-built packages for Windows (:pr:`2507`).
diff --git a/docs/whatsnew/3.8.rst b/docs/whatsnew/3.8.rst
index 31a8121d..b1b8747b 100644
--- a/docs/whatsnew/3.8.rst
+++ b/docs/whatsnew/3.8.rst
@@ -39,8 +39,8 @@ Exposing HDF5 functions
 -----------------------
 
 * ``H5Dchunk_iter`` as :meth:`h5py.h5d.DatasetID.chunk_iter`.
-* `H5Pset_meta_block_size <https://portal.hdfgroup.org/display/HDF5/H5P_SET_META_BLOCK_SIZE>`_
-  and `H5Pget_meta_block_size <https://portal.hdfgroup.org/display/HDF5/H5P_GET_META_BLOCK_SIZE>`_
+* `H5Pset_meta_block_size <https://support.hdfgroup.org/documentation/hdf5/latest/group___f_a_p_l.html#ga8822e3dedc8e1414f20871a87d533cb1>`_
+  and `H5Pget_meta_block_size <https://support.hdfgroup.org/documentation/hdf5/latest/group___f_a_p_l.html#gac17861181246af0209c0da5209305461>`_
   (:pr:`2106`).
 
 Bug fixes
diff --git a/docs/whatsnew/index.rst b/docs/whatsnew/index.rst
index 0f2018f9..3f7010a0 100644
--- a/docs/whatsnew/index.rst
+++ b/docs/whatsnew/index.rst
@@ -8,6 +8,7 @@ These document the changes between minor (or major) versions of h5py.
 
 .. toctree::
 
+    3.12
     3.11
     3.10
     3.9
diff --git a/h5py/_hl/dataset.py b/h5py/_hl/dataset.py
index c2a65346..1406cefb 100644
--- a/h5py/_hl/dataset.py
+++ b/h5py/_hl/dataset.py
@@ -38,7 +38,8 @@ def make_new_dset(parent, shape=None, dtype=None, data=None, name=None,
                   fillvalue=None, scaleoffset=None, track_times=False,
                   external=None, track_order=None, dcpl=None, dapl=None,
                   efile_prefix=None, virtual_prefix=None, allow_unknown_filter=False,
-                  rdcc_nslots=None, rdcc_nbytes=None, rdcc_w0=None):
+                  rdcc_nslots=None, rdcc_nbytes=None, rdcc_w0=None, *,
+                  fill_time=None):
     """ Return a new low-level dataset identifier """
 
     # Convert data to a C-contiguous ndarray
@@ -104,7 +105,8 @@ def make_new_dset(parent, shape=None, dtype=None, data=None, name=None,
     dcpl = filters.fill_dcpl(
         dcpl or h5p.create(h5p.DATASET_CREATE), shape, dtype,
         chunks, compression, compression_opts, shuffle, fletcher32,
-        maxshape, scaleoffset, external, allow_unknown_filter)
+        maxshape, scaleoffset, external, allow_unknown_filter,
+        fill_time=fill_time)
 
     if fillvalue is not None:
         # prepare string-type dtypes for fillvalue
diff --git a/h5py/_hl/files.py b/h5py/_hl/files.py
index bfcf3098..1a6d5f81 100644
--- a/h5py/_hl/files.py
+++ b/h5py/_hl/files.py
@@ -397,8 +397,7 @@ class File(Group):
             recommended), 'core', 'sec2', 'direct', 'stdio', 'mpio', 'ros3'.
         libver
             Library version bounds.  Supported values: 'earliest', 'v108',
-            'v110', 'v112'  and 'latest'. The 'v108', 'v110' and 'v112'
-            options can only be specified with the HDF5 1.10.2 library or later.
+            'v110', 'v112'  and 'latest'.
         userblock_size
             Desired size of user block.  Only allowed when creating a new
             file (mode w, w- or x).
diff --git a/h5py/_hl/filters.py b/h5py/_hl/filters.py
index 493e9e2f..ee89e232 100644
--- a/h5py/_hl/filters.py
+++ b/h5py/_hl/filters.py
@@ -52,6 +52,10 @@ _COMP_FILTERS = {'gzip': h5z.FILTER_DEFLATE,
                 'shuffle': h5z.FILTER_SHUFFLE,
                 'fletcher32': h5z.FILTER_FLETCHER32,
                 'scaleoffset': h5z.FILTER_SCALEOFFSET }
+_FILL_TIME_ENUM = {'alloc': h5d.FILL_TIME_ALLOC,
+                   'never': h5d.FILL_TIME_NEVER,
+                   'ifset': h5d.FILL_TIME_IFSET,
+                   }
 
 DEFAULT_GZIP = 4
 DEFAULT_SZIP = ('nn', 8)
@@ -146,7 +150,7 @@ class Gzip(FilterRefBase):
 
 def fill_dcpl(plist, shape, dtype, chunks, compression, compression_opts,
               shuffle, fletcher32, maxshape, scaleoffset, external,
-              allow_unknown_filter=False):
+              allow_unknown_filter=False, *, fill_time=None):
     """ Generate a dataset creation property list.
 
     Undocumented and subject to change without warning.
@@ -259,7 +263,14 @@ def fill_dcpl(plist, shape, dtype, chunks, compression, compression_opts,
 
     if chunks is not None:
         plist.set_chunk(chunks)
-        plist.set_fill_time(h5d.FILL_TIME_ALLOC)  # prevent resize glitch
+
+    if fill_time is not None:
+        if (ft := _FILL_TIME_ENUM.get(fill_time)) is not None:
+            plist.set_fill_time(ft)
+        else:
+            msg = ("fill_time must be one of the following choices: 'alloc', "
+                   f"'never' or 'ifset', but it is {fill_time}.")
+            raise ValueError(msg)
 
     # scale-offset must come before shuffle and compression
     if scaleoffset is not None:
diff --git a/h5py/_hl/group.py b/h5py/_hl/group.py
index f39f84d8..0f5a814d 100644
--- a/h5py/_hl/group.py
+++ b/h5py/_hl/group.py
@@ -89,7 +89,10 @@ class Group(HLObject, MutableMappingHDF5):
 
         maxshape
             (Tuple or int) Make the dataset resizable up to this shape. Use None for
-            axes you want to be unlimited. Integers can be used for 1D shape.
+            axes within the tuple you want to be unlimited. Integers can be used for 1D shape.
+            For 1D datsets with unlimited maxshape, a shape tuple of length 1 must be
+            provided, ``(None,)``. Passing ``None`` sets ``maxshape` to `shape`, making the
+            dataset un-resizable, which is the default.
         compression
             (String or int) Compression strategy.  Legal values are 'gzip',
             'szip', 'lzf'.  If an integer in range(10), this indicates gzip
diff --git a/h5py/_objects.pyx b/h5py/_objects.pyx
index e69f5453..82d970ae 100644
--- a/h5py/_objects.pyx
+++ b/h5py/_objects.pyx
@@ -170,17 +170,17 @@ cdef class ObjectID:
     cdef size_t _pyid
     """
 
-    property fileno:
-        def __get__(self):
-            cdef H5G_stat_t stat
-            with _phil:
-                H5Gget_objinfo(self.id, '.', 0, &stat)
-                return (stat.fileno[0], stat.fileno[1])
+    @property
+    def fileno(self):
+        cdef H5G_stat_t stat
+        with _phil:
+            H5Gget_objinfo(self.id, '.', 0, &stat)
+            return (stat.fileno[0], stat.fileno[1])
 
 
-    property valid:
-        def __get__(self):
-            return is_h5py_obj_valid(self)
+    @property
+    def valid(self):
+        return is_h5py_obj_valid(self)
 
 
     def __cinit__(self, id_):
diff --git a/h5py/api_functions.txt b/h5py/api_functions.txt
index 1a1a226d..4749afbe 100644
--- a/h5py/api_functions.txt
+++ b/h5py/api_functions.txt
@@ -118,9 +118,9 @@ hdf5:
   herr_t H5Dread_chunk(hid_t dset_id, hid_t dxpl_id, const hsize_t *offset, uint32_t *filters, void *buf) nogil
 
   # Chunk query functions
-  1.10.5    herr_t H5Dget_num_chunks(hid_t dset_id, hid_t fspace_id, hsize_t *nchunks)
-  1.10.5    herr_t H5Dget_chunk_info(hid_t dset_id, hid_t fspace_id, hsize_t chk_idx, hsize_t *offset, unsigned *filter_mask, haddr_t *addr, hsize_t *size)
-  1.10.5    herr_t H5Dget_chunk_info_by_coord(hid_t dset_id, const hsize_t *offset, unsigned *filter_mask, haddr_t *addr, hsize_t *size)
+  herr_t H5Dget_num_chunks(hid_t dset_id, hid_t fspace_id, hsize_t *nchunks)
+  herr_t H5Dget_chunk_info(hid_t dset_id, hid_t fspace_id, hsize_t chk_idx, hsize_t *offset, unsigned *filter_mask, haddr_t *addr, hsize_t *size)
+  herr_t H5Dget_chunk_info_by_coord(hid_t dset_id, const hsize_t *offset, unsigned *filter_mask, haddr_t *addr, hsize_t *size)
   1.10.10-1.10.99 herr_t H5Dchunk_iter(hid_t dset_id, hid_t dxpl_id, H5D_chunk_iter_op_t cb, void *op_data)
   1.12.3    herr_t H5Dchunk_iter(hid_t dset_id, hid_t dxpl_id, H5D_chunk_iter_op_t cb, void *op_data)
 
diff --git a/h5py/api_types_ext.pxd b/h5py/api_types_ext.pxd
index 55a239f5..17b046f3 100644
--- a/h5py/api_types_ext.pxd
+++ b/h5py/api_types_ext.pxd
@@ -22,10 +22,12 @@ from libc.time cimport time_t
 
 from libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t
 
-IF UNAME_SYSNAME != "Windows":
-    cdef extern from "unistd.h":
-        ctypedef long ssize_t
-ELSE:
+cdef extern from *:
+    """
+    #if !(defined(_WIN32) || defined(MS_WINDOWS) || defined(_MSC_VER))
+      #include <unistd.h>
+    #endif
+    """
     ctypedef long ssize_t
 
 # Can't use Cython defs because they keep moving them around
diff --git a/h5py/api_types_hdf5.pxd b/h5py/api_types_hdf5.pxd
index 560ff1fc..354b0e55 100644
--- a/h5py/api_types_hdf5.pxd
+++ b/h5py/api_types_hdf5.pxd
@@ -335,18 +335,17 @@ cdef extern from "hdf5.h":
     hsize_t alignment           # Allocation alignment
     hbool_t paged_aggr          # Paged aggregation for file space is enabled or not
 
-  IF HDF5_VERSION >= (1, 10, 6):
-    ctypedef struct H5FD_ros3_fapl_t:
-      int32_t version
-      hbool_t authenticate
-      char    aws_region[33]
-      char    secret_id[129]
-      char    secret_key[129]
-
-    unsigned int H5FD_CURR_ROS3_FAPL_T_VERSION # version of struct
-
-    IF HDF5_VERSION >= (1, 14, 2):
-      size_t H5FD_ROS3_MAX_SECRET_TOK_LEN
+  ctypedef struct H5FD_ros3_fapl_t:
+    int32_t version
+    hbool_t authenticate
+    char    aws_region[33]
+    char    secret_id[129]
+    char    secret_key[129]
+
+  unsigned int H5FD_CURR_ROS3_FAPL_T_VERSION # version of struct
+
+  IF HDF5_VERSION >= (1, 14, 2):
+    size_t H5FD_ROS3_MAX_SECRET_TOK_LEN
 # === H5G - Groups API ========================================================
 
   ctypedef enum H5G_link_t:
diff --git a/h5py/h5.pyx b/h5py/h5.pyx
index c55f4b89..6a0db475 100644
--- a/h5py/h5.pyx
+++ b/h5py/h5.pyx
@@ -68,143 +68,138 @@ cdef class H5PYConfig:
         self._bytestrings = ByteStringContext()
         self._track_order = False
 
-    property complex_names:
+    @property
+    def complex_names(self):
         """ Settable 2-tuple controlling how complex numbers are saved.
 
         Format is (real_name, imag_name), defaulting to ('r','i').
         """
-
-        def __get__(self):
-            with phil:
-                import sys
-                def handle_val(val):
-                    return val.decode('utf8')
-                return (handle_val(self._r_name), handle_val(self._i_name))
-
-        def __set__(self, val):
-            with phil:
-                def handle_val(val):
-                    if isinstance(val, unicode):
-                        return val.encode('utf8')
-                    elif isinstance(val, bytes):
-                        return val
-                    else:
-                        return bytes(val)
-                try:
-                    if len(val) != 2:
-                        raise TypeError()
-                    r = handle_val(val[0])
-                    i = handle_val(val[1])
-                except Exception:
-                    raise TypeError("complex_names must be a length-2 sequence of strings (real, img)")
-                self._r_name = r
-                self._i_name = i
-
-    property bool_names:
+        with phil:
+            import sys
+            def handle_val(val):
+                return val.decode('utf8')
+            return (handle_val(self._r_name), handle_val(self._i_name))
+
+    @complex_names.setter
+    def complex_names(self, val):
+        with phil:
+            def handle_val(val):
+                if isinstance(val, unicode):
+                    return val.encode('utf8')
+                elif isinstance(val, bytes):
+                    return val
+                else:
+                    return bytes(val)
+            try:
+                if len(val) != 2:
+                    raise TypeError()
+                r = handle_val(val[0])
+                i = handle_val(val[1])
+            except Exception:
+                raise TypeError("complex_names must be a length-2 sequence of strings (real, img)")
+            self._r_name = r
+            self._i_name = i
+
+    @property
+    def bool_names(self):
         """ Settable 2-tuple controlling HDF5 ENUM names for boolean types.
 
         Format is (false_name, real_name), defaulting to ('FALSE', 'TRUE').
         """
-        def __get__(self):
-            with phil:
-                return (self._f_name, self._t_name)
-
-        def __set__(self, val):
-            with phil:
-                try:
-                    if len(val) != 2: raise TypeError()
-                    f = str(val[0])
-                    t = str(val[1])
-                except Exception:
-                    raise TypeError("bool_names must be a length-2 sequence of of names (false, true)")
-                self._f_name = f
-                self._t_name = t
-
-    property read_byte_strings:
+
+        with phil:
+            return (self._f_name, self._t_name)
+
+    @bool_names.setter
+    def bool_names(self, val):
+        with phil:
+            try:
+                if len(val) != 2: raise TypeError()
+                f = str(val[0])
+                t = str(val[1])
+            except Exception:
+                raise TypeError("bool_names must be a length-2 sequence of of names (false, true)")
+            self._f_name = f
+            self._t_name = t
+
+    @property
+    def read_byte_strings(self):
         """ Returns a context manager which forces all strings to be returned
         as byte strings. """
+        with phil:
+            return self._bytestrings
 
-        def __get__(self):
-            with phil:
-                return self._bytestrings
-
-    property mpi:
+    @property
+    def mpi(self):
         """ Boolean indicating if Parallel HDF5 is available """
-        def __get__(self):
-            IF MPI:
-                return True
-            ELSE:
-                return False
+        return MPI
 
-    property ros3:
+    @property
+    def ros3(self):
         """ Boolean indicating if ROS3 VDS is available """
-        def __get__(self):
-            IF ROS3:
-                return True
-            ELSE:
-                return False
+        return ROS3
 
-    property direct_vfd:
+    @property
+    def direct_vfd(self):
         """ Boolean indicating if DIRECT VFD is available """
-        def __get__(self):
-            IF DIRECT_VFD:
-                return True
-            ELSE:
-                return False
+        return DIRECT_VFD
 
-    property swmr_min_hdf5_version:
+    @property
+    def swmr_min_hdf5_version(self):
         """ Tuple indicating the minimum HDF5 version required for SWMR features"""
-        def __get__(self):
-            warn(
-                "h5py.get_config().swmr_min_hdf5_version is deprecated. "
-                "This version of h5py does not support older HDF5 without SWMR.",
-                category=H5pyDeprecationWarning,
-            )
-            return (1, 9, 178)
-
-    property vds_min_hdf5_version:
+        warn(
+            "h5py.get_config().swmr_min_hdf5_version is deprecated. "
+            "This version of h5py does not support older HDF5 without SWMR.",
+            category=H5pyDeprecationWarning,
+        )
+        return (1, 9, 178)
+
+    @property
+    def vds_min_hdf5_version(self):
         """Tuple indicating the minimum HDF5 version required for virtual dataset (VDS) features"""
-        def __get__(self):
-            warn(
-                "h5py.get_config().vds_min_hdf5_version is deprecated. "
-                "This version of h5py does not support older HDF5 without VDS.",
-                category=H5pyDeprecationWarning,
-            )
-            return (1, 9, 233)
-
-    property track_order:
+        warn(
+            "h5py.get_config().vds_min_hdf5_version is deprecated. "
+            "This version of h5py does not support older HDF5 without VDS.",
+            category=H5pyDeprecationWarning,
+        )
+        return (1, 9, 233)
+
+    @property
+    def track_order(self):
         """ Default value for track_order argument of
         File.open()/Group.create_group()/Group.create_dataset() """
-        def __get__(self):
-            return self._track_order
-        def __set__(self, val):
-            self._track_order = val
+        return self._track_order
 
-    property default_file_mode:
+    @track_order.setter
+    def track_order(self, val):
+        self._track_order = val
+
+    @property
+    def default_file_mode(self):
         """Default mode for h5py.File()"""
-        def __get__(self):
+        warn(
+            "h5py.get_config().default_file_mode is deprecated. "
+            "The default mode is now always 'r' (read-only).",
+            category=H5pyDeprecationWarning,
+        )
+        return 'r'
+
+    @default_file_mode.setter
+    def default_file_mode(self, val):
+        if val == 'r':
             warn(
-                "h5py.get_config().default_file_mode is deprecated. "
-                "The default mode is now always 'r' (read-only).",
+                "Setting h5py.default_file_mode is deprecated. "
+                "'r' (read-only) is the default from h5py 3.0.",
                 category=H5pyDeprecationWarning,
             )
-            return 'r'
-
-        def __set__(self, val):
-            if val == 'r':
-                warn(
-                    "Setting h5py.default_file_mode is deprecated. "
-                    "'r' (read-only) is the default from h5py 3.0.",
-                    category=H5pyDeprecationWarning,
-                )
-            elif val in {'r+', 'x', 'w-', 'w', 'a'}:
-                raise ValueError(
-                    "Using default_file_mode other than 'r' is no longer "
-                    "supported. Pass the mode to h5py.File() instead."
-
-                )
-            else:
-                raise ValueError("Invalid mode; must be one of r, r+, w, w-, x, a")
+        elif val in {'r+', 'x', 'w-', 'w', 'a'}:
+            raise ValueError(
+                "Using default_file_mode other than 'r' is no longer "
+                "supported. Pass the mode to h5py.File() instead."
+
+            )
+        else:
+            raise ValueError("Invalid mode; must be one of r, r+, w, w-, x, a")
 
 cdef H5PYConfig cfg = H5PYConfig()
 
diff --git a/h5py/h5a.pyx b/h5py/h5a.pyx
index f4843989..e1f520ff 100644
--- a/h5py/h5a.pyx
+++ b/h5py/h5a.pyx
@@ -163,22 +163,25 @@ cdef class AttrInfo:
 
     cdef H5A_info_t info
 
-    property corder_valid:
+    @property
+    def corder_valid(self):
         """Indicates if the creation order is valid"""
-        def __get__(self):
-            return <bint>self.info.corder_valid
-    property corder:
+        return <bint>self.info.corder_valid
+
+    @property
+    def corder(self):
         """Creation order"""
-        def __get__(self):
-            return <int>self.info.corder
-    property cset:
+        return <int>self.info.corder
+
+    @property
+    def cset(self):
         """Character set of attribute name (integer typecode from h5t)"""
-        def __get__(self):
-            return <int>self.info.cset
-    property data_size:
+        return <int>self.info.cset
+
+    @property
+    def data_size(self):
         """Size of raw data"""
-        def __get__(self):
-            return self.info.data_size
+        return self.info.data_size
 
     def _hash(self):
         return hash((self.corder_valid, self.corder, self.cset, self.data_size))
@@ -313,27 +316,27 @@ cdef class AttrID(ObjectID):
         * Equality: Identifier comparison
     """
 
-    property name:
+    @property
+    def name(self):
         """The attribute's name"""
-        def __get__(self):
-            with phil:
-                return self.get_name()
+        with phil:
+            return self.get_name()
 
-    property shape:
+    @property
+    def shape(self):
         """A Numpy-style shape tuple representing the attribute's dataspace"""
-        def __get__(self):
-            cdef SpaceID space
-            with phil:
-                space = self.get_space()
-                return space.get_simple_extent_dims()
+        cdef SpaceID space
+        with phil:
+            space = self.get_space()
+            return space.get_simple_extent_dims()
 
-    property dtype:
+    @property
+    def dtype(self):
         """A Numpy-stype dtype object representing the attribute's datatype"""
-        def __get__(self):
-            cdef TypeID tid
-            with phil:
-                tid = self.get_type()
-                return tid.py_dtype()
+        cdef TypeID tid
+        with phil:
+            tid = self.get_type()
+            return tid.py_dtype()
 
 
     @with_phil
diff --git a/h5py/h5ac.pyx b/h5py/h5ac.pyx
index c41ec2e5..9c93c8a4 100644
--- a/h5py/h5ac.pyx
+++ b/h5py/h5ac.pyx
@@ -23,164 +23,213 @@ cdef class CacheConfig:
     def __cinit__(self):
         self.cache_config.version = H5AC__CURR_CACHE_CONFIG_VERSION
 
-    property version:
-        def __get__(self):
-            return self.cache_config.version
-        def __set__(self, int val):
-            self.cache_config.version = val
-
-    property rpt_fcn_enabled:
-        def __get__(self):
-            return self.cache_config.rpt_fcn_enabled
-        def __set__(self, hbool_t val):
-            self.cache_config.rpt_fcn_enabled = val
-
-    property evictions_enabled:
-        def __get__(self):
-            return self.cache_config.evictions_enabled
-        def __set__(self, hbool_t val):
-            self.cache_config.evictions_enabled = val
-
-    property set_initial_size:
-        def __get__(self):
-            return self.cache_config.set_initial_size
-        def __set__(self, hbool_t val):
-            self.cache_config.set_initial_size = val
-
-    property initial_size:
-        def __get__(self):
-            return self.cache_config.initial_size
-        def __set__(self, size_t val):
-            self.cache_config.initial_size = val
-
-    property min_clean_fraction:
-        def __get__(self):
-            return self.cache_config.min_clean_fraction
-        def __set__(self, double val):
-            self.cache_config.min_clean_fraction = val
-
-    property max_size:
-        def __get__(self):
-            return self.cache_config.max_size
-        def __set__(self, size_t val):
-            self.cache_config.max_size = val
-
-    property min_size:
-        def __get__(self):
-            return self.cache_config.min_size
-        def __set__(self, size_t val):
-            self.cache_config.min_size = val
-
-    property epoch_length:
-        def __get__(self):
-            return self.cache_config.epoch_length
-        def __set__(self, long int val):
-            self.cache_config.epoch_length = val
+    @property
+    def version(self):
+        return self.cache_config.version
+
+    @version.setter
+    def version(self, int val):
+        self.cache_config.version = val
+
+    @property
+    def rpt_fcn_enabled(self):
+        return self.cache_config.rpt_fcn_enabled
+
+    @rpt_fcn_enabled.setter
+    def rpt_fcn_enabled(self, hbool_t val):
+        self.cache_config.rpt_fcn_enabled = val
+
+    @property
+    def evictions_enabled(self):
+        return self.cache_config.evictions_enabled
+
+    @evictions_enabled.setter
+    def evictions_enabled(self, hbool_t val):
+        self.cache_config.evictions_enabled = val
+
+    @property
+    def set_initial_size(self):
+        return self.cache_config.set_initial_size
+
+    @set_initial_size.setter
+    def set_initial_size(self, hbool_t val):
+        self.cache_config.set_initial_size = val
+
+    @property
+    def initial_size(self):
+        return self.cache_config.initial_size
+
+    @initial_size.setter
+    def initial_size(self, size_t val):
+        self.cache_config.initial_size = val
+
+    @property
+    def min_clean_fraction(self):
+        return self.cache_config.min_clean_fraction
+
+    @min_clean_fraction.setter
+    def min_clean_fraction(self, double val):
+        self.cache_config.min_clean_fraction = val
+
+    @property
+    def max_size(self):
+        return self.cache_config.max_size
+
+    @max_size.setter
+    def max_size(self, size_t val):
+        self.cache_config.max_size = val
+
+    @property
+    def min_size(self):
+        return self.cache_config.min_size
+
+    @min_size.setter
+    def min_size(self, size_t val):
+        self.cache_config.min_size = val
+
+    @property
+    def epoch_length(self):
+        return self.cache_config.epoch_length
+
+    @epoch_length.setter
+    def epoch_length(self, long int val):
+        self.cache_config.epoch_length = val
 
     #    /* size increase control fields: */
-    property incr_mode:
-        def __get__(self):
-            return self.cache_config.incr_mode
-        def __set__(self, H5C_cache_incr_mode val):
-            self.cache_config.incr_mode = val
-
-    property lower_hr_threshold:
-        def __get__(self):
-            return self.cache_config.lower_hr_threshold
-        def __set__(self, double val):
-            self.cache_config.lower_hr_threshold = val
-
-    property increment:
-        def __get__(self):
-            return self.cache_config.increment
-        def __set__(self, double val):
-            self.cache_config.increment = val
-
-    property apply_max_increment:
-        def __get__(self):
-            return self.cache_config.apply_max_increment
-        def __set__(self, hbool_t val):
-            self.cache_config.apply_max_increment = val
-
-    property max_increment:
-        def __get__(self):
-            return self.cache_config.max_increment
-        def __set__(self, size_t val):
-            self.cache_config.max_increment = val
-
-    property flash_incr_mode:
-        def __get__(self):
-            return self.cache_config.flash_incr_mode
-        def __set__(self, H5C_cache_flash_incr_mode val):
-            self.cache_config.flash_incr_mode = val
-
-    property flash_multiple:
-        def __get__(self):
-            return self.cache_config.flash_multiple
-        def __set__(self, double val):
-            self.cache_config.flash_multiple = val
-
-    property flash_threshold:
-        def __get__(self):
-            return self.cache_config.flash_threshold
-        def __set__(self, double val):
-            self.cache_config.flash_threshold = val
+    @property
+    def incr_mode(self):
+        return self.cache_config.incr_mode
+
+    @incr_mode.setter
+    def incr_mode(self, H5C_cache_incr_mode val):
+        self.cache_config.incr_mode = val
+
+    @property
+    def lower_hr_threshold(self):
+        return self.cache_config.lower_hr_threshold
+
+    @lower_hr_threshold.setter
+    def lower_hr_threshold(self, double val):
+        self.cache_config.lower_hr_threshold = val
+
+    @property
+    def increment(self):
+        return self.cache_config.increment
+
+    @increment.setter
+    def increment(self, double val):
+        self.cache_config.increment = val
+
+    @property
+    def apply_max_increment(self):
+        return self.cache_config.apply_max_increment
+
+    @apply_max_increment.setter
+    def apply_max_increment(self, hbool_t val):
+        self.cache_config.apply_max_increment = val
+
+    @property
+    def max_increment(self):
+        return self.cache_config.max_increment
+
+    @max_increment.setter
+    def max_increment(self, size_t val):
+        self.cache_config.max_increment = val
+
+    @property
+    def flash_incr_mode(self):
+        return self.cache_config.flash_incr_mode
+
+    @flash_incr_mode.setter
+    def flash_incr_mode(self, H5C_cache_flash_incr_mode val):
+        self.cache_config.flash_incr_mode = val
+
+    @property
+    def flash_multiple(self):
+        return self.cache_config.flash_multiple
+
+    @flash_multiple.setter
+    def flash_multiple(self, double val):
+        self.cache_config.flash_multiple = val
+
+    @property
+    def flash_threshold(self):
+        return self.cache_config.flash_threshold
+
+    @flash_threshold.setter
+    def flash_threshold(self, double val):
+        self.cache_config.flash_threshold = val
 
     # /* size decrease control fields: */
-    property decr_mode:
-        def __get__(self):
-            return self.cache_config.decr_mode
-        def __set__(self, H5C_cache_decr_mode val):
-            self.cache_config.decr_mode = val
-
-    property upper_hr_threshold:
-        def __get__(self):
-            return self.cache_config.upper_hr_threshold
-        def __set__(self, double val):
-            self.cache_config.upper_hr_threshold = val
-
-    property decrement:
-        def __get__(self):
-            return self.cache_config.decrement
-        def __set__(self, double val):
-            self.cache_config.decrement = val
-
-    property apply_max_decrement:
-        def __get__(self):
-            return self.cache_config.apply_max_decrement
-        def __set__(self, hbool_t val):
-            self.cache_config.apply_max_decrement = val
-
-    property max_decrement:
-        def __get__(self):
-            return self.cache_config.max_decrement
-        def __set__(self, size_t val):
-            self.cache_config.max_decrement = val
-
-    property epochs_before_eviction:
-        def __get__(self):
-            return self.cache_config.epochs_before_eviction
-        def __set__(self, int val):
-            self.cache_config.epochs_before_eviction = val
-
-
-
-    property apply_empty_reserve:
-        def __get__(self):
-            return self.cache_config.apply_empty_reserve
-        def __set__(self, hbool_t val):
-            self.cache_config.apply_empty_reserve = val
-
-
-    property empty_reserve:
-        def __get__(self):
-            return self.cache_config.empty_reserve
-        def __set__(self, double val):
-            self.cache_config.empty_reserve = val
+    @property
+    def decr_mode(self):
+        return self.cache_config.decr_mode
+
+    @decr_mode.setter
+    def decr_mode(self, H5C_cache_decr_mode val):
+        self.cache_config.decr_mode = val
+
+    @property
+    def upper_hr_threshold(self):
+        return self.cache_config.upper_hr_threshold
+
+    @upper_hr_threshold.setter
+    def upper_hr_threshold(self, double val):
+        self.cache_config.upper_hr_threshold = val
+
+    @property
+    def decrements(self):
+        return self.cache_config.decrement
+
+    @decrements.setter
+    def decrements(self, double val):
+        self.cache_config.decrement = val
+
+    @property
+    def apply_max_decrement(self):
+        return self.cache_config.apply_max_decrement
+
+    @apply_max_decrement.setter
+    def apply_max_decrement(self, hbool_t val):
+        self.cache_config.apply_max_decrement = val
+
+    @property
+    def max_decrement(self):
+        return self.cache_config.max_decrement
+
+    @max_decrement.setter
+    def max_decrement(self, size_t val):
+        self.cache_config.max_decrement = val
+
+    @property
+    def epochs_before_eviction(self):
+        return self.cache_config.epochs_before_eviction
+
+    @epochs_before_eviction.setter
+    def epochs_before_eviction(self, int val):
+        self.cache_config.epochs_before_eviction = val
+
+    @property
+    def apply_empty_reserve(self):
+        return self.cache_config.apply_empty_reserve
+
+    @apply_empty_reserve.setter
+    def apply_empty_reserve(self, hbool_t val):
+        self.cache_config.apply_empty_reserve = val
+
+    @property
+    def empty_reserve(self):
+        return self.cache_config.empty_reserve
+
+    @empty_reserve.setter
+    def empty_reserve(self, double val):
+        self.cache_config.empty_reserve = val
 
     # /* parallel configuration fields: */
-    property dirty_bytes_threshold:
-        def __get__(self):
-            return self.cache_config.dirty_bytes_threshold
-        def __set__(self, int val):
-            self.cache_config.dirty_bytes_threshold = val
+    @property
+    def dirty_bytes_threshold(self):
+        return self.cache_config.dirty_bytes_threshold
+
+    @dirty_bytes_threshold.setter
+    def dirty_bytes_threshold(self, int val):
+        self.cache_config.dirty_bytes_threshold = val
diff --git a/h5py/h5d.pyx b/h5py/h5d.pyx
index e03c0699..12676364 100644
--- a/h5py/h5d.pyx
+++ b/h5py/h5d.pyx
@@ -64,9 +64,8 @@ VIRTUAL = H5D_VIRTUAL
 VDS_FIRST_MISSING   = H5D_VDS_FIRST_MISSING
 VDS_LAST_AVAILABLE  = H5D_VDS_LAST_AVAILABLE
 
-IF HDF5_VERSION >= (1, 10, 5):
-    StoreInfo = namedtuple('StoreInfo',
-                           'chunk_offset, filter_mask, byte_offset, size')
+StoreInfo = namedtuple('StoreInfo',
+                       'chunk_offset, filter_mask, byte_offset, size')
 
 # === Dataset chunk iterator ==================================================
 
@@ -173,33 +172,33 @@ cdef class DatasetID(ObjectID):
         * Equality: True HDF5 identity if unless anonymous
     """
 
-    property dtype:
+    @property
+    def dtype(self):
         """ Numpy dtype object representing the dataset type """
-        def __get__(self):
-            # Dataset type can't change
-            cdef TypeID tid
-            with phil:
-                if self._dtype is None:
-                    tid = self.get_type()
-                    self._dtype = tid.dtype
-                return self._dtype
-
-    property shape:
+        # Dataset type can't change
+        cdef TypeID tid
+        with phil:
+            if self._dtype is None:
+                tid = self.get_type()
+                self._dtype = tid.dtype
+            return self._dtype
+
+    @property
+    def shape(self):
         """ Numpy-style shape tuple representing the dataspace """
-        def __get__(self):
-            # Shape can change (DatasetID.extend), so don't cache it
-            cdef SpaceID sid
-            with phil:
-                sid = self.get_space()
-                return sid.get_simple_extent_dims()
-
-    property rank:
+        # Shape can change (DatasetID.extend), so don't cache it
+        cdef SpaceID sid
+        with phil:
+            sid = self.get_space()
+            return sid.get_simple_extent_dims()
+
+    @property
+    def rank(self):
         """ Integer giving the dataset rank (0 = scalar) """
-        def __get__(self):
-            cdef SpaceID sid
-            with phil:
-                sid = self.get_space()
-                return sid.get_simple_extent_ndims()
+        cdef SpaceID sid
+        with phil:
+            sid = self.get_space()
+            return sid.get_simple_extent_ndims()
 
 
     @with_phil
@@ -534,8 +533,6 @@ cdef class DatasetID(ObjectID):
         If the `out` argument is not None, it must be a writeable
         contiguous 1D array-like of bytes (e.g., `bytearray` or
         `numpy.ndarray`) and large enough to contain the whole chunk.
-
-        Feature requires: 1.10.2 HDF5
         """
         cdef hid_t dset_id
         cdef hid_t dxpl_id
@@ -581,98 +578,89 @@ cdef class DatasetID(ObjectID):
 
         return filters, retval
 
+    @with_phil
+    def get_num_chunks(self, SpaceID space=None):
+        """ (SpaceID space=None) => INT num_chunks
 
-    IF HDF5_VERSION >= (1, 10, 5):
-
-        @with_phil
-        def get_num_chunks(self, SpaceID space=None):
-            """ (SpaceID space=None) => INT num_chunks
-
-            Retrieve the number of chunks that have nonempty intersection with a
-            specified dataspace. Currently, this function only gets the number
-            of all written chunks, regardless of the dataspace.
-
-            Feature requires: HDF5 1.10.5
-
-            .. versionadded:: 3.0
-            """
-            cdef hsize_t num_chunks
-
-            if space is None:
-                space = self.get_space()
-            H5Dget_num_chunks(self.id, space.id, &num_chunks)
-            return num_chunks
+        Retrieve the number of chunks that have nonempty intersection with a
+        specified dataspace. Currently, this function only gets the number
+        of all written chunks, regardless of the dataspace.
 
-        @with_phil
-        def get_chunk_info(self, hsize_t index, SpaceID space=None):
-            """ (hsize_t index, SpaceID space=None) => StoreInfo
+        .. versionadded:: 3.0
+        """
+        cdef hsize_t num_chunks
 
-            Retrieve storage information about a chunk specified by its index.
+        if space is None:
+            space = self.get_space()
+        H5Dget_num_chunks(self.id, space.id, &num_chunks)
+        return num_chunks
 
-            Feature requires: HDF5 1.10.5
+    @with_phil
+    def get_chunk_info(self, hsize_t index, SpaceID space=None):
+        """ (hsize_t index, SpaceID space=None) => StoreInfo
 
-            .. versionadded:: 3.0
-            """
-            cdef haddr_t byte_offset
-            cdef hsize_t size
-            cdef hsize_t *chunk_offset
-            cdef unsigned filter_mask
-            cdef hid_t space_id = 0
-            cdef int rank
+        Retrieve storage information about a chunk specified by its index.
 
-            if space is None:
-                space_id = H5Dget_space(self.id)
-            else:
-                space_id = space.id
+        .. versionadded:: 3.0
+        """
+        cdef haddr_t byte_offset
+        cdef hsize_t size
+        cdef hsize_t *chunk_offset
+        cdef unsigned filter_mask
+        cdef hid_t space_id = 0
+        cdef int rank
 
-            rank = H5Sget_simple_extent_ndims(space_id)
-            chunk_offset = <hsize_t*>emalloc(sizeof(hsize_t) * rank)
-            H5Dget_chunk_info(self.id, space_id, index, chunk_offset,
-                              &filter_mask, &byte_offset, &size)
-            cdef tuple cot = convert_dims(chunk_offset, <hsize_t>rank)
-            efree(chunk_offset)
+        if space is None:
+            space_id = H5Dget_space(self.id)
+        else:
+            space_id = space.id
 
-            if space is None:
-                H5Sclose(space_id)
+        rank = H5Sget_simple_extent_ndims(space_id)
+        chunk_offset = <hsize_t*>emalloc(sizeof(hsize_t) * rank)
+        H5Dget_chunk_info(self.id, space_id, index, chunk_offset,
+                          &filter_mask, &byte_offset, &size)
+        cdef tuple cot = convert_dims(chunk_offset, <hsize_t>rank)
+        efree(chunk_offset)
 
-            return StoreInfo(cot if byte_offset != HADDR_UNDEF else None,
-                             filter_mask,
-                             byte_offset if byte_offset != HADDR_UNDEF else None,
-                             size)
+        if space is None:
+            H5Sclose(space_id)
 
+        return StoreInfo(cot if byte_offset != HADDR_UNDEF else None,
+                         filter_mask,
+                         byte_offset if byte_offset != HADDR_UNDEF else None,
+                         size)
 
-        @with_phil
-        def get_chunk_info_by_coord(self, tuple chunk_offset not None):
-            """ (TUPLE chunk_offset) => StoreInfo
 
-            Retrieve information about a chunk specified by the array
-            address of the chunk’s first element in each dimension.
+    @with_phil
+    def get_chunk_info_by_coord(self, tuple chunk_offset not None):
+        """ (TUPLE chunk_offset) => StoreInfo
 
-            Feature requires: HDF5 1.10.5
+        Retrieve information about a chunk specified by the array
+        address of the chunk’s first element in each dimension.
 
-            .. versionadded:: 3.0
-            """
-            cdef haddr_t byte_offset
-            cdef hsize_t size
-            cdef unsigned filter_mask
-            cdef hid_t space_id = 0
-            cdef int rank
-            cdef hsize_t *co = NULL
+        .. versionadded:: 3.0
+        """
+        cdef haddr_t byte_offset
+        cdef hsize_t size
+        cdef unsigned filter_mask
+        cdef hid_t space_id = 0
+        cdef int rank
+        cdef hsize_t *co = NULL
 
-            space_id = H5Dget_space(self.id)
-            rank = H5Sget_simple_extent_ndims(space_id)
-            H5Sclose(space_id)
-            co = <hsize_t*>emalloc(sizeof(hsize_t) * rank)
-            convert_tuple(chunk_offset, co, rank)
-            H5Dget_chunk_info_by_coord(self.id, co,
-                                       &filter_mask, &byte_offset,
-                                       &size)
-            efree(co)
-
-            return StoreInfo(chunk_offset if byte_offset != HADDR_UNDEF else None,
-                             filter_mask,
-                             byte_offset if byte_offset != HADDR_UNDEF else None,
-                             size)
+        space_id = H5Dget_space(self.id)
+        rank = H5Sget_simple_extent_ndims(space_id)
+        H5Sclose(space_id)
+        co = <hsize_t*>emalloc(sizeof(hsize_t) * rank)
+        convert_tuple(chunk_offset, co, rank)
+        H5Dget_chunk_info_by_coord(self.id, co,
+                                   &filter_mask, &byte_offset,
+                                   &size)
+        efree(co)
+
+        return StoreInfo(chunk_offset if byte_offset != HADDR_UNDEF else None,
+                         filter_mask,
+                         byte_offset if byte_offset != HADDR_UNDEF else None,
+                         size)
 
     IF HDF5_VERSION >= (1, 12, 3) or (HDF5_VERSION >= (1, 10, 10) and HDF5_VERSION < (1, 10, 99)):
 
diff --git a/h5py/h5f.pyx b/h5py/h5f.pyx
index 9823fb5f..cf69b575 100644
--- a/h5py/h5f.pyx
+++ b/h5py/h5f.pyx
@@ -317,11 +317,11 @@ cdef class FileID(GroupID):
         * Equality: Hash comparison
     """
 
-    property name:
+    @property
+    def name(self):
         """ File name on disk (according to h5f.get_name()) """
-        def __get__(self):
-            with phil:
-                return get_name(self)
+        with phil:
+            return get_name(self)
 
 
     @with_phil
diff --git a/h5py/h5fd.pyx b/h5py/h5fd.pyx
index d39cf68f..07e835c1 100644
--- a/h5py/h5fd.pyx
+++ b/h5py/h5fd.pyx
@@ -43,8 +43,7 @@ MULTI = H5FD_MULTI
 SEC2 = H5FD_SEC2
 DIRECT = H5FD_DIRECT
 STDIO = H5FD_STDIO
-IF HDF5_VERSION >= (1, 10, 6):
-    ROS3D = H5FD_ROS3
+ROS3D = H5FD_ROS3
 IF UNAME_SYSNAME == "Windows":
     WINDOWS = H5FD_WINDOWS
 ELSE:
diff --git a/h5py/h5g.pyx b/h5py/h5g.pyx
index 5df060bb..b2aacef4 100644
--- a/h5py/h5g.pyx
+++ b/h5py/h5g.pyx
@@ -14,6 +14,8 @@
 
 include "config.pxi"
 
+import sys
+
 # C-level imports
 from ._objects cimport pdefault
 from .utils cimport emalloc, efree
@@ -25,6 +27,9 @@ from ._errors cimport set_error_handler, err_cookie
 # Python level imports
 from ._objects import phil, with_phil
 
+
+_IS_WINDOWS = sys.platform.startswith("win")
+
 # === Public constants and data structures ====================================
 
 # Enumerated object types for groups "H5G_obj_t"
@@ -58,24 +63,29 @@ cdef class GroupStat:
     """
     cdef H5G_stat_t infostruct
 
-    property fileno:
-        def __get__(self):
-            return (self.infostruct.fileno[0], self.infostruct.fileno[1])
-    property objno:
-        def __get__(self):
-            return (self.infostruct.objno[0], self.infostruct.objno[1])
-    property nlink:
-        def __get__(self):
-            return self.infostruct.nlink
-    property type:
-        def __get__(self):
-            return self.infostruct.type
-    property mtime:
-        def __get__(self):
-            return self.infostruct.mtime
-    property linklen:
-        def __get__(self):
-            return self.infostruct.linklen
+    @property
+    def fileno(self):
+        return (self.infostruct.fileno[0], self.infostruct.fileno[1])
+
+    @property
+    def objno(self):
+        return (self.infostruct.objno[0], self.infostruct.objno[1])
+
+    @property
+    def nlink(self):
+        return self.infostruct.nlink
+
+    @property
+    def type(self):
+        return self.infostruct.type
+
+    @property
+    def mtime(self):
+        return self.infostruct.mtime
+
+    @property
+    def linklen(self):
+        return self.infostruct.linklen
 
     def _hash(self):
         return hash((self.fileno, self.objno, self.nlink, self.type, self.mtime, self.linklen))
@@ -389,9 +399,9 @@ cdef class GroupID(ObjectID):
         if statbuf.type != H5G_LINK:
             raise ValueError('"%s" is not a symbolic link.' % name)
 
-        IF UNAME_SYSNAME == "Windows":
+        if _IS_WINDOWS:
             linklen = 2049  # Windows statbuf.linklen seems broken
-        ELSE:
+        else:
             linklen = statbuf.linklen+1
         value = <char*>emalloc(sizeof(char)*linklen)
         try:
diff --git a/h5py/h5l.pyx b/h5py/h5l.pyx
index af725bdb..9a9d5d30 100644
--- a/h5py/h5l.pyx
+++ b/h5py/h5l.pyx
@@ -30,29 +30,33 @@ cdef class LinkInfo:
 
     cdef H5L_info_t infostruct
 
-    property type:
+    @property
+    def type(self):
         """ Integer type code for link (h5l.TYPE_*) """
-        def __get__(self):
-            return <int>self.infostruct.type
-    property corder_valid:
+        return <int>self.infostruct.type
+
+    @property
+    def corder_valid(self):
         """ Indicates if the creation order is valid """
-        def __get__(self):
-            return <bint>self.infostruct.corder_valid
-    property corder:
+        return <bint>self.infostruct.corder_valid
+
+    @property
+    def corder(self):
         """ Creation order """
-        def __get__(self):
-            return self.infostruct.corder
-    property cset:
+        return self.infostruct.corder
+
+    @property
+    def cset(self):
         """ Integer type code for character set (h5t.CSET_*) """
-        def __get__(self):
-            return self.infostruct.cset
-    property u:
+        return self.infostruct.cset
+
+    @property
+    def u(self):
         """ Either the address of a hard link or the size of a soft/UD link """
-        def __get__(self):
-            if self.infostruct.type == H5L_TYPE_HARD:
-                return self.infostruct.u.address
-            else:
-                return self.infostruct.u.val_size
+        if self.infostruct.type == H5L_TYPE_HARD:
+            return self.infostruct.u.address
+        else:
+            return self.infostruct.u.val_size
 
 cdef class _LinkVisitor:
 
diff --git a/h5py/h5o.pyx b/h5py/h5o.pyx
index f6f32c49..2c2d6c40 100644
--- a/h5py/h5o.pyx
+++ b/h5py/h5o.pyx
@@ -45,30 +45,34 @@ cdef class _ObjInfoBase:
 
 cdef class _OHdrMesg(_ObjInfoBase):
 
-    property present:
-        def __get__(self):
-            return self.istr[0].hdr.mesg.present
-    property shared:
-        def __get__(self):
-            return self.istr[0].hdr.mesg.shared
+    @property
+    def present(self):
+        return self.istr[0].hdr.mesg.present
+
+    @property
+    def shared(self):
+        return self.istr[0].hdr.mesg.shared
 
     def _hash(self):
         return hash((self.present, self.shared))
 
 cdef class _OHdrSpace(_ObjInfoBase):
 
-    property total:
-        def __get__(self):
-            return self.istr[0].hdr.space.total
-    property meta:
-        def __get__(self):
-            return self.istr[0].hdr.space.meta
-    property mesg:
-        def __get__(self):
-            return self.istr[0].hdr.space.mesg
-    property free:
-        def __get__(self):
-            return self.istr[0].hdr.space.free
+    @property
+    def total(self):
+        return self.istr[0].hdr.space.total
+
+    @property
+    def meta(self):
+        return self.istr[0].hdr.space.meta
+
+    @property
+    def mesg(self):
+        return self.istr[0].hdr.space.mesg
+
+    @property
+    def free(self):
+        return self.istr[0].hdr.space.free
 
     def _hash(self):
         return hash((self.total, self.meta, self.mesg, self.free))
@@ -78,18 +82,21 @@ cdef class _OHdr(_ObjInfoBase):
     cdef public _OHdrSpace space
     cdef public _OHdrMesg mesg
 
-    property version:
-        def __get__(self):
-            return self.istr[0].hdr.version
-    property nmesgs:
-        def __get__(self):
-            return self.istr[0].hdr.nmesgs
-    property nchunks:
-        def __get__(self):
-            return self.istr[0].hdr.nchunks
-    property flags:
-        def __get__(self):
-            return self.istr[0].hdr.flags
+    @property
+    def version(self):
+        return self.istr[0].hdr.version
+
+    @property
+    def nmesgs(self):
+        return self.istr[0].hdr.nmesgs
+
+    @property
+    def nchunks(self):
+        return self.istr[0].hdr.nchunks
+
+    @property
+    def flags(self):
+        return self.istr[0].hdr.flags
 
     def __init__(self):
         self.space = _OHdrSpace()
@@ -102,12 +109,13 @@ cdef class _ObjMetaInfo:
 
     cdef H5_ih_info_t *istr
 
-    property index_size:
-        def __get__(self):
-            return self.istr[0].index_size
-    property heap_size:
-        def __get__(self):
-            return self.istr[0].heap_size
+    @property
+    def index_size(self):
+        return self.istr[0].index_size
+
+    @property
+    def heap_size(self):
+        return self.istr[0].heap_size
 
     def _hash(self):
         return hash((self.index_size, self.heap_size))
@@ -126,33 +134,41 @@ cdef class _OMetaSize(_ObjInfoBase):
 
 cdef class _ObjInfo(_ObjInfoBase):
 
-    property fileno:
-        def __get__(self):
-            return self.istr[0].fileno
-    property addr:
-        def __get__(self):
-            return self.istr[0].addr
-    property type:
-        def __get__(self):
-            return <int>self.istr[0].type
-    property rc:
-        def __get__(self):
-            return self.istr[0].rc
-    property atime:
-        def __get__(self):
-            return self.istr[0].atime
-    property mtime:
-        def __get__(self):
-            return self.istr[0].mtime
-    property ctime:
-        def __get__(self):
-            return self.istr[0].ctime
-    property btime:
-        def __get__(self):
-            return self.istr[0].btime
-    property num_attrs:
-        def __get__(self):
-            return self.istr[0].num_attrs
+    @property
+    def fileno(self):
+        return self.istr[0].fileno
+
+    @property
+    def addr(self):
+        return self.istr[0].addr
+
+    @property
+    def type(self):
+        return <int>self.istr[0].type
+
+    @property
+    def rc(self):
+        return self.istr[0].rc
+
+    @property
+    def atime(self):
+        return self.istr[0].atime
+
+    @property
+    def mtime(self):
+        return self.istr[0].mtime
+
+    @property
+    def ctime(self):
+        return self.istr[0].ctime
+
+    @property
+    def btime(self):
+        return self.istr[0].btime
+
+    @property
+    def num_attrs(self):
+        return self.istr[0].num_attrs
 
     def _hash(self):
         return hash((self.fileno, self.addr, self.type, self.rc, self.atime, self.mtime, self.ctime, self.btime, self.num_attrs))
diff --git a/h5py/h5p.pyx b/h5py/h5p.pyx
index 2a163414..58134187 100644
--- a/h5py/h5p.pyx
+++ b/h5py/h5p.pyx
@@ -1313,8 +1313,8 @@ cdef class PropFAID(PropInstanceID):
         Set the compatibility level for file format. Legal values are:
 
         - h5f.LIBVER_EARLIEST
-        - h5f.LIBVER_V18 (HDF5 1.10.2 or later)
-        - h5f.LIBVER_V110 (HDF5 1.10.2 or later)
+        - h5f.LIBVER_V18
+        - h5f.LIBVER_V110
         - h5f.LIBVER_V112 (HDF5 1.11.4 or later)
         - h5f.LIBVER_V114 (HDF5 1.13.0 or later)
         - h5f.LIBVER_LATEST
@@ -1346,8 +1346,8 @@ cdef class PropFAID(PropInstanceID):
         Get the compatibility level for file format. Returned values are from:
 
         - h5f.LIBVER_EARLIEST
-        - h5f.LIBVER_V18 (HDF5 1.10.2 or later)
-        - h5f.LIBVER_V110 (HDF5 1.10.2 or later)
+        - h5f.LIBVER_V18
+        - h5f.LIBVER_V110
         - h5f.LIBVER_V112 (HDF5 1.11.4 or later)
         - h5f.LIBVER_V114 (HDF5 1.13.0 or later)
         - h5f.LIBVER_LATEST
diff --git a/h5py/h5pl.pyx b/h5py/h5pl.pyx
index 26014a44..473467b9 100644
--- a/h5py/h5pl.pyx
+++ b/h5py/h5pl.pyx
@@ -11,8 +11,7 @@
 """
     Provides access to the low-level HDF5 "H5PL" plugins interface.
 
-    These functions are only available with HDF5 1.10.1 or later.
-    They are probably not thread safe.
+    These functions are probably not thread safe.
 """
 
 include "config.pxi"
diff --git a/h5py/h5s.pyx b/h5py/h5s.pyx
index ac773800..80d35e65 100644
--- a/h5py/h5s.pyx
+++ b/h5py/h5s.pyx
@@ -133,12 +133,12 @@ cdef class SpaceID(ObjectID):
         * Equality: Unimplemented
     """
 
-    property shape:
+    @property
+    def shape(self):
         """ Numpy-style shape tuple representing dimensions.  () == scalar.
         """
-        def __get__(self):
-            with phil:
-                return self.get_simple_extent_dims()
+        with phil:
+            return self.get_simple_extent_dims()
 
 
     @with_phil
diff --git a/h5py/h5t.pyx b/h5py/h5t.pyx
index 31220f10..1d5d57d3 100644
--- a/h5py/h5t.pyx
+++ b/h5py/h5t.pyx
@@ -28,9 +28,9 @@ from .utils cimport  emalloc, efree, require_tuple, convert_dims,\
 
 # Python imports
 import codecs
+import platform
 import sys
 from collections import namedtuple
-import sys
 import numpy as np
 from .h5 import get_config
 
@@ -38,7 +38,10 @@ from ._objects import phil, with_phil
 
 cfg = get_config()
 
-DEF MACHINE = UNAME_MACHINE  # processor architecture, provided by Cython
+_UNAME_MACHINE = platform.uname()[4]
+_IS_PPC64 = _UNAME_MACHINE == "ppc64"
+_IS_PPC64LE = _UNAME_MACHINE == "ppc64le"
+
 cdef char* H5PY_PYTHON_OPAQUE_TAG = "PYTHON:OBJECT"
 
 # === Custom C API ============================================================
@@ -291,12 +294,12 @@ cdef (int, int, int) _correct_float_info(ftype_, finfo):
     maxexp = finfo.maxexp
     minexp = finfo.minexp
     # workaround for numpy's buggy finfo on float128 on ppc64 archs
-    if ftype_ == np.longdouble and MACHINE == 'ppc64':
+    if ftype_ == np.longdouble and _IS_PPC64:
         # values reported by hdf5
         nmant = 116
         maxexp = 1024
         minexp = -1022
-    elif ftype_ == np.longdouble and MACHINE == 'ppc64le':
+    elif ftype_ == np.longdouble and _IS_PPC64LE:
         # values reported by hdf5
         nmant = 52
         maxexp = 1024
@@ -435,12 +438,12 @@ cdef class TypeID(ObjectID):
             return cpy
 
 
-    property dtype:
+    @property
+    def dtype(self):
         """ A Numpy-style dtype object representing this object.
         """
-        def __get__(self):
-            with phil:
-                return self.py_dtype()
+        with phil:
+            return self.py_dtype()
 
 
     cdef object py_dtype(self):
diff --git a/h5py/version.py b/h5py/version.py
index f8df866d..54426eb9 100644
--- a/h5py/version.py
+++ b/h5py/version.py
@@ -23,7 +23,7 @@ _H5PY_VERSION_CLS = namedtuple("_H5PY_VERSION_CLS",
 
 hdf5_built_version_tuple = _h5.HDF5_VERSION_COMPILED_AGAINST
 
-version_tuple = _H5PY_VERSION_CLS(3, 11, 0, None, None, None)
+version_tuple = _H5PY_VERSION_CLS(3, 12, 1, None, None, None)
 
 version = "{0.major:d}.{0.minor:d}.{0.bugfix:d}".format(version_tuple)
 if version_tuple.pre is not None:
diff --git a/news/dep-drop_cp38.rst b/news/dep-drop_cp38.rst
deleted file mode 100644
index 58408d2c..00000000
--- a/news/dep-drop_cp38.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-Breaking Changes and Deprecations
----------------------------------
-
-* Support for Python 3.8 was dropped. Python 3.9 or newer is now required
-  to build or install h5py from this version on.
diff --git a/news/link_access_properties.rst b/news/link_access_properties.rst
deleted file mode 100644
index c156e2f5..00000000
--- a/news/link_access_properties.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Bug fixes
----------
-
-* Fixed values of ``H5F_close_degree_t`` enum.
-* External links are now accessed with libhdf5's default access properties.
diff --git a/setup.py b/setup.py
index c5387039..d5634c9c 100755
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ if '' not in sys.path:
 import setup_build, setup_configure
 
 
-VERSION = '3.11.0'
+VERSION = '3.12.1'
 
 
 # these are required to use h5py
diff --git a/setup_build.py b/setup_build.py
index 83af0676..a041fa69 100644
--- a/setup_build.py
+++ b/setup_build.py
@@ -142,9 +142,9 @@ class h5py_build_ext(build_ext):
         config = BuildConfig.from_env()
         config.summarise()
 
-        if config.hdf5_version < (1, 10, 4):
+        if config.hdf5_version < (1, 10, 6):
             raise Exception(
-                f"This version of h5py requires HDF5 >= 1.10.4 (got version "
+                f"This version of h5py requires HDF5 >= 1.10.6 (got version "
                 f"{config.hdf5_version} from environment variable or library)"
             )
 
@@ -152,7 +152,7 @@ class h5py_build_ext(build_ext):
 
         # Refresh low-level defs if missing or stale
         print("Executing api_gen rebuild of defs")
-        api_gen.run()
+        api_gen.run(config)
 
         # Rewrite config.pxi file if needed
         s = f"""\


Test patch:
diff --git a/h5py/tests/test_attrs.py b/h5py/tests/test_attrs.py
index 5a3ba23d..5253d888 100644
--- a/h5py/tests/test_attrs.py
+++ b/h5py/tests/test_attrs.py
@@ -245,7 +245,6 @@ class TestTrackOrder(BaseAttrs):
             attrs[str(i)] = i
         return attrs
 
-    @ut.skipUnless(h5py.version.hdf5_version_tuple >= (1, 10, 6), 'HDF5 1.10.6 required')
     # https://forum.hdfgroup.org/t/bug-h5arename-fails-unexpectedly/4881
     def test_track_order(self):
         attrs = self.fill_attrs(track_order=True)  # creation order
@@ -263,7 +262,6 @@ class TestTrackOrder(BaseAttrs):
             group.attrs[str(i)] = i
         return group
 
-    @ut.skipUnless(h5py.version.hdf5_version_tuple >= (1, 10, 6), 'HDF5 1.10.6 required')
     def test_track_order_overwrite_delete(self):
         # issue 1385
         group = self.fill_attrs2(track_order=True)  # creation order
diff --git a/h5py/tests/test_dataset.py b/h5py/tests/test_dataset.py
index 5f102385..dc0f4559 100644
--- a/h5py/tests/test_dataset.py
+++ b/h5py/tests/test_dataset.py
@@ -476,6 +476,90 @@ class TestCreateFillvalue(BaseDataset):
                     dtype=[('a', 'i'), ('b', 'f')], fillvalue=42)
 
 
+class TestFillTime(BaseDataset):
+
+    """
+        Feature: Datasets created with specified fill time property
+    """
+
+    def test_fill_time_default(self):
+        """ Fill time default to IFSET """
+        dset = self.f.create_dataset('foo', (10,), fillvalue=4.0)
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_IFSET)
+        self.assertEqual(dset[0], 4.0)
+        self.assertEqual(dset[7], 4.0)
+
+    @ut.skipIf('gzip' not in h5py.filters.encode, "DEFLATE is not installed")
+    def test_compressed_default(self):
+        """ Fill time is IFSET for compressed dataset (chunked) """
+        dset = self.f.create_dataset('foo', (10,), compression='gzip',
+                                     fillvalue=4.0)
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_IFSET)
+        self.assertEqual(dset[0], 4.0)
+        self.assertEqual(dset[7], 4.0)
+
+    def test_fill_time_never(self):
+        """ Fill time set to NEVER """
+        dset = self.f.create_dataset('foo', (10,), fillvalue=4.0,
+                                     fill_time='never')
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_NEVER)
+        # should not be equal to the explicitly set fillvalue
+        self.assertNotEqual(dset[0], 4.0)
+        self.assertNotEqual(dset[7], 4.0)
+
+    def test_fill_time_alloc(self):
+        """ Fill time explicitly set to ALLOC """
+        dset = self.f.create_dataset('foo', (10,), fillvalue=4.0,
+                                     fill_time='alloc')
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_ALLOC)
+
+    def test_fill_time_ifset(self):
+        """ Fill time explicitly set to IFSET """
+        dset = self.f.create_dataset('foo', (10,), chunks=(2,), fillvalue=4.0,
+                                     fill_time='ifset')
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_IFSET)
+
+    def test_invalid_fill_time(self):
+        """ Choice of fill_time is 'alloc', 'never', 'ifset' """
+        with self.assertRaises(ValueError):
+            dset = self.f.create_dataset('foo', (10,), fill_time='fill_bad')
+
+    def test_non_str_fill_time(self):
+        """ fill_time must be a string """
+        with self.assertRaises(ValueError):
+            dset = self.f.create_dataset('foo', (10,), fill_time=2)
+
+    def test_resize_chunk_fill_time_default(self):
+        """ The resize dataset will be filled (by default fill value 0) """
+        dset = self.f.create_dataset('foo', (50, ), maxshape=(100, ),
+                                     chunks=(5, ))
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_IFSET)
+
+        assert np.isclose(dset[:], 0.0).all()
+
+        dset.resize((100, ))
+        assert np.isclose(dset[:], 0.0).all()
+
+    def test_resize_chunk_fill_time_never(self):
+        """ The resize dataset won't be filled """
+        dset = self.f.create_dataset('foo', (50, ), maxshape=(100, ),
+                                     fillvalue=4.0, fill_time='never',
+                                     chunks=(5, ))
+        plist = dset.id.get_create_plist()
+        self.assertEqual(plist.get_fill_time(), h5py.h5d.FILL_TIME_NEVER)
+
+        assert not np.isclose(dset[:], 4.0).any()
+
+        dset.resize((100, ))
+        assert not np.isclose(dset[:], 4.0).any()
+
+
 @pytest.mark.parametrize('dt,expected', [
     (int, 0),
     (np.int32, 0),
@@ -825,11 +909,10 @@ class TestExternal(BaseDataset):
             contents = fid.read()
         assert contents == testdata.tobytes()
 
-        # check efile_prefix, only for 1.10.0 due to HDFFV-9716
-        if h5py.version.hdf5_version_tuple >= (1,10,0):
-            efile_prefix = pathlib.Path(dset.id.get_access_plist().get_efile_prefix().decode()).as_posix()
-            parent = pathlib.Path(ext_file).parent.as_posix()
-            assert efile_prefix == parent
+        # check efile_prefix
+        efile_prefix = pathlib.Path(dset.id.get_access_plist().get_efile_prefix().decode()).as_posix()
+        parent = pathlib.Path(ext_file).parent.as_posix()
+        assert efile_prefix == parent
 
         dset2 = self.f.require_dataset('foo', shape, testdata.dtype, efile_prefix=os.path.dirname(ext_file))
         assert dset2.external is not None
@@ -1809,8 +1892,6 @@ class TestLowOpen(BaseDataset):
         self.assertIsInstance(dsid, h5py.h5d.DatasetID)
 
 
-@ut.skipUnless(h5py.version.hdf5_version_tuple >= (1, 10, 5),
-               "chunk info requires  HDF5 >= 1.10.5")
 def test_get_chunk_details():
     from io import BytesIO
     buf = BytesIO()
diff --git a/h5py/tests/test_h5d_direct_chunk.py b/h5py/tests/test_h5d_direct_chunk.py
index ad489a84..5bcf5584 100644
--- a/h5py/tests/test_h5d_direct_chunk.py
+++ b/h5py/tests/test_h5d_direct_chunk.py
@@ -131,10 +131,6 @@ class TestReadDirectChunkToOut:
         assert filter_mask == 0
         assert len(chunk) == ref_data.nbytes
 
-    @pytest.mark.skipif(
-        h5py.version.hdf5_version_tuple < (1, 10, 5),
-        reason="chunk info requires HDF5 >= 1.10.5",
-    )
     @pytest.mark.skipif(
         'gzip' not in h5py.filters.encode,
         reason="DEFLATE is not installed",