From c6f9d02e590e285ace1e91d8d30c2440240bcc04 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 28 Oct 2026 17:19:33 +0000 Subject: [PATCH 01/26] Align pyproject.toml with one in SciTools/.github/templates. --- pyproject.toml | 196 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 168 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aac3b80..4debbea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,17 @@ [build-system] # Defined by PEP 518 requires = [ - "setuptools>=77", - "setuptools_scm[toml]>=7.0", + "setuptools>=77.0.3", + "setuptools_scm[toml]>=8", "wheel", ] # Defined by PEP 517 build-backend = "setuptools.build_meta" [project] -name = "ncdata" authors = [ {name = "Iris Contributors", email = "scitools.pub@gmail.com"} ] -description = "Abstract NetCDF data objects, providing fast data transfer between analysis packages." -requires-python = ">=3.10" -keywords = [ - "cf-metadata", - "data-analysis", - "netcdf", - "iris", - "xarray", -] -license = "BSD-3-Clause" -license-files = ["LICENSE"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", @@ -41,13 +29,26 @@ classifiers = [ "Topic :: Scientific/Engineering", ] dynamic = [ + # NOTE: dependencies are NOT dynamic (for now). "readme", "version", ] +description = "Abstract NetCDF data objects, providing fast data transfer between analysis packages." # Strictly, dask + netcdf are not core dependencies. # But almost *any* conceivable use will need them. # For now, this works in place of a dependencies file, so 'dependencies' is NOT dynamic. dependencies = ["numpy", "dask", "netCDF4"] +keywords = [ + "cf-metadata", + "data-analysis", + "netcdf", + "iris", + "xarray", +] +license = "BSD-3-Clause" +license-files = ["LICENSE"] +name = "ncdata" +requires-python = ">=3.10" [project.urls] @@ -56,30 +57,169 @@ Discussions = "https://github.com/SciTools/ncdata/discussions" Documentation = "https://ncdata.readthedocs.io" Issues = "https://github.com/SciTools/ncdata/issues" +[tool.check-manifest] +# Ignores here will vary by repository. +ignore = ["lib/ncdata/_version.py/_version.py"] + +[tool.codespell] +ignore-words-list = "alpha-numeric,assertIn,degreee,discontiguities,lazyness,meaned,nin,re-use,re-uses,re-using,re-used,anc,abl,ND,Nd" + +tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] + +[tool.coverage.run] +branch = true +include = [ + "lib/ncdata/*" +] +# NB no omit, sinces tests are not within the main package + + +[tool.mypy] +# https://mypy.readthedocs.io/en/stable/error_code_list2.html +disable_error_code = [] # Include any error codes that you want to ignore +enable_error_code = [ + "ignore-without-code", + "redundant-expr", + "truthy-bool", +] +strict = true +warn_unreachable = true + +[tool.numpydoc_validation] +# We have agreed a that some of the numpydoc rules that we are going to ignore. +# Our repositories are going to have extra rules that they are ignoring that is expected, but they are not included here. +# https://numpydoc.readthedocs.io/en/latest/validation.html#validation-checks +checks = [ + "all", # Enable all numpydoc validation rules, apart from the following: + + # -> Docstring text (summary) should start in the line immediately + # after the opening quotes (not in the same line, or leaving a + # blank line in between) + # Reason for ignore: A chosen house-style + "GL01", # Permit summary line on same line as docstring opening quotes. + + # -> Closing quotes should be placed in the line after the last text + # in the docstring (do not close the quotes in the same line as + # the text, or leave a blank line between the last text and the + # quotes) + # Reason for ignore: A chosen house-style + "GL02", # Permit a blank line before docstring closing quotes. + + # -> Double line break found; please use only one blank line to + # separate sections or paragraphs, and do not leave blank lines + # at the end of docstrings + # Reason for ignore: We benefit from more flexability in formatting + "GL03", # Ignoring. + + # -> See Also section not found + "SA01", # Not all docstrings require a "See Also" section. + + # -> No extended summary found + "ES01", # Not all docstrings require an "Extended Summary" section. + + # -> No examples section found + "EX01", # Not all docstrings require an "Examples" section. + + # -> No Yields section found + "YD01", # Not all docstrings require a "Yields" section. + + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): +] +exclude = [ + '\.__eq__$', + '\.__ne__$', + '\.__repr__$', +] + +[tool.pytest.ini_options] # The list of keys vary by repository +addopts = ["-ra", "-v", "--strict-config", "--strict-markers", "--doctest-modules"] # This addopts list varies by repository +filterwarnings = ["default"] +log_level = "INFO" +minversion = "6.0" +testpaths = "" # e.g "cf_units" or "lib/iris" +xfail_strict = "True" + +[tool.repo-review] +# We have agreed that we will permanently ignore the repo-review rules in the first half of this ignore list. +ignore = [ + # https://learn.scientific-python.org/development/guides/style/#PC180 + # Not possible to run on the hardware used by the majority of our developers. Might change in future! + "PC180", # Uses prettier. + # https://learn.scientific-python.org/development/guides/packaging-simple#PP006 + "PP006", # Uses dev dependency group + + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): +] + +[tool.ruff] +line-length = 88 + +[tool.ruff.format] +preview = false + +[tool.ruff.lint] +# We have agreed that we will permanently ignore the ruff rules in the first half of this ignore list. +ignore = [ + # flake8-commas (COM) + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing. + "COM819", # Trailing comma prohibited. + + # flake8-implicit-str-concat (ISC) + # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ + # NOTE: This rule may cause conflicts when used with "ruff format". + "ISC001", # Implicitly concatenate string literals on one line. + + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): +] +preview = false +select = [ + "ALL", + # Note: the above "all" disables conflicting rules, if you want that + # rule it needs to be explicitly enabled below: + "D212", # Multi-line docstring summary should start at the first line +] + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = [""] # e.g "iris" or "cf_units" + +[tool.ruff.lint.mccabe] +max-complexity = 22 + +[tool.ruff.lint.per-file-ignores] +# All test scripts +"/*.py" = [ # e.g "cf_units/tests" or "lib/iris/tests" + # https://docs.astral.sh/ruff/rules + "D100", # Missing docstring in public module + "D205", # 1 blank line required between summary line and description + "D401", # 1 First line of docstring should be in imperative mood +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.setuptools] +zip-safe = false + [tool.setuptools.dynamic] +# NOTE: no dependencies, for now. readme = {file = "README.md", content-type = "text/markdown"} + [tool.setuptools.packages.find] include = ["ncdata*"] where = ["lib"] [tool.setuptools_scm] -version_file = "lib/ncdata/_version.py" local_scheme = "dirty-tag" version_scheme = "release-branch-semver" - - -[tool.black] -line-length = 79 -target-version = ['py310'] -include = '\.pyi?$' - -[tool.isort] -line_length = "79" -profile = "black" -known_local_folder = ["tests"] -known_first_party = "ncdata" -skip = ["lib/ncdata/__init__.py"] +write_to = "lib/ncdata/_version.py" [tool.towncrier] package = "ncdata" From 755f20ea8df4da393f35ad0fef6dffe0a5ee068d Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 28 Oct 2026 17:54:03 +0000 Subject: [PATCH 02/26] Align .pre-commit-config.yaml with SciTools/.github/templates, and autoupdate. --- .pre-commit-config.yaml | 102 +++++++++++++++++++++++++++++++--------- pyproject.toml | 3 +- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99409a1..ea5c3f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,24 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +# See https://pre-commit.ci/#configuration +# See https://github.com/scientific-python/cookie#sp-repo-review +ci: + autofix_prs: false + autofix_commit_msg: "style: pre-commit fixes" + autoupdate_commit_msg: "chore: update pre-commit hooks" + autoupdate_schedule: "monthly" + + +# Alphabetised, for lack of a better order. files: | (?x)( + .github\/.*| docs\/.+\.py| lib\/.+\.py| - tests\/.+\.py + pyproject\.toml| + tests\/.+\.py| + tools\/.+\.py ) minimum_pre_commit_version: 1.21.0 @@ -17,50 +30,93 @@ repos: - id: check-added-large-files # Check whether files parse as valid Python. - id: check-ast - # Check for file name conflicts on case-insensitive filesytems. + # Check for file name conflicts on case-insensitive filesystems. - id: check-case-conflict # Check for files that contain merge conflict strings. - id: check-merge-conflict # Check for debugger imports and py37+ `breakpoint()` calls in Python source. - id: debug-statements + # Check TOML file syntax. + - id: check-toml + # Check YAML file syntax. + - id: check-yaml + # Makes sure files end in a newline and only a newline. + # Duplicates Ruff W292 but also works on non-Python files. + - id: end-of-file-fixer + # Replaces or checks mixed line ending. + - id: mixed-line-ending # Don't commit to main branch. - id: no-commit-to-branch + # Trims trailing whitespace. + # Duplicates Ruff W291 but also works on non-Python files. + - id: trailing-whitespace + +# Hooks from all other repos +# In terms of ordering: hooks that modify files should be placed before hooks that do not. + +- repo: https://github.com/adamchainz/blacken-docs + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: 1.20.0 + hooks: + - id: blacken-docs + types: [file, rst] + +- repo: https://github.com/astral-sh/ruff-pre-commit + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: "v0.17.3" + hooks: + - id: ruff-check + types: [file, python] + args: [--fix, --show-fixes] + - id: ruff-format + types: [file, python] + +- repo: https://github.com/aio-libs/sort-all + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: v1.3.0 + hooks: + - id: sort-all + types: [file, python] - repo: https://github.com/codespell-project/codespell + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. rev: "v2.4.3" hooks: - id: codespell types_or: [asciidoc, python, markdown, rst] additional_dependencies: [tomli] -- repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.10.1 +- repo: https://github.com/pre-commit/mirrors-mypy + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: 'v2.4.0' hooks: - - id: black - pass_filenames: false - args: [--config=./pyproject.toml, .] + - id: mypy + exclude: 'noxfile\.py|docs/conf\.py' -- repo: https://github.com/PyCQA/flake8 - rev: 7.4.1 +- repo: https://github.com/numpy/numpydoc + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: v1.11.0 hooks: - - id: flake8 + - id: numpydoc-validation types: [file, python] -- repo: https://github.com/pycqa/isort - rev: 9.0.2 +- repo: https://github.com/scientific-python/cookie + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: 2026.08.14 hooks: - - id: isort - name: isort (python) + - id: sp-repo-review + additional_dependencies: ["repo-review[cli]"] + args: ["--show=errskip"] -- repo: https://github.com/asottile/blacken-docs - rev: 1.20.0 +- repo: https://github.com/abravalheri/validate-pyproject + # More exhaustive than Ruff RUF200. + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: "v0.27" hooks: - - id: blacken-docs - types: [file, rst] + - id: validate-pyproject -- repo: https://github.com/pycqa/pydocstyle - rev: 6.3.0 +- repo: https://github.com/zizmorcore/zizmor-pre-commit + # This template does not keep up-to-date with versions, visit the repo to see the most recent release. + rev: v1.31.1 hooks: - - id: pydocstyle - args: [--convention=numpy] - + - id: zizmor diff --git a/pyproject.toml b/pyproject.toml index 4debbea..86bbb98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ ignore = ["lib/ncdata/_version.py/_version.py"] [tool.codespell] ignore-words-list = "alpha-numeric,assertIn,degreee,discontiguities,lazyness,meaned,nin,re-use,re-uses,re-using,re-used,anc,abl,ND,Nd" -tool.coverage.report] +[tool.coverage.report] exclude_lines = [ "pragma: no cover", "if __name__ == .__main__.:", @@ -253,4 +253,3 @@ showcontent = true directory = "misc" name = "Developer and Internal changes" showcontent = true - From d0ae1f05e9c7234c45b77d9c7e1534af0a2e416d Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 13:59:15 +0000 Subject: [PATCH 03/26] Add project-specific ruff excludes. --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 86bbb98..24cb546 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,6 +176,7 @@ ignore = [ "ISC001", # Implicitly concatenate string literals on one line. # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): + "ANN", # not needed as we are using MyPy ] preview = false select = [ @@ -194,11 +195,15 @@ max-complexity = 22 [tool.ruff.lint.per-file-ignores] # All test scripts -"/*.py" = [ # e.g "cf_units/tests" or "lib/iris/tests" +"./tests/**/*.py" = [ # e.g "cf_units/tests" or "lib/iris/tests" # https://docs.astral.sh/ruff/rules + "S101", # Use of assert. "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method "D205", # 1 blank line required between summary line and description "D401", # 1 First line of docstring should be in imperative mood + "T201", # Print statement. ] [tool.ruff.lint.pydocstyle] From 40346bf7500c9cea2dcf83ebed4d997a08e27065 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 15:11:18 +0000 Subject: [PATCH 04/26] More ruff excludes. --- pyproject.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 24cb546..afee245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,8 +175,17 @@ ignore = [ # NOTE: This rule may cause conflicts when used with "ruff format". "ISC001", # Implicitly concatenate string literals on one line. - # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): + # project-specific ignores "ANN", # not needed as we are using MyPy + "C408", # Unnecessary `dict()` call (rewrite as a literal) + # [[TRUST: where used, readabiility is wanted over efficiency]] + "FBT", # flake8-boolean-trap (no bools in positional args) + "FIX002", # TODO comments. + "PLC0415", # Imports at top [[TRUST: exceptions are intentional]] + "SLF", # flake8 private member accesses + "TD", # flake8 TODO bans + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): + "ERA001", # Found commented-out code ] preview = false select = [ @@ -201,8 +210,10 @@ max-complexity = 22 "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D102", # Missing docstring in public method + "D103", # Missing docstring in public function "D205", # 1 blank line required between summary line and description "D401", # 1 First line of docstring should be in imperative mood + "N801", # Class name should use CapWords convention "T201", # Print statement. ] From 3cd1e225e4048145922fc772945a5a3c53c61f68 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 16:45:20 +0000 Subject: [PATCH 05/26] Correct various overlength lines. --- lib/ncdata/_core.py | 20 ++++++++++--------- lib/ncdata/dataset_like.py | 14 +++++++++---- lib/ncdata/netcdf4.py | 3 ++- lib/ncdata/utils/_compare_nc_datasets.py | 11 ++++++---- lib/ncdata/utils/_dim_indexing.py | 6 ++++-- lib/ncdata/utils/_rename_dim.py | 3 ++- lib/ncdata/utils/_save_errors.py | 3 ++- tests/data_testcase_schemas.py | 2 -- .../integration/equivalence_testing_utils.py | 4 ++-- .../test_iris_xarray_roundtrips.py | 3 ++- tests/unit/core/test_NcAttribute.py | 3 ++- tests/unit/core/test_NcVariable.py | 7 ++++--- ...test_dataset_differences__mainfunctions.py | 3 ++- .../test_variable_differences.py | 4 ++-- tests/unit/utils/test_rename_dimension.py | 10 ++++++++-- 15 files changed, 60 insertions(+), 36 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index 843e223..c9a4558 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -104,6 +104,8 @@ def rename(self, name: str, new_name: str): If "new_name == name", has no effect. Otherwise, if `new_name` already exists, the old item of that name is removed, but the renamed item remains in its original order place. + """ + """ Examples -------- @@ -116,7 +118,7 @@ def rename(self, name: str, new_name: str): >>> mymap.rename('a', 'c') >>> mymap {'c': NcAttribute('c', 'A'), 'qqq': NcAttribute('qqq', 'B'), 'd': NcAttribute('d', 'D')} - """ + """ # noqa: E501 if new_name == name: # skip this case, to avoid removing the item because it matches 'new_name'. pass @@ -266,8 +268,8 @@ def __delitem__(self, key): # noqa: D105 def __str__(self): """Provide a string representation. - A convenient representation, somewhat consistent with how we print the attributes - in the printout of an NcData / NcVariable. + A convenient representation, somewhat consistent with how we print the + attributes in the printout of an NcData / NcVariable. """ text = "AttrvalsDict{" if not self._attrs: @@ -300,11 +302,11 @@ def copy(self, deepcopy=True): ------- AttrvalsDict - The copy is 'shallow', in that it contains a NameMap with the *same* NcAttributes - as the original. These are themselves mutable, so the copy will change if the - original content attributes are modified, and vice versa. + The copy is 'shallow', in that it contains a NameMap with the *same* + NcAttributes as the original. These are themselves mutable, so the copy will + change if the original content attributes are modified, and vice versa. - N.B. if a truly independent 'deep' copy is required, you can use `copy.deepcopy`. + N.B. for a truly independent 'deep' copy, use `copy.deepcopy`. """ return self.__class__(self._attrs.copy()) @@ -345,7 +347,7 @@ def get_attrval(self, name: str) -> Any: If no such attribute exists, returns None. .. warning:: - This legacy method is now deprecated, and will be removed in a future release. + This legacy method is deprecated, and will be removed in a future release. """ return self.avals.get(name, None) @@ -355,7 +357,7 @@ def set_attrval(self, name: str, value) -> "NcAttribute": Set the Python value of a named attribute in self.attributes. .. warning:: - This legacy method is now deprecated, and will be removed in a future release. + This legacy method is deprecated, and will be removed in a future release. """ self.avals[name] = value diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index 4d58410..62842ab 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -83,7 +83,10 @@ def __setattr__(self, attr, value): def set_auto_chartostring(self, value): if bool(value): - msg = "Cannot enable 'auto_chartostring' for Nc4Dataselike or Nc4VariableLike." + msg = ( + "Cannot enable 'auto_chartostring' for Nc4Dataselike " + "or Nc4VariableLike." + ) raise ValueError(msg) @@ -208,9 +211,11 @@ def __lt__(self, other): class Nc4VariableLike(_Nc4DatalikeWithNcattrs): """ - An object which contains a :class:`ncdata.NcVariable` and emulates a :class:`netCDF4.Variable`. + An object which contains a :class:`ncdata.NcVariable` """ + """and emulates a :class:`netCDF4.Variable`. - The core, contained content object, ``self._ncdata``, is a :class:`~ncdata.NcVariable`. + The core, contained content object, ``self._ncdata``, is a + :class:`~ncdata.NcVariable`. This completely defines the parent object state. The property ``._data_array`` is detected by Iris to do direct data transfer @@ -466,7 +471,8 @@ class Nc4DimensionLike: """ An object which emulates a :class:`netCDF4.Dimension` object. - The core, contained content object, ``self._ncdata``, is a :class:`ncdata.NcDimension`. + The core, contained content object, ``self._ncdata``, is a + :class:`ncdata.NcDimension`. This completely defines the parent object state. """ diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index b70a928..268bfdc 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -214,7 +214,8 @@ def _from_nc4_group(nc4ds: Union[nc.Dataset, nc.Group], dim_chunks) -> NcData: Inner routine for :func:`from_nc4`. See docstring there. - Provided mainly for recursion into groups, also to keep the dataset open/close separate. + Provided mainly for recursion into groups, also to keep the dataset + open/close separate. """ parent_ds = nc4ds group_names_path = [] diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index e123830..5a61d61 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -109,14 +109,15 @@ def dataset_differences( >>> data2.avals.update({"a1":3, "v":7}) >>> data2.variables["b"].data = np.array([1, 7, 3, 99]) # must be an array! >>> print('\n'.join(dataset_differences(data, data2))) - Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] + """ + r"""Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] Dataset "a1" attribute values differ : 4 != 3 Dataset variable "b" data contents differ, at 2 points: @INDICES[(1,), (3,)] : LHS=[2, 4], RHS=[7, 99] See Also -------- :func:`~ncdata.utils.variable_differences` - """ + """ # noqa: E501 ds1_was_path = not hasattr(dataset_or_path_1, "variables") ds2_was_path = not hasattr(dataset_or_path_2, "variables") ds1, ds2 = None, None @@ -353,11 +354,13 @@ def variable_differences( variables to compare check_attrs_order : bool, default True If False, no error results from the same contents in a different order, - however unless `suppress_warnings` is True, the error string is issued as a warning. + however unless `suppress_warnings` is True, the error string is issued + as a warning. check_var_data : bool, default True If True, all variable data is also checked for equality. If False, only dtype and shape are compared. - NOTE: comparison of large arrays is done in-memory, so may be highly inefficient. + NOTE: comparison of large arrays is done in-memory, so may be highly + inefficient. show_n_first_different: int, default 2 Number of value differences to display. suppress_warnings : bool, default False diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index de08717..c37a2a9 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -127,13 +127,15 @@ class Slicer: This wraps the :meth:`index_by_dimensions` method for convenience, returning an object which supports the Python extended slicing syntax. - Examples + """ + """Examples -------- .. testsetup:: >>> from ncdata import NcDimension >>> from ncdata.utils import index_by_dimensions >>> data = NcData(dimensions=[NcDimension(nn, 10) for nn in ("time", "levels", "X")]) - + """ # noqa: E501 + """ >>> subdata = Slicer(data, "time")[:3] >>> ds = Slicer(data, 'levels', 'time') diff --git a/lib/ncdata/utils/_rename_dim.py b/lib/ncdata/utils/_rename_dim.py index 8f86d6f..d4ead95 100644 --- a/lib/ncdata/utils/_rename_dim.py +++ b/lib/ncdata/utils/_rename_dim.py @@ -14,7 +14,8 @@ def _rename_dims_in_vars(ncdata: NcData, name_from: str, name_to: str) -> None: ] ) - # Also rename in all sub-groups, except where the dimension is redefined ("scope hole"). + # Also rename in all sub-groups, except where the dimension is redefined + # (i.e. inside a "scope hole"). for grp in ncdata.groups.values(): if name_from not in grp.dimensions: _rename_dims_in_vars(grp, name_from, name_to) diff --git a/lib/ncdata/utils/_save_errors.py b/lib/ncdata/utils/_save_errors.py index 5670e8b..229815f 100644 --- a/lib/ncdata/utils/_save_errors.py +++ b/lib/ncdata/utils/_save_errors.py @@ -205,7 +205,8 @@ def save_errors(ncdata: NcData) -> List[str]: ----- The checks made are roughly the following: - **(1)** check names in all components (dimensions, variables, attributes and groups): + **(1)** check names in all components + (dimensions, variables, attributes and groups): * all names are valid netcdf names * all element names match their key in the component, diff --git a/tests/data_testcase_schemas.py b/tests/data_testcase_schemas.py index 8212577..8e4a687 100644 --- a/tests/data_testcase_schemas.py +++ b/tests/data_testcase_schemas.py @@ -365,9 +365,7 @@ def _define_iris_testdata_testcases(): # replace path-separators and other awkward chars with dunder for char in ("/", ".", "-"): param_name = param_name.replace(char, "__") - # TEMPORARY: skip unstructured ones, just for now, as it makes the run faster if not any(key in param_name for key in EXCLUDES): - # if "small_theta_colpex" in param_name: testcases[f"testdata__{param_name}"] = filepath return testcases diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index 521a2a5..101c246 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -64,8 +64,8 @@ def fix_arrays(a1, a2): for arr in (a1, a2) ) if allmasked_1 and allmasked_2: - # IN this case == will return 'masked', but for our purposes should be - # considered equal, i.e. 'True' + # IN this case == will return 'masked', but for our purposes + # should be considered equal, i.e. 'True' a1 = np.zeros(a1.shape, dtype=bool) a2 = a1 diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 7554516..6da444c 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -305,7 +305,8 @@ def test_roundtrip_xix( var.data = data if "calendar" in var.avals: if var.avals["calendar"] == "gregorian": - # the calendar name 'gregorian' is now deprecated, so Iris replaces it. + # the calendar name 'gregorian' is now deprecated, + # so Iris replaces it. var.avals["calendar"] = "standard" result = dataset_differences( diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index 064b29e..09982f3 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -219,7 +219,8 @@ def test_eq_onestring_arrayofonestring(self): assert attr1 == attr2 assert attr1.value.dtype == " Date: Thu, 29 Oct 2026 17:12:13 +0000 Subject: [PATCH 06/26] Reconfigure to enforce docstring summaries on the _second_ line, i.e. not immediately after the triple-quote. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index afee245..4e3d51d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ ignore = [ "PLC0415", # Imports at top [[TRUST: exceptions are intentional]] "SLF", # flake8 private member accesses "TD", # flake8 TODO bans + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): "ERA001", # Found commented-out code ] @@ -192,7 +193,7 @@ select = [ "ALL", # Note: the above "all" disables conflicting rules, if you want that # rule it needs to be explicitly enabled below: - "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line ] [tool.ruff.lint.isort] From b6aa0d7866e4b720f5f7bd0bf04603216a0e9fb2 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 17:30:34 +0000 Subject: [PATCH 07/26] Fix docstring indent at split. --- lib/ncdata/utils/_compare_nc_datasets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index 5a61d61..c16ac76 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -109,8 +109,8 @@ def dataset_differences( >>> data2.avals.update({"a1":3, "v":7}) >>> data2.variables["b"].data = np.array([1, 7, 3, 99]) # must be an array! >>> print('\n'.join(dataset_differences(data, data2))) - """ - r"""Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] + """ + r"""Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] Dataset "a1" attribute values differ : 4 != 3 Dataset variable "b" data contents differ, at 2 points: @INDICES[(1,), (3,)] : LHS=[2, 4], RHS=[7, 99] From 2a91750917351479c62376f3dd260c96d1d1b716 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 17:51:39 +0000 Subject: [PATCH 08/26] Don't provide overlength parts of docstring separately, as it doesn't work. --- lib/ncdata/_core.py | 2 -- lib/ncdata/dataset_like.py | 3 +-- lib/ncdata/utils/_compare_nc_datasets.py | 3 +-- lib/ncdata/utils/_dim_indexing.py | 8 +++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index c9a4558..bce3711 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -104,8 +104,6 @@ def rename(self, name: str, new_name: str): If "new_name == name", has no effect. Otherwise, if `new_name` already exists, the old item of that name is removed, but the renamed item remains in its original order place. - """ - """ Examples -------- diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index 62842ab..1b0e86c 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -211,8 +211,7 @@ def __lt__(self, other): class Nc4VariableLike(_Nc4DatalikeWithNcattrs): """ - An object which contains a :class:`ncdata.NcVariable` """ - """and emulates a :class:`netCDF4.Variable`. + An object which contains a :class:`ncdata.NcVariable` and emulates a :class:`netCDF4.Variable`. The core, contained content object, ``self._ncdata``, is a :class:`~ncdata.NcVariable`. diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index c16ac76..07faef0 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -109,8 +109,7 @@ def dataset_differences( >>> data2.avals.update({"a1":3, "v":7}) >>> data2.variables["b"].data = np.array([1, 7, 3, 99]) # must be an array! >>> print('\n'.join(dataset_differences(data, data2))) - """ - r"""Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] + Dataset attribute lists do not match: ['a1'] != ['a1', 'v'] Dataset "a1" attribute values differ : 4 != 3 Dataset variable "b" data contents differ, at 2 points: @INDICES[(1,), (3,)] : LHS=[2, 4], RHS=[7, 99] diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index c37a2a9..443da40 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -127,15 +127,13 @@ class Slicer: This wraps the :meth:`index_by_dimensions` method for convenience, returning an object which supports the Python extended slicing syntax. - """ - """Examples + Examples -------- .. testsetup:: >>> from ncdata import NcDimension >>> from ncdata.utils import index_by_dimensions >>> data = NcData(dimensions=[NcDimension(nn, 10) for nn in ("time", "levels", "X")]) - """ # noqa: E501 - """ + >>> subdata = Slicer(data, "time")[:3] >>> ds = Slicer(data, 'levels', 'time') @@ -156,7 +154,7 @@ class Slicer: See Also -------- :meth:`index_by_dimensions` - """ + """ # noqa: E501 def __init__(self, ncdata: NcData, *dimension_names: tuple[str]): """ From bdbc09aa21d4f9cc8eb5226c457c88f9daba9ea2 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 17:26:36 +0000 Subject: [PATCH 09/26] Add licence headers to all sourcefiles. --- docs/conf.py | 15 ++++++++++----- lib/ncdata/__init__.py | 4 ++++ lib/ncdata/_core.py | 4 ++++ lib/ncdata/dataset_like.py | 4 ++++ lib/ncdata/iris.py | 4 ++++ lib/ncdata/iris_xarray.py | 4 ++++ lib/ncdata/netcdf4.py | 4 ++++ lib/ncdata/threadlock_sharing.py | 4 ++++ lib/ncdata/utils/__init__.py | 4 ++++ lib/ncdata/utils/_compare_nc_datasets.py | 4 ++++ lib/ncdata/utils/_copy.py | 4 ++++ lib/ncdata/utils/_dim_indexing.py | 4 ++++ lib/ncdata/utils/_rename_dim.py | 4 ++++ lib/ncdata/utils/_save_errors.py | 4 ++++ lib/ncdata/xarray.py | 4 ++++ tests/__init__.py | 4 ++++ tests/data_testcase_schemas.py | 4 ++++ tests/integration/__init__.py | 4 ++++ tests/integration/equivalence_testing_utils.py | 4 ++++ tests/integration/example_scripts/__init__.py | 4 ++++ .../example_scripts/ex_dataset_print.py | 4 ++++ .../example_scripts/ex_iris_saveto_ncdata.py | 4 ++++ .../example_scripts/ex_iris_xarray_conversion.py | 4 ++++ .../ex_ncdata_netcdf_conversion.py | 4 ++++ tests/integration/test_exercises.py | 4 ++++ .../test_iris_load_and_save_equivalence.py | 4 ++++ tests/integration/test_iris_xarray_roundtrips.py | 4 ++++ tests/integration/test_netcdf_roundtrips.py | 4 ++++ .../test_xarray_load_and_save_equivalence.py | 4 ++++ tests/integration/test_zarr_to_iris.py | 4 ++++ tests/test_samplecode_cdlgen_comparablecdl.py | 4 ++++ tests/unit/__init__.py | 4 ++++ tests/unit/core/__init__.py | 4 ++++ tests/unit/core/test_AttributeAccessMixin.py | 4 ++++ tests/unit/core/test_AttrvalsDict.py | 4 ++++ tests/unit/core/test_NameMap.py | 4 ++++ tests/unit/core/test_NcAttribute.py | 4 ++++ tests/unit/core/test_NcData.py | 4 ++++ tests/unit/core/test_NcDimension.py | 4 ++++ tests/unit/core/test_NcVariable.py | 4 ++++ tests/unit/iris/__init__.py | 4 ++++ tests/unit/iris/test_from_iris.py | 4 ++++ tests/unit/iris/test_to_iris.py | 4 ++++ tests/unit/netcdf/__init__.py | 4 ++++ tests/unit/netcdf/test_from_nc4.py | 4 ++++ tests/unit/netcdf/test_to_nc4.py | 4 ++++ tests/unit/tests/test_version_tuple.py | 4 ++++ tests/unit/utils/__init__.py | 4 ++++ tests/unit/utils/compare_nc_datasets/__init__.py | 4 ++++ .../test_dataset_differences__additional.py | 4 ++++ .../test_dataset_differences__mainfunctions.py | 4 ++++ .../test_variable_differences.py | 4 ++++ tests/unit/utils/dim_indexing/__init__.py | 4 ++++ tests/unit/utils/dim_indexing/test_Slicer.py | 4 ++++ .../dim_indexing/test_index_by_dimensions.py | 4 ++++ tests/unit/utils/test_ncdata_copy.py | 4 ++++ tests/unit/utils/test_rename_dimension.py | 4 ++++ tests/unit/utils/test_save_errors.py | 4 ++++ tests/unit/xarray/__init__.py | 4 ++++ tests/unit/xarray/test_from_xarray.py | 4 ++++ tests/unit/xarray/test_to_xarray.py | 4 ++++ 61 files changed, 250 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f354243..b2e896c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,14 @@ -# noqa -# Configuration file for the Sphinx documentation builder. +# Copyright SciTools contributors # -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +""" +Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full + list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" # -- Path setup -------------------------------------------------------------- diff --git a/lib/ncdata/__init__.py b/lib/ncdata/__init__.py index 02d25dd..e5a003a 100644 --- a/lib/ncdata/__init__.py +++ b/lib/ncdata/__init__.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ An abstract representation of Netcdf data with groups, variables + attributes. diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index bce3711..da62fea 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Our main classes for representing netCDF data. diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index 1b0e86c..a58b064 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. r""" An adaptor layer for :mod:`ncdata` to emulate :mod:`netCDF4`. diff --git a/lib/ncdata/iris.py b/lib/ncdata/iris.py index f20bbd3..225a387 100644 --- a/lib/ncdata/iris.py +++ b/lib/ncdata/iris.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. r""" Interface routines for converting data between ncdata and Iris. diff --git a/lib/ncdata/iris_xarray.py b/lib/ncdata/iris_xarray.py index 4198200..d5fa320 100644 --- a/lib/ncdata/iris_xarray.py +++ b/lib/ncdata/iris_xarray.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. r""" Interface routines for converting data between Xarray and Iris. diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index 268bfdc..7d47412 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Interface routines for converting data between ncdata and netCDF4. diff --git a/lib/ncdata/threadlock_sharing.py b/lib/ncdata/threadlock_sharing.py index a245fc3..c526435 100644 --- a/lib/ncdata/threadlock_sharing.py +++ b/lib/ncdata/threadlock_sharing.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. r""" Support for "unifying" the thread-safety mechanisms between ncdata and other packages. diff --git a/lib/ncdata/utils/__init__.py b/lib/ncdata/utils/__init__.py index f44fe95..ec5eb40 100644 --- a/lib/ncdata/utils/__init__.py +++ b/lib/ncdata/utils/__init__.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """General user utility functions.""" from ._compare_nc_datasets import dataset_differences, variable_differences diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index 07faef0..abeeb20 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Utility for comparing 2 netcdf datasets. diff --git a/lib/ncdata/utils/_copy.py b/lib/ncdata/utils/_copy.py index 7ae7c04..0e59acb 100644 --- a/lib/ncdata/utils/_copy.py +++ b/lib/ncdata/utils/_copy.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Utility to copy NcData objects, but not copying any contained data arrays.""" from ncdata import NameMap, NcAttribute, NcData diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index 443da40..80563fc 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. from numbers import Number from typing import Any, Mapping diff --git a/lib/ncdata/utils/_rename_dim.py b/lib/ncdata/utils/_rename_dim.py index d4ead95..7779743 100644 --- a/lib/ncdata/utils/_rename_dim.py +++ b/lib/ncdata/utils/_rename_dim.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Utility to rename dimensions.""" from ncdata import NcData diff --git a/lib/ncdata/utils/_save_errors.py b/lib/ncdata/utils/_save_errors.py index 229815f..550f38c 100644 --- a/lib/ncdata/utils/_save_errors.py +++ b/lib/ncdata/utils/_save_errors.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """User utility routines for ncdata.""" from typing import Dict, List, Union diff --git a/lib/ncdata/xarray.py b/lib/ncdata/xarray.py index 2a7306c..7246e18 100644 --- a/lib/ncdata/xarray.py +++ b/lib/ncdata/xarray.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. r""" Interface routines for converting data between ncdata and xarray. diff --git a/tests/__init__.py b/tests/__init__.py index 9d8ecca..d9b742b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for :mod:`ncdata`.""" from pathlib import Path diff --git a/tests/data_testcase_schemas.py b/tests/data_testcase_schemas.py index 8e4a687..8ef244f 100644 --- a/tests/data_testcase_schemas.py +++ b/tests/data_testcase_schemas.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Define a set of "standard" test cases, built as actual netcdf files. diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 3b98f77..19a55c5 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Integration tests for :mod:`ncdata`.""" diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index 101c246..ab96157 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Utility routines for conversion equivalence testing. diff --git a/tests/integration/example_scripts/__init__.py b/tests/integration/example_scripts/__init__.py index cb4b2fa..b0157d9 100644 --- a/tests/integration/example_scripts/__init__.py +++ b/tests/integration/example_scripts/__init__.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Example scripts for :mod:`ncdata`. diff --git a/tests/integration/example_scripts/ex_dataset_print.py b/tests/integration/example_scripts/ex_dataset_print.py index 02fbac8..ceffd07 100644 --- a/tests/integration/example_scripts/ex_dataset_print.py +++ b/tests/integration/example_scripts/ex_dataset_print.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Temporary integrational proof-of-concept example for dataset printout.""" import iris diff --git a/tests/integration/example_scripts/ex_iris_saveto_ncdata.py b/tests/integration/example_scripts/ex_iris_saveto_ncdata.py index 361ff06..24f67d0 100644 --- a/tests/integration/example_scripts/ex_iris_saveto_ncdata.py +++ b/tests/integration/example_scripts/ex_iris_saveto_ncdata.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ A proof-of-concept example workflow for :func:`ncdata.iris.from_iris`. diff --git a/tests/integration/example_scripts/ex_iris_xarray_conversion.py b/tests/integration/example_scripts/ex_iris_xarray_conversion.py index 0a193ef..0b968a5 100644 --- a/tests/integration/example_scripts/ex_iris_xarray_conversion.py +++ b/tests/integration/example_scripts/ex_iris_xarray_conversion.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ A proof-of-concept example workflow for :mod:`ncdata.iris_xarray`. diff --git a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py index 3fde5bf..7ee92aa 100644 --- a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py +++ b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ A proof-of-concept example workflow for :mod:`ncdata.netcdf4`. diff --git a/tests/integration/test_exercises.py b/tests/integration/test_exercises.py index 68a33f4..e863b05 100644 --- a/tests/integration/test_exercises.py +++ b/tests/integration/test_exercises.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Temporary : simple tests to exercise the 'ex_' scripts. diff --git a/tests/integration/test_iris_load_and_save_equivalence.py b/tests/integration/test_iris_load_and_save_equivalence.py index 922fe6d..5253fa9 100644 --- a/tests/integration/test_iris_load_and_save_equivalence.py +++ b/tests/integration/test_iris_load_and_save_equivalence.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Test ncdata.iris by checking roundtrips for standard testcases. diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 6da444c..0d0f1df 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Test ncdata.iris by checking roundtrips for standard testcases. diff --git a/tests/integration/test_netcdf_roundtrips.py b/tests/integration/test_netcdf_roundtrips.py index d5e136b..2b33155 100644 --- a/tests/integration/test_netcdf_roundtrips.py +++ b/tests/integration/test_netcdf_roundtrips.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Test ncdata.netcdf by checking load-save roundtrips for standard testcases. """ diff --git a/tests/integration/test_xarray_load_and_save_equivalence.py b/tests/integration/test_xarray_load_and_save_equivalence.py index aa236db..5e60db3 100644 --- a/tests/integration/test_xarray_load_and_save_equivalence.py +++ b/tests/integration/test_xarray_load_and_save_equivalence.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Test ncdata.xarray by checking roundtrips for standard testcases. diff --git a/tests/integration/test_zarr_to_iris.py b/tests/integration/test_zarr_to_iris.py index 9127ce4..8b88b55 100644 --- a/tests/integration/test_zarr_to_iris.py +++ b/tests/integration/test_zarr_to_iris.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Test conversion of remote and local Zarr store to iris Cube.""" from importlib.resources import files as importlib_files diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index 2245cff..19ea144 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ PoC code for netcdf file CDL-based testing mechanisms. diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 358a9a2..1fb04e5 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata`.""" diff --git a/tests/unit/core/__init__.py b/tests/unit/core/__init__.py index 4269eb7..dbf4412 100644 --- a/tests/unit/core/__init__.py +++ b/tests/unit/core/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.core`.""" diff --git a/tests/unit/core/test_AttributeAccessMixin.py b/tests/unit/core/test_AttributeAccessMixin.py index c3f243f..2a30d7b 100644 --- a/tests/unit/core/test_AttributeAccessMixin.py +++ b/tests/unit/core/test_AttributeAccessMixin.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for class :class:`ncdata._core._AttributeAccessMixin`. diff --git a/tests/unit/core/test_AttrvalsDict.py b/tests/unit/core/test_AttrvalsDict.py index 7a92911..af0039f 100644 --- a/tests/unit/core/test_AttrvalsDict.py +++ b/tests/unit/core/test_AttrvalsDict.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata._core.AttrvalsDict`.""" from copy import deepcopy diff --git a/tests/unit/core/test_NameMap.py b/tests/unit/core/test_NameMap.py index 67e22b9..0912282 100644 --- a/tests/unit/core/test_NameMap.py +++ b/tests/unit/core/test_NameMap.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for class :class:`ncdata.NameMap`. """ diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index 09982f3..9780a9d 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for class :class:`ncdata.NcAttribute`. diff --git a/tests/unit/core/test_NcData.py b/tests/unit/core/test_NcData.py index 1e901e6..0a7034e 100644 --- a/tests/unit/core/test_NcData.py +++ b/tests/unit/core/test_NcData.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata.NcData`. There is almost no behaviour, but we can test some constructor usages. diff --git a/tests/unit/core/test_NcDimension.py b/tests/unit/core/test_NcDimension.py index 9f334da..efca8d4 100644 --- a/tests/unit/core/test_NcDimension.py +++ b/tests/unit/core/test_NcDimension.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata.NcDimension`.""" import numpy as np diff --git a/tests/unit/core/test_NcVariable.py b/tests/unit/core/test_NcVariable.py index e74c5be..13b8aa0 100644 --- a/tests/unit/core/test_NcVariable.py +++ b/tests/unit/core/test_NcVariable.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata.NcVariable`. There is almost no actual behaviour, but we can test some constructor behaviours, diff --git a/tests/unit/iris/__init__.py b/tests/unit/iris/__init__.py index a8e3fc0..6a664c4 100644 --- a/tests/unit/iris/__init__.py +++ b/tests/unit/iris/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.iris`.""" diff --git a/tests/unit/iris/test_from_iris.py b/tests/unit/iris/test_from_iris.py index 7dc8823..e32b699 100644 --- a/tests/unit/iris/test_from_iris.py +++ b/tests/unit/iris/test_from_iris.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.iris.from_iris`. diff --git a/tests/unit/iris/test_to_iris.py b/tests/unit/iris/test_to_iris.py index e530831..3d0cc59 100644 --- a/tests/unit/iris/test_to_iris.py +++ b/tests/unit/iris/test_to_iris.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.iris.to_iris`. diff --git a/tests/unit/netcdf/__init__.py b/tests/unit/netcdf/__init__.py index 80ee6af..e6e8423 100644 --- a/tests/unit/netcdf/__init__.py +++ b/tests/unit/netcdf/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.netcdf`.""" diff --git a/tests/unit/netcdf/test_from_nc4.py b/tests/unit/netcdf/test_from_nc4.py index d19703d..1ad3939 100644 --- a/tests/unit/netcdf/test_from_nc4.py +++ b/tests/unit/netcdf/test_from_nc4.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.netcdf.from_nc4`. diff --git a/tests/unit/netcdf/test_to_nc4.py b/tests/unit/netcdf/test_to_nc4.py index 85c8589..6acde50 100644 --- a/tests/unit/netcdf/test_to_nc4.py +++ b/tests/unit/netcdf/test_to_nc4.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.netcdf.to_nc4`. diff --git a/tests/unit/tests/test_version_tuple.py b/tests/unit/tests/test_version_tuple.py index dabdbec..2ac5a60 100644 --- a/tests/unit/tests/test_version_tuple.py +++ b/tests/unit/tests/test_version_tuple.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for the test utility :func:`tests.version_majorminor`. """ diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py index 7e82563..2a13dd9 100644 --- a/tests/unit/utils/__init__.py +++ b/tests/unit/utils/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.utils`.""" diff --git a/tests/unit/utils/compare_nc_datasets/__init__.py b/tests/unit/utils/compare_nc_datasets/__init__.py index 7f699aa..3b7b71c 100644 --- a/tests/unit/utils/compare_nc_datasets/__init__.py +++ b/tests/unit/utils/compare_nc_datasets/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.utils._compare_nc_datasets`.""" diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py index 9969a13..4a482af 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :mod:`tests.unit.netcdf._compare_nc_files` Split in two files ... diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py index 9e0a2de..0d7b211 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :mod:`tests.unit.netcdf._compare_nc_files` Split in two files ... diff --git a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py index c9ca919..0d47f82 100644 --- a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py +++ b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. import dask.array as da import numpy as np import pytest diff --git a/tests/unit/utils/dim_indexing/__init__.py b/tests/unit/utils/dim_indexing/__init__.py index 7ad084b..0618931 100644 --- a/tests/unit/utils/dim_indexing/__init__.py +++ b/tests/unit/utils/dim_indexing/__init__.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.utils._dim_indexing`.""" import numpy as np diff --git a/tests/unit/utils/dim_indexing/test_Slicer.py b/tests/unit/utils/dim_indexing/test_Slicer.py index 4652cc2..5b8ff67 100644 --- a/tests/unit/utils/dim_indexing/test_Slicer.py +++ b/tests/unit/utils/dim_indexing/test_Slicer.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata.utils.Slicer`. This is the "indirect" approach, with a Slicer object. diff --git a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py index 099b1d7..8b1c607 100644 --- a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py +++ b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for class :class:`ncdata.utils.index_by_dimension`. This is the more direct indexer approach. diff --git a/tests/unit/utils/test_ncdata_copy.py b/tests/unit/utils/test_ncdata_copy.py index aff9734..bd0629e 100644 --- a/tests/unit/utils/test_ncdata_copy.py +++ b/tests/unit/utils/test_ncdata_copy.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for :func:`ncdata.utils.ncdata_copy`. This is generic utility function version of the copy operation. diff --git a/tests/unit/utils/test_rename_dimension.py b/tests/unit/utils/test_rename_dimension.py index b7fabb0..19c2605 100644 --- a/tests/unit/utils/test_rename_dimension.py +++ b/tests/unit/utils/test_rename_dimension.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Tests for :func:`ncdata.utils.rename_dimension`.""" import numpy as np diff --git a/tests/unit/utils/test_save_errors.py b/tests/unit/utils/test_save_errors.py index fff1853..aaaeea3 100644 --- a/tests/unit/utils/test_save_errors.py +++ b/tests/unit/utils/test_save_errors.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :mod:`ncdata.utils.save_errors` """ diff --git a/tests/unit/xarray/__init__.py b/tests/unit/xarray/__init__.py index c143809..afc3502 100644 --- a/tests/unit/xarray/__init__.py +++ b/tests/unit/xarray/__init__.py @@ -1 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """Unit tests for :mod:`ncdata.xarray`.""" diff --git a/tests/unit/xarray/test_from_xarray.py b/tests/unit/xarray/test_from_xarray.py index dd502e1..e5c43b1 100644 --- a/tests/unit/xarray/test_from_xarray.py +++ b/tests/unit/xarray/test_from_xarray.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.xarray.from_xarray`. diff --git a/tests/unit/xarray/test_to_xarray.py b/tests/unit/xarray/test_to_xarray.py index 592c975..4b88a8f 100644 --- a/tests/unit/xarray/test_to_xarray.py +++ b/tests/unit/xarray/test_to_xarray.py @@ -1,3 +1,7 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. """ Tests for :func:`ncdata.xarray.to_xarray`. From 14e8ba7e51d502407d0bb5ada12eb822853aa5ff Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 29 Oct 2026 18:00:37 +0000 Subject: [PATCH 10/26] Recognise custom licence-header. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4e3d51d..1f4c6f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -200,6 +200,9 @@ select = [ force-sort-within-sections = true known-first-party = [""] # e.g "iris" or "cf_units" +[tool.ruff.lint.flake8-copyright] +notice-rgx = "^[ #]*Copyright SciTools contributors" + [tool.ruff.lint.mccabe] max-complexity = 22 From a3659b9b1416a6e4d650ee5a87326e00d1484ad7 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 30 Oct 2026 11:05:09 +0000 Subject: [PATCH 11/26] Allow print anywhere. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1f4c6f7..9640673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,7 @@ ignore = [ "FIX002", # TODO comments. "PLC0415", # Imports at top [[TRUST: exceptions are intentional]] "SLF", # flake8 private member accesses + "T201", # Print statement. "TD", # flake8 TODO bans # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): @@ -218,7 +219,6 @@ max-complexity = 22 "D205", # 1 blank line required between summary line and description "D401", # 1 First line of docstring should be in imperative mood "N801", # Class name should use CapWords convention - "T201", # Print statement. ] [tool.ruff.lint.pydocstyle] From 9f7416d2e59a924e27a175c04213d0db9a4f1a93 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 30 Oct 2026 11:19:15 +0000 Subject: [PATCH 12/26] Don't lint conf.py at all. --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea5c3f4..8cbba13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,10 @@ files: | tests\/.+\.py| tools\/.+\.py ) + +exclude: | + docs/conf.py # sufficiently different that linting is unhelpful + minimum_pre_commit_version: 1.21.0 repos: From ba6017c0e99443e9eb0b0ebdf8129d11f864e7ec Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 30 Oct 2026 15:27:03 +0000 Subject: [PATCH 13/26] Configure ruff isort to control automatic import sorting. --- pyproject.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9640673..bf03386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,6 +158,7 @@ ignore = [ [tool.ruff] line-length = 88 +src = ["lib"] [tool.ruff.format] preview = false @@ -198,8 +199,12 @@ select = [ ] [tool.ruff.lint.isort] -force-sort-within-sections = true -known-first-party = [""] # e.g "iris" or "cf_units" +#known-first-party = ["ncdata"] +known-local-folder = ["tests"] +# The identification of first-party modules is wrong here, and **different from isort**. +# It seems to consider these first-party because there is an 'ncdata.xarray' etc. +# Also that confuses it and it then treats all ncdata as **third-party**. +#known-third-party = ["iris", "xarray"] [tool.ruff.lint.flake8-copyright] notice-rgx = "^[ #]*Copyright SciTools contributors" From 3c9b7ff948b57cdc8020a9e5f35674d521c61619 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 30 Oct 2026 17:51:46 +0000 Subject: [PATCH 14/26] Avoid some sorting operations. --- lib/ncdata/__init__.py | 3 ++- lib/ncdata/netcdf4.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ncdata/__init__.py b/lib/ncdata/__init__.py index e5a003a..98e48ea 100644 --- a/lib/ncdata/__init__.py +++ b/lib/ncdata/__init__.py @@ -18,7 +18,8 @@ from ._core import NameMap, NcAttribute, NcData, NcDimension, NcVariable from ._version import __version__ -__all__ = [ +# N.B. avoiding sorting this, the order is convenient for understanding. +__all__ = [ # noqa: RUF022 "NcData", "NcDimension", "NcVariable", diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index 7d47412..9ebc180 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -109,7 +109,8 @@ class _NetCDFDataProxy: Copied from Iris, with some simplifications. """ - __slots__ = ("shape", "dtype", "path", "variable_name", "group_names_path") + # N.B. avoiding sorting this, the order is convenient for understanding. + __slots__ = ("shape", "dtype", "path", "variable_name", "group_names_path") # noqa: RUF023 def __init__( self, shape, dtype, filepath, variable_name, group_names_path=None From 46cfa7ef4f4b8aee5243c4dc0e6dc66078a90d5e Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 30 Oct 2026 17:52:35 +0000 Subject: [PATCH 15/26] Ruff-check autofixes: modernise type hints; remove dead noqa's; import ordering; docstring summary lines; if/elif tidies. --- lib/ncdata/_core.py | 81 +++++++++---------- lib/ncdata/dataset_like.py | 23 +++--- lib/ncdata/iris.py | 7 +- lib/ncdata/netcdf4.py | 15 ++-- lib/ncdata/utils/_compare_nc_datasets.py | 24 +++--- lib/ncdata/utils/_dim_indexing.py | 7 +- lib/ncdata/utils/_save_errors.py | 26 +++--- lib/ncdata/xarray.py | 6 +- tests/__init__.py | 3 +- tests/data_testcase_schemas.py | 14 ++-- .../example_scripts/ex_dataset_print.py | 2 +- .../example_scripts/ex_iris_saveto_ncdata.py | 2 +- .../ex_iris_xarray_conversion.py | 4 +- .../ex_ncdata_netcdf_conversion.py | 4 +- tests/integration/test_zarr_to_iris.py | 3 +- tests/test_samplecode_cdlgen_comparablecdl.py | 6 +- tests/unit/core/test_NcAttribute.py | 15 ++-- tests/unit/core/test_NcData.py | 3 +- tests/unit/core/test_NcDimension.py | 2 +- tests/unit/core/test_NcVariable.py | 3 +- tests/unit/iris/test_to_iris.py | 1 - tests/unit/netcdf/test_to_nc4.py | 3 +- tests/unit/utils/dim_indexing/__init__.py | 2 +- tests/unit/utils/dim_indexing/test_Slicer.py | 3 +- .../dim_indexing/test_index_by_dimensions.py | 3 +- tests/unit/utils/test_ncdata_copy.py | 5 +- tests/unit/utils/test_rename_dimension.py | 8 +- tests/unit/xarray/test_to_xarray.py | 1 - 28 files changed, 139 insertions(+), 137 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index da62fea..e2b4119 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -16,15 +16,11 @@ """ +from collections.abc import Iterable, Mapping, MutableMapping from functools import wraps from typing import ( Any, - Dict, - Iterable, - Mapping, - MutableMapping, Optional, - Tuple, Union, ) @@ -137,7 +133,7 @@ def rename(self, name: str, new_name: str): @classmethod def from_items( - cls, arg: Union[Iterable, Mapping], item_type=None + cls, arg: Iterable | Mapping, item_type=None ) -> "NameMap": """ Convert an iterable or mapping of items to a NameMap. @@ -235,10 +231,10 @@ class AttrvalsDict(MutableMapping): def __init__(self, attrs: NameMap): self._attrs = attrs - def __getitem__(self, key): # noqa: D105 + def __getitem__(self, key): return self._attrs[key].as_python_value() - def __setitem__(self, key, value): # noqa: D105 + def __setitem__(self, key, value): # Special case : assigning an NcAttribute object is equivalent to assigning # its value (which forces rename). # This is supposed to avoid potential confusion (!) @@ -255,20 +251,21 @@ def __setitem__(self, key, value): # noqa: D105 # # Provide the minimal set of methods required for a MutableMapping # - def __len__(self): # noqa: D105 + def __len__(self): return len(self._attrs) - def __iter__(self): # noqa: D105 + def __iter__(self): return iter(self._attrs) - def __delitem__(self, key): # noqa: D105 + def __delitem__(self, key): self._attrs.__delitem__(key) # # Other useful methods # def __str__(self): - """Provide a string representation. + """ + Provide a string representation. A convenient representation, somewhat consistent with how we print the attributes in the printout of an NcData / NcVariable. @@ -293,7 +290,7 @@ def __str__(self): # Also really nice-to-have : the "rename" operation, as implemented by NameMap @wraps(NameMap.rename) # N.B. this is *not* missing a docstring, actually.. - def rename(self, name: str, new_name: str): # noqa: D102 + def rename(self, name: str, new_name: str): self._attrs.rename(name, new_name) def copy(self, deepcopy=True): @@ -390,28 +387,28 @@ class NcData(_AttributeAccessMixin): def __init__( self, - name: Optional[str] = None, - dimensions: Union[Mapping, Iterable] = None, - variables: Union[Mapping, Iterable] = None, - attributes: Union[Mapping, Iterable] = None, - groups: Union[Mapping, Iterable] = None, - ): # noqa: D107 + name: str | None = None, + dimensions: Mapping | Iterable = None, + variables: Mapping | Iterable = None, + attributes: Mapping | Iterable = None, + groups: Mapping | Iterable = None, + ): #: a group/dataset name (optional) self.name: str = name #: group/dataset dimensions - self.dimensions: Dict[str, "NcDimension"] = as_namemap( + self.dimensions: dict[str, NcDimension] = as_namemap( dimensions, NcDimension ) #: group/dataset variables - self.variables: Dict[str, "NcVariable"] = as_namemap( + self.variables: dict[str, NcVariable] = as_namemap( variables, NcVariable ) #: group/dataset global attributes - self.attributes: Dict[str, "NcAttribute"] = as_namemap( + self.attributes: dict[str, NcAttribute] = as_namemap( attributes, NcAttribute ) #: sub-groups - self.groups: Dict[str, "NcData"] = as_namemap(groups, NcData) + self.groups: dict[str, NcData] = as_namemap(groups, NcData) def _print_content(self) -> str: """ @@ -421,7 +418,7 @@ def _print_content(self) -> str: class, so it isn't technically an abstract method). This "NcData._print_content()" is called recursively for groups. """ - global _STANDARD_INDENT # noqa: F824 + global _STANDARD_INDENT # Define a header line (always a separate line). noname = "<'no-name'>" lines = [f" str: # Join with linefeeds for a simple string result. return "\n".join(lines) - def __str__(self): # noqa: D105 + def __str__(self): return self._print_content() # NOTE: for 'repr', an interpretable literal string is too complex. @@ -504,7 +501,7 @@ def slicer(self, *dim_names): return Slicer(self, *dim_names) - def __getitem__(self, indices): # noqa: D105 + def __getitem__(self, indices): # N.B. Python [,,] syntax ensures there is only ever one argument, # either a single key or a tuple : # : [x] --> keys == x @@ -512,7 +509,7 @@ def __getitem__(self, indices): # noqa: D105 return self.slicer().__getitem__(indices) # Define equality in terms of dataset comparison utility - def __eq__(self, other): # noqa: D105 + def __eq__(self, other): if id(other) == id(self): result = True elif not isinstance(other, NcData): @@ -532,8 +529,8 @@ class NcDimension: """ def __init__( - self, name: str, size: int, unlimited: Optional[bool] = None - ): # noqa: D107 + self, name: str, size: int, unlimited: bool | None = None + ): #: dimension name self.name: str = name #: dimension size (current size, if unlimited) @@ -545,15 +542,15 @@ def __init__( #: whether dimension is unlimited self.unlimited: bool = unlimited - def _print_content(self) -> str: # noqa: D105 + def _print_content(self) -> str: str_unlim = " **UNLIMITED**" if self.unlimited else "" return f"{self.name} = {self.size}{str_unlim}" - def __repr__(self): # noqa: D105 + def __repr__(self): str_unlim = ", unlimited=True" if self.unlimited else "" return f"NcDimension({self.name!r}, {self.size}{str_unlim})" - def __str__(self): # noqa: D105 + def __str__(self): return repr(self) def copy(self): @@ -599,7 +596,7 @@ def __init__( Union[np.ndarray, "dask.array.array"] # noqa: F821 ] = None, dtype: np.dtype = None, - attributes: Union[Mapping, Iterable] = None, + attributes: Mapping | Iterable = None, group: "NcData" = None, ): """ @@ -612,7 +609,7 @@ def __init__( #: variable name self.name: str = name #: variable dimension names (a list of strings, *not* a dict of objects) - self.dimensions: Tuple[str] = tuple(dimensions) + self.dimensions: tuple[str] = tuple(dimensions) if data is not None: if not hasattr(data, "dtype"): data = np.asanyarray(data) @@ -626,7 +623,7 @@ def __init__( #: variable attributes self.attributes: NameMap = as_namemap(attributes, NcAttribute) #: parent group - self.group: Optional[NcData] = group + self.group: NcData | None = group # # Provide some array-like readonly properties reflected from the data. # @property @@ -638,7 +635,7 @@ def __init__( # return self.data.shape def _print_content(self): - global _STANDARD_INDENT # noqa: F824 + global _STANDARD_INDENT dimstr = ", ".join(self.dimensions) typestr = str(self.dtype) if self.dtype else "" hdr = f""] return "\n".join(lines) - def __str__(self): # noqa: D105 + def __str__(self): return self._print_content() # NOTE: as for NcData, an interpretable 'repr' string is too complex. @@ -681,7 +678,7 @@ def copy(self): return var # Define equality in terms of variable comparison utility - def __eq__(self, other): # noqa: D105 + def __eq__(self, other): if id(other) == id(self): result = True elif not isinstance(other, NcVariable): @@ -703,7 +700,7 @@ class NcAttribute: In an actual netcdf dataset, a "scalar" is actually just an array of length 1. """ - def __init__(self, name: str, value): # noqa: D107 + def __init__(self, name: str, value): #: attribute name self.name: str = name # Attribute values are arraylike, have dtype @@ -711,7 +708,7 @@ def __init__(self, name: str, value): # noqa: D107 self.value: np.ndarray = value @property - def value(self): # noqa: D102 + def value(self): return self._value @value.setter @@ -789,10 +786,10 @@ def _print_value(self): def _print_content(self): return f"{self.name} = {self._print_value()}" - def __repr__(self): # noqa: D105 + def __repr__(self): return f"NcAttribute({self.name!r}, {self._print_value()})" - def __str__(self): # noqa: D105 + def __str__(self): return repr(self) def copy(self): diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index a58b064..b08e636 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -34,7 +34,7 @@ """ -from typing import Any, Dict, List +from typing import Any import dask.array as da import netCDF4 @@ -50,7 +50,7 @@ class _Nc4DatalikeWithNcattrs: # N.B. "self._ncdata" is the underlying NcData object : either an NcData or # NcVariable object. - def ncattrs(self) -> List[str]: + def ncattrs(self) -> list[str]: return list(self._ncdata.attributes.keys()) def getncattr(self, attr: str): @@ -150,10 +150,10 @@ def createVariable( self, varname: str, datatype: np.dtype, - dimensions: List[str] = (), + dimensions: list[str] = (), fill_value=None, - **kwargs: Dict[str, Any], - ): # noqa: D102 + **kwargs: dict[str, Any], + ): if varname in self.variables: msg = f'creating duplicate variable "{varname}".' raise ValueError(msg) @@ -205,7 +205,8 @@ def filepath() -> str: # noqa: D102 return "" def __lt__(self, other): - """Support comparison operations. + """ + Support comparison operations. Not a real operation: we provide "comparison", just so that Iris can load a list of them. @@ -241,7 +242,7 @@ class Nc4VariableLike(_Nc4DatalikeWithNcattrs): _local_instance_props = ("_ncdata", "name", "datatype", "_data_array") - def __init__(self, ncvar: NcVariable, datatype: np.dtype): # noqa: D107 + def __init__(self, ncvar: NcVariable, datatype: np.dtype): self._ncdata = ncvar self.name = ncvar.name # Note: datatype must be known at creation, which may be before an actual data @@ -403,7 +404,7 @@ def group(self): # noqa: D102 return self._ncdata.group @property - def dimensions(self) -> List[str]: # noqa: D102 + def dimensions(self) -> list[str]: # noqa: D102 return self._ncdata.dimensions # @@ -459,7 +460,7 @@ def shape(self): # noqa: D102 def size(self): # noqa: D102 return np.prod(self.shape) - def chunking(self): # noqa: D102 + def chunking(self): """ Return chunk sizes. @@ -467,7 +468,7 @@ def chunking(self): # noqa: D102 For now, simply returns ``None``. Could be replaced when required. """ - return None + return class Nc4DimensionLike: @@ -480,7 +481,7 @@ class Nc4DimensionLike: """ - def __init__(self, ncdim: NcDimension): # noqa: D107 + def __init__(self, ncdim: NcDimension): self._ncdata = ncdim @property diff --git a/lib/ncdata/iris.py b/lib/ncdata/iris.py index 225a387..5a6d46a 100644 --- a/lib/ncdata/iris.py +++ b/lib/ncdata/iris.py @@ -9,7 +9,8 @@ """ -from typing import Any, AnyStr, Dict, Iterable, List, Union +from collections.abc import Iterable +from typing import Any, AnyStr # # NOTE: This uses the :mod:`ncdata.dataset_like` interface ability to mimic a @@ -30,7 +31,7 @@ def to_iris( - ncdata: NcData | List[NcData], **iris_load_kwargs: Dict[AnyStr, Any] + ncdata: NcData | list[NcData], **iris_load_kwargs: dict[AnyStr, Any] ) -> CubeList: """ Read Iris cubes from an :class:`~ncdata.NcData`. @@ -59,7 +60,7 @@ def to_iris( def from_iris( - cubes: Union[Cube, Iterable[Cube]], **iris_save_kwargs: Dict[AnyStr, Any] + cubes: Cube | Iterable[Cube], **iris_save_kwargs: dict[AnyStr, Any] ) -> NcData: """ Create an :class:`~ncdata.NcData` from Iris cubes. diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index 9ebc180..f7a38d5 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -11,7 +11,6 @@ from pathlib import Path from threading import Lock -from typing import Dict, Optional, Union import dask.array as da import netCDF4 as nc @@ -29,8 +28,8 @@ def _to_nc4_group( ncdata: NcData, - nc4object: Union[nc.Dataset, nc.Group], - var_kwargs: Optional[Dict[str, Dict]] = None, + nc4object: nc.Dataset | nc.Group, + var_kwargs: dict[str, dict] | None = None, ) -> None: """ Inner routine supporting ``to_nc4``, and recursive calls for sub-groups. @@ -165,8 +164,8 @@ def __setstate__(self, state): def to_nc4( ncdata: NcData, - nc4_dataset_or_file: Union[nc.Dataset, Path, str], - var_kwargs: Dict[str, Dict] = None, + nc4_dataset_or_file: nc.Dataset | Path | str, + var_kwargs: dict[str, dict] = None, ) -> None: """ Save NcData to a netCDF file. @@ -214,7 +213,7 @@ def to_nc4( nc4ds.close() -def _from_nc4_group(nc4ds: Union[nc.Dataset, nc.Group], dim_chunks) -> NcData: +def _from_nc4_group(nc4ds: nc.Dataset | nc.Group, dim_chunks) -> NcData: """ Inner routine for :func:`from_nc4`. @@ -287,8 +286,8 @@ def _from_nc4_group(nc4ds: Union[nc.Dataset, nc.Group], dim_chunks) -> NcData: def from_nc4( - nc4_dataset_or_file: Union[nc.Dataset, nc.Group, Path, str], - dim_chunks: Dict[str, Union[int, str]] = None, + nc4_dataset_or_file: nc.Dataset | nc.Group | Path | str, + dim_chunks: dict[str, int | str] = None, ) -> NcData: """ Load NcData from a :class:`netCDF4.Dataset` or netCDF file. diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index abeeb20..774afe3 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -12,7 +12,7 @@ """ from pathlib import Path -from typing import AnyStr, List, Union +from typing import AnyStr from warnings import warn import netCDF4 @@ -23,8 +23,8 @@ def dataset_differences( - dataset_or_path_1: Union[Path, AnyStr, nc.Dataset, NcData], - dataset_or_path_2: Union[Path, AnyStr, nc.Dataset, NcData], + dataset_or_path_1: Path | AnyStr | nc.Dataset | NcData, + dataset_or_path_2: Path | AnyStr | nc.Dataset | NcData, check_names: bool = False, check_dims_order: bool = True, check_dims_unlimited: bool = True, @@ -34,7 +34,7 @@ def dataset_differences( check_var_data: bool = True, show_n_first_different: int = 2, suppress_warnings: bool = False, -) -> List[str]: +) -> list[str]: r""" Compare two netcdf datasets. @@ -210,7 +210,8 @@ def _attribute_arrays_eq(a1, a2): def _array_element_str(x): - """Make a string representation of a numpy array element (scalar). + """ + Make a string representation of a numpy array element (scalar). Does *not* rely on numpy array printing. Instead converts to an equivalent Python object, and takes str(that). @@ -236,7 +237,8 @@ def _array_element_str(x): def _attribute_str(x): - """Make a string representing an attribute value. + """ + Make a string representing an attribute value. Like the above, not depending on numpy array printing. """ @@ -259,7 +261,7 @@ def _attribute_differences( attrs_order=True, suppress_warnings=False, force_first_attrnames=None, -) -> List[str]: +) -> list[str]: """ Compare attribute name lists. @@ -345,7 +347,7 @@ def variable_differences( show_n_first_different: int = 2, suppress_warnings: bool = False, _group_id_string: str = None, -) -> List[str]: +) -> list[str]: r""" Compare variables. @@ -559,8 +561,8 @@ def getdata(var): def _group_differences( - g1: Union[netCDF4.Dataset, netCDF4.Group], - g2: Union[netCDF4.Dataset, netCDF4.Group], + g1: netCDF4.Dataset | netCDF4.Group, + g2: netCDF4.Dataset | netCDF4.Group, group_id_string: str, dims_order: bool = True, vars_order: bool = True, @@ -571,7 +573,7 @@ def _group_differences( check_names: bool = False, check_unlimited: bool = True, show_n_diffs: int = 2, -) -> List[str]: +) -> list[str]: """ Inner routine to compare either whole datasets or subgroups. diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index 80563fc..ad4fe0a 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -2,8 +2,9 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. +from collections.abc import Mapping from numbers import Number -from typing import Any, Mapping +from typing import Any import dask.array as da import numpy as np @@ -76,12 +77,12 @@ def index_by_dimensions( f"Key for dimension {dim_name!r} is multi-dimensional: {key}. " "Multi-dimensional keys are not supported." ) - elif key is Ellipsis: + if key is Ellipsis: raise ValueError( f'Key for dimension {dim_name!r} is Ellipsis / "...": ' "Ellipsis is not supported." ) - elif key in (np.newaxis, None): + if key in (np.newaxis, None): raise ValueError( f"Key for dimension {dim_name!r} is np.newaxis / None: " "New-axis is not supported. " diff --git a/lib/ncdata/utils/_save_errors.py b/lib/ncdata/utils/_save_errors.py index 550f38c..d8802e5 100644 --- a/lib/ncdata/utils/_save_errors.py +++ b/lib/ncdata/utils/_save_errors.py @@ -4,7 +4,6 @@ # See LICENSE in the root of the repository for full licensing details. """User utility routines for ncdata.""" -from typing import Dict, List, Union import netCDF4 as nc import numpy as np @@ -17,12 +16,11 @@ def _name_is_valid(name) -> bool: if not isinstance(name, str) or not name: # Catches non-string (e.g. None, 0, ..) and empty string result = False - else: - # The name rules for netCDF are not fully clear, but seem *extremely* liberal. - # It seems that "/" is not allowed, and that's about it - # So *allow* whitespace, backslash, initial digit, initial underscore ... - if "/" in name: - result = False + # The name rules for netCDF are not fully clear, but seem *extremely* liberal. + # It seems that "/" is not allowed, and that's about it + # So *allow* whitespace, backslash, initial digit, initial underscore ... + elif "/" in name: + result = False return result @@ -53,8 +51,8 @@ def _valid_attr_dtype(dtype): def _invalid_attr_errors( - element: Union[NcData, NcVariable], name_prefix: str -) -> List[str]: + element: NcData | NcVariable, name_prefix: str +) -> list[str]: errors = [] for attr in element.attributes.values(): dtype = attr.value.dtype @@ -67,8 +65,8 @@ def _invalid_attr_errors( def _variable_errors( - var: NcVariable, var_prefix: str, known_dimensions: Dict[str, int] -) -> List[str]: + var: NcVariable, var_prefix: str, known_dimensions: dict[str, int] +) -> list[str]: errors = [] if var.data is None: errors.append(f"{var_prefix} has no data array.") @@ -102,9 +100,9 @@ def _variable_errors( def _save_errors_inner( ncdata: NcData, - enclosing_dimensions: Dict[str, int] = None, + enclosing_dimensions: dict[str, int] = None, group_path: str = None, -) -> List[str]: +) -> list[str]: """ Scan dataset, with context allowing operation over inner groups. @@ -183,7 +181,7 @@ def _save_errors_inner( return errors -def save_errors(ncdata: NcData) -> List[str]: +def save_errors(ncdata: NcData) -> list[str]: """ Scan a dataset for consistency and completeness. diff --git a/lib/ncdata/xarray.py b/lib/ncdata/xarray.py index 7246e18..9429d5d 100644 --- a/lib/ncdata/xarray.py +++ b/lib/ncdata/xarray.py @@ -15,7 +15,7 @@ # from pathlib import Path -from typing import AnyStr, Union +from typing import AnyStr import dask.array as da import numpy as np @@ -151,7 +151,7 @@ def close(self): # @classmethod def from_xarray( - cls, dataset_or_file: Union[xr.Dataset, AnyStr, Path], **xr_load_kwargs + cls, dataset_or_file: xr.Dataset | AnyStr | Path, **xr_load_kwargs ): if not isinstance(dataset_or_file, xr.Dataset): # It's a "file" (or pathstring, or Path ?). @@ -194,7 +194,7 @@ def to_xarray(ncdata: NcData, **xarray_load_kwargs) -> xr.Dataset: def from_xarray( - xrds: Union[xr.Dataset, Path, AnyStr], **xarray_save_kwargs + xrds: xr.Dataset | Path | AnyStr, **xarray_save_kwargs ) -> NcData: """ Convert an xarray :class:`xarray.Dataset` to a :class:`NcData`. diff --git a/tests/__init__.py b/tests/__init__.py index d9b742b..ee75528 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -32,7 +32,8 @@ def __getitem__(self, keys): def version_tuple(version_string): - """Make a comparable tuple from a package version string. + """ + Make a comparable tuple from a package version string. Allows only the final segment to have non-numeric parts. Returns a tuple of the parts. diff --git a/tests/data_testcase_schemas.py b/tests/data_testcase_schemas.py index 8ef244f..6922937 100644 --- a/tests/data_testcase_schemas.py +++ b/tests/data_testcase_schemas.py @@ -16,7 +16,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import Dict, Union import iris.tests import netCDF4 as nc @@ -62,10 +61,10 @@ def data_types(): assert set(data_types()) == _nc_dtypes # Suitable test values for each attribute/data type. -_INT_Approx_2x31 = int(2**31 - 1) -_INT_Approx_2x32 = int(2**32 - 2) -_INT_Approx_2x63 = int(2**63 - 3) -_INT_Approx_2x64 = int(2**64 - 4) +_INT_Approx_2x31 = 2**31 - 1 +_INT_Approx_2x32 = 2**32 - 2 +_INT_Approx_2x63 = 2**63 - 3 +_INT_Approx_2x64 = 2**64 - 4 _Datatype_Sample_Values = { "u1": np.array([3, 250], dtype="u1"), @@ -303,7 +302,7 @@ def make_testcase_dataset(filepath, spec): } # Define a sequence of standard testfile specs, with suitable param-names. -_STANDARD_TESTCASES: Dict[str, Union[Path, dict]] = {} +_STANDARD_TESTCASES: dict[str, Path | dict] = {} # A decorator for spec-generating routines. @@ -364,8 +363,7 @@ def _define_iris_testdata_testcases(): param_name = str(filepath) # remove unwanted path elements param_name = param_name.replace(str(_testdirpath), "") - if param_name.endswith(".nc"): - param_name = param_name[:-3] + param_name = param_name.removesuffix(".nc") # replace path-separators and other awkward chars with dunder for char in ("/", ".", "-"): param_name = param_name.replace(char, "__") diff --git a/tests/integration/example_scripts/ex_dataset_print.py b/tests/integration/example_scripts/ex_dataset_print.py index ceffd07..f72fb16 100644 --- a/tests/integration/example_scripts/ex_dataset_print.py +++ b/tests/integration/example_scripts/ex_dataset_print.py @@ -12,7 +12,7 @@ from tests import testdata_dir -def sample_printout(): # noqa: D103 +def sample_printout(): iris.FUTURE.datum_support = True filepath = testdata_dir / "toa_brightness_temperature.nc" cubes = iris.load(filepath) diff --git a/tests/integration/example_scripts/ex_iris_saveto_ncdata.py b/tests/integration/example_scripts/ex_iris_saveto_ncdata.py index 24f67d0..18c47ee 100644 --- a/tests/integration/example_scripts/ex_iris_saveto_ncdata.py +++ b/tests/integration/example_scripts/ex_iris_saveto_ncdata.py @@ -17,7 +17,7 @@ def example_ncdata_from_iris(): """Demonstrate loading from iris and printing the NcData object.""" - print("") + print() print("==============") print("TEMPORARY: iris save-to-ncdata test") iris.FUTURE.datum_support = True diff --git a/tests/integration/example_scripts/ex_iris_xarray_conversion.py b/tests/integration/example_scripts/ex_iris_xarray_conversion.py index 0b968a5..aee9031 100644 --- a/tests/integration/example_scripts/ex_iris_xarray_conversion.py +++ b/tests/integration/example_scripts/ex_iris_xarray_conversion.py @@ -18,7 +18,7 @@ from tests import testdata_dir -def example_from_xr(): # noqa: D103 +def example_from_xr(): filepath = testdata_dir / "toa_brightness_temperature.nc" xrds = xr.open_dataset(filepath, chunks="auto") iris.FUTURE.datum_support = True @@ -51,7 +51,7 @@ def example_from_xr(): # noqa: D103 print("\n") print("============ CONVERT cubes TO xr.Dataset ... =========") - print("") + print() xrds2 = cubes_to_xarray(cubes) print("\nxrds2:\n", xrds2) print("\ntime:\n", xrds2["time"]) diff --git a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py index 7ee92aa..1171a7b 100644 --- a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py +++ b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py @@ -22,7 +22,7 @@ from tests import testdata_dir -def example_nc4_load_save_roundtrip(): # noqa: D103 +def example_nc4_load_save_roundtrip(): """Load an existing file ; save to netcdf4 ; check they are the same.""" print("\n----\nNetcdf4 load-save example.") @@ -67,7 +67,7 @@ def example_nc4_save_reload_unlimited_roundtrip(): tempdir_path = Path(tempfile.mkdtemp()) try: - print("") + print() filepath = tempdir_path / "temp_nc_2_unlim.nc" print("Saving to file ", filepath) to_nc4(ncdata, filepath) diff --git a/tests/integration/test_zarr_to_iris.py b/tests/integration/test_zarr_to_iris.py index 8b88b55..bbb27d7 100644 --- a/tests/integration/test_zarr_to_iris.py +++ b/tests/integration/test_zarr_to_iris.py @@ -112,7 +112,8 @@ def _is_url_ok(url): ) @pytest.mark.skipif(not _S3_accessible, reason="S3 url not accessible") def test_load_remote_zarr(): - """Test loading a remote Zarr store. + """ + Test loading a remote Zarr store. This is a ~250MB compressed Zarr in an S3 bucket. Conversion is done fully lazily, by passing chunks={} diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index 19ea144..a206614 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -13,7 +13,7 @@ import os import subprocess from pathlib import Path -from typing import AnyStr, List, Optional +from typing import AnyStr import pytest @@ -54,7 +54,7 @@ def _env_bin_path(exe_name: AnyStr = None): def ncgen_from_cdl( - cdl_str: Optional[str], cdl_path: Optional[str], nc_path: str + cdl_str: str | None, cdl_path: str | None, nc_path: str ): """ Generate a test netcdf file from cdl. @@ -141,7 +141,7 @@ def ncgen_from_cdl( """ -def comparable_cdl(text: str) -> List[str]: +def comparable_cdl(text: str) -> list[str]: """ Convert a CDL string to a list of stripped lines, with certain problematic things removed. The resulting list of strings should be comparable between identical diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index 9780a9d..2f6a22f 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -59,7 +59,7 @@ def structuretype(request): def attrvalue(datatype, structuretype): # get basic type = 'int', 'float', 'string' (datatype,) = ( - key for key in _attribute_testdata.keys() if key in datatype + key for key in _attribute_testdata if key in datatype ) # fetch a set of values @@ -111,14 +111,13 @@ def test_value(self, datatype, structuretype): assert all(isinstance(x, str) for x in result) else: assert isinstance(result, str) + # Numeric data is returned as arrays or array scalars. + elif is_vector: + assert isinstance(result, np.ndarray) + assert result.ndim == 1 else: - # Numeric data is returned as arrays or array scalars. - if is_vector: - assert isinstance(result, np.ndarray) - assert result.ndim == 1 - else: - # scalar : result is *always* an array scalar (e.g. np.float64) - assert result.shape == () + # scalar : result is *always* an array scalar (e.g. np.float64) + assert result.shape == () class Test_NcAttribute__str_repr: diff --git a/tests/unit/core/test_NcData.py b/tests/unit/core/test_NcData.py index 0a7034e..a2f13a9 100644 --- a/tests/unit/core/test_NcData.py +++ b/tests/unit/core/test_NcData.py @@ -2,7 +2,8 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Tests for class :class:`ncdata.NcData`. +""" +Tests for class :class:`ncdata.NcData`. There is almost no behaviour, but we can test some constructor usages. """ diff --git a/tests/unit/core/test_NcDimension.py b/tests/unit/core/test_NcDimension.py index efca8d4..c915ab4 100644 --- a/tests/unit/core/test_NcDimension.py +++ b/tests/unit/core/test_NcDimension.py @@ -85,7 +85,7 @@ def unlimited(self, request): def size(self, request): return request.param - @pytest.fixture() + @pytest.fixture def refdim(self, unlimited, size): return NcDimension(name="ref_name", size=size, unlimited=unlimited) diff --git a/tests/unit/core/test_NcVariable.py b/tests/unit/core/test_NcVariable.py index 13b8aa0..6d4e701 100644 --- a/tests/unit/core/test_NcVariable.py +++ b/tests/unit/core/test_NcVariable.py @@ -2,7 +2,8 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Tests for class :class:`ncdata.NcVariable`. +""" +Tests for class :class:`ncdata.NcVariable`. There is almost no actual behaviour, but we can test some constructor behaviours, such as valid calling options and possibilities for data and dtype. diff --git a/tests/unit/iris/test_to_iris.py b/tests/unit/iris/test_to_iris.py index 3d0cc59..19a9bfa 100644 --- a/tests/unit/iris/test_to_iris.py +++ b/tests/unit/iris/test_to_iris.py @@ -126,7 +126,6 @@ def test_kwargs__load_by_name(): def test_iris_loadchain(): """Check that standard iris load-chain processing is applied, including a merge.""" - # Create a pair of datasets with mergeable variables. # Start by making one, with a scalar 'z' coord. ncdata = NcData( diff --git a/tests/unit/netcdf/test_to_nc4.py b/tests/unit/netcdf/test_to_nc4.py index 6acde50..bcc1839 100644 --- a/tests/unit/netcdf/test_to_nc4.py +++ b/tests/unit/netcdf/test_to_nc4.py @@ -14,7 +14,6 @@ """ from pathlib import Path -from typing import List import netCDF4 as nc import numpy as np @@ -70,7 +69,7 @@ def test_target_types(targettype, tmp_path): assert dataset_differences(target_path, original_path) == [] -def fetch_nc_var(nc_file: nc.Dataset, var_path: str or List[str]): +def fetch_nc_var(nc_file: nc.Dataset, var_path: str or list[str]): """ Return a variable instance from an open dataset for inspection. diff --git a/tests/unit/utils/dim_indexing/__init__.py b/tests/unit/utils/dim_indexing/__init__.py index 0618931..9170a66 100644 --- a/tests/unit/utils/dim_indexing/__init__.py +++ b/tests/unit/utils/dim_indexing/__init__.py @@ -101,7 +101,7 @@ def make_dims_testdata_1d(x): return data -class Slicekeys: # noqa: D101 +class Slicekeys: def __getitem__(self, keys): # noqa: D105 return keys diff --git a/tests/unit/utils/dim_indexing/test_Slicer.py b/tests/unit/utils/dim_indexing/test_Slicer.py index 5b8ff67..f03c56c 100644 --- a/tests/unit/utils/dim_indexing/test_Slicer.py +++ b/tests/unit/utils/dim_indexing/test_Slicer.py @@ -2,7 +2,8 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Tests for class :class:`ncdata.utils.Slicer`. +""" +Tests for class :class:`ncdata.utils.Slicer`. This is the "indirect" approach, with a Slicer object. """ diff --git a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py index 8b1c607..4c0529a 100644 --- a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py +++ b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py @@ -2,7 +2,8 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Tests for class :class:`ncdata.utils.index_by_dimension`. +""" +Tests for class :class:`ncdata.utils.index_by_dimension`. This is the more direct indexer approach. """ diff --git a/tests/unit/utils/test_ncdata_copy.py b/tests/unit/utils/test_ncdata_copy.py index bd0629e..025f7e4 100644 --- a/tests/unit/utils/test_ncdata_copy.py +++ b/tests/unit/utils/test_ncdata_copy.py @@ -2,7 +2,8 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Tests for :func:`ncdata.utils.ncdata_copy`. +""" +Tests for :func:`ncdata.utils.ncdata_copy`. This is generic utility function version of the copy operation. """ @@ -53,7 +54,7 @@ def test_empty(self): result = ncdata_copy(sample) assert not differences_or_duplicated_objects(sample, result) - @pytest.fixture() + @pytest.fixture def sample(self): attrs = NameMap.from_items( [NcAttribute("q", 3)], item_type=NcAttribute diff --git a/tests/unit/utils/test_rename_dimension.py b/tests/unit/utils/test_rename_dimension.py index 19c2605..c426111 100644 --- a/tests/unit/utils/test_rename_dimension.py +++ b/tests/unit/utils/test_rename_dimension.py @@ -14,7 +14,8 @@ def make_saveable( ncdata: NcData, _outer_dims: dict[str, NcDimension] | None = None ): - """Add missing dimensions + data, to make a sample NcData save-able. + """ + Add missing dimensions + data, to make a sample NcData save-able. Create any missing variable dimensions (length==2). Add any missing variable data arrays. @@ -27,7 +28,8 @@ def make_saveable( outer_dim_names = [dim.name for dim in _outer_dims.values()] def getdim(dimname): - """Fetch a known dimension by name. + """ + Fetch a known dimension by name. Check own, then "outer" ones, to correctly implement *scope masking*. """ @@ -125,7 +127,7 @@ def test_name_ingroup_masked_nofail(self): # No error in this case, because the "z" is inside a group with its own "x". rename_dimension(ncdata, "x", "z") - @pytest.fixture() + @pytest.fixture def group_example(self, setup): ncdata = self.ncdata.copy() ncdata.groups.addall( diff --git a/tests/unit/xarray/test_to_xarray.py b/tests/unit/xarray/test_to_xarray.py index 4b88a8f..f1ffa88 100644 --- a/tests/unit/xarray/test_to_xarray.py +++ b/tests/unit/xarray/test_to_xarray.py @@ -75,7 +75,6 @@ def test_real_nocopy(): @pytest.mark.parametrize("scaleandoffset", ["WITHscaling", "NOscaling"]) def test_kwargs__scaleandoffset(scaleandoffset): """Check the operation of kwargs (= xarray load controls).""" - # Make a test NcData dataset with a scaled-and-offset variable. raw_int_data = np.array([66, 75, 99], dtype=np.int16) ncdata = NcData( From 86daa1fe0dd97eebafbd611054ce265259cc930f Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 2 Nov 2026 10:30:20 +0000 Subject: [PATCH 16/26] Ruff-format (i.e. 'black'-like) format simplifications. --- lib/ncdata/_core.py | 46 ++++----------- lib/ncdata/dataset_like.py | 15 ++--- lib/ncdata/netcdf4.py | 8 +-- lib/ncdata/utils/_compare_nc_datasets.py | 51 ++++------------- lib/ncdata/utils/_dim_indexing.py | 8 +-- lib/ncdata/utils/_rename_dim.py | 9 +-- lib/ncdata/utils/_save_errors.py | 8 +-- lib/ncdata/xarray.py | 15 ++--- tests/__init__.py | 4 +- tests/data_testcase_schemas.py | 20 ++----- .../integration/equivalence_testing_utils.py | 7 +-- .../ex_iris_xarray_conversion.py | 15 +---- .../test_iris_load_and_save_equivalence.py | 18 ++---- .../test_iris_xarray_roundtrips.py | 12 +--- tests/integration/test_netcdf_roundtrips.py | 4 +- .../test_xarray_load_and_save_equivalence.py | 4 +- tests/integration/test_zarr_to_iris.py | 3 +- tests/test_samplecode_cdlgen_comparablecdl.py | 4 +- tests/unit/core/test_AttrvalsDict.py | 4 +- tests/unit/core/test_NameMap.py | 8 +-- tests/unit/core/test_NcAttribute.py | 4 +- tests/unit/core/test_NcData.py | 8 +-- tests/unit/core/test_NcVariable.py | 4 +- tests/unit/iris/test_from_iris.py | 8 +-- tests/unit/iris/test_to_iris.py | 8 +-- tests/unit/netcdf/test_from_nc4.py | 4 +- tests/unit/netcdf/test_to_nc4.py | 16 ++---- .../test_dataset_differences__additional.py | 21 ++----- ...test_dataset_differences__mainfunctions.py | 34 +++-------- .../test_variable_differences.py | 56 ++++++------------- tests/unit/utils/dim_indexing/__init__.py | 8 +-- .../dim_indexing/test_index_by_dimensions.py | 8 +-- tests/unit/utils/test_ncdata_copy.py | 18 ++---- tests/unit/utils/test_rename_dimension.py | 7 +-- tests/unit/utils/test_save_errors.py | 12 +--- tests/unit/xarray/test_from_xarray.py | 10 +--- tests/unit/xarray/test_to_xarray.py | 8 +-- 37 files changed, 126 insertions(+), 371 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index e2b4119..c3a3114 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -132,9 +132,7 @@ def rename(self, name: str, new_name: str): self.addall(items) @classmethod - def from_items( - cls, arg: Iterable | Mapping, item_type=None - ) -> "NameMap": + def from_items(cls, arg: Iterable | Mapping, item_type=None) -> "NameMap": """ Convert an iterable or mapping of items to a NameMap. @@ -186,9 +184,7 @@ def from_items( if ( item_type == NcAttribute and len(arg) > 0 - and not isinstance( - next(iter(arg.values())), NcAttribute - ) + and not isinstance(next(iter(arg.values())), NcAttribute) ): # for attributes only, also allow simple name=value map # for which, convert each value to an NcAttribute @@ -202,9 +198,7 @@ def from_items( elif isinstance(arg, Iterable): result.addall(arg) else: - msg = ( - f"Argument must be an iterable or mapping: got {arg}." - ) + msg = f"Argument must be an iterable or mapping: got {arg}." raise TypeError(msg) return result @@ -275,8 +269,7 @@ def __str__(self): text += "}" else: lines = [ - f"{attr.name!s}: {attr._print_value()}" - for attr in self._attrs.values() + f"{attr.name!s}: {attr._print_value()}" for attr in self._attrs.values() ] if len(lines) == 1: text += lines[0] + "}" @@ -396,17 +389,11 @@ def __init__( #: a group/dataset name (optional) self.name: str = name #: group/dataset dimensions - self.dimensions: dict[str, NcDimension] = as_namemap( - dimensions, NcDimension - ) + self.dimensions: dict[str, NcDimension] = as_namemap(dimensions, NcDimension) #: group/dataset variables - self.variables: dict[str, NcVariable] = as_namemap( - variables, NcVariable - ) + self.variables: dict[str, NcVariable] = as_namemap(variables, NcVariable) #: group/dataset global attributes - self.attributes: dict[str, NcAttribute] = as_namemap( - attributes, NcAttribute - ) + self.attributes: dict[str, NcAttribute] = as_namemap(attributes, NcAttribute) #: sub-groups self.groups: dict[str, NcData] = as_namemap(groups, NcData) @@ -432,8 +419,7 @@ def _print_content(self) -> str: lines += [f"{_STANDARD_INDENT}global attributes:"] # NOTE: #2 show like variable attributes, but *no parent name*. attrs_lines = [ - f":{attr._print_content()}" - for attr in self.attributes.values() + f":{attr._print_content()}" for attr in self.attributes.values() ] lines += _addlines_indent( "\n".join(attrs_lines), _STANDARD_INDENT * 2 @@ -528,9 +514,7 @@ class NcDimension: Associates a name with a length, and also an 'unlimited' flag. """ - def __init__( - self, name: str, size: int, unlimited: bool | None = None - ): + def __init__(self, name: str, size: int, unlimited: bool | None = None): #: dimension name self.name: str = name #: dimension size (current size, if unlimited) @@ -716,9 +700,7 @@ def value(self, value): if not hasattr(value, "dtype"): value = np.asanyarray(value) if value.ndim > 1: - raise ValueError( - "Attribute value should only be 0- or 1-dimensional." - ) + raise ValueError("Attribute value should only be 0- or 1-dimensional.") self._value = value def as_python_value(self): @@ -751,9 +733,7 @@ def as_python_value(self): result = self.value # Attributes are either scalar or 1-D if result.ndim >= 2: - raise ValueError( - "Attribute value should only be 0- or 1-dimensional." - ) + raise ValueError("Attribute value should only be 0- or 1-dimensional.") if result.shape == (1,): # Reduce the dimension, but *always* return an array scalar. @@ -806,8 +786,6 @@ def __eq__(self, other): v1 = self.value v2 = other.value result = ( - v1.shape == v2.shape - and v1.dtype == v2.dtype - and np.all(v1 == v2) + v1.shape == v2.shape and v1.dtype == v2.dtype and np.all(v1 == v2) ) return result diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index b08e636..4f71c76 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -125,8 +125,7 @@ def __init__(self, ncdata: NcData = None): for name, ncvar in self._ncdata.variables.items() } self.dimensions = { - name: Nc4DimensionLike(dim) - for name, dim in self._ncdata.dimensions.items() + name: Nc4DimensionLike(dim) for name, dim in self._ncdata.dimensions.items() } @property @@ -162,9 +161,7 @@ def createVariable( # N.B. to correctly mirror netCDF4, a variable should be created with all-masked # content. For this we need to decode the dims + work out the shape. # NOTE: simplistic version here, as we don't support groups. - shape = tuple( - self._ncdata.dimensions[dim_name].size for dim_name in dimensions - ) + shape = tuple(self._ncdata.dimensions[dim_name].size for dim_name in dimensions) # Note: initial content is an all-masked lazy array, to avoid allocating space # in memory. This is not assignable, so we cannot mimic partial writes, but # that's okay as we don't currently provide any Nc4VariableLike.__setitem__ : @@ -254,9 +251,7 @@ def __init__(self, ncvar: NcVariable, datatype: np.dtype): if array is None: # temporary empty data (to correctly support never-written content) # NOTE: significantly, does *not* allocate an actual full array in memory - array = da.ma.masked_array( - da.zeros(self.shape, self.datatype), mask=True - ) + array = da.ma.masked_array(da.zeros(self.shape, self.datatype), mask=True) # Convert from the "inner" raw form to masked-and-scaled, and then back again # (by assigning self._data_array). @@ -367,9 +362,7 @@ def _maskandscale_apparent_to_inner(self, array): # 'meta' argument, but in this case we don't know what 'array' might be, # so we have no (other) generic way to construct a suitable 'meta'. array = da.from_array(array, chunks="auto") - array = da.map_blocks( - self._fill_masked, array, fv, meta=array._meta - ) + array = da.map_blocks(self._fill_masked, array, fv, meta=array._meta) return array diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index f7a38d5..c741cc7 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -111,9 +111,7 @@ class _NetCDFDataProxy: # N.B. avoiding sorting this, the order is convenient for understanding. __slots__ = ("shape", "dtype", "path", "variable_name", "group_names_path") # noqa: RUF023 - def __init__( - self, shape, dtype, filepath, variable_name, group_names_path=None - ): + def __init__(self, shape, dtype, filepath, variable_name, group_names_path=None): self.shape = shape self.dtype = dtype self.path = filepath @@ -266,9 +264,7 @@ def _from_nc4_group(nc4ds: nc.Dataset | nc.Group, dim_chunks) -> NcData: group_names_path=group_names_path, ) chunks = [dim_chunks.get(name, "auto") for name in var.dimensions] - var.data = da.from_array( - proxy, chunks=chunks, asarray=True, meta=np.ndarray - ) + var.data = da.from_array(proxy, chunks=chunks, asarray=True, meta=np.ndarray) for attrname in nc4var.ncattrs(): var.avals[attrname] = nc4var.getncattr(attrname) diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index 774afe3..56ac9b2 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -162,9 +162,7 @@ def dataset_differences( return errs -def _namelist_differences( - l1, l2, elemname, order_strict=True, suppress_warnings=False -): +def _namelist_differences(l1, l2, elemname, order_strict=True, suppress_warnings=False): errs = [] msg = f"{elemname} do not match: {list(l1)} != {list(l2)}" ok = l1 == l2 @@ -298,11 +296,7 @@ def fix_orders(attrlist): continue attr, attr2 = [ - ( - obj.avals[attrname] - if _isncdata(obj) - else obj.getncattr(attrname) - ) + (obj.avals[attrname] if _isncdata(obj) else obj.getncattr(attrname)) for obj in (obj1, obj2) ] @@ -314,10 +308,7 @@ def fix_orders(attrlist): getattr(attr, "dtype", type(attr)) for attr in (attr, attr2) ] - if all( - isinstance(dt, np.dtype) and dt.kind in "SUb" - for dt in (dtype, dtype2) - ): + if all(isinstance(dt, np.dtype) and dt.kind in "SUb" for dt in (dtype, dtype2)): dtype = dtype2 = "string" if dtype != dtype2: msg = ( @@ -458,12 +449,7 @@ def _is_strtype(dt): is_str, is_str2 = (_is_strtype(dt) for dt in (dtype, dtype2)) # TODO: is this correct check to allow compare between different dtypes? - if ( - check_var_data - and dims == dims2 - and shape == shape2 - and is_str == is_str2 - ): + if check_var_data and dims == dims2 and shape == shape2 and is_str == is_str2: # NOTE: no attempt to use laziness here. Could be improved. def getdata(var): if _isncdata(var): @@ -487,14 +473,10 @@ def getdata(var): return data data, data2 = (getdata(v) for v in (v1, v2)) - flatdata, flatdata2 = ( - np.asanyarray(arr).flatten() for arr in (data, data2) - ) + flatdata, flatdata2 = (np.asanyarray(arr).flatten() for arr in (data, data2)) # For simpler checking, use flat versions - flat_diff_inds = ( - [] - ) # NB *don't* make this an array, it causes problems + flat_diff_inds = [] # NB *don't* make this an array, it causes problems # Work out whether string : N.B. array type does not ALWAYS match the # variable type, because apparently the scalar content of a *masked* scalar @@ -507,9 +489,7 @@ def getdata(var): # Where data is masked, count mask mismatches and skip those points if any(np.ma.is_masked(arr) for arr in (data, data2)): - mask, mask2 = ( - np.ma.getmaskarray(array) for array in (flatdata, flatdata2) - ) + mask, mask2 = (np.ma.getmaskarray(array) for array in (flatdata, flatdata2)) flat_diff_inds = list(np.where(mask != mask2)[0]) # Replace all masked points to exclude them from unmasked-point checks. either_masked = mask | mask2 @@ -532,14 +512,10 @@ def getdata(var): flat_diff_inds = sorted(flat_diff_inds) n_diffs = len(flat_diff_inds) if n_diffs: - msg = ( - f"{var_id_string} data contents differ, at {n_diffs} points: " - ) + msg = f"{var_id_string} data contents differ, at {n_diffs} points: " ellps = ", ..." if n_diffs > show_n_first_different else "" diffinds = flat_diff_inds[:show_n_first_different] - diffinds = [ - np.unravel_index(ind, shape=data.shape) for ind in diffinds - ] + diffinds = [np.unravel_index(ind, shape=data.shape) for ind in diffinds] diffinds_str = ", ".join( str(tuple([int(ind) for ind in x])) for x in diffinds ) @@ -552,10 +528,7 @@ def getdata(var): ) points_lhs_str = f"[{points_lhs_str}{ellps}]" points_rhs_str = f"[{points_rhs_str}{ellps}]" - msg += ( - f"@INDICES{inds_str}" - f" : LHS={points_lhs_str}, RHS={points_rhs_str}" - ) + msg += f"@INDICES{inds_str} : LHS={points_lhs_str}, RHS={points_rhs_str}" errs.append(msg) return errs @@ -583,9 +556,7 @@ def _group_differences( if check_names: if g1.name != g2.name: - errs.append( - f"Datasets have different names: {g1.name!r} != {g2.name!r}." - ) + errs.append(f"Datasets have different names: {g1.name!r} != {g2.name!r}.") # Compare lists of dimension names dimnames, dimnames2 = [list(grp.dimensions.keys()) for grp in (g1, g2)] errs += _namelist_differences( diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index ad4fe0a..b4d5730 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -67,9 +67,7 @@ def index_by_dimensions( # Dimension names must occur in the ncdata. dimension = ncdata.dimensions.get(dim_name) if dimension is None: - raise ValueError( - f"Dimension {dim_name!r} is not present in 'ncdata'." - ) + raise ValueError(f"Dimension {dim_name!r} is not present in 'ncdata'.") # Specifically error forbidden key types. if np.array(key).ndim > 1: @@ -96,9 +94,7 @@ def index_by_dimensions( if dim_name in var.dimensions: # construct a list of slice objects (i_slicedim,) = [ - i - for i, name in enumerate(var.dimensions) - if name == dim_name + i for i, name in enumerate(var.dimensions) if name == dim_name ] slices = [slice(None) for dim in var.dimensions] slices[i_slicedim] = key diff --git a/lib/ncdata/utils/_rename_dim.py b/lib/ncdata/utils/_rename_dim.py index 7779743..e4a2259 100644 --- a/lib/ncdata/utils/_rename_dim.py +++ b/lib/ncdata/utils/_rename_dim.py @@ -12,10 +12,7 @@ def _rename_dims_in_vars(ncdata: NcData, name_from: str, name_to: str) -> None: for var in ncdata.variables.values(): if name_from in var.dimensions: var.dimensions = tuple( - [ - name_to if name == name_from else name - for name in var.dimensions - ] + [name_to if name == name_from else name for name in var.dimensions] ) # Also rename in all sub-groups, except where the dimension is redefined @@ -39,9 +36,7 @@ def _check_name_collisions(ncdata, name_from, name_to, group_path=""): # Skip this group as its 'name_from' dim makes it a "scope hole". continue inner_path = group_path + "/" + group.name - _check_name_collisions( - group, name_from, name_to, group_path=inner_path - ) + _check_name_collisions(group, name_from, name_to, group_path=inner_path) def rename_dimension(ncdata: NcData, name_from: str, name_to: str) -> None: diff --git a/lib/ncdata/utils/_save_errors.py b/lib/ncdata/utils/_save_errors.py index d8802e5..4b4b6b4 100644 --- a/lib/ncdata/utils/_save_errors.py +++ b/lib/ncdata/utils/_save_errors.py @@ -4,7 +4,6 @@ # See LICENSE in the root of the repository for full licensing details. """User utility routines for ncdata.""" - import netCDF4 as nc import numpy as np @@ -35,8 +34,7 @@ def _name_errors(element_container, id_string): ) if not _name_is_valid(name): errors.append( - f"{id_string}s has an element with an invalid netCDF name : " - f"{name!r}" + f"{id_string}s has an element with an invalid netCDF name : {name!r}" ) return errors @@ -50,9 +48,7 @@ def _valid_attr_dtype(dtype): return dtype.kind in "SU" or dtype in _NETCDF_VALID_DTYPES -def _invalid_attr_errors( - element: NcData | NcVariable, name_prefix: str -) -> list[str]: +def _invalid_attr_errors(element: NcData | NcVariable, name_prefix: str) -> list[str]: errors = [] for attr in element.attributes.values(): dtype = attr.value.dtype diff --git a/lib/ncdata/xarray.py b/lib/ncdata/xarray.py index 9429d5d..6c30b26 100644 --- a/lib/ncdata/xarray.py +++ b/lib/ncdata/xarray.py @@ -110,8 +110,7 @@ def store( ) attrs = { - name: NcAttribute(name, value) - for name, value in var.attrs.items() + name: NcAttribute(name, value) for name, value in var.attrs.items() } data = var.data @@ -150,14 +149,10 @@ def close(self): # either real or lazy arrays. # @classmethod - def from_xarray( - cls, dataset_or_file: xr.Dataset | AnyStr | Path, **xr_load_kwargs - ): + def from_xarray(cls, dataset_or_file: xr.Dataset | AnyStr | Path, **xr_load_kwargs): if not isinstance(dataset_or_file, xr.Dataset): # It's a "file" (or pathstring, or Path ?). - dataset_or_file = xr.load_dataset( - dataset_or_file, **xr_load_kwargs - ) + dataset_or_file = xr.load_dataset(dataset_or_file, **xr_load_kwargs) nc_data = cls() dataset_or_file.dump_to_store(nc_data, **xr_load_kwargs) return nc_data @@ -193,9 +188,7 @@ def to_xarray(ncdata: NcData, **xarray_load_kwargs) -> xr.Dataset: return _XarrayNcDataStore(ncdata).to_xarray(**xarray_load_kwargs) -def from_xarray( - xrds: xr.Dataset | Path | AnyStr, **xarray_save_kwargs -) -> NcData: +def from_xarray(xrds: xr.Dataset | Path | AnyStr, **xarray_save_kwargs) -> NcData: """ Convert an xarray :class:`xarray.Dataset` to a :class:`NcData`. diff --git a/tests/__init__.py b/tests/__init__.py index ee75528..6939a06 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -47,7 +47,5 @@ def version_tuple(version_string): else: parts = parts[:-1] if any(str(int(part)) != part for part in parts): - raise ValueError( - f"invalid numerics in version string: {version_string!r}" - ) + raise ValueError(f"invalid numerics in version string: {version_string!r}") return tuple(int(part) for part in parts) diff --git a/tests/data_testcase_schemas.py b/tests/data_testcase_schemas.py index 6922937..059ef34 100644 --- a/tests/data_testcase_schemas.py +++ b/tests/data_testcase_schemas.py @@ -51,9 +51,7 @@ def data_types(): # Get the dtype list from the netCDF default fill-values. _nc_dtypes = set(nc.default_fillvals.keys()) # Remove the numpy-only complex types -_nc_dtypes = set( - typename for typename in _nc_dtypes if np.dtype(typename).kind != "c" -) +_nc_dtypes = set(typename for typename in _nc_dtypes if np.dtype(typename).kind != "c") # Also replace 'S1' with our own 'string' type marker _nc_dtypes.remove("S1") _nc_dtypes.add("string") @@ -148,9 +146,7 @@ def objmap(objs): shape = nc_var.shape n_points = int(np.prod(shape)) if nc_var.dtype.kind == "S": - data = np.array( - "abcdefghijklmnopqrstuvwxyz"[:n_points], dtype="S1" - ) + data = np.array("abcdefghijklmnopqrstuvwxyz"[:n_points], dtype="S1") else: data = np.arange(1, n_points + 1) data = data.astype(dtype) @@ -209,9 +205,7 @@ def make_testcase_dataset(filepath, spec): ds.close() -_minimal_variable_test_spec = { - "vars": [dict(name="var_0", dims=[], dtype=int)] -} +_minimal_variable_test_spec = {"vars": [dict(name="var_0", dims=[], dtype=int)]} _simple_test_spec = { @@ -326,9 +320,7 @@ def _define_simple_testcases(): "ds_Minimal": _minimal_variable_test_spec, "ds_Basic": _simple_test_spec, "ds_testdata1": ( - Path(__file__).parent - / "testdata" - / "toa_brightness_temperature.nc" + Path(__file__).parent / "testdata" / "toa_brightness_temperature.nc" ), "ds_scaleoffset": _scaleoffset_test_spec, "ds_masked_floats": _masked_floats_test_spec, @@ -417,9 +409,7 @@ def _define_unit_dtype_testcases(): "attrs": { f"tstatt_type__{dtype_name}__single": _Datatype_Sample_Values[ dtype_name - ][ - 0 - ], + ][0], f"tstatt_type__{dtype_name}__multi": _Datatype_Sample_Values[ dtype_name ], diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index ab96157..047b0e0 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -64,8 +64,7 @@ def fix_arrays(a1, a2): """ if a1 is not None and a2 is not None: allmasked_1, allmasked_2 = ( - da.all(da.ma.getmaskarray(arr).compute()) - for arr in (a1, a2) + da.all(da.ma.getmaskarray(arr).compute()) for arr in (a1, a2) ) if allmasked_1 and allmasked_2: # IN this case == will return 'masked', but for our purposes @@ -85,9 +84,7 @@ def fix_arrays(a1, a2): return a1, a2 # Fix matching of cube data arrays - c1.data, c2.data = fix_arrays( - *(cube.core_data() for cube in (c1, c2)) - ) + c1.data, c2.data = fix_arrays(*(cube.core_data() for cube in (c1, c2))) # Fix matching of all coords points + bounds for co1, co2 in zip(c1.coords(), c2.coords()): if isinstance(co1, iris.mesh.MeshCoord): diff --git a/tests/integration/example_scripts/ex_iris_xarray_conversion.py b/tests/integration/example_scripts/ex_iris_xarray_conversion.py index aee9031..d91ac71 100644 --- a/tests/integration/example_scripts/ex_iris_xarray_conversion.py +++ b/tests/integration/example_scripts/ex_iris_xarray_conversion.py @@ -78,10 +78,7 @@ def example_from_xr(): print( "xrds2['x'].data is cube.coord('projection_x_coordinate').core_points() : ", - bool( - xrds2["x"].data - is cube.coord("projection_x_coordinate").core_points() - ), + bool(xrds2["x"].data is cube.coord("projection_x_coordinate").core_points()), ) # NOTE: this one does **not** succeed. # TODO: find out exactly why -- ? in some way, because it is a dim coord ? @@ -95,15 +92,9 @@ def example_from_xr(): # special care is needed. print( "np.all(xrds2['x'].data == cube.coord('projection_x_coordinate').points) : ", - bool( - np.all( - xrds2["x"].data == cube.coord("projection_x_coordinate").points - ) - ), - ) - assert np.all( - xrds2["x"].data == cube.coord("projection_x_coordinate").points + bool(np.all(xrds2["x"].data == cube.coord("projection_x_coordinate").points)), ) + assert np.all(xrds2["x"].data == cube.coord("projection_x_coordinate").points) if __name__ == "__main__": diff --git a/tests/integration/test_iris_load_and_save_equivalence.py b/tests/integration/test_iris_load_and_save_equivalence.py index 5253fa9..39cbc43 100644 --- a/tests/integration/test_iris_load_and_save_equivalence.py +++ b/tests/integration/test_iris_load_and_save_equivalence.py @@ -52,9 +52,7 @@ def use_irislock(): set_tiny_chunks(_USE_TINY_CHUNKS) -def test_load_direct_vs_viancdata( - standard_testcase, use_irislock, adjust_chunks -): +def test_load_direct_vs_viancdata(standard_testcase, use_irislock, adjust_chunks): specific_excludes = [ # This one has latitude points which exceed the valid_min/max attributes # The netcdf-variable-like transform in Nc4VariableLike._data_array don't @@ -71,8 +69,7 @@ def test_load_direct_vs_viancdata( ] ) if any( - name_fragment in standard_testcase.name - for name_fragment in specific_excludes + name_fragment in standard_testcase.name for name_fragment in specific_excludes ): pytest.skip("excluded testcase") @@ -136,8 +133,7 @@ def test_save_direct_vs_viancdata(standard_testcase, tmp_path): # TODO: remove backwards compatibility when we drop Python 3.10 support specific_excludes.append("theta_nodal_xios") if any( - name_fragment in standard_testcase.name - for name_fragment in specific_excludes + name_fragment in standard_testcase.name for name_fragment in specific_excludes ): pytest.skip("excluded testcase") @@ -167,14 +163,10 @@ def test_save_direct_vs_viancdata(standard_testcase, tmp_path): txt = check_output([f"ncdump {source_filepath}"], shell=True).decode() print(txt) print("\nncdump DIRECT FROM IRIS =") - txt = check_output( - [f"ncdump {temp_iris_savepath}"], shell=True - ).decode() + txt = check_output([f"ncdump {temp_iris_savepath}"], shell=True).decode() print(txt) print("\nncdump VIA NCDATA =") - txt = check_output( - [f"ncdump {temp_ncdata_savepath}"], shell=True - ).decode() + txt = check_output([f"ncdump {temp_ncdata_savepath}"], shell=True).decode() print(txt) # Check equivalence diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 0d0f1df..53bbded 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -195,9 +195,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): if not result: # FOR NOW: compare with experimental ncdata comparison. # I know this is a bit circular, but it is useful for debugging, for now ... - result = dataset_differences( - from_iris(iris_cubes), from_iris(iris_xr_cubes) - ) + result = dataset_differences(from_iris(iris_cubes), from_iris(iris_xr_cubes)) assert result == [] @@ -206,9 +204,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): # It's not currently clear if we *can* find a simple view of what "ought" to work here, # or whether we should be attempting to test this at all. @pytest.mark.skip("Roundtrip testing xarray-iris-xarray : currently disabled.") -def test_roundtrip_xix( - standard_testcase, use_irislock, adjust_chunks, tmp_path -): +def test_roundtrip_xix(standard_testcase, use_irislock, adjust_chunks, tmp_path): source_filepath = standard_testcase.filepath # Skip some cases @@ -313,9 +309,7 @@ def test_roundtrip_xix( # so Iris replaces it. var.avals["calendar"] = "standard" - result = dataset_differences( - ncds_xr, ncds_xr_iris - ) # , check_var_data=False) + result = dataset_differences(ncds_xr, ncds_xr_iris) # , check_var_data=False) assert result == [] # TODO: check equivalence, in Xarray terms diff --git a/tests/integration/test_netcdf_roundtrips.py b/tests/integration/test_netcdf_roundtrips.py index 2b33155..fa1dc83 100644 --- a/tests/integration/test_netcdf_roundtrips.py +++ b/tests/integration/test_netcdf_roundtrips.py @@ -38,9 +38,7 @@ def test_basic(standard_testcase, tmp_path): print("\nncdata =") print(ncdata) print("\nncdump =") - txt = check_output( - [f"ncdump {intermediate_filepath}"], shell=True - ).decode() + txt = check_output([f"ncdump {intermediate_filepath}"], shell=True).decode() print(txt) # Check that the re-saved file matches the original diff --git a/tests/integration/test_xarray_load_and_save_equivalence.py b/tests/integration/test_xarray_load_and_save_equivalence.py index 5e60db3..378fcc1 100644 --- a/tests/integration/test_xarray_load_and_save_equivalence.py +++ b/tests/integration/test_xarray_load_and_save_equivalence.py @@ -67,9 +67,7 @@ def check_vars_equivalent(v1, v2): assert v1.dims == v2.dims assert v1.dtype == v2.dtype # Numeric compare may need to allow for NaNs : floats *and datetimes* - equal_nan = ( - v1.dtype.kind in "fM" - ) # cannot set kwarg when not applicable + equal_nan = v1.dtype.kind in "fM" # cannot set kwarg when not applicable result = np.array_equal(v1.data, v2.data, equal_nan=equal_nan) if hasattr(result, "compute"): result = result.compute() diff --git a/tests/integration/test_zarr_to_iris.py b/tests/integration/test_zarr_to_iris.py index bbb27d7..b207587 100644 --- a/tests/integration/test_zarr_to_iris.py +++ b/tests/integration/test_zarr_to_iris.py @@ -102,8 +102,7 @@ def _is_url_ok(url): # Check 3 files, as we apparently sometimes get 'partial success' _S3_accessible = all( - _is_url_ok(S3_TEST_PATH + subpath) - for subpath in ["", "/.zattrs", "/.zmetadata"] + _is_url_ok(S3_TEST_PATH + subpath) for subpath in ["", "/.zattrs", "/.zmetadata"] ) diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index a206614..e6ceb81 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -53,9 +53,7 @@ def _env_bin_path(exe_name: AnyStr = None): NCDUMP_PATHSTR = str(_env_bin_path("ncdump")) -def ncgen_from_cdl( - cdl_str: str | None, cdl_path: str | None, nc_path: str -): +def ncgen_from_cdl(cdl_str: str | None, cdl_path: str | None, nc_path: str): """ Generate a test netcdf file from cdl. diff --git a/tests/unit/core/test_AttrvalsDict.py b/tests/unit/core/test_AttrvalsDict.py index af0039f..7b7c076 100644 --- a/tests/unit/core/test_AttrvalsDict.py +++ b/tests/unit/core/test_AttrvalsDict.py @@ -168,9 +168,7 @@ def test_del(self): def test_update(self): v = self.var # renaming : name of NcAttribute supplied as a value is lost - v.avals.update( - {"q": 49.3, "ZZ": NcAttribute("", [1, 2]), "a": 17} - ) + v.avals.update({"q": 49.3, "ZZ": NcAttribute("", [1, 2]), "a": 17}) # ordering : "q" and "ZZ" are added after, but "a" stays where it was assert list(v.attributes.keys()) == ["a", "b", "q", "ZZ"] assert v.attributes["a"] == NcAttribute("a", 17) diff --git a/tests/unit/core/test_NameMap.py b/tests/unit/core/test_NameMap.py index 0912282..0740448 100644 --- a/tests/unit/core/test_NameMap.py +++ b/tests/unit/core/test_NameMap.py @@ -225,9 +225,7 @@ def test_attributes__namevaluemap(self, target_itemtype, multiple): vals = [val.value for val in vals] assert vals == list(arg.values()) - @pytest.mark.parametrize( - "arg", [[], {}, None], ids=["list", "map", "none"] - ) + @pytest.mark.parametrize("arg", [[], {}, None], ids=["list", "map", "none"]) def test_attributes_empty(self, arg): # Just check correct construction from empty args. namemap = NameMap.from_items(arg, item_type=NcAttribute) @@ -242,9 +240,7 @@ def test_copy(self, item_type): assert result == source assert result.item_type == item_type # NOTE: not a *deep* copy : contains the same item objects - assert all( - e1 is e2 for e1, e2 in zip(result.values(), source.values()) - ) + assert all(e1 is e2 for e1, e2 in zip(result.values(), source.values())) class Test_add: diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index 2f6a22f..c5d8538 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -58,9 +58,7 @@ def structuretype(request): def attrvalue(datatype, structuretype): # get basic type = 'int', 'float', 'string' - (datatype,) = ( - key for key in _attribute_testdata if key in datatype - ) + (datatype,) = (key for key in _attribute_testdata if key in datatype) # fetch a set of values values = _attribute_testdata[datatype] diff --git a/tests/unit/core/test_NcData.py b/tests/unit/core/test_NcData.py index a2f13a9..eb04235 100644 --- a/tests/unit/core/test_NcData.py +++ b/tests/unit/core/test_NcData.py @@ -115,9 +115,7 @@ def test(self, mocker, multiple_indices): ncdata1 = NcData() mock_slicer = mocker.MagicMock(spec=Slicer) - called = mocker.patch( - "ncdata._core.NcData.slicer", return_value=mock_slicer - ) + called = mocker.patch("ncdata._core.NcData.slicer", return_value=mock_slicer) if multiple_indices: # Index with multiple keys, which should be passed as a tuple # Slightly awkward code, because the "[*keys]" syntax is only valid from @@ -130,7 +128,5 @@ def test(self, mocker, multiple_indices): result = ncdata1[dim_keys] assert called.call_args_list == [mocker.call()] - assert mock_slicer.__getitem__.call_args_list == [ - mocker.call(dim_keys) - ] + assert mock_slicer.__getitem__.call_args_list == [mocker.call(dim_keys)] assert result == mock_slicer[dim_keys] diff --git a/tests/unit/core/test_NcVariable.py b/tests/unit/core/test_NcVariable.py index 6d4e701..3f2d1c5 100644 --- a/tests/unit/core/test_NcVariable.py +++ b/tests/unit/core/test_NcVariable.py @@ -134,9 +134,7 @@ def test_data__listsofstrings(self): assert var.data.shape == (2, 3) assert var.dtype == var.data.dtype == np.dtype("U5") - @pytest.mark.parametrize( - "dtype", ["i4", "i8", "f4", "f8", "u1", "i1", "U4"] - ) + @pytest.mark.parametrize("dtype", ["i4", "i8", "f4", "f8", "u1", "i1", "U4"]) def test_data__numpy(self, dtype): elem = "abc" if dtype.startswith("U") else 12 dtype = np.dtype(dtype) diff --git a/tests/unit/iris/test_from_iris.py b/tests/unit/iris/test_from_iris.py index e32b699..fb647f8 100644 --- a/tests/unit/iris/test_from_iris.py +++ b/tests/unit/iris/test_from_iris.py @@ -35,9 +35,7 @@ def sample_cube(data_array=None): """ if data_array is None: data_array = np.arange(3.0) - time_co = DimCoord( - [0.0, 1, 2], standard_name="time", units="days since 2001-01-01" - ) + time_co = DimCoord([0.0, 1, 2], standard_name="time", units="days since 2001-01-01") cube = Cube(data_array, standard_name="air_temperature", units="K") cube.add_dim_coord(time_co, 0) return cube @@ -141,9 +139,7 @@ def test_kwargs_packing(): cube = sample_cube(data) # Save with packing controls in a 'packing' keyword. - kwargs = dict( - packing=dict(dtype=np.int16, scale_factor=0.1, add_offset=-5.3) - ) + kwargs = dict(packing=dict(dtype=np.int16, scale_factor=0.1, add_offset=-5.3)) ncdata = from_iris(cube, **kwargs) # Check that the resulting variable has the expected dtype and (raw) data values diff --git a/tests/unit/iris/test_to_iris.py b/tests/unit/iris/test_to_iris.py index 19a9bfa..7da60af 100644 --- a/tests/unit/iris/test_to_iris.py +++ b/tests/unit/iris/test_to_iris.py @@ -31,9 +31,7 @@ def test_lazy_nocompute(): dask_array = da.from_array(monitored_array, chunks=(2,), meta=np.ndarray) ncdata = NcData( dimensions=[NcDimension("x", 3)], - variables=[ - NcVariable(name="var_x", dimensions=["x"], data=dask_array) - ], + variables=[NcVariable(name="var_x", dimensions=["x"], data=dask_array)], ) # convert to cubes @@ -59,9 +57,7 @@ def test_real_nocopy(): monitored_array = MonitoredArray(real_array) ncdata = NcData( dimensions=[NcDimension("x", 3)], - variables=[ - NcVariable(name="var_x", dimensions=["x"], data=monitored_array) - ], + variables=[NcVariable(name="var_x", dimensions=["x"], data=monitored_array)], ) # convert to cubes diff --git a/tests/unit/netcdf/test_from_nc4.py b/tests/unit/netcdf/test_from_nc4.py index 1ad3939..92ef316 100644 --- a/tests/unit/netcdf/test_from_nc4.py +++ b/tests/unit/netcdf/test_from_nc4.py @@ -47,9 +47,7 @@ def test_target_types(sourcetype, tmp_path): # In effect, this is also exercising tricky bits of 'dataset_differences' !! test_spec = { "dims": [dict(name="xdim", size=3)], - "vars": [ - dict(name="x", dims=["xdim"], dtype=np.float32, data=[1.23, 2, 9]) - ], + "vars": [dict(name="x", dims=["xdim"], dtype=np.float32, data=[1.23, 2, 9])], "groups": [ { "dims": [dict(name="ydim", size=2)], diff --git a/tests/unit/netcdf/test_to_nc4.py b/tests/unit/netcdf/test_to_nc4.py index bcc1839..1a656c0 100644 --- a/tests/unit/netcdf/test_to_nc4.py +++ b/tests/unit/netcdf/test_to_nc4.py @@ -43,9 +43,7 @@ def file_and_ncdata_from_spec(filepath: Path, test_spec: dict) -> NcData: def test_target_types(targettype, tmp_path): """Check the various ways of specifying the output location.""" # Testcase is minimal spec with just one scalar variable. - test_spec = { - "vars": [dict(name="x", dims=[], dtype=np.float32, data=[1.23])] - } + test_spec = {"vars": [dict(name="x", dims=[], dtype=np.float32, data=[1.23])]} original_path = tmp_path / "input.nc" ncdata = file_and_ncdata_from_spec(original_path, test_spec) @@ -111,9 +109,7 @@ def test_var_kwargs(tmp_path): "groups": [ { "name": "sub_subgroup_1_2", - "vars": [ - dict(name="var_xx", dims=["x"], dtype=np.float32) - ], + "vars": [dict(name="var_xx", dims=["x"], dtype=np.float32)], }, ], }, @@ -146,9 +142,7 @@ def test_var_kwargs(tmp_path): assert var_2.filters()["zlib"] is False assert var_2.chunking() == "contiguous" - var_3 = fetch_nc_var( - saved_ds, ["subgroup_1", "sub_subgroup_1_2", "var_xx"] - ) + var_3 = fetch_nc_var(saved_ds, ["subgroup_1", "sub_subgroup_1_2", "var_xx"]) assert var_3.filters()["zlib"] is False assert var_3.chunking() == [2] @@ -156,9 +150,7 @@ def test_var_kwargs(tmp_path): def test_var_kwargs__bad_kwarg(tmp_path): """Check that unsuitable var_kwargs entries are errored.""" # Testcase is a minimal spec with just one scalar variable. - test_spec = { - "vars": [dict(name="x", dims=[], dtype=np.float32, data=[1.23])] - } + test_spec = {"vars": [dict(name="x", dims=[], dtype=np.float32, data=[1.23])]} original_path = tmp_path / "input.nc" ncdata = file_and_ncdata_from_spec(original_path, test_spec) diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py index 4a482af..c0a4a98 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py @@ -58,14 +58,10 @@ def test_diff(self): def test_difforder(self): errs = _namelist_differences(["a", "b"], ["b", "a"], "named-elements") - assert errs == [ - "named-elements do not match: ['a', 'b'] != ['b', 'a']" - ] + assert errs == ["named-elements do not match: ['a', 'b'] != ['b', 'a']"] def test_difforder_tolerant_warns(self): - with pytest.warns( - UserWarning, match="Ignoring: named-elements do not match" - ): + with pytest.warns(UserWarning, match="Ignoring: named-elements do not match"): errs = _namelist_differences( ["a", "b"], ["b", "a"], @@ -171,9 +167,7 @@ def test_compare_attributes_values__data_mismatch(self): obj1 = self.Nc4ObjectWithAttrsMimic(a=1, b=2, c=3) obj2 = self.Nc4ObjectWithAttrsMimic(a=1, b=-77, c=3) errs = _attribute_differences(obj1, obj2, "") - assert errs == [ - ' "b" attribute values differ : 2 != -77' - ] + assert errs == [' "b" attribute values differ : 2 != -77'] def test_compare_attributes_values__dtype_mismatch__length(self): # Attributes of different dtypes, even though values == @@ -261,10 +255,7 @@ def test_compare_attributes_values__data_arrays_shape_mismatch(self): obj2 = self.Nc4ObjectWithAttrsMimic(a=array[:-1]) errs = _attribute_differences(obj1, obj2, "") assert errs == [ - ( - ' "a" attribute values differ : ' - "[0, 1, 2] != [0, 1]" - ) + (' "a" attribute values differ : [0, 1, 2] != [0, 1]') ] def test_compare_attributes_values__data_arrays_value_mismatch(self): @@ -412,9 +403,7 @@ def test_identical_datasets(self, samefiles_filesonly, sourcetype): if ds: ds.close() - def test_small_difference( - self, samefiles_bothtypes, temp_ncfiles_dir, sourcetype - ): + def test_small_difference(self, samefiles_bothtypes, temp_ncfiles_dir, sourcetype): source1, source2 = samefiles_bothtypes if sourcetype == "InputsFile": diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py index 0d7b211..ed4608f 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py @@ -21,9 +21,7 @@ # data_types # avoid 'unused' warning -@pytest.fixture( - params=["in_named", "in_unnamed", "in_namedgroup", "in_unnamedgroup"] -) +@pytest.fixture(params=["in_named", "in_unnamed", "in_namedgroup", "in_unnamedgroup"]) def group_context(request): """ The different contexts of locations in a dataset @@ -124,9 +122,7 @@ def dimension_testdata(self, group_context): @pytest.fixture(autouse=True) def _dims_data(self, group_context): - data1, data2 = [ - self.dimension_testdata(group_context) for _ in range(2) - ] + data1, data2 = [self.dimension_testdata(group_context) for _ in range(2)] location = data2 if "group" in group_context: location = location.groups["inner_group"] @@ -155,9 +151,7 @@ def test_size(self): ] check(errs, expected) - @pytest.mark.parametrize( - "check_unlim", ["unlims_checked", "unlims_unchecked"] - ) + @pytest.mark.parametrize("check_unlim", ["unlims_checked", "unlims_unchecked"]) def test_unlimited(self, check_unlim): self.dims["y"].unlimited = True @@ -208,8 +202,7 @@ def test_extra_or_missing(self): errs = dataset_differences(self.data1, self.data2) expected = [ - f"{self.location_string} dimension lists do not match: " - "['x', 'y'] != ['x']" + f"{self.location_string} dimension lists do not match: ['x', 'y'] != ['x']" ] check(errs, expected) @@ -239,9 +232,7 @@ def attribute_testdata(self, group_context): @pytest.fixture(autouse=True) def _attrs_data(self, group_context, attr_context): - data1, data2 = [ - self.attribute_testdata(group_context) for _ in range(2) - ] + data1, data2 = [self.attribute_testdata(group_context) for _ in range(2)] location = data2 if "group" in group_context: location = location.groups["inner_group"] @@ -321,9 +312,7 @@ def test_fillvalue_anyorder(self, attname): attr_pair = [NcAttribute(name, 1), NcAttribute("x", 1)] data1, data2 = [ NcData( - variables=[ - NcVariable("vx", (), data=np.array(0.0), attributes=attrs) - ] + variables=[NcVariable("vx", (), data=np.array(0.0), attributes=attrs)] ) for attrs in (attr_pair, attr_pair[::-1]) ] @@ -446,9 +435,7 @@ def test_group_names(self): errs = dataset_differences(self.data1, self.data2) - expected = [ - "Dataset subgroup lists do not match: ['g1', 'g2'] != ['g1', 'q']" - ] + expected = ["Dataset subgroup lists do not match: ['g1', 'g2'] != ['g1', 'q']"] check(errs, expected) def test_group_order(self, order_checking): @@ -463,8 +450,7 @@ def test_group_order(self, order_checking): if do_ordercheck: expected = [ - "Dataset subgroup lists do not match: " - "['g1', 'g2'] != ['g2', 'g1']" + "Dataset subgroup lists do not match: ['g1', 'g2'] != ['g2', 'g1']" ] else: expected = [] @@ -479,7 +465,5 @@ def test_groups_extra_or_missing(self, order_checking): ) # NB since the sets are different, the ordering control has no effect - expected = [ - "Dataset subgroup lists do not match: ['g1', 'g2'] != ['g2']" - ] + expected = ["Dataset subgroup lists do not match: ['g1', 'g2'] != ['g2']"] check(errs, expected) diff --git a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py index 0d47f82..a9af118 100644 --- a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py +++ b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py @@ -25,15 +25,14 @@ class TestSimpleProperties: @pytest.fixture(autouse=True) def _vars_data(self): self.var1, self.var2 = [ - NcVariable("v1", ("y", "x"), data=np.zeros((2, 3))) - for _ in range(2) + NcVariable("v1", ("y", "x"), data=np.zeros((2, 3))) for _ in range(2) ] def test_var_names(self): self.var2.name = "q" errs = variable_differences(self.var1, self.var2) - expected = ['Variable "v1 / q" names differ : ' "'v1' != 'q'"] + expected = ["Variable \"v1 / q\" names differ : 'v1' != 'q'"] check(errs, expected) def test_var_dims__reorder(self): @@ -45,9 +44,7 @@ def test_var_dims__reorder(self): errs = variable_differences(self.var1, self.var2) - expected = [ - 'Variable "v1" dimensions differ : ' "('y', 'x') != ('x', 'y')" - ] + expected = ["Variable \"v1\" dimensions differ : ('y', 'x') != ('x', 'y')"] check(errs, expected) def test_var_dims__extra_or_missing(self): @@ -81,8 +78,7 @@ def test_numbers_v_strings(self): # Test the comparison errs = variable_differences(self.var1, self.var2) expected = [ - 'Variable "v1" datatypes differ : ' - "dtype('float64') != dtype('S5')" + "Variable \"v1\" datatypes differ : dtype('float64') != dtype('S5')" ] check(errs, expected) @@ -101,8 +97,7 @@ def test_ints_v_floats(self, equaldata): errs = variable_differences(self.var1, self.var2) expected = [ - 'Variable "v1" datatypes differ : ' - "dtype('float64') != dtype('int32')" + "Variable \"v1\" datatypes differ : dtype('float64') != dtype('int32')" ] if not equaldata: expected.append( @@ -127,8 +122,7 @@ def test_wordlengths(self, equaldata): errs = variable_differences(self.var1, self.var2) expected = [ - 'Variable "v1" datatypes differ : ' - "dtype('float64') != dtype('float32')" + "Variable \"v1\" datatypes differ : dtype('float64') != dtype('float32')" ] if not equaldata: expected.append( @@ -157,8 +151,7 @@ def test_signed_unsigned(self, equaldata): errs = variable_differences(self.var1, self.var2) expected = [ - 'Variable "v1" datatypes differ : ' - "dtype('int64') != dtype('uint64')" + "Variable \"v1\" datatypes differ : dtype('int64') != dtype('uint64')" ] if not equaldata: expected.append( @@ -184,9 +177,7 @@ def test_nodata_nodtype(self, given): ] elif given == "dtype": kwargs["dtype"] = np.float32 - expected = [ - "Variable \"x\" datatypes differ : None != dtype('float32')" - ] + expected = ["Variable \"x\" datatypes differ : None != dtype('float32')"] elif given == "nodata": expected = [] else: @@ -229,9 +220,7 @@ def test_differences_ignore_datacheck(self, differs): nodata = differs.endswith("nodata") if differs.startswith("dims"): dims2 = ["x"] - expected = [ - "Variable \"x\" dimensions differ : ('x', 'y') != ('x',)" - ] + expected = ["Variable \"x\" dimensions differ : ('x', 'y') != ('x',)"] if nodata: # In this case, either one or both have no data. shape1 = None @@ -240,9 +229,7 @@ def test_differences_ignore_datacheck(self, differs): shape2 = None else: # Only one has data --> triggers a shape warning too. - expected.append( - 'Variable "x" shapes differ : None != (2, 3)' - ) + expected.append('Variable "x" shapes differ : None != (2, 3)') elif differs.startswith("shapes"): if nodata: # In this case one has data (and therefore a shape), and the other not. @@ -282,9 +269,7 @@ def test_no_values_check(self): def test_print_bad_nprint(self): msg = "'show_n_diffs' must be >=1 : got 0." with pytest.raises(ValueError, match=msg): - variable_differences( - self.var1, self.var2, show_n_first_different=0 - ) + variable_differences(self.var1, self.var2, show_n_first_different=0) @pytest.mark.parametrize("ndiffs", [1, 2, 3]) def test_ndiffs(self, ndiffs): @@ -293,31 +278,24 @@ def test_ndiffs(self, ndiffs): detail = { 1: "[(0, 1)] : LHS=[1.0], RHS=[2.0]", 2: "[(0, 1), (0, 2)] : LHS=[1.0, 2.0], RHS=[2.0, 3.0]", - 3: ( - "[(0, 1), (0, 2), ...] : " - "LHS=[1.0, 2.0, ...], RHS=[2.0, 3.0, ...]" - ), + 3: ("[(0, 1), (0, 2), ...] : LHS=[1.0, 2.0, ...], RHS=[2.0, 3.0, ...]"), }[ndiffs] expected = [ - f'Variable "v1" data contents differ, at {ndiffs} points: ' - f"@INDICES{detail}" + f'Variable "v1" data contents differ, at {ndiffs} points: @INDICES{detail}' ] check(errs, expected) @pytest.mark.parametrize("nprint", [1, 2, 3]) def test_show_n_first_different(self, nprint): self.var2.data.flat[1:3] += 1 - errs = variable_differences( - self.var1, self.var2, show_n_first_different=nprint - ) + errs = variable_differences(self.var1, self.var2, show_n_first_different=nprint) detail = { 1: "[(0, 1), ...] : LHS=[1.0, ...], RHS=[2.0, ...]", 2: "[(0, 1), (0, 2)] : LHS=[1.0, 2.0], RHS=[2.0, 3.0]", 3: "[(0, 1), (0, 2)] : LHS=[1.0, 2.0], RHS=[2.0, 3.0]", }[nprint] expected = [ - f'Variable "v1" data contents differ, at 2 points: ' - f"@INDICES{detail}" + f'Variable "v1" data contents differ, at 2 points: @INDICES{detail}' ] check(errs, expected) @@ -383,9 +361,7 @@ def test_scalar(self): ] check(errs, expected) - @pytest.mark.parametrize( - "argtypes", ["real_real", "real_lazy", "lazy_lazy"] - ) + @pytest.mark.parametrize("argtypes", ["real_real", "real_lazy", "lazy_lazy"]) def test_real_and_lazy(self, argtypes): type1, type2 = argtypes[:4], argtypes[-4:] # fix the testvar to create a difference diff --git a/tests/unit/utils/dim_indexing/__init__.py b/tests/unit/utils/dim_indexing/__init__.py index 9170a66..37b1cc6 100644 --- a/tests/unit/utils/dim_indexing/__init__.py +++ b/tests/unit/utils/dim_indexing/__init__.py @@ -54,9 +54,7 @@ def make_dims_testdata(x, y): else: ydims = [] - data.variables.add( - NcVariable("Y", ydims, data=y, attributes={"role": "Y dim var"}) - ) + data.variables.add(NcVariable("Y", ydims, data=y, attributes={"role": "Y dim var"})) data.variables.add( NcVariable( @@ -72,9 +70,7 @@ def make_dims_testdata(x, y): else: xdims = [] - data.variables.add( - NcVariable("X", xdims, data=x, attributes={"role": "X dim var"}) - ) + data.variables.add(NcVariable("X", xdims, data=x, attributes={"role": "X dim var"})) xydims = ["Z"] + (["Y"] if y.ndim else []) + (["X"] if x.ndim else []) shape = [2] + ([len(y)] if y.ndim else []) + ([len(x)] if x.ndim else []) data.variables.add( diff --git a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py index 4c0529a..a107814 100644 --- a/tests/unit/utils/dim_indexing/test_index_by_dimensions.py +++ b/tests/unit/utils/dim_indexing/test_index_by_dimensions.py @@ -80,9 +80,7 @@ def test_integer_array(self): ) def test_boolean_array(self): - res1 = index_by_dimensions( - self.sample, X=[True, False, True, False, False] - ) + res1 = index_by_dimensions(self.sample, X=[True, False, True, False, False]) res2 = index_by_dimensions(self.sample, X=[0, 2]) assert dataset_differences(res1, res2) == [] @@ -96,9 +94,7 @@ def test_ellipsis_fail(self): with pytest.raises(ValueError, match=msg): index_by_dimensions(self.sample, Z=...) - @pytest.mark.parametrize( - "key", [np.newaxis, None], ids=["newaxis", "None"] - ) + @pytest.mark.parametrize("key", [np.newaxis, None], ids=["newaxis", "None"]) def test_newaxis_fail(self, key): msg = "Key for dimension 'Y' is np.newaxis / None.* not supported." with pytest.raises(ValueError, match=msg): diff --git a/tests/unit/utils/test_ncdata_copy.py b/tests/unit/utils/test_ncdata_copy.py index 025f7e4..e1daed2 100644 --- a/tests/unit/utils/test_ncdata_copy.py +++ b/tests/unit/utils/test_ncdata_copy.py @@ -56,14 +56,10 @@ def test_empty(self): @pytest.fixture def sample(self): - attrs = NameMap.from_items( - [NcAttribute("q", 3)], item_type=NcAttribute - ) + attrs = NameMap.from_items([NcAttribute("q", 3)], item_type=NcAttribute) dims = NameMap.from_items([NcDimension("x", 3)], item_type=NcDimension) data_array = np.array([1, 2, 3]) - var = NcVariable( - name="a", dimensions=("x"), data=data_array, attributes=attrs - ) + var = NcVariable(name="a", dimensions=("x"), data=data_array, attributes=attrs) sample = NcData( dimensions=dims, variables=[var], @@ -102,15 +98,11 @@ def test_sample_attribute_arraydata(self, sample): rva = result.variables["a"] assert ( - result.attributes["extra"].value - is not sample.attributes["extra"].value + result.attributes["extra"].value is not sample.attributes["extra"].value ) and np.all( - result.attributes["extra"].value - == sample.attributes["extra"].value + result.attributes["extra"].value == sample.attributes["extra"].value ) assert ( rva.attributes["xx2"].value is not sva.attributes["xx2"].value - ) and np.all( - rva.attributes["xx2"].value == sva.attributes["xx2"].value - ) + ) and np.all(rva.attributes["xx2"].value == sva.attributes["xx2"].value) diff --git a/tests/unit/utils/test_rename_dimension.py b/tests/unit/utils/test_rename_dimension.py index c426111..b9926a4 100644 --- a/tests/unit/utils/test_rename_dimension.py +++ b/tests/unit/utils/test_rename_dimension.py @@ -11,9 +11,7 @@ from ncdata.utils import rename_dimension, save_errors -def make_saveable( - ncdata: NcData, _outer_dims: dict[str, NcDimension] | None = None -): +def make_saveable(ncdata: NcData, _outer_dims: dict[str, NcDimension] | None = None): """ Add missing dimensions + data, to make a sample NcData save-able. @@ -104,8 +102,7 @@ def test_name_collision_ingroup_fail(self, innergroup): ncdata = self.ncdata grp = NcData(name="inner", dimensions=[NcDimension("z", 2)]) msg = ( - "Cannot rename dimension 'x' to 'z', " - "because a 'z' dimension already exists" + "Cannot rename dimension 'x' to 'z', because a 'z' dimension already exists" ) if innergroup: grp = NcData(name="main", groups=[grp]) diff --git a/tests/unit/utils/test_save_errors.py b/tests/unit/utils/test_save_errors.py index aaaeea3..1811cb5 100644 --- a/tests/unit/utils/test_save_errors.py +++ b/tests/unit/utils/test_save_errors.py @@ -23,9 +23,7 @@ def _basic_testdata(): ncdata = NcData( name="test_ds", dimensions=[ - NcDimension( - "xxx", 2 - ), # unused first dim, so rename doesn't break variable + NcDimension("xxx", 2), # unused first dim, so rename doesn't break variable NcDimension("x", 3), ], variables=[ @@ -78,9 +76,7 @@ def component_propname(component: str): propname = component + "s" return propname - @pytest.mark.parametrize( - "badnametype", ["None", "empty", "number", "object"] - ) + @pytest.mark.parametrize("badnametype", ["None", "empty", "number", "object"]) def test_bad_name_type(self, badnametype, component): bad_name = { "None": None, @@ -125,9 +121,7 @@ def test_key_name_mismatch(self, component): debug_errors(errors) assert len(errors) == 1 - msg = ( - f"{component} element {key!r} has a different element.name : 'qqq'" - ) + msg = f"{component} element {key!r} has a different element.name : 'qqq'" assert re.search(msg, errors[0]) diff --git a/tests/unit/xarray/test_from_xarray.py b/tests/unit/xarray/test_from_xarray.py index e5c43b1..c3c1a0c 100644 --- a/tests/unit/xarray/test_from_xarray.py +++ b/tests/unit/xarray/test_from_xarray.py @@ -112,9 +112,7 @@ def test_kwargs__unlimited_dims(tmp_path, unlim_dims): """Check xarray-save kwargs control : make the "x" dimension unlimited.""" test_spec = { "dims": [dict(name="x", size=3)], - "vars": [ - dict(name="var_x", dims=["x"], dtype=np.float32, data=[1.0, 3, 7]) - ], + "vars": [dict(name="var_x", dims=["x"], dtype=np.float32, data=[1.0, 3, 7])], } original_path = tmp_path / "input.nc" xrds = file_and_xarray_from_spec(original_path, test_spec) @@ -132,11 +130,7 @@ def test_kwargs__encoding__packing(tmp_path): """Repeat the above, checking use of the 'encoding' kwarg.""" test_spec = { "dims": [dict(name="x", size=3)], - "vars": [ - dict( - name="var_x", dims=["x"], dtype=np.float32, data=[1.0, 2, 3.3] - ) - ], + "vars": [dict(name="var_x", dims=["x"], dtype=np.float32, data=[1.0, 2, 3.3])], } original_path = tmp_path / "input.nc" xrds = file_and_xarray_from_spec(original_path, test_spec) diff --git a/tests/unit/xarray/test_to_xarray.py b/tests/unit/xarray/test_to_xarray.py index f1ffa88..78aec2d 100644 --- a/tests/unit/xarray/test_to_xarray.py +++ b/tests/unit/xarray/test_to_xarray.py @@ -30,9 +30,7 @@ def test_lazy_nocompute(): dask_array = da.from_array(monitored_array, chunks=(2,), meta=np.ndarray) ncdata = NcData( dimensions=[NcDimension("x", 3)], - variables=[ - NcVariable(name="var_x", dimensions=["x"], data=dask_array) - ], + variables=[NcVariable(name="var_x", dimensions=["x"], data=dask_array)], ) # convert to xarray.Dataset @@ -56,9 +54,7 @@ def test_real_nocopy(): real_numpy_data = np.array([1.23, 4, 7.3, 4.1], dtype=np.float32) ncdata = NcData( dimensions=[NcDimension("x", 3)], - variables=[ - NcVariable(name="var_x", dimensions=["x"], data=real_numpy_data) - ], + variables=[NcVariable(name="var_x", dimensions=["x"], data=real_numpy_data)], ) # convert to xarray.Dataset From ee4553b63a0675f528cf109cef3f3f1299bb8238 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 2 Nov 2026 11:47:36 +0000 Subject: [PATCH 17/26] More Ruff controls. --- pyproject.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bf03386..3ec82a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,10 +157,16 @@ ignore = [ ] [tool.ruff] +extend-exclude = [ + "docs/conf.py", # based on Sphix template, is too different to be worth checking. +] line-length = 88 src = ["lib"] [tool.ruff.format] +exclude = [ + "*.md", "*.rst" #don't reformat code in docs etc +] preview = false [tool.ruff.lint] @@ -186,6 +192,12 @@ ignore = [ "SLF", # flake8 private member accesses "T201", # Print statement. "TD", # flake8 TODO bans + "RET504", # unnecessary assignment before return [[readabiility wins]] + "TRY003", # Avoid...long messages outside the exception class [[over-applied. unhelpful]] + "EM", # (string construstion in raised exceptions) [[unhelpful]] + "B028", # warn without stacklevel= [[probably not]] + "FLY002", # use f-string instead of joins [[TRUST: readability]] + "RUF015", # prefer 'next' over [0] [[readability]] # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): "ERA001", # Found commented-out code From 6c8a324f4d829426508dc1fd7840c9f3bb2b7886 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 2 Nov 2026 12:51:26 +0000 Subject: [PATCH 18/26] Multiple changes from ruff check, fix(including unsafe) and format, manually checked + slightly improved. --- lib/ncdata/_core.py | 29 +++---- lib/ncdata/dataset_like.py | 9 +-- lib/ncdata/netcdf4.py | 11 +-- lib/ncdata/threadlock_sharing.py | 3 +- lib/ncdata/utils/_compare_nc_datasets.py | 27 ++----- lib/ncdata/utils/_dim_indexing.py | 2 +- lib/ncdata/utils/_save_errors.py | 6 +- lib/ncdata/xarray.py | 2 +- tests/data_testcase_schemas.py | 4 +- .../integration/equivalence_testing_utils.py | 31 ++++---- .../ex_ncdata_netcdf_conversion.py | 6 +- .../test_iris_load_and_save_equivalence.py | 2 +- .../test_iris_xarray_roundtrips.py | 8 +- tests/integration/test_netcdf_roundtrips.py | 4 +- tests/test_samplecode_cdlgen_comparablecdl.py | 5 +- tests/unit/core/test_AttrvalsDict.py | 8 +- tests/unit/core/test_NameMap.py | 23 +++--- tests/unit/core/test_NcAttribute.py | 8 +- tests/unit/tests/test_version_tuple.py | 4 +- .../test_dataset_differences__additional.py | 20 +++-- ...test_dataset_differences__mainfunctions.py | 77 ++++++++++++------- .../test_variable_differences.py | 40 ++++++---- tests/unit/utils/test_ncdata_copy.py | 20 ++--- tests/unit/utils/test_rename_dimension.py | 2 +- tests/unit/utils/test_save_errors.py | 6 +- tests/unit/xarray/test_from_xarray.py | 8 +- 26 files changed, 186 insertions(+), 179 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index c3a3114..462eb70 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -20,7 +20,6 @@ from functools import wraps from typing import ( Any, - Optional, Union, ) @@ -70,11 +69,10 @@ def add(self, item): If the NameMap has a non-None 'item_type', the added item is type checked. """ - if self.item_type is not None: - if not isinstance(item, self.item_type): - raise TypeError( - f'Item expected to be of type {self.item_type} : got {item}."' - ) + if self.item_type is not None and not isinstance(item, self.item_type): + raise TypeError( + f'Item expected to be of type {self.item_type} : got {item}."' + ) if not hasattr(item, "name"): raise TypeError(f"Item has no '.name' property : {item}.") self[item.name] = item @@ -381,10 +379,10 @@ class NcData(_AttributeAccessMixin): def __init__( self, name: str | None = None, - dimensions: Mapping | Iterable = None, - variables: Mapping | Iterable = None, - attributes: Mapping | Iterable = None, - groups: Mapping | Iterable = None, + dimensions: Mapping | Iterable | None = None, + variables: Mapping | Iterable | None = None, + attributes: Mapping | Iterable | None = None, + groups: Mapping | Iterable | None = None, ): #: a group/dataset name (optional) self.name: str = name @@ -519,10 +517,7 @@ def __init__(self, name: str, size: int, unlimited: bool | None = None): self.name: str = name #: dimension size (current size, if unlimited) self.size: int = size # N.B. we retain the 'zero size means unlimited' - if size == 0: - unlimited = True - else: - unlimited = bool(unlimited) + unlimited = True if size == 0 else bool(unlimited) #: whether dimension is unlimited self.unlimited: bool = unlimited @@ -576,11 +571,9 @@ def __init__( # a quoted reference when it's not otherwise needed (and we don't want to # import it). # TODO: remove the flake8 annotation if this gets fixed. - data: Optional[ - Union[np.ndarray, "dask.array.array"] # noqa: F821 - ] = None, + data: Union[np.ndarray, "dask.array.array"] | None = None, dtype: np.dtype = None, - attributes: Mapping | Iterable = None, + attributes: Mapping | Iterable | None = None, group: "NcData" = None, ): """ diff --git a/lib/ncdata/dataset_like.py b/lib/ncdata/dataset_like.py index 4f71c76..b4e8d62 100644 --- a/lib/ncdata/dataset_like.py +++ b/lib/ncdata/dataset_like.py @@ -300,11 +300,10 @@ def _get_fillvalue(self): * for byte data, there is no netCDF default fill, so the result can be None. """ fv = self._ncdata.avals.get("_FillValue", None) - if fv is None: - if self.dtype.itemsize != 1: - # NOTE: single-byte types have NO default fill-value - dtype_code = self.dtype.str[1:] - fv = netCDF4.default_fillvals[dtype_code] + if fv is None and self.dtype.itemsize != 1: + # NOTE: single-byte types have NO default fill-value + dtype_code = self.dtype.str[1:] + fv = netCDF4.default_fillvals[dtype_code] return fv def _maskandscale_inner_to_apparent(self, array): diff --git a/lib/ncdata/netcdf4.py b/lib/ncdata/netcdf4.py index c741cc7..cca2dbf 100644 --- a/lib/ncdata/netcdf4.py +++ b/lib/ncdata/netcdf4.py @@ -47,10 +47,7 @@ def _to_nc4_group( for varname, var in ncdata.variables.items(): fillattr = "_FillValue" - if fillattr in var.avals: - fill_value = var.avals[fillattr] - else: - fill_value = None + fill_value = var.avals.get(fillattr, None) kwargs = var_kwargs.get(varname, {}) forbidden_keys = set(_Forbidden_Variable_Kwargs) & set(kwargs) @@ -163,7 +160,7 @@ def __setstate__(self, state): def to_nc4( ncdata: NcData, nc4_dataset_or_file: nc.Dataset | Path | str, - var_kwargs: dict[str, dict] = None, + var_kwargs: dict[str, dict] | None = None, ) -> None: """ Save NcData to a netCDF file. @@ -222,7 +219,7 @@ def _from_nc4_group(nc4ds: nc.Dataset | nc.Group, dim_chunks) -> NcData: parent_ds = nc4ds group_names_path = [] while parent_ds.parent is not None: - group_names_path = [parent_ds.name] + group_names_path + group_names_path = [parent_ds.name, *group_names_path] parent_ds = parent_ds.parent ncdata = NcData(name=nc4ds.name) @@ -283,7 +280,7 @@ def _from_nc4_group(nc4ds: nc.Dataset | nc.Group, dim_chunks) -> NcData: def from_nc4( nc4_dataset_or_file: nc.Dataset | nc.Group | Path | str, - dim_chunks: dict[str, int | str] = None, + dim_chunks: dict[str, int | str] | None = None, ) -> NcData: """ Load NcData from a :class:`netCDF4.Dataset` or netCDF file. diff --git a/lib/ncdata/threadlock_sharing.py b/lib/ncdata/threadlock_sharing.py index c526435..750dce4 100644 --- a/lib/ncdata/threadlock_sharing.py +++ b/lib/ncdata/threadlock_sharing.py @@ -108,7 +108,8 @@ def enable_lockshare(iris: bool = False, xarray: bool = False): patches = [mock.patch(_NCDATA_TARGET, new=XARRAY_LOCK)] else: - assert iris and xarray + assert iris + assert xarray # set both ncdata AND Iris to use the Xarray lock from xarray.backends.netCDF4_ import ( NETCDF4_PYTHON_LOCK as XARRAY_LOCK, diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index 56ac9b2..f20934b 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -126,15 +126,9 @@ def dataset_differences( ds1, ds2 = None, None try: # convert path-likes to netCDF4.Dataset - if ds1_was_path: - ds1 = nc.Dataset(dataset_or_path_1) - else: - ds1 = dataset_or_path_1 + ds1 = nc.Dataset(dataset_or_path_1) if ds1_was_path else dataset_or_path_1 - if ds2_was_path: - ds2 = nc.Dataset(dataset_or_path_2) - else: - ds2 = dataset_or_path_2 + ds2 = nc.Dataset(dataset_or_path_2) if ds2_was_path else dataset_or_path_2 # NOTE: Both ds1 and ds2 are now *either* NcData *or* netCDF4.Dataset # _isncdata() will be used to distinguish. @@ -337,7 +331,7 @@ def variable_differences( check_var_data: bool = True, show_n_first_different: int = 2, suppress_warnings: bool = False, - _group_id_string: str = None, + _group_id_string: str | None = None, ) -> list[str]: r""" Compare variables. @@ -382,10 +376,7 @@ def variable_differences( msg = f"'show_n_diffs' must be >=1 : got {show_n_first_different!r}." raise ValueError(msg) - if v1.name == v2.name: - varname = v1.name - else: - varname = f"{v1.name} / {v2.name}" + varname = v1.name if v1.name == v2.name else f"{v1.name} / {v2.name}" if _group_id_string: var_id_string = f'{_group_id_string} variable "{varname}"' @@ -441,10 +432,7 @@ def safe_varshape(var): # data values def _is_strtype(dt): - if dt is None: - result = False - else: - result = dt.kind in "SUb" + result = False if dt is None else dt.kind in "SUb" return result is_str, is_str2 = (_is_strtype(dt) for dt in (dtype, dtype2)) @@ -554,9 +542,8 @@ def _group_differences( """ errs = [] - if check_names: - if g1.name != g2.name: - errs.append(f"Datasets have different names: {g1.name!r} != {g2.name!r}.") + if check_names and g1.name != g2.name: + errs.append(f"Datasets have different names: {g1.name!r} != {g2.name!r}.") # Compare lists of dimension names dimnames, dimnames2 = [list(grp.dimensions.keys()) for grp in (g1, g2)] errs += _namelist_differences( diff --git a/lib/ncdata/utils/_dim_indexing.py b/lib/ncdata/utils/_dim_indexing.py index b4d5730..060ee5e 100644 --- a/lib/ncdata/utils/_dim_indexing.py +++ b/lib/ncdata/utils/_dim_indexing.py @@ -199,6 +199,6 @@ def __getitem__(self, keys) -> NcData: raise ValueError(msg) # NB too *few* keys is not a problem, since 'zip' truncates for us. - dim_kwargs = {name: key for name, key in zip(self.dim_names, keys)} + dim_kwargs = dict(zip(self.dim_names, keys, strict=False)) return index_by_dimensions(self.ncdata, **dim_kwargs) diff --git a/lib/ncdata/utils/_save_errors.py b/lib/ncdata/utils/_save_errors.py index 4b4b6b4..f0bbcc2 100644 --- a/lib/ncdata/utils/_save_errors.py +++ b/lib/ncdata/utils/_save_errors.py @@ -39,7 +39,7 @@ def _name_errors(element_container, id_string): return errors -_NETCDF_VALID_DTYPES = [np.dtype(key) for key in nc.default_fillvals.keys()] +_NETCDF_VALID_DTYPES = [np.dtype(key) for key in nc.default_fillvals] def _valid_attr_dtype(dtype): @@ -96,8 +96,8 @@ def _variable_errors( def _save_errors_inner( ncdata: NcData, - enclosing_dimensions: dict[str, int] = None, - group_path: str = None, + enclosing_dimensions: dict[str, int] | None = None, + group_path: str | None = None, ) -> list[str]: """ Scan dataset, with context allowing operation over inner groups. diff --git a/lib/ncdata/xarray.py b/lib/ncdata/xarray.py index 6c30b26..582336a 100644 --- a/lib/ncdata/xarray.py +++ b/lib/ncdata/xarray.py @@ -95,7 +95,7 @@ def store( if varname in self.ncdata.variables: raise ValueError(f'duplicate variable : "{varname}"') - for dim_name, size in zip(var.dims, var.shape): + for dim_name, size in zip(var.dims, var.shape, strict=True): if dim_name in self.ncdata.dimensions: if self.ncdata.dimensions[dim_name].size != size: raise ValueError( diff --git a/tests/data_testcase_schemas.py b/tests/data_testcase_schemas.py index 059ef34..fa7bdaf 100644 --- a/tests/data_testcase_schemas.py +++ b/tests/data_testcase_schemas.py @@ -51,7 +51,7 @@ def data_types(): # Get the dtype list from the netCDF default fill-values. _nc_dtypes = set(nc.default_fillvals.keys()) # Remove the numpy-only complex types -_nc_dtypes = set(typename for typename in _nc_dtypes if np.dtype(typename).kind != "c") +_nc_dtypes = {typename for typename in _nc_dtypes if np.dtype(typename).kind != "c"} # Also replace 'S1' with our own 'string' type marker _nc_dtypes.remove("S1") _nc_dtypes.add("string") @@ -84,7 +84,7 @@ def data_types(): def _write_nc4_dataset( spec: dict, ds: nc.Dataset, # or an inner Group - parent_spec: dict = None, + parent_spec: dict | None = None, ): """ Create a netcdf test file from a 'testcase spec'. diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index 047b0e0..b760cfc 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -86,8 +86,11 @@ def fix_arrays(a1, a2): # Fix matching of cube data arrays c1.data, c2.data = fix_arrays(*(cube.core_data() for cube in (c1, c2))) # Fix matching of all coords points + bounds - for co1, co2 in zip(c1.coords(), c2.coords()): - if isinstance(co1, iris.mesh.MeshCoord): + for co1, co2 in zip(c1.coords(), c2.coords(), strict=False): + if ( + isinstance(co1, iris.mesh.MeshCoord) + or isinstance(co2, iris.mesh.MeshCoord) + ): # Can't write MeshCoords continue co1.points, co2.points = fix_arrays( @@ -223,12 +226,12 @@ def prune_cube_varproperties(cube_or_cubes): names = set() for cube in cube_or_cubes: - components = ( - [cube] - + list(cube.coords()) - + list(cube.cell_measures()) - + list(cube.ancillary_variables()) - ) + components = [ + cube, + *list(cube.coords()), + *list(cube.cell_measures()), + *list(cube.ancillary_variables()), + ] for comp in components: names |= prune_attrs_varproperties(comp.attributes) @@ -262,12 +265,12 @@ def remove_cube_nounits(cube_or_cubes): cube_or_cubes = [cube_or_cubes] for cube in cube_or_cubes: - components = ( - [cube] - + list(cube.coords()) - + list(cube.cell_measures()) - + list(cube.ancillary_variables()) - ) + components = [ + cube, + *list(cube.coords()), + *list(cube.cell_measures()), + *list(cube.ancillary_variables()), + ] for comp in components: remove_element_nounits(comp) diff --git a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py index 1171a7b..e75a822 100644 --- a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py +++ b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py @@ -95,8 +95,10 @@ def example_nc4_save_reload_unlimited_roundtrip(): assert list(ds_back.dimensions.keys()) == ["dim_x", "dim_y"] dimx, dimy = [ds_back.dimensions[name] for name in ("dim_x", "dim_y")] - assert dimx.size == len_x and dimx.unlimited - assert dimy.size == len_y and not dimy.unlimited + assert dimx.size == len_x + assert dimx.unlimited + assert dimy.size == len_y + assert not dimy.unlimited assert list(ds_back.variables.keys()) == ["var_x"] varx = ds_back.variables["var_x"] diff --git a/tests/integration/test_iris_load_and_save_equivalence.py b/tests/integration/test_iris_load_and_save_equivalence.py index 39cbc43..1dab67d 100644 --- a/tests/integration/test_iris_load_and_save_equivalence.py +++ b/tests/integration/test_iris_load_and_save_equivalence.py @@ -100,7 +100,7 @@ def test_load_direct_vs_viancdata(standard_testcase, use_irislock, adjust_chunks results = [ (c1.name(), cubes_equal__corrected(c1, c2)) - for c1, c2 in zip(iris_cubes, iris_ncdata_cubes) + for c1, c2 in zip(iris_cubes, iris_ncdata_cubes, strict=False) ] expected = [(cube.name(), True) for cube in iris_cubes] result = results == expected diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 53bbded..630b840 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -126,9 +126,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): # _PRUNE_CFVAR_PROPNAMES = False if _PRUNE_CFVAR_PROPNAMES: removed_props = prune_cube_varproperties(iris_cubes) - assert removed_props == set() or removed_props == { - "name", - } + assert removed_props in (set(), {"name"}) # Remove any 'no_units' units, since these do not save correctly # see : https://github.com/SciTools/iris/issues/5368 @@ -162,7 +160,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): _UNIFY_TIME_UNITS = True # _UNIFY_TIME_UNITS = False if _UNIFY_TIME_UNITS: - for iris_cube, iris_xr_cube in zip(iris_cubes, iris_xr_cubes): + for iris_cube, iris_xr_cube in zip(iris_cubes, iris_xr_cubes, strict=False): for xr_coord in iris_xr_cube.coords(): iris_coords = iris_cube.coords(xr_coord.name()) if len(iris_coords) != 1: @@ -187,7 +185,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): results = [ (c1.name(), cubes_equal__corrected(c1, c2)) - for c1, c2 in zip(iris_cubes, iris_xr_cubes) + for c1, c2 in zip(iris_cubes, iris_xr_cubes, strict=False) ] expected = [(cube.name(), True) for cube in iris_cubes] result = results == expected diff --git a/tests/integration/test_netcdf_roundtrips.py b/tests/integration/test_netcdf_roundtrips.py index fa1dc83..38a23ff 100644 --- a/tests/integration/test_netcdf_roundtrips.py +++ b/tests/integration/test_netcdf_roundtrips.py @@ -2,9 +2,7 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Test ncdata.netcdf by checking load-save roundtrips for standard testcases. -""" +"""Test ncdata.netcdf by checking load-save roundtrips for standard testcases.""" from subprocess import check_output diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index e6ceb81..79aa3b0 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -22,7 +22,7 @@ # https://github.com/SciTools/iris/blob/v3.4.1/lib/iris/tests/stock/netcdf.py#L18-L63 -def _env_bin_path(exe_name: AnyStr = None): +def _env_bin_path(exe_name: AnyStr | None = None): """ Return a Path object for (an executable in) the environment bin directory. @@ -159,7 +159,8 @@ def comparable_cdl(text: str) -> list[str]: ] # Also exclude the first line, which includes the filename hdr, lines = lines[0], lines[1:] - assert hdr.startswith("netcdf") and hdr[-1] == "{" + assert hdr.startswith("netcdf") + assert hdr[-1] == "{" return lines diff --git a/tests/unit/core/test_AttrvalsDict.py b/tests/unit/core/test_AttrvalsDict.py index 7b7c076..0084153 100644 --- a/tests/unit/core/test_AttrvalsDict.py +++ b/tests/unit/core/test_AttrvalsDict.py @@ -40,16 +40,16 @@ def setup(self): @pytest.fixture(params=_alltype_opts) def itemtype(request): - yield request.param + return request.param @pytest.fixture(params=_alltype_opts) def value_oftype(request): - yield _testitem[request.param] + return _testitem[request.param] class Test_setitem(MixinEquivTest): - """Testing equivalence to ".attributes" """ + """Testing equivalence to ".attributes".""" def test_types(self, value_oftype): a, b = self.vars_a_b @@ -83,7 +83,7 @@ def test_preexist(self): class Test_getitems(MixinEquivTest): - """Testing equivalence to ".attributes" """ + """Testing equivalence to ".attributes".""" def test_getitem(self, value_oftype): v = self.var diff --git a/tests/unit/core/test_NameMap.py b/tests/unit/core/test_NameMap.py index 0740448..43310aa 100644 --- a/tests/unit/core/test_NameMap.py +++ b/tests/unit/core/test_NameMap.py @@ -2,9 +2,7 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Tests for class :class:`ncdata.NameMap`. -""" +"""Tests for class :class:`ncdata.NameMap`.""" from copy import deepcopy @@ -65,7 +63,7 @@ def test_item_pairs(self, item_type_and_kwargs): assert namemap.item_type == item_type assert len(namemap) == len(_test_items) assert list(namemap.keys()) == [item.name for item in _test_items] - assert all(namemap[key].name == key for key in namemap.keys()) + assert all(namemap[key].name == key for key in namemap) def test_nameless_items(self): """Check we can construct like a "normal" dictionary when item_type is None.""" @@ -92,18 +90,18 @@ def test_list_arg(self, item_type_and_kwargs): assert namemap.item_type == item_type assert len(namemap) == len(_test_items) assert list(namemap.keys()) == [item.name for item in _test_items] - assert all(namemap[key].name == key for key in namemap.keys()) + assert all(namemap[key].name == key for key in namemap) def test_map_arg(self, item_type_and_kwargs): item_type, kwargs = item_type_and_kwargs # Construct a dictionary, with *arbitrary* keys (they are ignored). - arg = {count: item for count, item in enumerate(_test_items)} + arg = dict(enumerate(_test_items)) namemap = NameMap.from_items(arg, **kwargs) assert isinstance(namemap, NameMap) assert namemap.item_type == item_type assert len(namemap) == len(_test_items) assert list(namemap.keys()) == [item.name for item in _test_items] - assert all(namemap[key].name == key for key in namemap.keys()) + assert all(namemap[key].name == key for key in namemap) def test_items_typemismatch(self, item_type_and_kwargs): item_type, kwargs = item_type_and_kwargs @@ -229,7 +227,8 @@ def test_attributes__namevaluemap(self, target_itemtype, multiple): def test_attributes_empty(self, arg): # Just check correct construction from empty args. namemap = NameMap.from_items(arg, item_type=NcAttribute) - assert namemap == {} and namemap is not arg + assert namemap == {} + assert namemap is not arg class Test_copy: @@ -240,7 +239,9 @@ def test_copy(self, item_type): assert result == source assert result.item_type == item_type # NOTE: not a *deep* copy : contains the same item objects - assert all(e1 is e2 for e1, e2 in zip(result.values(), source.values())) + assert all( + e1 is e2 for e1, e2 in zip(result.values(), source.values(), strict=True) + ) class Test_add: @@ -248,7 +249,7 @@ def test_sametyped_item(self, item_type): source = sample_namemap(item_type=item_type) testmap = source.copy() testmap.add(NamedItem("qq")) - assert set(testmap) == set(["qq"]) | set(source) + assert set(testmap) == {"qq"} | set(source) def test_differenttype_item(self, item_type): testmap = sample_namemap(item_type=item_type) @@ -288,7 +289,7 @@ def test_overwrites(self): testmap.addall(new_items) assert list(testmap.keys()) == ["a", "b", "c", "d", "x"] assert set(testmap.values()) == ( - (set(source.values()) - set([source["b"]])) | set(new_items) + (set(source.values()) - {source["b"]}) | set(new_items) ) diff --git a/tests/unit/core/test_NcAttribute.py b/tests/unit/core/test_NcAttribute.py index c5d8538..9427c91 100644 --- a/tests/unit/core/test_NcAttribute.py +++ b/tests/unit/core/test_NcAttribute.py @@ -166,11 +166,9 @@ def test_value(self, datatype, structuretype): assert result == attr assert result.name == attr.name assert result.value is not attr.value - assert ( - result.value.dtype == attr.value.dtype - and result.value.shape == attr.value.shape - and np.all(result.value == attr.value) - ) + assert result.value.dtype == attr.value.dtype + assert result.value.shape == attr.value.shape + assert np.all(result.value == attr.value) class Test_NcAttribute__eq__: diff --git a/tests/unit/tests/test_version_tuple.py b/tests/unit/tests/test_version_tuple.py index 2ac5a60..6068ce6 100644 --- a/tests/unit/tests/test_version_tuple.py +++ b/tests/unit/tests/test_version_tuple.py @@ -2,9 +2,7 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Tests for the test utility :func:`tests.version_majorminor`. -""" +"""Tests for the test utility :func:`tests.version_majorminor`.""" import pytest diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py index c0a4a98..d965c85 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__additional.py @@ -8,7 +8,7 @@ * HERE: "additional" tests cover subsidiary routines and the main API usage modes. * ( ALSO: "mainfunctions" (q.v.) cover the core functionality - -- which elements are compared and what errors this constructs. ) + -- which elements are compared and what errors this constructs. ). """ import shutil @@ -223,8 +223,10 @@ def test_compare_attributes_values__dtype_and_data_mismatch(self): obj2 = self.Nc4ObjectWithAttrsMimic(a=np.float64(1)) errs = _attribute_differences(obj1, obj2, "") assert errs == [ - ' "a" attribute datatypes differ : ' - "dtype('float32') != dtype('float64')" + ( + ' "a" attribute datatypes differ : ' + "dtype('float32') != dtype('float64')" + ) ] def test_compare_attributes_values__data_arrays_match(self): @@ -300,8 +302,10 @@ def test_compare_attributes_values__string_nonstring(self): obj2 = self.Nc4ObjectWithAttrsMimic(a="1") errs = _attribute_differences(obj1, obj2, "") assert errs == [ - ' "a" attribute datatypes differ : ' - "dtype('int64') != " + ( + ' "a" attribute datatypes differ : ' + "dtype('int64') != " + ) ] def test_compare_attributes_values__string_match(self): @@ -333,8 +337,10 @@ def test_compare_attributes_values__string_array_mismatch(self): obj2 = self.Nc4ObjectWithAttrsMimic(S=["a", "c"]) errs = _attribute_differences(obj1, obj2, "") assert errs == [ - ' "S" attribute values differ : ' - "['a', 'b'] != ['a', 'c']" + ( + ' "S" attribute values differ : ' + "['a', 'b'] != ['a', 'c']" + ) ] def test_compare_attributes__ncdata_string_scalar_array(self): diff --git a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py index ed4608f..60e674e 100644 --- a/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py +++ b/tests/unit/utils/compare_nc_datasets/test_dataset_differences__mainfunctions.py @@ -8,7 +8,7 @@ * HERE: "mainfunctions" cover the core functionality -- which elements are compared and what errors this constructs. * ( ALSO: "additional" tests (q.v.) cover subsidiary routines and the - main API usage modes. ) + main API usage modes. ). """ import numpy as np @@ -24,7 +24,7 @@ @pytest.fixture(params=["in_named", "in_unnamed", "in_namedgroup", "in_unnamedgroup"]) def group_context(request): """ - The different contexts of locations in a dataset + The different contexts of locations in a dataset. In which an element (dimension, group or variable) might be found, and which might appear different in the mismatch-error messages. @@ -136,8 +136,10 @@ def test_name(self): self.dims.rename("x", "q") errs = dataset_differences(self.data1, self.data2) expected = [ - f"{self.location_string} dimension lists do not match: " - "['x', 'y'] != ['q', 'y']" + ( + f"{self.location_string} dimension lists do not match: " + "['x', 'y'] != ['q', 'y']" + ) ] check(errs, expected) @@ -164,9 +166,11 @@ def test_unlimited(self, check_unlim): if do_check_unlims: expected = [ - f'{self.location_string} "y" dimension ' - 'has different "unlimited" status : ' - "False != True" + ( + f'{self.location_string} "y" dimension ' + 'has different "unlimited" status : ' + "False != True" + ) ] else: expected = [] @@ -185,8 +189,10 @@ def test_ordering(self, order_checking): if do_ordercheck: expected = [ - f"{self.location_string} dimension lists do not match: " - "['x', 'y'] != ['y', 'x']" + ( + f"{self.location_string} dimension lists do not match: " + "['x', 'y'] != ['y', 'x']" + ) ] else: expected = [] @@ -251,8 +257,10 @@ def test_name(self): errs = dataset_differences(self.data1, self.data2) expected = [ - f"{self.location_string} attribute lists do not match: " - "['att1', 'att2'] != ['changed', 'att2']" + ( + f"{self.location_string} attribute lists do not match: " + "['att1', 'att2'] != ['changed', 'att2']" + ) ] check(errs, expected) @@ -261,13 +269,12 @@ def test_value(self, attr_context): errs = dataset_differences(self.data1, self.data2) - if "variable" in attr_context: - value_string = "1" - else: - value_string = "11" + value_string = "1" if "variable" in attr_context else "11" expected = [ - f'{self.location_string} "att1" attribute values differ : ' - f"{value_string} != 999" + ( + f'{self.location_string} "att1" attribute values differ : ' + f"{value_string} != 999" + ) ] check(errs, expected) @@ -283,8 +290,10 @@ def test_ordering(self, order_checking): if do_ordercheck: expected = [ - f"{self.location_string} attribute lists do not match: " - "['att1', 'att2'] != ['att2', 'att1']" + ( + f"{self.location_string} attribute lists do not match: " + "['att1', 'att2'] != ['att2', 'att1']" + ) ] else: expected = [] @@ -299,8 +308,10 @@ def test_extra_or_missing(self, order_checking): ) expected = [ - f"{self.location_string} attribute lists do not match: " - "['att1', 'att2'] != ['att2']" + ( + f"{self.location_string} attribute lists do not match: " + "['att1', 'att2'] != ['att2']" + ) ] check(errs, expected) @@ -321,8 +332,10 @@ def test_fillvalue_anyorder(self, attname): if "generic" in attname: expected = [ - 'Dataset variable "vx" attribute lists do not match: ' - "['anyold', 'x'] != ['x', 'anyold']" + ( + 'Dataset variable "vx" attribute lists do not match: ' + "['anyold', 'x'] != ['x', 'anyold']" + ) ] else: expected = [] @@ -374,8 +387,10 @@ def test_var_names(self): errs = dataset_differences(self.data1, self.data2) expected = [ - f"{self.location_string} variable lists do not match: " - "['v1', 'v2'] != ['v1', 'q']" + ( + f"{self.location_string} variable lists do not match: " + "['v1', 'v2'] != ['v1', 'q']" + ) ] check(errs, expected) @@ -391,8 +406,10 @@ def test_var_order(self, order_checking): if do_ordercheck: expected = [ - f"{self.location_string} variable lists do not match: " - "['v1', 'v2'] != ['v2', 'v1']" + ( + f"{self.location_string} variable lists do not match: " + "['v1', 'v2'] != ['v2', 'v1']" + ) ] else: expected = [] @@ -407,8 +424,10 @@ def test_vars_extra_or_missing(self, order_checking): ) expected = [ - f"{self.location_string} variable lists do not match: " - "['v1', 'v2'] != ['v2']" + ( + f"{self.location_string} variable lists do not match: " + "['v1', 'v2'] != ['v2']" + ) ] check(errs, expected) diff --git a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py index a9af118..84e8581 100644 --- a/tests/unit/utils/compare_nc_datasets/test_variable_differences.py +++ b/tests/unit/utils/compare_nc_datasets/test_variable_differences.py @@ -328,8 +328,10 @@ def test_masked(self, datavalues, masks): expected = [] else: expected = [ - 'Variable "v1" data contents differ, at 1 points: ' - "@INDICES[(1,)] : LHS=[1.0], RHS=[masked]" + ( + 'Variable "v1" data contents differ, at 1 points: ' + "@INDICES[(1,)] : LHS=[1.0], RHS=[masked]" + ) ] check(errs, expected) @@ -344,8 +346,10 @@ def test_nans(self, nans): expected = [] else: expected = [ - 'Variable "v1" data contents differ, at 1 points: ' - "@INDICES[(1,)] : LHS=[1.0], RHS=[nan]" + ( + 'Variable "v1" data contents differ, at 1 points: ' + "@INDICES[(1,)] : LHS=[1.0], RHS=[nan]" + ) ] check(errs, expected) @@ -356,8 +360,10 @@ def test_scalar(self): var.data = np.array(value, dtype=var.dtype) errs = variable_differences(self.var1, self.var2) expected = [ - 'Variable "v1" data contents differ, at 1 points: ' - "@INDICES[(0,)] : LHS=[0.0], RHS=[1.0]" + ( + 'Variable "v1" data contents differ, at 1 points: ' + "@INDICES[(0,)] : LHS=[0.0], RHS=[1.0]" + ) ] check(errs, expected) @@ -367,15 +373,17 @@ def test_real_and_lazy(self, argtypes): # fix the testvar to create a difference self.var2.data[1:2] += 1 # setup vars with lazy/real data arrays - for arraytype, var in zip([type1, type2], [self.var1, self.var2]): + for arraytype, var in [(type1, self.var1), (type2, self.var2)]: if arraytype == "lazy": var.data = da.from_array(var.data, chunks=-1) # compare + check results errs = variable_differences(self.var1, self.var2) # N.B. the result should be the same in all cases expected = [ - 'Variable "v1" data contents differ, at 1 points: ' - "@INDICES[(1,)] : LHS=[1.0], RHS=[2.0]" + ( + 'Variable "v1" data contents differ, at 1 points: ' + "@INDICES[(1,)] : LHS=[1.0], RHS=[2.0]" + ) ] check(errs, expected) @@ -404,13 +412,17 @@ def test_string_data(self, ndiffs): expected = [] if ndiffs == 1: expected = [ - 'Variable "vx" data contents differ, at 1 points: ' - "@INDICES[(1, 1)] : LHS=[b'h'], RHS=[b'X']" + ( + 'Variable "vx" data contents differ, at 1 points: ' + "@INDICES[(1, 1)] : LHS=[b'h'], RHS=[b'X']" + ) ] elif ndiffs == 2: expected = [ - 'Variable "vx" data contents differ, at 7 points: ' - "@INDICES[(1, 1), (3, 3), ...] : " - "LHS=[b'h', b'e', ...], RHS=[b'X', b'', ...]" + ( + 'Variable "vx" data contents differ, at 7 points: ' + "@INDICES[(1, 1), (3, 3), ...] : " + "LHS=[b'h', b'e', ...], RHS=[b'X', b'', ...]" + ) ] check(errs, expected) diff --git a/tests/unit/utils/test_ncdata_copy.py b/tests/unit/utils/test_ncdata_copy.py index e1daed2..4ef7208 100644 --- a/tests/unit/utils/test_ncdata_copy.py +++ b/tests/unit/utils/test_ncdata_copy.py @@ -18,22 +18,24 @@ def _ncdata_duplicate_object(d1: NcData, d2: NcData): """Find and return the first known duplicate objects between two NcData.""" dup = None - for var1, var2 in zip(d1.variables.values(), d2.variables.values()): + for var1, var2 in zip(d1.variables.values(), d2.variables.values(), strict=True): if var1 is var2: dup = var1 break if not dup: - for dim1, dim2 in zip(d1.dimensions.values(), d2.dimensions.values()): + for dim1, dim2 in zip( + d1.dimensions.values(), d2.dimensions.values(), strict=True + ): if dim1 is dim2: dup = dim1 break if not dup: - for attr1, attr2 in zip(d1.avals.values(), d2.avals.values()): + for attr1, attr2 in zip(d1.avals.values(), d2.avals.values(), strict=True): if attr1 is attr2: dup = attr1 break if not dup: - for grp1, grp2 in zip(d1.groups.values(), d2.groups.values()): + for grp1, grp2 in zip(d1.groups.values(), d2.groups.values(), strict=True): if grp1 is grp2: dup = grp1 break @@ -97,12 +99,10 @@ def test_sample_attribute_arraydata(self, sample): result = ncdata_copy(sample) rva = result.variables["a"] - assert ( - result.attributes["extra"].value is not sample.attributes["extra"].value - ) and np.all( + assert result.attributes["extra"].value is not sample.attributes["extra"].value + assert np.all( result.attributes["extra"].value == sample.attributes["extra"].value ) - assert ( - rva.attributes["xx2"].value is not sva.attributes["xx2"].value - ) and np.all(rva.attributes["xx2"].value == sva.attributes["xx2"].value) + assert rva.attributes["xx2"].value is not sva.attributes["xx2"].value + assert np.all(rva.attributes["xx2"].value == sva.attributes["xx2"].value) diff --git a/tests/unit/utils/test_rename_dimension.py b/tests/unit/utils/test_rename_dimension.py index b9926a4..67df14d 100644 --- a/tests/unit/utils/test_rename_dimension.py +++ b/tests/unit/utils/test_rename_dimension.py @@ -143,7 +143,7 @@ def group_example(self, setup): ), ] ) - yield ncdata + return ncdata def test_groups(self, group_example): ncdata = group_example diff --git a/tests/unit/utils/test_save_errors.py b/tests/unit/utils/test_save_errors.py index 1811cb5..2088229 100644 --- a/tests/unit/utils/test_save_errors.py +++ b/tests/unit/utils/test_save_errors.py @@ -2,9 +2,7 @@ # # This file is part of SciTools/ncdata and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Tests for :mod:`ncdata.utils.save_errors` -""" +"""Tests for :mod:`ncdata.utils.save_errors`.""" import re @@ -248,7 +246,7 @@ def test_shape_mismatch(self): def test_missing_dims(self, extradims): ncdata = _basic_testdata() var = list(ncdata.variables.values())[0] - extradims = list(char for char in extradims) + extradims = list(extradims) var.dimensions = var.dimensions + tuple(extradims) errors = save_errors(ncdata) diff --git a/tests/unit/xarray/test_from_xarray.py b/tests/unit/xarray/test_from_xarray.py index c3c1a0c..78d05ed 100644 --- a/tests/unit/xarray/test_from_xarray.py +++ b/tests/unit/xarray/test_from_xarray.py @@ -40,9 +40,7 @@ def file_and_xarray_from_spec( def test_lazy_nocompute(tmp_path): - """ - Check that from_xarray converts lazy variable data without fetching. - """ + """Check that from_xarray converts lazy variable data without fetching.""" # Create a simple xarray dataset from a spec test_spec = { "dims": [dict(name="x", size=3)], @@ -79,9 +77,7 @@ def test_lazy_nocompute(tmp_path): def test_real_nocopy(tmp_path): - """ - Check that from_xarray converts real variable data directly without copying. - """ + """Check that from_xarray converts real variable data directly without copying.""" # Create a simple xarray dataset from a spec test_spec = { "dims": [dict(name="x", size=3)], From 9024cc1802e13d38e38416cd805bd7a08e615e38 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 2 Nov 2026 15:35:33 +0000 Subject: [PATCH 19/26] Fixes for various Ruff checks. --- lib/ncdata/_core.py | 25 ++++++++++++++++++++----- pyproject.toml | 8 ++++++++ tests/unit/core/test_NcData.py | 2 +- tests/unit/core/test_NcVariable.py | 2 +- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index 462eb70..05393db 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -23,7 +23,6 @@ Union, ) -import numpy import numpy as np @@ -284,7 +283,7 @@ def __str__(self): def rename(self, name: str, new_name: str): self._attrs.rename(name, new_name) - def copy(self, deepcopy=True): + def copy(self): """ Make a copy. @@ -504,6 +503,10 @@ def __eq__(self, other): result = dataset_differences(self, other) == [] return result + # Block hashability : NcData objects are mutable, so they cannot be hashed. + # N.B. this also avoids Ruff PLW1641 - q.v. + __hash__ = None # type: ignore + class NcDimension: """ @@ -544,6 +547,10 @@ def __eq__(self, other): and self.unlimited == other.unlimited ) + # Block hashability : NcData objects are mutable, so they cannot be hashed. + # N.B. this also avoids Ruff PLW1641 - q.v. + __hash__ = None # type: ignore + class NcVariable(_AttributeAccessMixin): """ @@ -591,10 +598,10 @@ def __init__( if not hasattr(data, "dtype"): data = np.asanyarray(data) dtype = data.dtype - #: variable datatype, as a numpy :class:`numpy.dtype` + #: variable datatype, as a numpy :class:`np.dtype` if dtype is not None and not isinstance(dtype, np.dtype): dtype = np.dtype(dtype) - self.dtype: numpy.dtype = dtype + self.dtype: np.dtype = dtype #: variable data (an array-like, typically a dask or numpy array) self.data = data # Supports lazy, and normally provides a dtype #: variable attributes @@ -666,6 +673,10 @@ def __eq__(self, other): result = variable_differences(self, other) == [] return result + # Block hashability : NcData objects are mutable, so they cannot be hashed. + # N.B. this also avoids Ruff PLW1641 - q.v. + __hash__ = None # type: ignore + class NcAttribute: """ @@ -725,7 +736,7 @@ def as_python_value(self): """ result = self.value # Attributes are either scalar or 1-D - if result.ndim >= 2: + if result.ndim >= 2: # noqa: PLR2004 raise ValueError("Attribute value should only be 0- or 1-dimensional.") if result.shape == (1,): @@ -782,3 +793,7 @@ def __eq__(self, other): v1.shape == v2.shape and v1.dtype == v2.dtype and np.all(v1 == v2) ) return result + + # Block hashability : NcData objects are mutable, so they cannot be hashed. + # N.B. this also avoids Ruff PLW1641 - q.v. + __hash__ = None # type: ignore diff --git a/pyproject.toml b/pyproject.toml index 3ec82a2..beaaead 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,6 +198,8 @@ ignore = [ "B028", # warn without stacklevel= [[probably not]] "FLY002", # use f-string instead of joins [[TRUST: readability]] "RUF015", # prefer 'next' over [0] [[readability]] + "N813", # Camelcase imported as lowercase [[ok for netCDF4]] + "E402", # module level import not at top of file [[TRUST: usages intended]] # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): "ERA001", # Found commented-out code @@ -236,6 +238,12 @@ max-complexity = 22 "D205", # 1 blank line required between summary line and description "D401", # 1 First line of docstring should be in imperative mood "N801", # Class name should use CapWords convention + "PLR2004", # Magic constants in comparison + "PLR09", # Various complexity warns: too many args, statements, branches... + "N802", # Function name should be lowercase + "N806", # Variable in function should be lowercase + "N813", # Argument name should be lowercase + "N999", # Argument name should be lowercase ] [tool.ruff.lint.pydocstyle] diff --git a/tests/unit/core/test_NcData.py b/tests/unit/core/test_NcData.py index eb04235..22715fc 100644 --- a/tests/unit/core/test_NcData.py +++ b/tests/unit/core/test_NcData.py @@ -80,7 +80,7 @@ def test(self, mocker): def test_self_equal(self, mocker): ds = NcData() called = mocker.patch("ncdata.utils.dataset_differences") - assert ds == ds + assert ds == ds # noqa: PLR0124 assert called.call_args_list == [] def test_badtype_nonequal(self, mocker): diff --git a/tests/unit/core/test_NcVariable.py b/tests/unit/core/test_NcVariable.py index 3f2d1c5..3f36a5f 100644 --- a/tests/unit/core/test_NcVariable.py +++ b/tests/unit/core/test_NcVariable.py @@ -269,7 +269,7 @@ def test_equal(self, mocker): # Check that == calls variable_differences var1, var2 = [NcVariable(name=name) for name in ("x", "y")] called = mocker.patch("ncdata.utils.variable_differences") - var1 == var2 + var1 == var2 # noqa: PLR0124 assert called.call_args_list == [mocker.call(var1, var2)] def test_self_equal(self, mocker): From e149c08c1add0c2147b65856c3dd81a7b18044c6 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 2 Nov 2026 15:47:45 +0000 Subject: [PATCH 20/26] Use common paths for ncdump/ncgen and avoid shell=True. --- tests/__init__.py | 35 ++++++++++++++++++ .../test_iris_load_and_save_equivalence.py | 10 ++--- .../test_iris_xarray_roundtrips.py | 4 +- tests/integration/test_netcdf_roundtrips.py | 3 +- tests/test_samplecode_cdlgen_comparablecdl.py | 37 +------------------ 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 6939a06..d5fe856 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,7 +4,42 @@ # See LICENSE in the root of the repository for full licensing details. """Tests for :mod:`ncdata`.""" +import os from pathlib import Path +from typing import AnyStr + + +# Note : `_env_bin_path` and the ncgen/ncdump path constants are shared across +# the tests package, so they live here rather than in an individual test module. +def _env_bin_path(exe_name: AnyStr | None = None): + """ + Return a Path object for (an executable in) the environment bin directory. + + Parameters + ---------- + exe_name : str + If set, the name of an executable to append to the path. + + Returns + ------- + exe_path : Path + A path to the bin directory, or an executable file within it. + + Notes + ----- + For use in tests which spawn commands which should call executables within + the Python environment, since many IDEs (Eclipse, PyCharm) don't + automatically include this location in $PATH (as opposed to $PYTHONPATH). + """ + exe_path = Path(os.__file__) + exe_path = (exe_path / "../../../bin").resolve() + if exe_name is not None: + exe_path = exe_path / exe_name + return exe_path + + +NCGEN_PATHSTR = str(_env_bin_path("ncgen")) +NCDUMP_PATHSTR = str(_env_bin_path("ncdump")) testdata_dir = Path(__file__).parent / "testdata" diff --git a/tests/integration/test_iris_load_and_save_equivalence.py b/tests/integration/test_iris_load_and_save_equivalence.py index 1dab67d..1eaf257 100644 --- a/tests/integration/test_iris_load_and_save_equivalence.py +++ b/tests/integration/test_iris_load_and_save_equivalence.py @@ -18,7 +18,7 @@ from ncdata.netcdf4 import from_nc4, to_nc4 from ncdata.utils import dataset_differences -from tests import version_tuple +from tests import NCDUMP_PATHSTR, version_tuple from tests.data_testcase_schemas import session_testdir, standard_testcase from tests.integration.equivalence_testing_utils import ( adjust_chunks, @@ -85,7 +85,7 @@ def test_load_direct_vs_viancdata(standard_testcase, use_irislock, adjust_chunks print("\nncdata =") print(ncdata) print("\nncdump =") - txt = check_output([f"ncdump {source_filepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() print(txt) # Load the testcase with Iris. @@ -160,13 +160,13 @@ def test_save_direct_vs_viancdata(standard_testcase, tmp_path): print("\nncdata =") print(ncdata) print("\nncdump ORIGINAL TESTCASE SOURCEFILE =") - txt = check_output([f"ncdump {source_filepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() print(txt) print("\nncdump DIRECT FROM IRIS =") - txt = check_output([f"ncdump {temp_iris_savepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(temp_iris_savepath)]).decode() print(txt) print("\nncdump VIA NCDATA =") - txt = check_output([f"ncdump {temp_ncdata_savepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(temp_ncdata_savepath)]).decode() print(txt) # Check equivalence diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 630b840..489bdc1 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -113,7 +113,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): print("spec =") print(standard_testcase.spec) print("\nncdump =") - txt = check_output([f"ncdump {source_filepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() print(txt) # Load the testcase with Iris. @@ -233,7 +233,7 @@ def test_roundtrip_xix(standard_testcase, use_irislock, adjust_chunks, tmp_path) print("spec =") print(standard_testcase.spec) print("\nncdump =") - txt = check_output([f"ncdump {source_filepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() print(txt) # Load the testcase with Xarray. diff --git a/tests/integration/test_netcdf_roundtrips.py b/tests/integration/test_netcdf_roundtrips.py index 38a23ff..2d7ca63 100644 --- a/tests/integration/test_netcdf_roundtrips.py +++ b/tests/integration/test_netcdf_roundtrips.py @@ -10,6 +10,7 @@ from ncdata.utils import dataset_differences from tests.data_testcase_schemas import session_testdir, standard_testcase +from tests import NCDUMP_PATHSTR # Avoid complaints that the imported fixtures are "unused" standard_testcase, session_testdir @@ -36,7 +37,7 @@ def test_basic(standard_testcase, tmp_path): print("\nncdata =") print(ncdata) print("\nncdump =") - txt = check_output([f"ncdump {intermediate_filepath}"], shell=True).decode() + txt = check_output([NCDUMP_PATHSTR, str(intermediate_filepath)]).decode() print(txt) # Check that the re-saved file matches the original diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index 79aa3b0..b759c6e 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -10,47 +10,12 @@ The status and usage of this are yet to be determined. """ -import os import subprocess from pathlib import Path -from typing import AnyStr import pytest -# Note : `_env_bin_path` and `ncgen_from_cdl` are taken from Iris test code. -# Code here duplicated from Iris v3.4.1 -# https://github.com/SciTools/iris/blob/v3.4.1/lib/iris/tests/stock/netcdf.py#L18-L63 - - -def _env_bin_path(exe_name: AnyStr | None = None): - """ - Return a Path object for (an executable in) the environment bin directory. - - Parameters - ---------- - exe_name : str - If set, the name of an executable to append to the path. - - Returns - ------- - exe_path : Path - A path to the bin directory, or an executable file within it. - - Notes - ----- - For use in tests which spawn commands which should call executables within - the Python environment, since many IDEs (Eclipse, PyCharm) don't - automatically include this location in $PATH (as opposed to $PYTHONPATH). - """ - exe_path = Path(os.__file__) - exe_path = (exe_path / "../../../bin").resolve() - if exe_name is not None: - exe_path = exe_path / exe_name - return exe_path - - -NCGEN_PATHSTR = str(_env_bin_path("ncgen")) -NCDUMP_PATHSTR = str(_env_bin_path("ncdump")) +from tests import NCGEN_PATHSTR, NCDUMP_PATHSTR def ncgen_from_cdl(cdl_str: str | None, cdl_path: str | None, nc_path: str): From c8dd8534ffd2c14c2265c85068e91300acfa06ce Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 5 Nov 2026 14:50:37 +0000 Subject: [PATCH 21/26] Changes to address various ruff warnings. --- .pre-commit-config.yaml | 8 ++----- lib/ncdata/_core.py | 13 ++++++------ lib/ncdata/dataset_like.py | 8 +++---- lib/ncdata/netcdf4.py | 14 ++++++------- lib/ncdata/threadlock_sharing.py | 8 +++---- lib/ncdata/utils/_compare_nc_datasets.py | 21 ++++++++++++------- lib/ncdata/xarray.py | 4 ++-- pyproject.toml | 6 +++++- .../integration/equivalence_testing_utils.py | 7 ++----- .../ex_ncdata_netcdf_conversion.py | 7 ++++--- .../test_iris_load_and_save_equivalence.py | 8 +++---- .../test_iris_xarray_roundtrips.py | 15 +++++++------ tests/integration/test_netcdf_roundtrips.py | 4 ++-- tests/test_samplecode_cdlgen_comparablecdl.py | 13 ++++++------ tests/unit/core/test_AttributeAccessMixin.py | 2 +- tests/unit/core/test_NcData.py | 12 ++++++----- tests/unit/core/test_NcVariable.py | 6 +++--- tests/unit/tests/__init__.py | 5 +++++ tests/unit/utils/test_save_errors.py | 2 +- 19 files changed, 84 insertions(+), 79 deletions(-) create mode 100644 tests/unit/tests/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8cbba13..ff05415 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,12 +13,8 @@ ci: # Alphabetised, for lack of a better order. files: | (?x)( - .github\/.*| - docs\/.+\.py| - lib\/.+\.py| - pyproject\.toml| - tests\/.+\.py| - tools\/.+\.py + \./.*\.py| + tests/.+\.py ) exclude: | diff --git a/lib/ncdata/_core.py b/lib/ncdata/_core.py index 05393db..f71d59f 100644 --- a/lib/ncdata/_core.py +++ b/lib/ncdata/_core.py @@ -402,7 +402,6 @@ def _print_content(self) -> str: class, so it isn't technically an abstract method). This "NcData._print_content()" is called recursively for groups. """ - global _STANDARD_INDENT # Define a header line (always a separate line). noname = "<'no-name'>" lines = [f" NcData: # Work out the shape of the variable. # It may refer to dimensions in enclosing groups. - group = parent_ds - dims_map = group.dimensions.copy() + nc4_group = parent_ds + dims_map = nc4_group.dimensions.copy() for group_name in group_names_path: # Inner groups take priority, of course : # their dimensions mask any of the same name in outer groups - group = group.groups[group_name] - dims_map.update(group.dimensions) + nc4_group = nc4_group.groups[group_name] + dims_map.update(nc4_group.dimensions) shape = tuple(dims_map[dimname].size for dimname in var.dimensions) @@ -270,10 +270,8 @@ def _from_nc4_group(nc4ds: nc.Dataset | nc.Group, dim_chunks) -> NcData: ncdata.avals[attrname] = nc4ds.getncattr(attrname) # And finally, groups -- by the magic of recursion ... - for group_name, group in nc4ds.groups.items(): - ncdata.groups[group_name] = _from_nc4_group( - nc4ds.groups[group_name], dim_chunks=dim_chunks - ) + for group_name, nc4_group in nc4ds.groups.items(): + ncdata.groups[group_name] = _from_nc4_group(nc4_group, dim_chunks=dim_chunks) return ncdata diff --git a/lib/ncdata/threadlock_sharing.py b/lib/ncdata/threadlock_sharing.py index 750dce4..23798de 100644 --- a/lib/ncdata/threadlock_sharing.py +++ b/lib/ncdata/threadlock_sharing.py @@ -90,7 +90,7 @@ def enable_lockshare(iris: bool = False, xarray: bool = False): lock as xarray. """ - global _SHARE_PATCHES + global _SHARE_PATCHES # noqa: PLW0603 # N.B. implement sharing *only if* none already exists if not _SHARE_PATCHES and (iris or xarray): if iris and not xarray: @@ -107,9 +107,7 @@ def enable_lockshare(iris: bool = False, xarray: bool = False): ) patches = [mock.patch(_NCDATA_TARGET, new=XARRAY_LOCK)] - else: - assert iris - assert xarray + else: # case (iris AND xarray) # set both ncdata AND Iris to use the Xarray lock from xarray.backends.netCDF4_ import ( NETCDF4_PYTHON_LOCK as XARRAY_LOCK, @@ -131,7 +129,7 @@ def disable_lockshare(): Does nothing if no lock share is in operation. """ - global _SHARE_PATCHES + global _SHARE_PATCHES # noqa: PLW0603 for patch in _SHARE_PATCHES: patch.stop() _SHARE_PATCHES = [] diff --git a/lib/ncdata/utils/_compare_nc_datasets.py b/lib/ncdata/utils/_compare_nc_datasets.py index f20934b..aafacec 100644 --- a/lib/ncdata/utils/_compare_nc_datasets.py +++ b/lib/ncdata/utils/_compare_nc_datasets.py @@ -34,7 +34,7 @@ def dataset_differences( check_var_data: bool = True, show_n_first_different: int = 2, suppress_warnings: bool = False, -) -> list[str]: +) -> list[str]: # noqa: PLR0913, PLR0917 r""" Compare two netcdf datasets. @@ -253,7 +253,7 @@ def _attribute_differences( attrs_order=True, suppress_warnings=False, force_first_attrnames=None, -) -> list[str]: +) -> list[str]: # noqa: PLR0913, PLR0917 """ Compare attribute name lists. @@ -332,7 +332,7 @@ def variable_differences( show_n_first_different: int = 2, suppress_warnings: bool = False, _group_id_string: str | None = None, -) -> list[str]: +) -> list[str]: # noqa: PLR0912, PLR0913, PLR0915, PLR0917 r""" Compare variables. @@ -409,7 +409,7 @@ def variable_differences( def safe_varshape(var): if _isncdata(var): # NcVariable passed - if var.data is None: + if var.data is None: # noqa: SIM108 # Allow for NcVariable.data to be empty shape = None else: @@ -447,7 +447,13 @@ def getdata(var): else: # expect var to be an actual netCDF4.Variable # (check for obscure property NOT provided by mimics) - assert hasattr(var, "use_nc_get_vars") + if not hasattr(var, "use_nc_get_vars"): + msg = ( + f"Expected var to be a netCDF4.Variable, or compatible. " + f"Got {var!r}, of type {type(var)}: " + "does not provide '.use_nc_get_vars()'." + ) + raise TypeError(msg) data = var[:] if data is None: @@ -457,7 +463,8 @@ def getdata(var): # Return 0D as 1D, as this makes results simpler to interpret. if data.ndim == 0: data = data.flatten() - assert data.shape == (1,) + # NOTE: data.shape == (1,) + return data data, data2 = (getdata(v) for v in (v1, v2)) @@ -534,7 +541,7 @@ def _group_differences( check_names: bool = False, check_unlimited: bool = True, show_n_diffs: int = 2, -) -> list[str]: +) -> list[str]: # noqa: PLR0913, PLR0917 """ Inner routine to compare either whole datasets or subgroups. diff --git a/lib/ncdata/xarray.py b/lib/ncdata/xarray.py index 582336a..b5abe87 100644 --- a/lib/ncdata/xarray.py +++ b/lib/ncdata/xarray.py @@ -71,8 +71,8 @@ def store( self, variables, attributes, - check_encoding_set=frozenset(), - writer=None, + check_encoding_set=frozenset(), # noqa: ARG002 + writer=None, # noqa: ARG002 unlimited_dims=None, ): """ diff --git a/pyproject.toml b/pyproject.toml index beaaead..b4c5e5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,7 +224,7 @@ known-local-folder = ["tests"] notice-rgx = "^[ #]*Copyright SciTools contributors" [tool.ruff.lint.mccabe] -max-complexity = 22 +max-complexity = 25 # TODO: reduce, and ignore in specific cases. [tool.ruff.lint.per-file-ignores] # All test scripts @@ -235,6 +235,7 @@ max-complexity = 22 "D101", # Missing docstring in public class "D102", # Missing docstring in public method "D103", # Missing docstring in public function + "D106", # Missing docstring in public nested class "D205", # 1 blank line required between summary line and description "D401", # 1 First line of docstring should be in imperative mood "N801", # Class name should use CapWords convention @@ -244,6 +245,9 @@ max-complexity = 22 "N806", # Variable in function should be lowercase "N813", # Argument name should be lowercase "N999", # Argument name should be lowercase + "B018", # useless expression [[used for explicit pytest fixture imports]] + "ARG001", # Unused function argument [[does not recognise explicitly imported pytest fixtures]] + "ARG002", # Unused method argument [[does not recognise explicitly imported pytest fixtures]] ] [tool.ruff.lint.pydocstyle] diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index b760cfc..7549c0f 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -87,10 +87,7 @@ def fix_arrays(a1, a2): c1.data, c2.data = fix_arrays(*(cube.core_data() for cube in (c1, c2))) # Fix matching of all coords points + bounds for co1, co2 in zip(c1.coords(), c2.coords(), strict=False): - if ( - isinstance(co1, iris.mesh.MeshCoord) - or isinstance(co2, iris.mesh.MeshCoord) - ): + if any(isinstance(co, iris.mesh.MeshCoord) for co in (co1, co2)): # Can't write MeshCoords continue co1.points, co2.points = fix_arrays( @@ -116,7 +113,7 @@ def fix_arrays(a1, a2): def set_tiny_chunks(on, size_spec="20Kib"): """Turn on and off the tiny-chunks effect of the 'adjust_chunks' fixture.""" - global _USE_TINY_CHUNKS, _CHUNKSIZE_SPEC + global _USE_TINY_CHUNKS, _CHUNKSIZE_SPEC # noqa: PLW0603 _CHUNKSIZE_SPEC = size_spec _USE_TINY_CHUNKS = on diff --git a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py index e75a822..c240eb3 100644 --- a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py +++ b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py @@ -11,6 +11,7 @@ import tempfile from pathlib import Path from shutil import rmtree +from subprocess import check_call import netCDF4 as nc import numpy as np @@ -71,10 +72,10 @@ def example_nc4_save_reload_unlimited_roundtrip(): filepath = tempdir_path / "temp_nc_2_unlim.nc" print("Saving to file ", filepath) to_nc4(ncdata, filepath) - import os - print("\n\nCreated output:") - os.system(f"ncdump -h {filepath!s}") + print("\n\nCreated output (ncdump):") + _NCDUMP_EXE_PATH = Path(nc.__file__).parents[4] / "bin" / "ncdump" + check_call([_NCDUMP_EXE_PATH, "-h", filepath]) # noqa: S603 print("\n") ds = nc.Dataset(filepath) diff --git a/tests/integration/test_iris_load_and_save_equivalence.py b/tests/integration/test_iris_load_and_save_equivalence.py index 1eaf257..a82f4ea 100644 --- a/tests/integration/test_iris_load_and_save_equivalence.py +++ b/tests/integration/test_iris_load_and_save_equivalence.py @@ -85,7 +85,7 @@ def test_load_direct_vs_viancdata(standard_testcase, use_irislock, adjust_chunks print("\nncdata =") print(ncdata) print("\nncdump =") - txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() # noqa: S603 print(txt) # Load the testcase with Iris. @@ -160,13 +160,13 @@ def test_save_direct_vs_viancdata(standard_testcase, tmp_path): print("\nncdata =") print(ncdata) print("\nncdump ORIGINAL TESTCASE SOURCEFILE =") - txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() # noqa: S603 print(txt) print("\nncdump DIRECT FROM IRIS =") - txt = check_output([NCDUMP_PATHSTR, str(temp_iris_savepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(temp_iris_savepath)]).decode() # noqa: S603 print(txt) print("\nncdump VIA NCDATA =") - txt = check_output([NCDUMP_PATHSTR, str(temp_ncdata_savepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(temp_ncdata_savepath)]).decode() # noqa: S603 print(txt) # Check equivalence diff --git a/tests/integration/test_iris_xarray_roundtrips.py b/tests/integration/test_iris_xarray_roundtrips.py index 489bdc1..30d1799 100644 --- a/tests/integration/test_iris_xarray_roundtrips.py +++ b/tests/integration/test_iris_xarray_roundtrips.py @@ -26,7 +26,7 @@ from ncdata.utils import dataset_differences from ncdata.xarray import from_xarray -from tests import version_tuple +from tests import NCDUMP_PATHSTR, version_tuple from tests.data_testcase_schemas import ( BAD_LOADSAVE_TESTCASES, session_testdir, @@ -113,7 +113,7 @@ def test_roundtrip_ixi(standard_testcase, use_irislock, adjust_chunks): print("spec =") print(standard_testcase.spec) print("\nncdump =") - txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() # noqa: S603 print(txt) # Load the testcase with Iris. @@ -233,7 +233,7 @@ def test_roundtrip_xix(standard_testcase, use_irislock, adjust_chunks, tmp_path) print("spec =") print(standard_testcase.spec) print("\nncdump =") - txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(source_filepath)]).decode() # noqa: S603 print(txt) # Load the testcase with Xarray. @@ -301,11 +301,10 @@ def test_roundtrip_xix(standard_testcase, use_irislock, adjust_chunks, tmp_path) mask |= data == fv data = da.ma.masked_array(data, mask=mask) var.data = data - if "calendar" in var.avals: - if var.avals["calendar"] == "gregorian": - # the calendar name 'gregorian' is now deprecated, - # so Iris replaces it. - var.avals["calendar"] = "standard" + if var.avals.get("calendar", "") == "gregorian": + # the calendar name 'gregorian' is now deprecated, + # so Iris replaces it. + var.avals["calendar"] = "standard" result = dataset_differences(ncds_xr, ncds_xr_iris) # , check_var_data=False) assert result == [] diff --git a/tests/integration/test_netcdf_roundtrips.py b/tests/integration/test_netcdf_roundtrips.py index 2d7ca63..ad455a1 100644 --- a/tests/integration/test_netcdf_roundtrips.py +++ b/tests/integration/test_netcdf_roundtrips.py @@ -9,8 +9,8 @@ from ncdata.netcdf4 import from_nc4, to_nc4 from ncdata.utils import dataset_differences -from tests.data_testcase_schemas import session_testdir, standard_testcase from tests import NCDUMP_PATHSTR +from tests.data_testcase_schemas import session_testdir, standard_testcase # Avoid complaints that the imported fixtures are "unused" standard_testcase, session_testdir @@ -37,7 +37,7 @@ def test_basic(standard_testcase, tmp_path): print("\nncdata =") print(ncdata) print("\nncdump =") - txt = check_output([NCDUMP_PATHSTR, str(intermediate_filepath)]).decode() + txt = check_output([NCDUMP_PATHSTR, str(intermediate_filepath)]).decode() # noqa: S603 print(txt) # Check that the re-saved file matches the original diff --git a/tests/test_samplecode_cdlgen_comparablecdl.py b/tests/test_samplecode_cdlgen_comparablecdl.py index b759c6e..9bd9bc0 100644 --- a/tests/test_samplecode_cdlgen_comparablecdl.py +++ b/tests/test_samplecode_cdlgen_comparablecdl.py @@ -11,11 +11,10 @@ """ import subprocess -from pathlib import Path import pytest -from tests import NCGEN_PATHSTR, NCDUMP_PATHSTR +from tests import NCDUMP_PATHSTR, NCGEN_PATHSTR def ncgen_from_cdl(cdl_str: str | None, cdl_path: str | None, nc_path: str): @@ -43,7 +42,7 @@ def ncgen_from_cdl(cdl_str: str | None, cdl_path: str | None, nc_path: str): """ if cdl_str and cdl_path: - with open(cdl_path, "w") as f_out: + with cdl_path.open("w") as f_out: f_out.write(cdl_str) if cdl_path: # Create netcdf from stored CDL file. @@ -56,7 +55,7 @@ def ncgen_from_cdl(cdl_str: str | None, cdl_path: str | None, nc_path: str): call_args = [NCGEN_PATHSTR, "-k3", "-o", nc_path] call_kwargs = dict(input=cdl_str, encoding="ascii") - subprocess.run(call_args, check=True, **call_kwargs) + subprocess.run(call_args, check=True, **call_kwargs) # noqa: S603 # CDL to create a reference file with "all" features included. @@ -135,9 +134,9 @@ def testdata_cdl(tmp_path_factory): cdl_path = tmpdir_path / "tmp_nccompare_test.cdl" nc_path = tmpdir_path / "tmp_nccompare_test.nc" ncgen_from_cdl(cdl_str=_base_cdl, cdl_path=cdl_path, nc_path=nc_path) - bytes = subprocess.check_output([NCDUMP_PATHSTR, "-h", nc_path]) - cdl_regen_text = bytes.decode() - return cdl_regen_text + cdl_bytes = subprocess.check_output([NCDUMP_PATHSTR, "-h", nc_path]) # noqa: S603 + cdl_text = cdl_bytes.decode() + return cdl_text def test_ncgen_from_cdl(testdata_cdl): diff --git a/tests/unit/core/test_AttributeAccessMixin.py b/tests/unit/core/test_AttributeAccessMixin.py index 2a30d7b..148bead 100644 --- a/tests/unit/core/test_AttributeAccessMixin.py +++ b/tests/unit/core/test_AttributeAccessMixin.py @@ -30,7 +30,7 @@ def test_gettattr(self, sample_object): def test_getattr_absent(self, sample_object): # Check that fetching a non-existent attribute returns None. - sample_object.get_attrval("q") is None + assert sample_object.get_attrval("q") is None def test_setattr(self, sample_object): content = np.array([1, 2]) diff --git a/tests/unit/core/test_NcData.py b/tests/unit/core/test_NcData.py index 22715fc..3d137b1 100644 --- a/tests/unit/core/test_NcData.py +++ b/tests/unit/core/test_NcData.py @@ -25,13 +25,14 @@ def test_noargs(self): def test_allargs(self): name = "data_name" dims = [NcDimension(f"dimname_{i}", i) for i in range(3)] - vars = [NcVariable(f"varname_{i}", (), int) for i in range(5)] + # Use "vars_" just to avoid Ruff A001 "shadows python builtin". + vars_ = [NcVariable(f"varname_{i}", (), int) for i in range(5)] attrs = {f"attrname_{i}": i for i in range(6)} grps = [NcData(f"groupname_{i}") for i in range(3)] sample1 = NcData( name=name, dimensions=dims, - variables=vars, + variables=vars_, attributes=attrs, groups=grps, ) @@ -39,14 +40,14 @@ def test_allargs(self): assert sample1.name is name assert list(sample1.dimensions.values()) == dims assert sample1.dimensions.item_type == NcDimension - assert list(sample1.variables.values()) == vars + assert list(sample1.variables.values()) == vars_ assert sample1.variables.item_type == NcVariable assert list(sample1.groups.values()) == grps assert sample1.groups.item_type == NcData assert sample1.avals == attrs # Also check construction with arguments alone (no keywords). - sample2 = NcData(name, dims, vars, attrs, grps) + sample2 = NcData(name, dims, vars_, attrs, grps) # result is a new object, but contents are references identical to the other assert sample2 is not sample1 for name in ("dimensions", "variables", "attributes", "groups"): @@ -74,13 +75,14 @@ class Test_NcData_eq: def test(self, mocker): ncdata1, ncdata2 = [NcData(name) for name in ("data1", "data2")] called = mocker.patch("ncdata.utils.dataset_differences") - ncdata1 == ncdata2 + ncdata1 == ncdata2 # noqa: B015 assert called.call_args_list == [mocker.call(ncdata1, ncdata2)] def test_self_equal(self, mocker): ds = NcData() called = mocker.patch("ncdata.utils.dataset_differences") assert ds == ds # noqa: PLR0124 + # Did NOT call difference assert called.call_args_list == [] def test_badtype_nonequal(self, mocker): diff --git a/tests/unit/core/test_NcVariable.py b/tests/unit/core/test_NcVariable.py index 3f36a5f..b1f2f11 100644 --- a/tests/unit/core/test_NcVariable.py +++ b/tests/unit/core/test_NcVariable.py @@ -100,7 +100,7 @@ def test_invalid_dtype__with_and_without_data(self, withdata_or_not): assert var.dtype == data.dtype else: # with no data, invalid dtype causes error - with pytest.raises(TypeError, match="data type.*not understood"): + with pytest.raises(TypeError, match=r"data type.*not understood"): _ = NcVariable("x", data=data, dtype=test_dtype_nonfunctional) @pytest.mark.parametrize( @@ -269,13 +269,13 @@ def test_equal(self, mocker): # Check that == calls variable_differences var1, var2 = [NcVariable(name=name) for name in ("x", "y")] called = mocker.patch("ncdata.utils.variable_differences") - var1 == var2 # noqa: PLR0124 + var1 == var2 # noqa: B015 assert called.call_args_list == [mocker.call(var1, var2)] def test_self_equal(self, mocker): var = NcVariable(name="x") called = mocker.patch("ncdata.utils.variable_differences") - assert var == var + assert var == var # noqa: PLR0124 assert called.call_args_list == [] def test_badtype_nonequal(self, mocker): diff --git a/tests/unit/tests/__init__.py b/tests/unit/tests/__init__.py new file mode 100644 index 0000000..efb8edb --- /dev/null +++ b/tests/unit/tests/__init__.py @@ -0,0 +1,5 @@ +# Copyright SciTools contributors +# +# This file is part of SciTools/ncdata and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Unit tests for :mod:`tests`.""" diff --git a/tests/unit/utils/test_save_errors.py b/tests/unit/utils/test_save_errors.py index 2088229..2ce875a 100644 --- a/tests/unit/utils/test_save_errors.py +++ b/tests/unit/utils/test_save_errors.py @@ -67,7 +67,7 @@ def component(self, request): @staticmethod def component_propname(component: str): """Return the property name corresponding to the component type.""" - if component == "attribute": + if component == "attribute": # noqa: SIM108 # Access the NameMap of NcAttributes (not the .attributes values map) propname = "attributes" else: From bda914c495d5e5d48f2b07f863467f95bdd042dd Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 5 Nov 2026 16:18:00 +0000 Subject: [PATCH 22/26] Control numpydoc-validation; temporarily disable repo-review; Trial adjustments for MyPy. --- pyproject.toml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b4c5e5e..fe5dba5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,9 @@ enable_error_code = [ "redundant-expr", "truthy-bool", ] +exclude = [ + '.*tests/unit/xarray.*', +] strict = true warn_unreachable = true @@ -135,6 +138,7 @@ exclude = [ '\.__ne__$', '\.__repr__$', ] +exclude_files = ["^tests/.*", "^lib/ncdata/_version.py$"] [tool.pytest.ini_options] # The list of keys vary by repository addopts = ["-ra", "-v", "--strict-config", "--strict-markers", "--doctest-modules"] # This addopts list varies by repository @@ -154,8 +158,14 @@ ignore = [ "PP006", # Uses dev dependency group # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): -] + # VERY TEMPORARY FOR-NOW + "PY007", + "PP003", + "GH102", + "GH212", + "PC170", +] [tool.ruff] extend-exclude = [ "docs/conf.py", # based on Sphix template, is too different to be worth checking. From 4f349f54753d0c7f3df3646e1d8e7debc9609935 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 5 Nov 2026 16:27:55 +0000 Subject: [PATCH 23/26] Exclude test sources from MyPy: must be in '.pre-commit-config.yaml' to work. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff05415..2cb5c61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -91,7 +91,7 @@ repos: rev: 'v2.4.0' hooks: - id: mypy - exclude: 'noxfile\.py|docs/conf\.py' + exclude: 'noxfile\.py|docs/conf\.py|^tests/' # NB controlling this in pyproject.toml DOES NOT WORK, as MyPy doesn't apply excludes to a passed list of (changed) files - repo: https://github.com/numpy/numpydoc # This template does not keep up-to-date with versions, visit the repo to see the most recent release. diff --git a/pyproject.toml b/pyproject.toml index fe5dba5..5281ed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,9 +87,6 @@ enable_error_code = [ "redundant-expr", "truthy-bool", ] -exclude = [ - '.*tests/unit/xarray.*', -] strict = true warn_unreachable = true From b4a849ed0713a6e7ccab8e6a09fd2dad7c1d903c Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 5 Nov 2026 17:28:07 +0000 Subject: [PATCH 24/26] Fix or exclude repo-review warnings. --- .github/dependabot.yml | 4 ++++ .github/workflows/ci-tests.yml | 4 ++++ .pre-commit-config.yaml | 6 ++++++ pyproject.toml | 7 +------ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c4ecb86..dac24ad 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,3 +15,7 @@ updates: labels: - "new: pull request" - "bot" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 0046f51..1f14364 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -18,6 +18,10 @@ on: - "*" workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tests: name: "Test session=${{ matrix.session }} Python=${{ matrix.python-version }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2cb5c61..f756b40 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -100,6 +100,12 @@ repos: - id: numpydoc-validation types: [file, python] +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + types: [rst] + - repo: https://github.com/scientific-python/cookie # This template does not keep up-to-date with versions, visit the repo to see the most recent release. rev: 2026.08.14 diff --git a/pyproject.toml b/pyproject.toml index 5281ed0..2175b84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ requires = [ "setuptools>=77.0.3", "setuptools_scm[toml]>=8", - "wheel", ] # Defined by PEP 517 build-backend = "setuptools.build_meta" @@ -157,11 +156,7 @@ ignore = [ # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): # VERY TEMPORARY FOR-NOW - "PY007", - "PP003", - "GH102", - "GH212", - "PC170", + "PY007", # should have a task runner (nox/tox etc). ] [tool.ruff] extend-exclude = [ From d2279fd4476c631cb7de3dce306648f22207c476 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 6 Nov 2026 17:36:04 +0000 Subject: [PATCH 25/26] tidy pyproject.toml --- pyproject.toml | 40 +++++++++---------- .../integration/equivalence_testing_utils.py | 2 +- .../example_scripts/ex_dataset_print.py | 2 +- .../ex_ncdata_netcdf_conversion.py | 2 +- tests/unit/iris/test_from_iris.py | 4 +- tests/unit/iris/test_to_iris.py | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2175b84..39933e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,10 +153,10 @@ ignore = [ # https://learn.scientific-python.org/development/guides/packaging-simple#PP006 "PP006", # Uses dev dependency group - # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): - - # VERY TEMPORARY FOR-NOW + # Project-specific. May be reviewed in future. "PY007", # should have a task runner (nox/tox etc). + + # TODO: exceptions that still need investigating are below. Might be fixable, or might become permanent (above): ] [tool.ruff] extend-exclude = [ @@ -215,12 +215,9 @@ select = [ ] [tool.ruff.lint.isort] -#known-first-party = ["ncdata"] +force-sort-within-sections = true +#known-first-party = ["ncdata"] # not needed, follows ruff 'src' setting known-local-folder = ["tests"] -# The identification of first-party modules is wrong here, and **different from isort**. -# It seems to consider these first-party because there is an 'ncdata.xarray' etc. -# Also that confuses it and it then treats all ncdata as **third-party**. -#known-third-party = ["iris", "xarray"] [tool.ruff.lint.flake8-copyright] notice-rgx = "^[ #]*Copyright SciTools contributors" @@ -232,24 +229,27 @@ max-complexity = 25 # TODO: reduce, and ignore in specific cases. # All test scripts "./tests/**/*.py" = [ # e.g "cf_units/tests" or "lib/iris/tests" # https://docs.astral.sh/ruff/rules - "S101", # Use of assert. - "D100", # Missing docstring in public module + # Generic ignores. "D101", # Missing docstring in public class + "D205", # 1 blank line required between summary line and description + "D401", # 1 First line of docstring should be in imperative mood + + # Project-specific + "ARG001", # Unused function argument [[does not recognise explicitly imported pytest fixtures]] + "ARG002", # Unused method argument [[does not recognise explicitly imported pytest fixtures]] + "B018", # useless expression [[used for explicit pytest fixture imports]] + "D100", # Missing docstring in public module "D102", # Missing docstring in public method "D103", # Missing docstring in public function "D106", # Missing docstring in public nested class - "D205", # 1 blank line required between summary line and description - "D401", # 1 First line of docstring should be in imperative mood "N801", # Class name should use CapWords convention - "PLR2004", # Magic constants in comparison - "PLR09", # Various complexity warns: too many args, statements, branches... "N802", # Function name should be lowercase "N806", # Variable in function should be lowercase "N813", # Argument name should be lowercase - "N999", # Argument name should be lowercase - "B018", # useless expression [[used for explicit pytest fixture imports]] - "ARG001", # Unused function argument [[does not recognise explicitly imported pytest fixtures]] - "ARG002", # Unused method argument [[does not recognise explicitly imported pytest fixtures]] + "N999", # Invalid module name (e.g. other 'test_xx' modules) + "PLR09", # Various complexity warns: too many args, statements, branches... + "PLR2004", # Magic constants in comparison + "S101", # Use of assert. ] [tool.ruff.lint.pydocstyle] @@ -273,11 +273,11 @@ version_scheme = "release-branch-semver" write_to = "lib/ncdata/_version.py" [tool.towncrier] -package = "ncdata" -package_dir = "lib" directory = "docs/changelog_fragments" filename = "docs/change_log.rst" issue_format = "`ISSUE#{issue} `_" +package = "ncdata" +package_dir = "lib" underlines = ["~", "^", "*", "+"] [[tool.towncrier.type]] diff --git a/tests/integration/equivalence_testing_utils.py b/tests/integration/equivalence_testing_utils.py index 7549c0f..6d59299 100644 --- a/tests/integration/equivalence_testing_utils.py +++ b/tests/integration/equivalence_testing_utils.py @@ -178,9 +178,9 @@ def nanmask_cube(cube): # Horrible code to list the properties of a netCDF4.Variable object # import inspect +from pathlib import Path import shutil import tempfile -from pathlib import Path import netCDF4 as nc diff --git a/tests/integration/example_scripts/ex_dataset_print.py b/tests/integration/example_scripts/ex_dataset_print.py index f72fb16..a5223bc 100644 --- a/tests/integration/example_scripts/ex_dataset_print.py +++ b/tests/integration/example_scripts/ex_dataset_print.py @@ -6,8 +6,8 @@ import iris -import ncdata.iris as nci from ncdata import NcData, NcDimension, NcVariable +import ncdata.iris as nci from tests import testdata_dir diff --git a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py index c240eb3..d8d2c03 100644 --- a/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py +++ b/tests/integration/example_scripts/ex_ncdata_netcdf_conversion.py @@ -8,10 +8,10 @@ Showing loading and saving ncdata to/from netcdf files. """ -import tempfile from pathlib import Path from shutil import rmtree from subprocess import check_call +import tempfile import netCDF4 as nc import numpy as np diff --git a/tests/unit/iris/test_from_iris.py b/tests/unit/iris/test_from_iris.py index fb647f8..6616cca 100644 --- a/tests/unit/iris/test_from_iris.py +++ b/tests/unit/iris/test_from_iris.py @@ -16,10 +16,10 @@ from unittest.mock import patch import dask.array as da -import numpy as np -import pytest from iris.coords import DimCoord from iris.cube import Cube +import numpy as np +import pytest from ncdata.iris import from_iris diff --git a/tests/unit/iris/test_to_iris.py b/tests/unit/iris/test_to_iris.py index 7da60af..b88b29b 100644 --- a/tests/unit/iris/test_to_iris.py +++ b/tests/unit/iris/test_to_iris.py @@ -14,9 +14,9 @@ """ import dask.array as da -import numpy as np from iris._constraints import NameConstraint from iris.cube import CubeList +import numpy as np from ncdata import NcData, NcDimension, NcVariable from ncdata.iris import to_iris From 29e9e9da905559218812589236021f494c1a1a73 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 6 Nov 2026 17:57:48 +0000 Subject: [PATCH 26/26] Apply pre-commit to .github and automation files. Apply pre-commit to .github and automation files. --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f756b40..9172fdb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,9 @@ ci: # Alphabetised, for lack of a better order. files: | (?x)( + \.github/\.*| + pyproject\.toml| + \.pre-commit-config\.yaml| \./.*\.py| tests/.+\.py )