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 aac3b800..4debbea0 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 99409a1e..ea5c3f4e 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 4debbea0..86bbb98c 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 86bbb98c..24cb5469 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 24cb5469..afee2456 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 843e2230..c9a45586 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 4d58410d..62842abe 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 b70a9288..268bfdc4 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 e1238308..5a61d618 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 de087171..c37a2a94 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 8f86d6fb..d4ead959 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 5670e8b8..229815f8 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 82125773..8e4a6878 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 521a2a52..101c2465 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 75545163..6da444c5 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 064b29ef..09982f39 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 afee2456..4e3d51d0 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 5a61d618..c16ac76d 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 c9a45586..bce3711a 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 62842abe..1b0e86c9 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 c16ac76d..07faef07 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 c37a2a94..443da40e 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 f3542438..b2e896cd 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 02d25dd2..e5a003ad 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 bce3711a..da62fea0 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 1b0e86c9..a58b0646 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 f20bbd32..225a3875 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 41982005..d5fa320c 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 268bfdc4..7d474128 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 a245fc3b..c526435b 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 f44fe956..ec5eb40b 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 07faef07..abeeb20a 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 7ae7c041..0e59acb4 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 443da40e..80563fc4 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 d4ead959..7779743e 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 229815f8..550f38c6 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 2a7306c1..7246e18b 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 9d8eccaf..d9b742b8 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 8e4a6878..8ef244fe 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 3b98f776..19a55c51 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 101c2465..ab961579 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 cb4b2fa1..b0157d94 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 02fbac85..ceffd074 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 361ff069..24f67d0d 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 0a193ef6..0b968a5b 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 3fde5bfd..7ee92aad 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 68a33f4d..e863b05c 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 922fe6d7..5253fa93 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 6da444c5..0d0f1df8 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 d5e136b2..2b331556 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 aa236dbc..5e60db34 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 9127ce4f..8b88b556 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 2245cffb..19ea1444 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 358a9a29..1fb04e5e 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 4269eb70..dbf44126 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 c3f243fd..2a30d7b3 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 7a929119..af0039f9 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 67e22b98..09122822 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 09982f39..9780a9d4 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 1e901e6c..0a7034eb 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 9f334da9..efca8d4f 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 e74c5bec..13b8aa08 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 a8e3fc0d..6a664c45 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 7dc8823c..e32b699e 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 e5308314..3d0cc596 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 80ee6af8..e6e8423f 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 d19703d6..1ad3939f 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 85c8589b..6acde501 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 dabdbec9..2ac5a605 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 7e82563f..2a13dd90 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 7f699aae..3b7b71c3 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 9969a134..4a482af2 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 9e0a2de0..0d7b211c 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 c9ca919e..0d47f820 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 7ad084b3..0618931a 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 4652cc23..5b8ff679 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 099b1d75..8b1c607d 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 aff9734b..bd0629e1 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 b7fabb07..19c26059 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 fff18534..aaaeea38 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 c1438090..afc35028 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 dd502e1b..e5c43b1e 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 592c9758..4b88a8f9 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 4e3d51d0..1f4c6f79 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 1f4c6f79..9640673c 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 ea5c3f4e..8cbba137 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 9640673c..bf033867 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 e5a003ad..98e48eab 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 7d474128..9ebc1802 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 da62fea0..e2b41193 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 a58b0646..b08e6366 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 225a3875..5a6d46a6 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 9ebc1802..f7a38d5f 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 abeeb20a..774afe3e 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 80563fc4..ad4fe0a6 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 550f38c6..d8802e55 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 7246e18b..9429d5d4 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 d9b742b8..ee75528f 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 8ef244fe..6922937f 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 ceffd074..f72fb160 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 24f67d0d..18c47ee4 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 0b968a5b..aee9031d 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 7ee92aad..1171a7b2 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 8b88b556..bbb27d71 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 19ea1444..a206614b 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 9780a9d4..2f6a22f4 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 0a7034eb..a2f13a9f 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 efca8d4f..c915ab49 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 13b8aa08..6d4e7010 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 3d0cc596..19a9bfac 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 6acde501..bcc18398 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 0618931a..9170a66c 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 5b8ff679..f03c56c6 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 8b1c607d..4c0529a0 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 bd0629e1..025f7e4f 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 19c26059..c4261117 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 4b88a8f9..f1ffa88d 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 e2b41193..c3a3114e 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 b08e6366..4f71c76d 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 f7a38d5f..c741cc7c 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 774afe3e..56ac9b24 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 ad4fe0a6..b4d57305 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 7779743e..e4a2259e 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 d8802e55..4b4b6b4e 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 9429d5d4..6c30b26e 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 ee75528f..6939a06c 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 6922937f..059ef340 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 ab961579..047b0e0c 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 aee9031d..d91ac711 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 5253fa93..39cbc43d 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 0d0f1df8..53bbded1 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 2b331556..fa1dc83a 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 5e60db34..378fcc16 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 bbb27d71..b2075876 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 a206614b..e6ceb81f 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 af0039f9..7b7c076a 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 09122822..0740448a 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 2f6a22f4..c5d85389 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 a2f13a9f..eb04235e 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 6d4e7010..3f2d1c53 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 e32b699e..fb647f8a 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 19a9bfac..7da60aff 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 1ad3939f..92ef3169 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 bcc18398..1a656c00 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 4a482af2..c0a4a98c 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 0d7b211c..ed4608fc 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 0d47f820..a9af118b 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 9170a66c..37b1cc68 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 4c0529a0..a1078144 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 025f7e4f..e1daed23 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 c4261117..b9926a41 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 aaaeea38..1811cb59 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 e5c43b1e..c3c1a0cd 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 f1ffa88d..78aec2dd 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 bf033867..3ec82a24 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 c3a3114e..462eb707 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 4f71c76d..b4e8d623 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 c741cc7c..cca2dbf4 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 c526435b..750dce42 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 56ac9b24..f20934be 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 b4d57305..060ee5ed 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 4b4b6b4e..f0bbcc2a 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 6c30b26e..582336a0 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 059ef340..fa7bdaf8 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 047b0e0c..b760cfc3 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 1171a7b2..e75a8229 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 39cbc43d..1dab67de 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 53bbded1..630b840b 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 fa1dc83a..38a23ff2 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 e6ceb81f..79aa3b03 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 7b7c076a..00841534 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 0740448a..43310aa7 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 c5d85389..9427c910 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 2ac5a605..6068ce6a 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 c0a4a98c..d965c854 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 ed4608fc..60e674e6 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 a9af118b..84e8581e 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 e1daed23..4ef72080 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 b9926a41..67df14db 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 1811cb59..20882290 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 c3c1a0cd..78d05ede 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 462eb707..05393db4 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 3ec82a24..beaaeade 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 eb04235e..22715fcb 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 3f2d1c53..3f36a5f1 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 6939a06c..d5fe8569 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 1dab67de..1eaf2579 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 630b840b..489bdc17 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 38a23ff2..2d7ca635 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 79aa3b03..b759c6e0 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 8cbba137..ff054154 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 05393db4..f71d59f5 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 750dce42..23798de9 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 f20934be..aafaceca 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 582336a0..b5abe87e 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 beaaeade..b4c5e5eb 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 b760cfc3..7549c0fc 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 e75a8229..c240eb3b 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 1eaf2579..a82f4eae 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 489bdc17..30d17994 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 2d7ca635..ad455a15 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 b759c6e0..9bd9bc0a 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 2a30d7b3..148beadf 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 22715fcb..3d137b1c 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 3f36a5f1..b1f2f110 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 00000000..efb8edb6 --- /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 20882290..2ce875ad 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 b4c5e5eb..fe5dba53 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 ff054154..2cb5c61b 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 fe5dba53..5281ed0a 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 c4ecb863..dac24ad9 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 0046f51e..1f14364e 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 2cb5c61b..f756b408 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 5281ed0a..2175b849 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 2175b849..39933e40 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 7549c0fc..6d59299b 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 f72fb160..a5223bce 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 c240eb3b..d8d2c032 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 fb647f8a..6616ccae 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 7da60aff..b88b29bc 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 f756b408..9172fdb3 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 )