diff --git a/.github/workflows/check_external_links.yml b/.github/workflows/check_sphinx_links.yml similarity index 83% rename from .github/workflows/check_external_links.yml rename to .github/workflows/check_sphinx_links.yml index e030f37ae..15fc61e30 100644 --- a/.github/workflows/check_external_links.yml +++ b/.github/workflows/check_sphinx_links.yml @@ -1,4 +1,4 @@ -name: Check Sphinx external links +name: Check Sphinx links on: pull_request: schedule: @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - check-external-links: + check-sphinx-links: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -29,5 +29,5 @@ jobs: python -m pip install -r requirements-doc.txt -r requirements-opt.txt python -m pip install . - - name: Check Sphinx external links - run: sphinx-build -b linkcheck ./docs/source ./test_build + - name: Check Sphinx internal and external links + run: sphinx-build -W -b linkcheck ./docs/source ./test_build diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f29be1e..e256efeb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added `add_ref_termset`, updated helper methods for `HERD`, revised `add_ref` to support validations prior to populating the tables and added `add_ref_container`. @mavaylon1 [#968](https://github.com/hdmf-dev/hdmf/pull/968) - Use `stacklevel` in most warnings. @rly [#1027](https://github.com/hdmf-dev/hdmf/pull/1027) +- Fixed broken links in documentation and added internal link checking to workflows. @stephprince [#1031](https://github.com/hdmf-dev/hdmf/pull/1031) ### Minor Improvements - Updated `__gather_columns` to ignore the order of bases when generating columns from the super class. @mavaylon1 [#991](https://github.com/hdmf-dev/hdmf/pull/991) diff --git a/docs/Makefile b/docs/Makefile index 5129f2240..f01af1f8b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -149,7 +149,7 @@ changes: @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + $(SPHINXBUILD) -W -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." diff --git a/docs/gallery/plot_external_resources.py b/docs/gallery/plot_external_resources.py index 5bf8dd5d8..36e84b357 100644 --- a/docs/gallery/plot_external_resources.py +++ b/docs/gallery/plot_external_resources.py @@ -153,8 +153,8 @@ def __init__(self, **kwargs): # ------------------------------------------------------ # It is important to keep in mind that when adding and :py:class:`~hdmf.common.resources.Object` to # the :py:class:~hdmf.common.resources.ObjectTable, the parent object identified by -# :py:class:`~hdmf.common.resources.Object.object_id` must be the closest parent to the target object -# (i.e., :py:class:`~hdmf.common.resources.Object.relative_path` must be the shortest possible path and +# ``Object.object_id`` must be the closest parent to the target object +# (i.e., ``Object.relative_path`` must be the shortest possible path and # as such cannot contain any objects with a ``data_type`` and associated ``object_id``). # # A common example would be with the :py:class:`~hdmf.common.table.DynamicTable` class, which holds diff --git a/docs/gallery/plot_generic_data_chunk_tutorial.py b/docs/gallery/plot_generic_data_chunk_tutorial.py index 96d55c8a4..09607397b 100644 --- a/docs/gallery/plot_generic_data_chunk_tutorial.py +++ b/docs/gallery/plot_generic_data_chunk_tutorial.py @@ -119,10 +119,10 @@ def _get_dtype(self): # optimal performance (typically 1 MB or less). In contrast, a :py:class:`~hdmf.data_utils.DataChunk` in # HDMF acts as a block of data for writing data to dataset, and spans multiple HDF5 chunks to improve performance. # This is achieved by avoiding repeat -# updates to the same `Chunk` in the HDF5 file, :py:class:`~hdmf.data_utils.DataChunk` objects for write -# should align with `Chunks` in the HDF5 file, i.e., the :py:class:`~hdmf.data_utils.DataChunk.selection` -# should fully cover one or more `Chunks` in the HDF5 file to avoid repeat updates to the same -# `Chunks` in the HDF5 file. This is what the `buffer` of the :py:class`~hdmf.data_utils.GenericDataChunkIterator` +# updates to the same ``Chunk`` in the HDF5 file, :py:class:`~hdmf.data_utils.DataChunk` objects for write +# should align with ``Chunks`` in the HDF5 file, i.e., the ``DataChunk.selection`` +# should fully cover one or more ``Chunks`` in the HDF5 file to avoid repeat updates to the same +# ``Chunks`` in the HDF5 file. This is what the `buffer` of the :py:class`~hdmf.data_utils.GenericDataChunkIterator` # does, which upon each iteration returns a single # :py:class:`~hdmf.data_utils.DataChunk` object (by default > 1 GB) that perfectly spans many HDF5 chunks # (by default < 1 MB) to help reduce the number of small I/O operations diff --git a/docs/gallery/plot_term_set.py b/docs/gallery/plot_term_set.py index 71053bba5..c1f7c7257 100644 --- a/docs/gallery/plot_term_set.py +++ b/docs/gallery/plot_term_set.py @@ -107,7 +107,7 @@ ###################################################### # Viewing TermSet values # ---------------------------------------------------- -# :py:class:`~hdmf.term_set.TermSet` has methods to retrieve terms. The :py:func:`~hdmf.term_set.TermSet:view_set` +# :py:class:`~hdmf.term_set.TermSet` has methods to retrieve terms. The :py:func:`~hdmf.term_set.TermSet.view_set` # method will return a dictionary of all the terms and the corresponding information for each term. # Users can index specific terms from the :py:class:`~hdmf.term_set.TermSet`. LinkML runtime will need to be installed. # You can do so by first running ``pip install linkml-runtime``. diff --git a/docs/make.bat b/docs/make.bat index 25d3a04d4..dc48f5b3e 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -183,7 +183,7 @@ if "%1" == "changes" ( ) if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + %SPHINXBUILD% -W -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ diff --git a/docs/source/conf.py b/docs/source/conf.py index 533056dcd..caff737e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -76,6 +76,7 @@ "matplotlib": ("https://matplotlib.org/stable/", None), "h5py": ("https://docs.h5py.org/en/latest/", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "zarr": ("https://zarr.readthedocs.io/en/stable/", None), } # these links cannot be checked in github actions @@ -84,6 +85,14 @@ "https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request", ] +nitpicky = True +nitpick_ignore = [('py:class', 'Intracomm'), + ('py:class', 'h5py.RegionReference'), + ('py:class', 'h5py._hl.dataset.Dataset'), + ('py:class', 'function'), + ('py:class', 'unittest.case.TestCase'), + ] + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/source/overview_software_architecture.rst b/docs/source/overview_software_architecture.rst index 05b808ff2..973a01b2f 100644 --- a/docs/source/overview_software_architecture.rst +++ b/docs/source/overview_software_architecture.rst @@ -81,19 +81,19 @@ Spec * Interface for writing extensions or custom specification * There are several main specification classes: - * :py:class:`~hdmf.spec.AttributeSpec` - specification for metadata - * :py:class:`~hdmf.spec.GroupSpec` - specification for a collection of + * :py:class:`~hdmf.spec.spec.AttributeSpec` - specification for metadata + * :py:class:`~hdmf.spec.spec.GroupSpec` - specification for a collection of objects (i.e. subgroups, datasets, link) - * :py:class:`~hdmf.spec.DatasetSpec` - specification for dataset (like + * :py:class:`~hdmf.spec.spec.DatasetSpec` - specification for dataset (like and n-dimensional array). Specifies data type, dimensions, etc. - * :py:class:`~hdmf.spec.LinkSpec` - specification for link (like a POSIX + * :py:class:`~hdmf.spec.spec.LinkSpec` - specification for link (like a POSIX soft link) * :py:class:`~hdmf.spec.spec.RefSpec` - specification for references (References are like links, but stored as data) - * :py:class:`~hdmf.spec.DtypeSpec` - specification for compound data + * :py:class:`~hdmf.spec.spec.DtypeSpec` - specification for compound data types. Used to build complex data type specification, e.g., to define tables (used only in :py:class:`~hdmf.spec.spec.DatasetSpec` and - correspondingly :py:class:`~hdmf.spec.DatasetSpec`) + correspondingly :py:class:`~hdmf.spec.spec.DatasetSpec`) * **Main Modules:** :py:class:`hdmf.spec` diff --git a/docs/source/validation.rst b/docs/source/validation.rst index c4034b87b..cd5168cb5 100644 --- a/docs/source/validation.rst +++ b/docs/source/validation.rst @@ -3,7 +3,7 @@ Validating HDMF Data ==================== -Validation of NWB files is available through :py:mod:`~pynwb`. See the `PyNWB documentation +Validation of NWB files is available through ``pynwb``. See the `PyNWB documentation `_ for more information. -------- diff --git a/src/hdmf/backends/hdf5/h5_utils.py b/src/hdmf/backends/hdf5/h5_utils.py index 85be494c2..be3368f2b 100644 --- a/src/hdmf/backends/hdf5/h5_utils.py +++ b/src/hdmf/backends/hdf5/h5_utils.py @@ -77,7 +77,7 @@ def append(self, dataset, data): Append a value to the queue :param dataset: The dataset where the DataChunkIterator is written to - :type dataset: Dataset + :type dataset: :py:class:`~h5py.Dataset` :param data: DataChunkIterator with the data to be written :type data: AbstractDataChunkIterator """ @@ -604,7 +604,7 @@ def filter_available(filter, allow_plugin_filters): :param filter: String with the name of the filter, e.g., gzip, szip etc. int with the registered filter ID, e.g. 307 - :type filter: String, int + :type filter: str, int :param allow_plugin_filters: bool indicating whether the given filter can be dynamically loaded :return: bool indicating whether the given filter is available """ diff --git a/src/hdmf/backends/hdf5/h5tools.py b/src/hdmf/backends/hdf5/h5tools.py index 643d9a7be..3cf2e715b 100644 --- a/src/hdmf/backends/hdf5/h5tools.py +++ b/src/hdmf/backends/hdf5/h5tools.py @@ -484,7 +484,7 @@ def read(self, **kwargs): raise UnsupportedOperation("Cannot read data from file %s in mode '%s'. There are no values." % (self.source, self.__mode)) - @docval(returns='a GroupBuilder representing the data object', rtype='GroupBuilder') + @docval(returns='a GroupBuilder representing the data object', rtype=GroupBuilder) def read_builder(self): """ Read data and return the GroupBuilder representing it. @@ -978,7 +978,7 @@ def _filler(): 'default': True}, {'name': 'export_source', 'type': str, 'doc': 'The source of the builders when exporting', 'default': None}, - returns='the Group that was created', rtype='Group') + returns='the Group that was created', rtype=Group) def write_group(self, **kwargs): parent, builder = popargs('parent', 'builder', kwargs) self.logger.debug("Writing GroupBuilder '%s' to parent group '%s'" % (builder.name, parent.name)) @@ -1033,7 +1033,7 @@ def __get_path(self, builder): {'name': 'builder', 'type': LinkBuilder, 'doc': 'the LinkBuilder to write'}, {'name': 'export_source', 'type': str, 'doc': 'The source of the builders when exporting', 'default': None}, - returns='the Link that was created', rtype='Link') + returns='the Link that was created', rtype=(SoftLink, ExternalLink)) def write_link(self, **kwargs): parent, builder, export_source = getargs('parent', 'builder', 'export_source', kwargs) self.logger.debug("Writing LinkBuilder '%s' to parent group '%s'" % (builder.name, parent.name)) diff --git a/src/hdmf/common/alignedtable.py b/src/hdmf/common/alignedtable.py index 2cc20bbdc..f8126690a 100644 --- a/src/hdmf/common/alignedtable.py +++ b/src/hdmf/common/alignedtable.py @@ -29,7 +29,7 @@ class AlignedDynamicTable(DynamicTable): @docval(*get_docval(DynamicTable.__init__), {'name': 'category_tables', 'type': list, - 'doc': 'List of DynamicTables to be added to the container. NOTE: Only regular ' + 'doc': 'List of DynamicTables to be added to the container. NOTE - Only regular ' 'DynamicTables are allowed. Using AlignedDynamicTable as a category for ' 'AlignedDynamicTable is currently not supported.', 'default': None}, {'name': 'categories', 'type': 'array_data', diff --git a/src/hdmf/common/resources.py b/src/hdmf/common/resources.py index f7f08b944..29d61ea79 100644 --- a/src/hdmf/common/resources.py +++ b/src/hdmf/common/resources.py @@ -897,7 +897,7 @@ def get_object_entities(self, **kwargs): @docval({'name': 'use_categories', 'type': bool, 'default': False, 'doc': 'Use a multi-index on the columns to indicate which category each column belongs to.'}, - rtype=pd.DataFrame, returns='A DataFrame with all data merged into a flat, denormalized table.') + rtype='pandas.DataFrame', returns='A DataFrame with all data merged into a flat, denormalized table.') def to_dataframe(self, **kwargs): """ Convert the data from the keys, resources, entities, objects, and object_keys tables diff --git a/src/hdmf/data_utils.py b/src/hdmf/data_utils.py index f1eee655f..2df66106d 100644 --- a/src/hdmf/data_utils.py +++ b/src/hdmf/data_utils.py @@ -36,7 +36,7 @@ def extend_data(data, arg): """Add all the elements of the iterable arg to the end of data. :param data: The array to extend - :type data: list, DataIO, np.ndarray, h5py.Dataset + :type data: list, DataIO, numpy.ndarray, h5py.Dataset """ if isinstance(data, (list, DataIO)): data.extend(arg) @@ -383,15 +383,12 @@ def _get_data(self, selection: Tuple[slice]) -> np.ndarray: The developer of a new implementation of the GenericDataChunkIterator must ensure the data is actually loaded into memory, and not simply mapped. - :param selection: Tuple of slices, each indicating the selection indexed with respect to maxshape for that axis - :type selection: tuple of slices + :param selection: tuple of slices, each indicating the selection indexed with respect to maxshape for that axis. + Each axis of tuple is a slice of the full shape from which to pull data into the buffer. + :type selection: Tuple[slice] :returns: Array of data specified by selection - :rtype: np.ndarray - Parameters - ---------- - selection : tuple of slices - Each axis of tuple is a slice of the full shape from which to pull data into the buffer. + :rtype: numpy.ndarray """ raise NotImplementedError("The data fetching method has not been built for this DataChunkIterator!") @@ -615,7 +612,7 @@ def __next__(self): .. tip:: - :py:attr:`numpy.s_` provides a convenient way to generate index tuples using standard array slicing. This + :py:obj:`numpy.s_` provides a convenient way to generate index tuples using standard array slicing. This is often useful to define the DataChunk.selection of the current chunk :returns: DataChunk object with the data and selection of the current chunk @@ -800,17 +797,17 @@ def assertEqualShape(data1, Ensure that the shape of data1 and data2 match along the given dimensions :param data1: The first input array - :type data1: List, Tuple, np.ndarray, DataChunkIterator etc. + :type data1: List, Tuple, numpy.ndarray, DataChunkIterator :param data2: The second input array - :type data2: List, Tuple, np.ndarray, DataChunkIterator etc. + :type data2: List, Tuple, numpy.ndarray, DataChunkIterator :param name1: Optional string with the name of data1 :param name2: Optional string with the name of data2 :param axes1: The dimensions of data1 that should be matched to the dimensions of data2. Set to None to compare all axes in order. - :type axes1: int, Tuple of ints, List of ints, or None + :type axes1: int, Tuple(int), List(int), None :param axes2: The dimensions of data2 that should be matched to the dimensions of data1. Must have the same length as axes1. Set to None to compare all axes in order. - :type axes1: int, Tuple of ints, List of ints, or None + :type axes1: int, Tuple(int), List(int), None :param ignore_undetermined: Boolean indicating whether non-matching unlimited dimensions should be ignored, i.e., if two dimension don't match because we can't determine the shape of either one, then should we ignore that case or treat it as no match diff --git a/src/hdmf/spec/write.py b/src/hdmf/spec/write.py index 799ffb88a..d397c9f26 100644 --- a/src/hdmf/spec/write.py +++ b/src/hdmf/spec/write.py @@ -240,9 +240,9 @@ def export_spec(ns_builder, new_data_types, output_dir): the given data type specs. Args: - ns_builder - NamespaceBuilder instance used to build the + ns_builder: NamespaceBuilder instance used to build the namespace and extension - new_data_types - Iterable of specs that represent new data types + new_data_types: Iterable of specs that represent new data types to be added """ diff --git a/src/hdmf/testing/testcase.py b/src/hdmf/testing/testcase.py index f36ecc186..798df6fe4 100644 --- a/src/hdmf/testing/testcase.py +++ b/src/hdmf/testing/testcase.py @@ -239,8 +239,8 @@ def assertBuilderEqual(self, :type check_path: bool :param check_source: Check that the builder.source values are equal :type check_source: bool - :param message: Custom message to add when any asserts as part of this assert are failing - :type message: str or None (default=None) + :param message: Custom message to add when any asserts as part of this assert are failing (default=None) + :type message: str or None """ self.assertTrue(isinstance(builder1, Builder), message) self.assertTrue(isinstance(builder2, Builder), message) diff --git a/src/hdmf/utils.py b/src/hdmf/utils.py index b3c8129b7..12acebbc8 100644 --- a/src/hdmf/utils.py +++ b/src/hdmf/utils.py @@ -72,10 +72,10 @@ def check_type(value, argtype, allow_none=False): The difference between this function and :py:func:`isinstance` is that it allows specifying a type as a string. Furthermore, strings allow for specifying more general - types, such as a simple numeric type (i.e. ``argtype``="num"). + types, such as a simple numeric type (i.e. ``argtype="num"``). Args: - value (any): the value to check + value (Any): the value to check argtype (type, str): the type to check for allow_none (bool): whether or not to allow None as a valid value @@ -568,7 +568,7 @@ def foo(self, **kwargs): :param rtype: String describing the data type of the return values :param is_method: True if this is decorating an instance or class method, False otherwise (Default=True) :param enforce_shape: Enforce the dimensions of input arrays (Default=True) - :param validator: :py:func:`dict` objects specifying the method parameters + :param validator: :py:class:`dict` objects specifying the method parameters :param allow_extra: Allow extra arguments (Default=False) :param allow_positional: Allow positional arguments (Default=True) :param options: additional options for documenting and validating method parameters @@ -668,8 +668,6 @@ def func_call(*args, **kwargs): return func(**pargs) _rtype = rtype - if isinstance(rtype, type): - _rtype = rtype.__name__ docstring = __googledoc(func, _docval[__docval_args_loc], returns=returns, rtype=_rtype) docval_idx = {a['name']: a for a in _docval[__docval_args_loc]} # cache a name-indexed dictionary of args setattr(func_call, '__doc__', docstring) @@ -702,8 +700,10 @@ def to_str(argtype): module = argtype.__module__ name = argtype.__name__ - if module.startswith("h5py") or module.startswith("pandas") or module.startswith("builtins"): + if module.startswith("builtins"): return ":py:class:`~{name}`".format(name=name) + elif module.startswith("h5py") or module.startswith('pandas'): + return ":py:class:`~{module}.{name}`".format(name=name, module=module.split('.')[0]) else: return ":py:class:`~{module}.{name}`".format(name=name, module=module) return argtype @@ -712,18 +712,23 @@ def __sphinx_arg(arg): fmt = dict() fmt['name'] = arg.get('name') fmt['doc'] = arg.get('doc') - if isinstance(arg['type'], tuple) or isinstance(arg['type'], list): - fmt['type'] = " or ".join(map(to_str, arg['type'])) - else: - fmt['type'] = to_str(arg['type']) + fmt['type'] = type_to_str(arg['type']) return arg_fmt.format(**fmt) + def type_to_str(type_arg, string=" or "): + if isinstance(type_arg, tuple) or isinstance(type_arg, list): + type_str = f"{string}".join(type_to_str(t, string=', ') for t in type_arg) + else: + type_str = to_str(type_arg) + return type_str + sig = "%s(%s)\n\n" % (func.__name__, ", ".join(map(__sig_arg, validator))) desc = func.__doc__.strip() if func.__doc__ is not None else "" sig += docstring_fmt.format(description=desc, args="\n".join(map(__sphinx_arg, validator))) if not (ret_fmt is None or returns is None or rtype is None): - sig += ret_fmt.format(returns=returns, rtype=rtype) + rtype_fmt = type_to_str(rtype) + sig += ret_fmt.format(returns=returns, rtype=rtype_fmt) return sig @@ -852,7 +857,7 @@ def post_init(cls, func): An example use of this method would be to define a classmethod that gathers any defined methods or attributes after the base Python type construction (i.e. after - :py:func:`type` has been called) + :py:obj:`type` has been called) ''' setattr(func, cls.__postinit, True) return classmethod(func) @@ -880,8 +885,8 @@ def get_data_shape(data, strict_no_data_load=False): to enforce that this does not happen, at the cost that we may not be able to determine the shape of the array. - :param data: Array for which we should determine the shape. - :type data: List, numpy.ndarray, DataChunkIterator, any object that support __len__ or .shape. + :param data: Array for which we should determine the shape. Can be any object that supports __len__ or .shape. + :type data: List, numpy.ndarray, DataChunkIterator :param strict_no_data_load: If True and data is an out-of-core iterator, None may be returned. If False (default), the first element of data may be loaded into memory. :return: Tuple of ints indicating the size of known dimensions. Dimensions for which the size is unknown diff --git a/src/hdmf/validate/validator.py b/src/hdmf/validate/validator.py index 35e647e4a..6bea85975 100644 --- a/src/hdmf/validate/validator.py +++ b/src/hdmf/validate/validator.py @@ -635,7 +635,7 @@ def unmatched_builders(self): @property def spec_matches(self): - """Returns a list of tuples of: (spec, assigned builders)""" + """Returns a list of tuples of (spec, assigned builders)""" return [(sm.spec, sm.builders) for sm in self._spec_matches] def assign_to_specs(self, builders): diff --git a/tests/unit/utils_test/test_docval.py b/tests/unit/utils_test/test_docval.py index d0ea934f7..50c487182 100644 --- a/tests/unit/utils_test/test_docval.py +++ b/tests/unit/utils_test/test_docval.py @@ -827,6 +827,17 @@ def test_enum_forbidden_values(self): def method(self, **kwargs): pass + def test_nested_return_types(self): + """Test that having nested tuple rtype creates valid sphinx references""" + @docval({'name': 'arg1', 'type': int, 'doc': 'an arg'}, + returns='output', rtype=(list, (list, bool), (list, 'Test'))) + def method(self, **kwargs): + return [] + + doc = ('method(arg1)\n\n\n\nArgs:\n arg1 (:py:class:`~int`): an arg\n\nReturns:\n ' + ':py:class:`~list` or :py:class:`~list`, :py:class:`~bool` or :py:class:`~list`, Test: output') + self.assertEqual(method.__doc__, doc) + class TestDocValidatorChain(TestCase):