From 68c040e9850a7d12a1923feff4a48dd720ce80a5 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 12 Nov 2024 13:59:28 +0000 Subject: [PATCH 01/33] Added extra pre-commit default hooks --- .pre-commit-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b709b7f..8b789213 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,8 +16,21 @@ repos: - 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 + - repo: https://github.com/aio-libs/sort-all rev: v1.3.0 From b0692213467772aac55d1a102c60001c7de7c866 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 12 Nov 2024 14:00:22 +0000 Subject: [PATCH 02/33] Added codespell pre-commit hook --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b789213..b229c099 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,3 +60,10 @@ repos: args: [--fix, --show-fixes] # Run the formatter. - id: ruff-format + +- repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + - id: codespell + types_or: [asciidoc, python, markdown, rst] + additional_dependencies: [tomli] From b46036f53eb1ce21e185ab786a1599763a9353e8 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Wed, 13 Nov 2024 12:24:13 +0000 Subject: [PATCH 03/33] Added mypy and configuration --- .pre-commit-config.yaml | 6 ++++++ pyproject.toml | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b229c099..cc392eec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,3 +67,9 @@ repos: - id: codespell types_or: [asciidoc, python, markdown, rst] additional_dependencies: [tomli] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.13.0' + hooks: + - id: mypy + exclude: 'docs|cf_units/_udunits2_parser' diff --git a/pyproject.toml b/pyproject.toml index 2b2ac024..cdf2733f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,3 +164,13 @@ ignore = [ "MY100", # Uses MyPy (pyproject config) "RF001", # Uses RUFF ] + +[tool.mypy] +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +exclude = [ + "docs", + "cf_units/_udunits2_parser", +] +ignore_missing_imports = true +strict = true +warn_unreachable = true From 6b8f31f61818c8847f7bdf93e55efa5ecc84a6a0 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 13:55:25 +0000 Subject: [PATCH 04/33] Added mypy exrts dependency --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc392eec..490de407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,4 +72,6 @@ repos: rev: 'v1.13.0' hooks: - id: mypy + additional_dependencies: + - 'types-requests' exclude: 'docs|cf_units/_udunits2_parser' From 344d820e91a8e2a9d2ab1b79912ffb97d1bdce77 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 14:04:07 +0000 Subject: [PATCH 05/33] Added ignores for mypy (including one in sp-repo-review) --- pyproject.toml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdf2733f..437b697f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,11 +162,30 @@ ignore = [ "PC190", # Uses Ruff "PC901", # Custom pre-commit CI message "MY100", # Uses MyPy (pyproject config) + "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) "RF001", # Uses RUFF ] [tool.mypy] -enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +disable_error_code = [ + # TODO: Fix these: + "arg-type", + "assignment", + "attr-defined", + "func-returns-value", + "misc", + "no-untyped-call", + "no-untyped-def", + "operator", + "redundant-expr", + "unreachable", + "var-annotated", +] +enable_error_code = [ + "ignore-without-code", +# "redundant-expr", # TODO: Add back in when above ignores fixed + "truthy-bool" +] exclude = [ "docs", "cf_units/_udunits2_parser", From 95192b5170aec9e247f9e04eeb23d2d7cc1743e0 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 14:09:10 +0000 Subject: [PATCH 06/33] Added validate-pyproject --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 490de407..af567437 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,3 +75,9 @@ repos: additional_dependencies: - 'types-requests' exclude: 'docs|cf_units/_udunits2_parser' + +- repo: https://github.com/abravalheri/validate-pyproject + # More exhaustive than Ruff RUF200. + rev: "v0.23" + hooks: + - id: validate-pyproject From 2a800958b36a7fae7ccc0f3582b239476c4e7ba5 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 14:30:40 +0000 Subject: [PATCH 07/33] Added numpydoc precommit hook and ignored errors in pyproject.toml --- .pre-commit-config.yaml | 6 +++++ pyproject.toml | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af567437..426ab450 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -81,3 +81,9 @@ repos: rev: "v0.23" hooks: - id: validate-pyproject + +- repo: https://github.com/numpy/numpydoc + rev: v1.8.0 + hooks: + - id: numpydoc-validation + types: [file, python] diff --git a/pyproject.toml b/pyproject.toml index 437b697f..66fef30a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,3 +193,57 @@ exclude = [ ignore_missing_imports = true strict = true warn_unreachable = true + +[tool.numpydoc_validation] +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) + "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) + "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 + "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. + + # Record temporarily ignored checks below; will be reviewed at a later date: + "GL08", # The object does not have a docstring + + "PR01", # Parameters {xxxx} not documented + + "RT01", # No Returns section found + + "SS03", # Summary does not end with a period + + "SS06", # Summary should fit in a single line + + "SS05", #Summary must start with infinitive verb, not third person + # (e.g. use "Generate" instead of "Generates") + + +] +exclude = [ + '\.__eq__$', + '\.__ne__$', + '\.__repr__$', +] From f9902a111757b958b21b2e43fa72fbcc316000fa Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 14:47:33 +0000 Subject: [PATCH 08/33] Updated ignore_list of sp-repo-review for errors that have been addressed in this branch --- pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66fef30a..549a2526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,25 +145,17 @@ convention = "numpy" [tool.repo-review] # These are a list of the currently failing tests: ignore = [ - "PY005", # Has tests folder "PP003", # Does not list wheel as a build-dep "PP304", # Sets the log level in pytest "PP305", # Specifies xfail_strict "PP306", # Specifies strict config "PP307", # Specifies strict markers "PP309", # Filter warnings specified - "GH104", # Use unique names for upload-artifact "GH212", # Require GHA update grouping - "PC110", # Uses black or ruff-format - "PC140", # Uses a type checker - "PC160", # Uses a spell checker "PC170", # Uses PyGrep hooks (only needed if rST present) "PC180", # Uses a markdown formatter - "PC190", # Uses Ruff "PC901", # Custom pre-commit CI message - "MY100", # Uses MyPy (pyproject config) "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) - "RF001", # Uses RUFF ] [tool.mypy] From 9e082df983deb95f5bf5162f0f40a368c3eeeeb6 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 14:57:55 +0000 Subject: [PATCH 09/33] Fixed sp-repo-review "PC901" (Custom pre-commit CI message) --- .pre-commit-config.yaml | 4 ++++ pyproject.toml | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 426ab450..fe09a304 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,10 @@ # See https://pre-commit.com/hooks.html for more hooks # See https://github.com/scientific-python/cookie#sp-repo-review for repo-review +ci: + autofix_prs: false + autoupdate_commit_msg: "chore: update pre-commit hooks" + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 diff --git a/pyproject.toml b/pyproject.toml index 549a2526..8fefd038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,6 @@ ignore = [ "GH212", # Require GHA update grouping "PC170", # Uses PyGrep hooks (only needed if rST present) "PC180", # Uses a markdown formatter - "PC901", # Custom pre-commit CI message "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) ] From c13d29d3b1e94db9a52a5c18922fb1b829dd0b1e Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 15:21:59 +0000 Subject: [PATCH 10/33] Added extra pytest config, as recommended by sp-repo-review. --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8fefd038..b5fbeed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,10 +95,12 @@ exclude_lines = [ ] [tool.pytest.ini_options] -addopts = "-ra -v --doctest-modules" +addopts = ["-ra", "-v", "--strict-config", "--strict-markers", "--doctest-modules"] +log_cli_level = "INFO" doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS NUMBER" minversion = "6.0" testpaths = "cf_units" +xfail_strict = true [tool.setuptools.packages.find] include = ["cf_units"] @@ -146,10 +148,6 @@ convention = "numpy" # These are a list of the currently failing tests: ignore = [ "PP003", # Does not list wheel as a build-dep - "PP304", # Sets the log level in pytest - "PP305", # Specifies xfail_strict - "PP306", # Specifies strict config - "PP307", # Specifies strict markers "PP309", # Filter warnings specified "GH212", # Require GHA update grouping "PC170", # Uses PyGrep hooks (only needed if rST present) From edba9c26b8d9ca7e99c6671f2659ca179af2381d Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Thu, 14 Nov 2024 15:31:43 +0000 Subject: [PATCH 11/33] Added `filterwarnings=error` option for pytest and an exception for thie existing known Deprecation warnings --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5fbeed6..0796480c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,10 @@ exclude_lines = [ addopts = ["-ra", "-v", "--strict-config", "--strict-markers", "--doctest-modules"] log_cli_level = "INFO" doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS NUMBER" +filterwarnings = [ + "error", + "default:This method is no longer needed:DeprecationWarning", # Added for known warnings +] minversion = "6.0" testpaths = "cf_units" xfail_strict = true @@ -148,7 +152,7 @@ convention = "numpy" # These are a list of the currently failing tests: ignore = [ "PP003", # Does not list wheel as a build-dep - "PP309", # Filter warnings specified + # "PP309", # Filter warnings specified "GH212", # Require GHA update grouping "PC170", # Uses PyGrep hooks (only needed if rST present) "PC180", # Uses a markdown formatter From 18f934fa3e6543f0b7f89d4598515db1b97ce3d3 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 12:19:28 +0000 Subject: [PATCH 12/33] - Enabled all Ruff checks. - Required lots of additions to the Ruff ignore list - Extra additions to mypy ignore list - Autofixes from Ruff linter - Fixes from codespell --- cf_units/__init__.py | 448 ++++++++---------- cf_units/_udunits2_parser/README.md | 2 +- cf_units/_udunits2_parser/__init__.py | 26 +- .../_antlr4_runtime/Recognizer.py | 2 +- .../_antlr4_runtime/error/ErrorStrategy.py | 2 +- cf_units/_udunits2_parser/compile.py | 15 +- cf_units/_udunits2_parser/graph.py | 27 +- cf_units/config.py | 3 +- .../tests/integration/parse/test_parse.py | 12 +- cf_units/tests/test_coding_standards.py | 35 +- cf_units/tests/test_unit.py | 33 +- cf_units/tests/unit/test__udunits2.py | 35 +- cf_units/util.py | 19 +- pyproject.toml | 117 ++++- 14 files changed, 355 insertions(+), 421 deletions(-) diff --git a/cf_units/__init__.py b/cf_units/__init__.py index b5dddaa7..3d47cbeb 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -2,8 +2,7 @@ # # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Units of measure. +"""Units of measure. Provision of a wrapper class to support Unidata/UCAR UDUNITS-2, and the cftime calendar functionality. @@ -166,10 +165,7 @@ @contextmanager def suppress_errors(): - """ - Suppresses all error messages from UDUNITS-2. - - """ + """Suppresses all error messages from UDUNITS-2.""" _default_handler = _ud.set_error_message_handler(_ud.ignore) try: yield @@ -220,8 +216,7 @@ def c_locale(): def encode_time(year, month, day, hour, minute, second): - """ - Return date/clock time encoded as a double precision value. + """Return date/clock time encoded as a double precision value. Encoding performed using UDUNITS-2 hybrid Gregorian/Julian calendar. Dates on or after 1582-10-15 are assumed to be Gregorian dates; @@ -243,7 +238,8 @@ def encode_time(year, month, day, hour, minute, second): * second (int): Second value to be encoded. - Returns: + Returns + ------- float. For example: @@ -253,13 +249,11 @@ def encode_time(year, month, day, hour, minute, second): -978307200.0 """ - return _ud.encode_time(year, month, day, hour, minute, second) def encode_date(year, month, day): - """ - Return date encoded as a double precision value. + """Return date encoded as a double precision value. Encoding performed using UDUNITS-2 hybrid Gergorian/Julian calendar. Dates on or after 1582-10-15 are assumed to be Gregorian dates; @@ -275,7 +269,8 @@ def encode_date(year, month, day): * day (int): Day value to be encoded. - Returns: + Returns + ------- float. For example: @@ -285,13 +280,11 @@ def encode_date(year, month, day): -978307200.0 """ - return _ud.encode_date(year, month, day) def encode_clock(hour, minute, second): - """ - Return clock time encoded as a double precision value. + """Return clock time encoded as a double precision value. Args: @@ -302,7 +295,8 @@ def encode_clock(hour, minute, second): * second (int): Second value to be encoded. - Returns: + Returns + ------- float. For example: @@ -312,13 +306,11 @@ def encode_clock(hour, minute, second): 0.0 """ - return _ud.encode_clock(hour, minute, second) def decode_time(time): - """ - Decode a double precision date/clock time value into its component + """Decode a double precision date/clock time value into its component parts and return as tuple. Decode time into it's year, month, day, hour, minute, second, and @@ -329,7 +321,8 @@ def decode_time(time): * time (float): Date/clock time encoded as a double precision value. - Returns: + Returns + ------- tuple of (year, month, day, hour, minute, second, resolution). For example: @@ -343,8 +336,7 @@ def decode_time(time): def date2num(date, unit, calendar): - """ - Return numeric time value (resolution of 1 second) encoding of + """Return numeric time value (resolution of 1 second) encoding of datetime object. The units of the numeric time values are described by the unit and @@ -369,7 +361,8 @@ def date2num(date, unit, calendar): * calendar (string): Name of the calendar, see cf_units.CALENDARS. - Returns: + Returns + ------- float/integer or numpy.ndarray of floats/integers For example: @@ -392,7 +385,6 @@ def date2num(date, unit, calendar): array([5, 6]) """ - # # ensure to strip out any 'UTC' postfix which is generated by # UDUNITS-2 formatted output and causes the cftime parser @@ -412,8 +404,7 @@ def num2date( only_use_cftime_datetimes=True, only_use_python_datetimes=False, ): - """ - Return datetime encoding of numeric time value (resolution of 1 second). + """Return datetime encoding of numeric time value (resolution of 1 second). The units of the numeric time value are described by the unit and calendar arguments. The returned datetime object represent UTC with @@ -435,7 +426,7 @@ def num2date( * time_value (float): Numeric time value/s. Maximum resolution is 1 second. - * unit (sting): + * unit (string): A string of the form ' since ' describing the time units. The can be days, hours, minutes or seconds. The is the date/time reference @@ -456,7 +447,8 @@ def num2date( possible, and raise an exception if not. Ignored if only_use_cftime_datetimes is True. Defaults to False. - Returns: + Returns + ------- datetime, or numpy.ndarray of datetime object. For example: @@ -472,7 +464,6 @@ def num2date( ['1970-01-01 06:00:00', '1970-01-01 07:00:00'] """ - # # ensure to strip out any 'UTC' postfix which is generated by # UDUNITS-2 formatted output and causes the cftime parser @@ -490,8 +481,7 @@ def num2date( def num2pydate(time_value, unit, calendar): - """ - Convert time value(s) to python datetime.datetime objects, or raise an + """Convert time value(s) to python datetime.datetime objects, or raise an exception if this is not possible. Same as:: num2date(time_value, unit, calendar, @@ -512,8 +502,7 @@ def num2pydate(time_value, unit, calendar): def as_unit(unit): - """ - Returns a Unit corresponding to the given unit. + """Returns a Unit corresponding to the given unit. .. note:: @@ -537,14 +526,14 @@ def as_unit(unit): def is_time(unit): - """ - Determine whether the unit is a related SI Unit of time. + """Determine whether the unit is a related SI Unit of time. Args: * unit (string/Unit): Unit to be compared. - Returns: + Returns + ------- Boolean. For example: @@ -560,14 +549,14 @@ def is_time(unit): def is_vertical(unit): - """ - Determine whether the unit is a related SI Unit of pressure or distance. + """Determine whether the unit is a related SI Unit of pressure or distance. Args: * unit (string/Unit): Unit to be compared. - Returns: + Returns + ------- Boolean. For example: @@ -583,10 +572,7 @@ def is_vertical(unit): def _ud_value_error(ud_err, message): - """ - Return a ValueError that has extra context from a _udunits2.UdunitsError. - - """ + """Return a ValueError that has extra context from a _udunits2.UdunitsError.""" # NOTE: We aren't raising here, just giving the caller a well formatted # exception that they can raise themselves. @@ -600,8 +586,7 @@ def _ud_value_error(ud_err, message): class Unit(_OrderedHashable): - """ - A class to represent S.I. units and support common operations to + """A class to represent S.I. units and support common operations to manipulate such units in a consistent manner as per UDUNITS-2. These operations include scaling the unit, offsetting the unit by a @@ -611,7 +596,7 @@ class Unit(_OrderedHashable): or another unit, comparing units, copying units and converting unit data to single precision or double precision floating point numbers. - This class also supports time and calendar defintion and manipulation. + This class also supports time and calendar definition and manipulation. """ @@ -637,14 +622,10 @@ def __lt__(self, other): # Prevent attribute updates def __setattr__(self, name, value): - raise AttributeError( - f"Instances of {type(self).__name__:s} are immutable" - ) + raise AttributeError(f"Instances of {type(self).__name__:s} are immutable") def __delattr__(self, name): - raise AttributeError( - f"Instances of {type(self).__name__:s} are immutable" - ) + raise AttributeError(f"Instances of {type(self).__name__:s} are immutable") # Declare the attribute names relevant to the ordered and hashable # behaviour. @@ -665,8 +646,7 @@ def __delattr__(self, name): __slots__ = () def __init__(self, unit, calendar=None): - """ - Create a wrapper instance for UDUNITS-2. + """Create a wrapper instance for UDUNITS-2. An optional calendar may be provided for a unit which defines a time reference of the form ' since ' @@ -700,8 +680,8 @@ def __init__(self, unit, calendar=None): default is 'standard' or 'gregorian' for a time reference unit. - Returns: - + Returns + ------- Unit object. Units should be set to "no_unit" for values which are strings. @@ -777,9 +757,7 @@ def __init__(self, unit, calendar=None): ) @classmethod - def _new_from_existing_ut( - cls, category, ut_unit, calendar=None, origin=None - ): + def _new_from_existing_ut(cls, category, ut_unit, calendar=None, origin=None): # Short-circuit __init__ if we know what we are doing and already # have a UT handle. unit = cls.__new__(cls) @@ -814,10 +792,10 @@ def __deepcopy__(self, memo): return self def is_time(self): - """ - Determine whether this unit is a related SI Unit of time. + """Determine whether this unit is a related SI Unit of time. - Returns: + Returns + ------- Boolean. For example: @@ -839,11 +817,11 @@ def is_time(self): return result def is_vertical(self): - """ - Determine whether the unit is a related SI Unit of pressure or + """Determine whether the unit is a related SI Unit of pressure or distance. - Returns: + Returns + ------- Boolean. For example: @@ -868,16 +846,16 @@ def is_vertical(self): return result def is_udunits(self): - """Return whether the unit is a vaild unit of UDUNITS.""" + """Return whether the unit is a valid unit of UDUNITS.""" return self.ut_unit is not _ud.NULL_UNIT def is_time_reference(self): - """ - Return whether the unit is a time reference unit of the form + """Return whether the unit is a time reference unit of the form ' since ' i.e. unit='days since 1970-01-01 00:00:00' - Returns: + Returns + ------- Boolean. For example: @@ -891,8 +869,7 @@ def is_time_reference(self): return self.calendar is not None def is_long_time_interval(self): - """ - Defines whether this unit describes a time unit with a long time + """Defines whether this unit describes a time unit with a long time interval ("months" or "years"). These long time intervals *are* supported by `UDUNITS2` but are not supported by `cftime`. This discrepancy means we cannot run self.num2date() on a time unit with @@ -904,7 +881,8 @@ def is_long_time_interval(self): cftime - do not use this routine, as cftime knows best what it can and cannot support. - Returns: + Returns + ------- Boolean. For example: @@ -927,20 +905,18 @@ def is_long_time_interval(self): result = False long_time_intervals = ["year", "month"] if self.is_time_reference(): - result = any( - interval in self.origin for interval in long_time_intervals - ) + result = any(interval in self.origin for interval in long_time_intervals) return result def title(self, value): - """ - Return the unit value as a title string. + """Return the unit value as a title string. Args: * value (float): Unit value to be incorporated into title string. - Returns: + Returns + ------- string. For example: @@ -961,15 +937,15 @@ def title(self, value): @property def modulus(self): - """ - *(read-only)* Return the modulus value of the unit. + """*(read-only)* Return the modulus value of the unit. Convenience method that returns the unit modulus value as follows, * 'radians' - pi*2 * 'degrees' - 360.0 * Otherwise None. - Returns: + Returns + ------- float. For example: @@ -980,7 +956,6 @@ def modulus(self): 360.0 """ - if self == "radians": result = np.pi * 2 elif self == "degrees": @@ -990,14 +965,14 @@ def modulus(self): return result def is_convertible(self, other): - """ - Return whether two units are convertible. + """Return whether two units are convertible. Args: * other (Unit): Unit to be compared. - Returns: + Returns + ------- Boolean. For example: @@ -1024,10 +999,10 @@ def is_convertible(self, other): return result def is_dimensionless(self): - """ - Return whether the unit is dimensionless. + """Return whether the unit is dimensionless. - Returns: + Returns + ------- Boolean. For example: @@ -1046,10 +1021,10 @@ def is_dimensionless(self): ) def is_unknown(self): - """ - Return whether the unit is defined to be an *unknown* unit. + """Return whether the unit is defined to be an *unknown* unit. - Returns: + Returns + ------- Boolean. For example: @@ -1066,14 +1041,14 @@ def is_unknown(self): return self.category == _CATEGORY_UNKNOWN or self.ut_unit is None def is_no_unit(self): - """ - Return whether the unit is defined to be a *no_unit* unit. + """Return whether the unit is defined to be a *no_unit* unit. Typically, a quantity such as a string, will have no associated unit to describe it. Such a class of quantity may be defined using the *no_unit* unit. - Returns: + Returns + ------- Boolean. For example: @@ -1090,8 +1065,7 @@ def is_no_unit(self): return self.category == _CATEGORY_NO_UNIT def format(self, option=None): - """ - Return a formatted string representation of the binary unit. + """Return a formatted string representation of the binary unit. Args: @@ -1111,7 +1085,8 @@ def format(self, option=None): Multiple options may be combined within a list. The default option is cf_units.UT_ASCII. - Returns: + Returns + ------- string. For example: @@ -1128,34 +1103,31 @@ def format(self, option=None): """ if self.is_unknown(): return _UNKNOWN_UNIT_STRING - elif self.is_no_unit(): + if self.is_no_unit(): return _NO_UNIT_STRING - else: - bitmask = UT_ASCII - if option is not None: - if not isinstance(option, list): - option = [option] - for i in option: - bitmask |= i - encoding = bitmask & ( - UT_ASCII | UT_ISO_8859_1 | UT_LATIN1 | UT_UTF8 - ) - encoding_str = _encoding_lookup[encoding] - result = _ud.format(self.ut_unit, bitmask) - - result = str(result.decode(encoding_str)) - return result + bitmask = UT_ASCII + if option is not None: + if not isinstance(option, list): + option = [option] + for i in option: + bitmask |= i + encoding = bitmask & (UT_ASCII | UT_ISO_8859_1 | UT_LATIN1 | UT_UTF8) + encoding_str = _encoding_lookup[encoding] + result = _ud.format(self.ut_unit, bitmask) + + result = str(result.decode(encoding_str)) + return result @property def name(self): - """ - *(read-only)* The full name of the unit. + """*(read-only)* The full name of the unit. Formats the binary unit into a string representation using method :func:`cf_units.Unit.format` with keyword argument option=cf_units.UT_NAMES. - Returns: + Returns + ------- string. For example: @@ -1170,13 +1142,13 @@ def name(self): @property def symbol(self): - """ - *(read-only)* The symbolic representation of the unit. + """*(read-only)* The symbolic representation of the unit. Formats the binary unit into a string representation using method :func:`cf_units.Unit.format`. - Returns: + Returns + ------- string. For example: @@ -1197,14 +1169,14 @@ def symbol(self): @property def definition(self): - """ - *(read-only)* The symbolic decomposition of the unit. + """*(read-only)* The symbolic decomposition of the unit. Formats the binary unit into a string representation using method :func:`cf_units.Unit.format` with keyword argument option=cf_units.UT_DEFINITION. - Returns: + Returns + ------- string. For example: @@ -1224,15 +1196,15 @@ def definition(self): return result def offset_by_time(self, origin): - """ - Returns the time unit offset with respect to the time origin. + """Returns the time unit offset with respect to the time origin. Args: * origin (float): Time origin as returned by the :func:`cf_units.encode_time` method. - Returns: + Returns + ------- None. For example: @@ -1243,27 +1215,22 @@ def offset_by_time(self, origin): Unit('h @ 19700101T000000.0000000 UTC') """ - if not isinstance(origin, float | int): - raise TypeError( - "a numeric type for the origin argument is required" - ) + raise TypeError("a numeric type for the origin argument is required") try: ut_unit = _ud.offset_by_time(self.ut_unit, origin) except _ud.UdunitsError as exception: - value_error = _ud_value_error( - exception, f"Failed to offset {self!r}" - ) + value_error = _ud_value_error(exception, f"Failed to offset {self!r}") raise value_error from None calendar = None return Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit, calendar) def invert(self): - """ - Invert the unit i.e. find the reciprocal of the unit, and return + """Invert the unit i.e. find the reciprocal of the unit, and return the Unit result. - Returns: + Returns + ------- Unit. For example: @@ -1286,14 +1253,14 @@ def invert(self): return result def root(self, root): - """ - Returns the given root of the unit. + """Returns the given root of the unit. Args: * root (int): Value by which the unit root is taken. - Returns: + Returns + ------- None. For example: @@ -1314,35 +1281,32 @@ def root(self, root): result = self elif self.is_no_unit(): raise ValueError("Cannot take the root of a 'no-unit'.") + # only update the unit if it is not scalar + elif self == Unit("1"): + result = self else: - # only update the unit if it is not scalar - if self == Unit("1"): - result = self - else: - try: - ut_unit = _ud.root(self.ut_unit, root) - except _ud.UdunitsError as exception: - value_error = _ud_value_error( - exception, - f"Failed to take the root of {self!r}", - ) - raise value_error from None - calendar = None - result = Unit._new_from_existing_ut( - _CATEGORY_UDUNIT, ut_unit, calendar + try: + ut_unit = _ud.root(self.ut_unit, root) + except _ud.UdunitsError as exception: + value_error = _ud_value_error( + exception, + f"Failed to take the root of {self!r}", ) + raise value_error from None + calendar = None + result = Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit, calendar) return result def log(self, base): - """ - Returns the logorithmic unit corresponding to the given - logorithmic base. + """Returns the logarithmic unit corresponding to the given + logarithmic base. Args: - * base (int/float): Value of the logorithmic base. + * base (int/float): Value of the logarithmic base. - Returns: + Returns + ------- None. For example: @@ -1361,26 +1325,22 @@ def log(self, base): try: ut_unit = _ud.log(base, self.ut_unit) except TypeError: - raise TypeError( - "A numeric type for the base argument is required" - ) + raise TypeError("A numeric type for the base argument is required") except _ud.UdunitsError as exception: value_err = _ud_value_error( exception, - f"Failed to calculate logorithmic base of {self!r}", + f"Failed to calculate logarithmic base of {self!r}", ) raise value_err from None calendar = None - result = Unit._new_from_existing_ut( - _CATEGORY_UDUNIT, ut_unit, calendar - ) + result = Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit, calendar) return result def __str__(self): - """ - Returns a simple string representation of the unit. + """Returns a simple string representation of the unit. - Returns: + Returns + ------- string. For example: @@ -1394,10 +1354,10 @@ def __str__(self): return self.origin or self.symbol def __repr__(self): - """ - Returns a string representation of the unit object. + """Returns a string representation of the unit object. - Returns: + Returns + ------- string. For example: @@ -1411,10 +1371,7 @@ def __repr__(self): if self.calendar is None: result = f"{self.__class__.__name__}('{self}')" else: - result = ( - f"{self.__class__.__name__}" - f"('{self}', calendar='{self.calendar}')" - ) + result = f"{self.__class__.__name__}('{self}', calendar='{self.calendar}')" return result def _offset_common(self, offset): @@ -1446,7 +1403,7 @@ def __sub__(self, other): return result def _op_common(self, other, op_func): - # Convienience method to create a new unit from an operation between + # Convenience method to create a new unit from an operation between # the units 'self' and 'other'. op_label = op_func.__name__ @@ -1468,9 +1425,7 @@ def _op_common(self, other, op_func): ) raise value_err from None calendar = None - result = Unit._new_from_existing_ut( - _CATEGORY_UDUNIT, ut_unit, calendar - ) + result = Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit, calendar) return result def __rmul__(self, other): @@ -1480,8 +1435,7 @@ def __rmul__(self, other): return self * other def __mul__(self, other): - """ - Multiply the self unit by the other scale factor or unit and + """Multiply the self unit by the other scale factor or unit and return the Unit result. Note that, multiplication involving an 'unknown' unit will always @@ -1492,7 +1446,8 @@ def __mul__(self, other): * other (int/float/string/Unit): Multiplication scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1507,8 +1462,7 @@ def __mul__(self, other): return self._op_common(other, _ud.multiply) def __div__(self, other): - """ - Divide the self unit by the other scale factor or unit and + """Divide the self unit by the other scale factor or unit and return the Unit result. Note that, division involving an 'unknown' unit will always @@ -1518,7 +1472,8 @@ def __div__(self, other): * other (int/float/string/Unit): Division scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1533,8 +1488,7 @@ def __div__(self, other): return self._op_common(other, _ud.divide) def __truediv__(self, other): - """ - Divide the self unit by the other scale factor or unit and + """Divide the self unit by the other scale factor or unit and return the Unit result. Note that, division involving an 'unknown' unit will always @@ -1544,7 +1498,8 @@ def __truediv__(self, other): * other (int/float/string/Unit): Division scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1559,8 +1514,7 @@ def __truediv__(self, other): return self.__div__(other) def __pow__(self, power): - """ - Raise the unit by the given power and return the Unit result. + """Raise the unit by the given power and return the Unit result. Note that, UDUNITS-2 does not support raising a non-dimensionless unit by a fractional power. @@ -1571,7 +1525,8 @@ def __pow__(self, power): * power (int/float): Value by which the unit power is raised. - Returns: + Returns + ------- Unit. For example: @@ -1585,9 +1540,7 @@ def __pow__(self, power): try: power = float(power) except ValueError: - raise TypeError( - "A numeric value is required for the power" " argument." - ) + raise TypeError("A numeric value is required for the power argument.") if self.is_unknown(): result = self @@ -1596,44 +1549,43 @@ def __pow__(self, power): elif self == Unit("1"): # 1 ** N -> 1 result = self + # UDUNITS-2 does not support floating point raise/root. + # But if the power is of the form 1/N, where N is an integer + # (within a certain acceptable accuracy) then we can find the Nth + # root. + elif not math.isclose(power, 0.0) and abs(power) < 1: + if not math.isclose(1 / power, round(1 / power)): + raise ValueError("Cannot raise a unit by a decimal.") + root = int(round(1 / power)) + result = self.root(root) else: - # UDUNITS-2 does not support floating point raise/root. - # But if the power is of the form 1/N, where N is an integer - # (within a certain acceptable accuracy) then we can find the Nth - # root. - if not math.isclose(power, 0.0) and abs(power) < 1: - if not math.isclose(1 / power, round(1 / power)): - raise ValueError("Cannot raise a unit by a decimal.") - root = int(round(1 / power)) - result = self.root(root) - else: - # Failing that, check for powers which are (very nearly) - # simple integer values. - if not math.isclose(power, round(power)): - msg = f"Cannot raise a unit by a decimal (got {power:s})." - raise ValueError(msg) - power = int(round(power)) + # Failing that, check for powers which are (very nearly) + # simple integer values. + if not math.isclose(power, round(power)): + msg = f"Cannot raise a unit by a decimal (got {power:s})." + raise ValueError(msg) + power = int(round(power)) - try: - ut_unit = _ud.raise_(self.ut_unit, power) - except _ud.UdunitsError as exception: - value_err = _ud_value_error( - exception, - f"Failed to raise the power of {self!r}", - ) - raise value_err from None - result = Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit) + try: + ut_unit = _ud.raise_(self.ut_unit, power) + except _ud.UdunitsError as exception: + value_err = _ud_value_error( + exception, + f"Failed to raise the power of {self!r}", + ) + raise value_err from None + result = Unit._new_from_existing_ut(_CATEGORY_UDUNIT, ut_unit) return result def __eq__(self, other): - """ - Compare the two units for equality and return the boolean result. + """Compare the two units for equality and return the boolean result. Args: * other (string/Unit): Unit to be compared. - Returns: + Returns + ------- Boolean. For example: @@ -1663,14 +1615,14 @@ def __eq__(self, other): return res == 0 def __ne__(self, other): - """ - Compare the two units for inequality and return the boolean result. + """Compare the two units for inequality and return the boolean result. Args: * other (string/Unit): Unit to be compared. - Returns: + Returns + ------- Boolean. For example: @@ -1710,8 +1662,7 @@ def change_calendar(self, calendar): return Unit(new_origin, calendar=calendar) def convert(self, value, other, ctype=FLOAT64, inplace=False): - """ - Converts a single value or NumPy array of values from the current unit + """Converts a single value or NumPy array of values from the current unit to the other target unit. If the units are not convertible, then no conversion will take place. @@ -1732,7 +1683,8 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): convert the values in-place. A new array will be created if ``value`` is an integer NumPy array. - Returns: + Returns + ------- float or numpy.ndarray of appropriate float type. For example: @@ -1790,9 +1742,7 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): result = result.astype(value.dtype) else: try: - ut_converter = _ud.get_converter( - self.ut_unit, other.ut_unit - ) + ut_converter = _ud.get_converter(self.ut_unit, other.ut_unit) except _ud.UdunitsError as exception: value_err = _ud_value_error( exception, @@ -1815,8 +1765,7 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): "array in-place. Consider byte-swapping " "first." ) - else: - result = result.astype(result.dtype.type) + result = result.astype(result.dtype.type) # Strict type check of numpy array. if result.dtype.type not in (np.float32, np.float64): raise TypeError( @@ -1828,15 +1777,11 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): # _cv_convert_array to convert our array in 1d form result_tmp = result.ravel(order="A") # Do the actual conversion. - _cv_convert_array[ctype]( - ut_converter, result_tmp, result_tmp - ) + _cv_convert_array[ctype](ut_converter, result_tmp, result_tmp) # If result_tmp was a copy, not a view (i.e. not C # contiguous), copy the data back to the original. if not np.shares_memory(result, result_tmp): - result_tmp = result_tmp.reshape( - result.shape, order="A" - ) + result_tmp = result_tmp.reshape(result.shape, order="A") if isinstance(result, np.ma.MaskedArray): result.data[...] = result_tmp else: @@ -1851,15 +1796,11 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): # _cv_convert_scalar result = _cv_convert_scalar[ctype](ut_converter, result) return result - else: - raise ValueError( - f"Unable to convert from '{self!r}' to '{other!r}'." - ) + raise ValueError(f"Unable to convert from '{self!r}' to '{other!r}'.") @property def cftime_unit(self): - """ - Returns a string suitable for passing as a unit to cftime.num2date and + """Returns a string suitable for passing as a unit to cftime.num2date and cftime.date2num. """ @@ -1873,8 +1814,7 @@ def cftime_unit(self): return str(self).rstrip(" UTC") def date2num(self, date): - """ - Returns the numeric time value calculated from the datetime + """Returns the numeric time value calculated from the datetime object using the current calendar and unit time reference. The current unit time reference must be of the form: @@ -1894,7 +1834,8 @@ def date2num(self, date): A datetime object or a sequence of datetime objects. The datetime objects should not include a time-zone offset. - Returns: + Returns + ------- float/integer or numpy.ndarray of floats/integers For example: @@ -1922,8 +1863,7 @@ def num2date( only_use_cftime_datetimes=True, only_use_python_datetimes=False, ): - """ - Returns a datetime-like object calculated from the numeric time + """Returns a datetime-like object calculated from the numeric time value using the current calendar and the unit time reference. The current unit time reference must be of the form: @@ -1958,7 +1898,8 @@ def num2date( possible, and raise an exception if not. Ignored if only_use_cftime_datetimes is True. Defaults to False. - Returns: + Returns + ------- datetime, or numpy.ndarray of datetime object. For example: @@ -1982,8 +1923,7 @@ def num2date( ) def num2pydate(self, time_value): - """ - Convert time value(s) to python datetime.datetime objects, or raise an + """Convert time value(s) to python datetime.datetime objects, or raise an exception if this is not possible. Same as:: unit.num2date(time_value, only_use_cftime_datetimes=False, diff --git a/cf_units/_udunits2_parser/README.md b/cf_units/_udunits2_parser/README.md index dae6f6c8..8582ef63 100644 --- a/cf_units/_udunits2_parser/README.md +++ b/cf_units/_udunits2_parser/README.md @@ -26,7 +26,7 @@ changes to the grammar being proposed so that the two can remain in synch. ### Updating the ANTLR version The [compile.py script](compile.py) copies the ANTLR4 runtime into the _antlr4_runtime -directory, and this should be commited to the repository. This means that we do not +directory, and this should be committed to the repository. This means that we do not have a runtime dependency on ANTLR4 (which was found to be challenging due to the fact that you need to pin to a specific version of the ANTLR4 runtime, and aligning this version with other libraries which also have an ANTLR4 dependency is impractical). diff --git a/cf_units/_udunits2_parser/__init__.py b/cf_units/_udunits2_parser/__init__.py index 63113cc5..059053f0 100644 --- a/cf_units/_udunits2_parser/__init__.py +++ b/cf_units/_udunits2_parser/__init__.py @@ -19,8 +19,7 @@ # Dictionary mapping token rule id to token name. TOKEN_ID_NAMES = { - getattr(udunits2Lexer, rule, None): rule - for rule in udunits2Lexer.ruleNames + getattr(udunits2Lexer, rule, None): rule for rule in udunits2Lexer.ruleNames } @@ -33,10 +32,7 @@ def handle_UNICODE_EXPONENT(string): class UnitParseVisitor(udunits2ParserVisitor): - """ - A visitor which converts the parse tree into an abstract expression graph. - - """ + """A visitor which converts the parse tree into an abstract expression graph.""" #: A dictionary mapping lexer TOKEN names to the action that should be #: taken on them when visited. For full context of what is allowed, see @@ -82,10 +78,7 @@ def visitChildren(self, node): return result def visitTerminal(self, ctx): - """ - Return a graph.Node, or None, to represent the given lexer terminal. - - """ + """Return a graph.Node, or None, to represent the given lexer terminal.""" content = ctx.getText() symbol_idx = ctx.symbol.type @@ -162,10 +155,7 @@ def visitUnit_spec(self, ctx): class SyntaxErrorRaiser(ErrorListener): - """ - Turn any parse errors into sensible SyntaxErrors. - - """ + """Turn any parse errors into sensible SyntaxErrors.""" def __init__(self, unit_string): self.unit_string = unit_string @@ -179,10 +169,7 @@ def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): def _debug_tokens(unit_string): - """ - A really handy way of printing the tokens produced for a given input. - - """ + """A really handy way of printing the tokens produced for a given input.""" unit_str = unit_string.strip() lexer = udunits2Lexer(InputStream(unit_str)) stream = CommonTokenStream(lexer) @@ -200,8 +187,7 @@ def _debug_tokens(unit_string): def normalize(unit_string): - """ - Parse the given unit string, and return its string representation. + """Parse the given unit string, and return its string representation. No standardisation of units, nor simplification of expressions is done, but some tokens and operators will be converted to their canonical form. diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py b/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py index f827ec86..0a7f62ad 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py +++ b/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py @@ -7,7 +7,7 @@ from .RuleContext import RuleContext from .Token import Token -# need forward delcaration +# need forward declaration RecognitionException = None diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py b/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py index 642239c2..31354c24 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py +++ b/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py @@ -212,7 +212,7 @@ def sync(self, recognizer: Parser): s = recognizer._interp.atn.states[recognizer.state] la = recognizer.getTokenStream().LA(1) - # try cheaper subset first; might get lucky. seems to shave a wee bit off + # try cheaper subset first; might get lucky. seems to shave a small bit off nextTokens = recognizer.atn.nextTokens(s) if la in nextTokens: self.nextTokensContext = None diff --git a/cf_units/_udunits2_parser/compile.py b/cf_units/_udunits2_parser/compile.py index 304fb033..faa75efa 100644 --- a/cf_units/_udunits2_parser/compile.py +++ b/cf_units/_udunits2_parser/compile.py @@ -3,8 +3,7 @@ # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Compiles the UDUNITS-2 grammar using ANTLR4. +"""Compiles the UDUNITS-2 grammar using ANTLR4. You may be interested in running this with entr to watch changes to the grammar: @@ -47,9 +46,7 @@ def expand_lexer(source, target): with open(source) as fh: content = fh.read() - template = jinja2.Environment(loader=jinja2.BaseLoader).from_string( - content - ) + template = jinja2.Environment(loader=jinja2.BaseLoader).from_string(content) current_mode = "DEFAULT_MODE" @@ -218,14 +215,10 @@ def main(): ) # Fix up comments such as "Generated from /some/path.g4 by ANTLR 4.11.1" - pattern = re.compile( - r"# Generated from .+? by ANTLR (?P.*)" - ) + pattern = re.compile(r"# Generated from .+? by ANTLR (?P.*)") for py_file in parser_dir.glob("*.py"): contents = py_file.read_text() - contents = re.sub( - pattern, r"# Generated by ANTLR \g", contents - ) + contents = re.sub(pattern, r"# Generated by ANTLR \g", contents) py_file.write_text(contents) vendor_antlr4_runtime(HERE) diff --git a/cf_units/_udunits2_parser/graph.py b/cf_units/_udunits2_parser/graph.py index 561271aa..4deb2513 100644 --- a/cf_units/_udunits2_parser/graph.py +++ b/cf_units/_udunits2_parser/graph.py @@ -5,19 +5,13 @@ class Node: - """ - Represents a node in an expression graph. - - """ + """Represents a node in an expression graph.""" def __init__(self, **kwargs): self._attrs = kwargs def children(self): - """ - Return the children of this node. - - """ + """Return the children of this node.""" # Since this is py>=36, the order of the attributes is well defined. return list(self._attrs.values()) @@ -27,9 +21,7 @@ def __getattr__(self, name): def _repr_ctx(self): # Return a dictionary that is useful for passing to string.format. - kwargs = ", ".join( - f"{key}={value!r}" for key, value in self._attrs.items() - ) + kwargs = ", ".join(f"{key}={value!r}" for key, value in self._attrs.items()) return {"cls_name": self.__class__.__name__, "kwargs": kwargs} def __repr__(self): @@ -37,10 +29,7 @@ def __repr__(self): class Terminal(Node): - """ - A generic terminal node in an expression graph. - - """ + """A generic terminal node in an expression graph.""" def __init__(self, content): super().__init__(content=content) @@ -63,8 +52,6 @@ class Number(Terminal): class Identifier(Terminal): """The unit itself (e.g. meters, m, km and π)""" - pass - class BinaryOp(Node): def __init__(self, lhs, rhs): @@ -103,8 +90,7 @@ class Timestamp(Terminal): class Visitor: - """ - This class may be used to help traversing an expression graph. + """This class may be used to help traversing an expression graph. It follows the same pattern as the Python ``ast.NodeVisitor``. Users should typically not need to override either ``visit`` or @@ -122,8 +108,7 @@ def visit(self, node): return visitor(node) def generic_visit(self, node): - """ - Called if no explicit visitor function exists for a node. + """Called if no explicit visitor function exists for a node. Can also be called by ``visit_`` implementations if children of the node are to be processed. diff --git a/cf_units/config.py b/cf_units/config.py index 0c970d90..2c13d2a4 100644 --- a/cf_units/config.py +++ b/cf_units/config.py @@ -12,8 +12,7 @@ # Returns simple string options. def get_option(section, option, default=None): - """ - Returns the option value for the given section, or the default value + """Returns the option value for the given section, or the default value if the section/option is not present. """ diff --git a/cf_units/tests/integration/parse/test_parse.py b/cf_units/tests/integration/parse/test_parse.py index c2dbf029..c36d0819 100644 --- a/cf_units/tests/integration/parse/test_parse.py +++ b/cf_units/tests/integration/parse/test_parse.py @@ -25,7 +25,7 @@ "2.e-6", ".1e2", ".1e2.2", - "2e", # <- TODO: Assert this isn't 2e1, but is infact the unit e *2 + "2e", # <- TODO: Assert this isn't 2e1, but is in fact the unit e *2 "m", "meter", # Multiplication @@ -148,7 +148,7 @@ def test_normed_units_equivalent(_, unit_str): unit_expr = normalize(unit_str) parsed_expr_symbol = cf_units.Unit(unit_expr).symbol - # Whilst the symbolic form from udunits is ugly, it *is* acurate, + # Whilst the symbolic form from udunits is ugly, it *is* accurate, # so check that the two represent the same unit. assert raw_symbol == parsed_expr_symbol @@ -167,9 +167,7 @@ def test_invalid_units(_, unit_str): cf_valid = False # Double check that udunits2 can't parse this. - assert ( - cf_valid is False - ), f"Unit {unit_str!r} is unexpectedly valid in UDUNITS2" + assert cf_valid is False, f"Unit {unit_str!r} is unexpectedly valid in UDUNITS2" try: normalize(unit_str) @@ -234,9 +232,7 @@ def test_invalid_in_udunits_but_still_parses(_, unit_str, expected): ] -@pytest.mark.parametrize( - "_, unit_str, expected", multi_enumerate(known_issues) -) +@pytest.mark.parametrize("_, unit_str, expected", multi_enumerate(known_issues)) def test_known_issues(_, unit_str, expected): # Unfortunately the grammar is not perfect. # These are the cases that don't work yet but which do work with udunits. diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 760f5b4a..652a40b2 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -20,9 +20,7 @@ REPO_DIR = Path(__file__).resolve().parents[2] DOCS_DIR = REPO_DIR / "docs" -DOCS_DIR = Path( - cf_units.config.get_option("Resources", "doc_dir", default=DOCS_DIR) -) +DOCS_DIR = Path(cf_units.config.get_option("Resources", "doc_dir", default=DOCS_DIR)) exclusion = ["Makefile", "make.bat", "build"] DOCS_DIRS = DOCS_DIR.glob("*") DOCS_DIRS = [DOC_DIR for DOC_DIR in DOCS_DIRS if DOC_DIR.name not in exclusion] @@ -33,8 +31,7 @@ class TestLicenseHeaders: @staticmethod def whatchanged_parse(whatchanged_output): - """ - Returns a generator of tuples of data parsed from + """Returns a generator of tuples of data parsed from "git whatchanged --pretty='TIME:%at'". The tuples are of the form ``(filename, last_commit_datetime)`` @@ -58,8 +55,7 @@ def whatchanged_parse(whatchanged_output): @staticmethod def last_change_by_fname(): - """ - Return a dictionary of all the files under git which maps to + """Return a dictionary of all the files under git which maps to the datetime of their last modification in the git history. .. note:: @@ -113,9 +109,7 @@ def test_license_headers(self): failed = True if failed: - raise AssertionError( - "There were license header failures. See stdout." - ) + raise AssertionError("There were license header failures. See stdout.") @pytest.mark.skipif(not IS_GIT_REPO, reason="Not a git repository.") @@ -138,10 +132,7 @@ def test_python_versions(): ( pyproject_toml_file, "\n ".join( - [ - f'"Programming Language :: Python :: {ver}",' - for ver in supported - ] + [f'"Programming Language :: Python :: {ver}",' for ver in supported] ), ), ( @@ -154,15 +145,11 @@ def test_python_versions(): ), ( tox_file, - "[testenv:py{" - + ",".join(supported_strip) - + "}-{linux,osx,win}-test]", + "[testenv:py{" + ",".join(supported_strip) + "}-{linux,osx,win}-test]", ), ( ci_locks_file, - "lock: [" - + ", ".join([f"py{p}-lock" for p in supported_strip]) - + "]", + "lock: [" + ", ".join([f"py{p}-lock" for p in supported_strip]) + "]", ), ( ci_tests_file, @@ -175,11 +162,11 @@ def test_python_versions(): ), ( ci_tests_file, - (f"os: ubuntu-latest\n" f"{12*' '}version: py{supported_latest}"), + (f"os: ubuntu-latest\n{12*' '}version: py{supported_latest}"), ), ( ci_tests_file, - (f"os: macos-latest\n" f"{12*' '}version: py{supported_latest}"), + (f"os: macos-latest\n{12*' '}version: py{supported_latest}"), ), ] @@ -200,7 +187,5 @@ def test_python_versions(): assert tox_text.count(f"{py_version}-lock") == 3 ci_wheels_text = ci_wheels_file.read_text() - (cibw_line,) = ( - line for line in ci_wheels_text.splitlines() if "CIBW_SKIP" in line - ) + (cibw_line,) = (line for line in ci_wheels_text.splitlines() if "CIBW_SKIP" in line) assert all(p not in cibw_line for p in supported_strip) diff --git a/cf_units/tests/test_unit.py b/cf_units/tests/test_unit.py index b8fabcf5..6af60833 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -2,10 +2,7 @@ # # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Test Unit the wrapper class for Unidata udunits2. - -""" +"""Test Unit the wrapper class for Unidata udunits2.""" import copy import datetime as datetime @@ -164,9 +161,7 @@ def test_format_multiple_options(self): def test_format_multiple_options_utf8(self): u = Unit("watt") assert ( - u.format( - [cf_units.UT_NAMES, cf_units.UT_DEFINITION, cf_units.UT_UTF8] - ) + u.format([cf_units.UT_NAMES, cf_units.UT_DEFINITION, cf_units.UT_UTF8]) == "meter²·kilogram·second⁻³" ) @@ -348,7 +343,7 @@ def test_base_10(self): def test_negative(self): u = Unit("hPa") - msg = re.escape("Failed to calculate logorithmic base of Unit('hPa')") + msg = re.escape("Failed to calculate logarithmic base of Unit('hPa')") with pytest.raises(ValueError, match=msg): u.log(-1) @@ -622,13 +617,13 @@ def test_unknown_no_unit(self): def test_not_implemented(self): u = Unit("meter") - assert not (u == {}) + assert u != {} class Test_non_equality: def test_basic(self): u = Unit("meter") - assert not (u != "meter") + assert u == "meter" def test_non_equivalent_units(self): u = Unit("meter") @@ -645,11 +640,11 @@ def test_ne_cross_category(self): def test_unknown(self): u = Unit("unknown") - assert not (u != "unknown") + assert u == "unknown" def test_no_unit(self): u = Unit("no_unit") - assert not (u != "no_unit") + assert u == "no_unit" def test_not_implemented(self): u = Unit("meter") @@ -887,11 +882,9 @@ def test_multidim_masked(self): c = Unit("deg_c") f = Unit("deg_f") - # Manufacture a Fortran-ordered nd array to be converted. + # Manufacture a Fortran-ordered nd-array to be converted. orig = ( - np.ma.masked_array( - np.arange(4, dtype=np.float32), mask=[1, 0, 0, 1] - ) + np.ma.masked_array(np.arange(4, dtype=np.float32), mask=[1, 0, 0, 1]) .reshape([2, 2]) .T ) @@ -972,9 +965,7 @@ def test_num2date_wrong_calendar(self): "hours since 2010-11-02 12:00:00", calendar=cf_units.CALENDAR_360_DAY, ) - with pytest.raises( - ValueError, match="illegal calendar or reference date" - ): + with pytest.raises(ValueError, match="illegal calendar or reference date"): u.num2date( 1, only_use_cftime_datetimes=False, @@ -1004,9 +995,7 @@ def test_num2pydate_wrong_calendar(self): "hours since 2010-11-02 12:00:00", calendar=cf_units.CALENDAR_360_DAY, ) - with pytest.raises( - ValueError, match="illegal calendar or reference date" - ): + with pytest.raises(ValueError, match="illegal calendar or reference date"): u.num2pydate(1) diff --git a/cf_units/tests/unit/test__udunits2.py b/cf_units/tests/unit/test__udunits2.py index 941b6a1d..514954be 100644 --- a/cf_units/tests/unit/test__udunits2.py +++ b/cf_units/tests/unit/test__udunits2.py @@ -6,7 +6,8 @@ In most cases, we don't test the correctness of the operations, only that they return valid objects or raise an -exception where expected.""" +exception where expected. +""" import errno @@ -20,10 +21,7 @@ class Test_get_system: - """ - Test case for operations which create a system object. - - """ + """Test case for operations which create a system object.""" def test_read_xml(self): try: @@ -41,10 +39,7 @@ def test_read_xml_invalid_path(self): class Test_system: - """ - Test case for system operations. - - """ + """Test case for system operations.""" def setup_method(self): try: @@ -77,9 +72,7 @@ def test_parse_ISO_8859_1(self): assert angstrom is not None def test_parse_UTF8(self): - angstrom = _ud.parse( - self.system, b"\xc3\xa5ngstr\xc3\xb6m", _ud.UT_UTF8 - ) + angstrom = _ud.parse(self.system, b"\xc3\xa5ngstr\xc3\xb6m", _ud.UT_UTF8) assert angstrom is not None @@ -89,10 +82,7 @@ def test_parse_invalid_unit(self): class Test_unit: - """ - Test case for unit operations. - - """ + """Test case for unit operations.""" def setup_method(self): try: @@ -222,9 +212,7 @@ def test_encode_date(self): assert self.date_encoding == res_date_encoding def test_encode_clock(self): - res_clock_encoding = _ud.encode_clock( - self.hours, self.minutes, self.seconds - ) + res_clock_encoding = _ud.encode_clock(self.hours, self.minutes, self.seconds) assert self.clock_encoding == res_clock_encoding @@ -259,17 +247,12 @@ def test_decode_time(self): self.minutes, ) assert ( - res_seconds - res_resolution - < self.seconds - < res_seconds + res_resolution + res_seconds - res_resolution < self.seconds < res_seconds + res_resolution ) class Test_convert: - """ - Test case for convert operations. - - """ + """Test case for convert operations.""" def setup_method(self): try: diff --git a/cf_units/util.py b/cf_units/util.py index 8977c351..d1828784 100644 --- a/cf_units/util.py +++ b/cf_units/util.py @@ -2,10 +2,7 @@ # # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -""" -Miscellaneous utility functions. - -""" +"""Miscellaneous utility functions.""" import abc import warnings @@ -13,8 +10,7 @@ def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): - """ - Returns whether two numbers are almost equal, allowing for the + """Returns whether two numbers are almost equal, allowing for the finite precision of floating point numbers. .. deprecated:: 3.2.0 @@ -38,8 +34,7 @@ def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): class _MetaOrderedHashable(abc.ABCMeta): - """ - A metaclass that ensures that non-abstract subclasses of _OrderedHashable + """A metaclass that ensures that non-abstract subclasses of _OrderedHashable without an explicit __init__ method are given a default __init__ method with the appropriate method signature. @@ -74,8 +69,7 @@ def __new__(cls, name, bases, namespace): if "_init" not in namespace: # Create a default _init method for the class method_source = ( - f"def _init(self, {args}):\n " - f"self._init_from_tuple(({args},))" + f"def _init(self, {args}):\n self._init_from_tuple(({args},))" ) exec(method_source, namespace) @@ -83,8 +77,7 @@ def __new__(cls, name, bases, namespace): class _OrderedHashable(Hashable, metaclass=_MetaOrderedHashable): - """ - Convenience class for creating "immutable", hashable, and ordered classes. + """Convenience class for creating "immutable", hashable, and ordered classes. Instance identity is defined by the specific list of attribute names declared in the abstract attribute "_names". Subclasses must declare the @@ -100,5 +93,3 @@ class _OrderedHashable(Hashable, metaclass=_MetaOrderedHashable): its attributes are themselves hashable. """ - - pass diff --git a/pyproject.toml b/pyproject.toml index 0796480c..b48e894b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,10 @@ Discussions = "https://github.com/SciTools/cf-units/discussions" Issues = "https://github.com/SciTools/cf-units/issues" Documentation = "https://cf-units.readthedocs.io" +[tool.codespell] +#ignore-words-list = "Wirth" +skip = 'cf_units/_udunits2_parser/parser/*,cf_units/_udunits2_parser/_antlr4_runtime/*' + [tool.coverage.run] branch = true plugins = [ @@ -115,29 +119,107 @@ local_scheme = "dirty-tag" [tool.ruff] # Ignore generated code. -exclude = [ +extend-exclude = [ "cf_units/_udunits2_parser/parser", "cf_units/_udunits2_parser/_antlr4_runtime", ] -line-length = 79 +line-length = 88 [tool.ruff.lint] select = [ - # pyflakes - "F", - # pycodestyle - "E", - "W", + "ALL", + "D212", # Multi-line docstring summary should start at the first line +# # pyflakes +# "F", +# # pycodestyle +# "E", +# "W", # flake8-bugbear - "B", - # flake8-comprehensions - "C4", - # isort - "I", - # pyupgrade - "UP", +# "B", +# # flake8-comprehensions +## "C4", +# # isort +# "I", +# # pyupgrade +# "UP", +] +ignore = [ + # NOTE: Non-permanent exclusions should be added to the ".ruff.toml" file. + + # flake8-commas (COM) + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "A001", + "ANN001", + "ANN003", # Missing type annotation for `**kwargs` + "ANN201", + "ANN202", + "ANN204", + "ANN205", + "ANN206", # Missing return type annotation for classmethod `_new_from_existing_ut` + "ARG002", # Unused method argument + "B904", # Raise statements in exception handlers that lack a from clause + "COM812", # Trailing comma missing. + "COM819", # Trailing comma prohibited. + "D100", + "D101", + "D102", + "D103", + "D105", + "D205", + "D301", # Use `r"""` if any backslashes in a docstring + "D400", + "D401", + "D404", # First word of the docstring should not be "This" + "DTZ001", + "DTZ006", + "EM101", + "EM102", + "F403", # Wildcard imports + "F405", # Use of name from wildcard import + "FBT002", + "FIX002", # Line contains TODO, consider resolving the issue + "FLY002", # Consider f-string instead of string join + "INP001", # File `cf_units/tests/integration/parse/test_parse.py` is part of an implicit namespace package. Add an `__init__.py`. + "N801", + "N802", + "N803", # Argument name `nextResult` should be lowercase + "N803", # Argument name `offendingSymbol` should be lowercase + "N806", # Variable `MODE_P` in function should be lowercase + "N806", # Variable `TOKEN_P` in function should be lowercase + "PGH004", # Use a colon when specifying `noqa` rule codes + "PLC0414", # Import alias does not rename original package + "PLR0912", # Too many branches (14 > 12) + "PLR0912", # Too many branches (18 > 12) + "PLR0913", + "PLR2004", + "PT006", + "PT011", + "PT012", + "PT019", + "PTH123", + "RET504", # Unnecessary assignment to `contents` before `return` statement + "RET504", # Unnecessary assignment to `result` before `return` statement + "RUF001", + "RUF003", + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "S101", + "S102", + "S310", # Audit URL open for permitted schemes. Allowing use of `file + "S603", + "S607", + "S701", # By default, jinja2 sets `autoescape` to `False`. Consider using `autoescape=True` or the `select_autoescape` function to mitigate XSS vulnerabilities. + "SIM108", # Use ternary operator `op_type = graph.Divide if node.content == "/" else graph.Multiply` instead of `if`-`else`-block + "SIM108", # Use ternary operator `result = handler(content) if callable(handler) else handler` instead of `if`-`else`-block + "SIM108", # Use ternary operator `result = value if inplace else copy.deepcopy(value)` instead of `if`-`else`-block + "SLF001", + "T201", #`print` found + "TD002", #Missing author in TODO; try + "TD003", #Missing issue link on the line following this TODO + "TD004", # Missing colon in TODO + "TRY003", + ] -ignore = ["B904", "F403", "F405"] +preview = false [tool.ruff.lint.isort] known-first-party = ["cf_units"] @@ -222,6 +304,8 @@ checks = [ # Record temporarily ignored checks below; will be reviewed at a later date: "GL08", # The object does not have a docstring + "GL09", # Deprecation warning should precede extended + "PR01", # Parameters {xxxx} not documented "RT01", # No Returns section found @@ -233,6 +317,9 @@ checks = [ "SS05", #Summary must start with infinitive verb, not third person # (e.g. use "Generate" instead of "Generates") + "RT03", # Return value has no description + + "RT05", # Return value description should finish with "." ] exclude = [ From bf35397dc521892d223ccc054b87642802e62e3f Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 12:31:31 +0000 Subject: [PATCH 13/33] Tidy up pyproject.toml. - Removed some duplicate ruff ignores - Reorded some subsections --- pyproject.toml | 87 ++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b48e894b..0b393dc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,29 +125,25 @@ extend-exclude = [ ] line-length = 88 +[tool.ruff.format] +preview = false + [tool.ruff.lint] -select = [ - "ALL", - "D212", # Multi-line docstring summary should start at the first line -# # pyflakes -# "F", -# # pycodestyle -# "E", -# "W", - # flake8-bugbear -# "B", -# # flake8-comprehensions -## "C4", -# # isort -# "I", -# # pyupgrade -# "UP", -] ignore = [ # NOTE: Non-permanent exclusions should be added to the ".ruff.toml" file. # 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): "A001", "ANN001", "ANN003", # Missing type annotation for `**kwargs` @@ -155,11 +151,9 @@ ignore = [ "ANN202", "ANN204", "ANN205", - "ANN206", # Missing return type annotation for classmethod `_new_from_existing_ut` + "ANN206", # Missing return type annotation for classmethod "ARG002", # Unused method argument - "B904", # Raise statements in exception handlers that lack a from clause - "COM812", # Trailing comma missing. - "COM819", # Trailing comma prohibited. + "B904", # Raise statements in exception handlers that lack a from clause "D100", "D101", "D102", @@ -179,17 +173,14 @@ ignore = [ "FBT002", "FIX002", # Line contains TODO, consider resolving the issue "FLY002", # Consider f-string instead of string join - "INP001", # File `cf_units/tests/integration/parse/test_parse.py` is part of an implicit namespace package. Add an `__init__.py`. + "INP001", # part of an implicit namespace package. Add an `__init__.py`. "N801", "N802", - "N803", # Argument name `nextResult` should be lowercase - "N803", # Argument name `offendingSymbol` should be lowercase - "N806", # Variable `MODE_P` in function should be lowercase - "N806", # Variable `TOKEN_P` in function should be lowercase + "N803", # Argument name should be lowercase + "N806", # Variable in function should be lowercase "PGH004", # Use a colon when specifying `noqa` rule codes "PLC0414", # Import alias does not rename original package - "PLR0912", # Too many branches (14 > 12) - "PLR0912", # Too many branches (18 > 12) + "PLR0912", # Too many branches "PLR0913", "PLR2004", "PT006", @@ -197,29 +188,30 @@ ignore = [ "PT012", "PT019", "PTH123", - "RET504", # Unnecessary assignment to `contents` before `return` statement - "RET504", # Unnecessary assignment to `result` before `return` statement + "RET504", # Unnecessary assignment to before statement "RUF001", "RUF003", "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "S101", "S102", - "S310", # Audit URL open for permitted schemes. Allowing use of `file + "S310", # Audit URL open for permitted schemes. "S603", "S607", - "S701", # By default, jinja2 sets `autoescape` to `False`. Consider using `autoescape=True` or the `select_autoescape` function to mitigate XSS vulnerabilities. - "SIM108", # Use ternary operator `op_type = graph.Divide if node.content == "/" else graph.Multiply` instead of `if`-`else`-block - "SIM108", # Use ternary operator `result = handler(content) if callable(handler) else handler` instead of `if`-`else`-block - "SIM108", # Use ternary operator `result = value if inplace else copy.deepcopy(value)` instead of `if`-`else`-block + "S701", # By default, jinja2 sets `autoescape` to `False`. + "SIM108", # Use ternary operator instead of `if`-`else`-block "SLF001", - "T201", #`print` found - "TD002", #Missing author in TODO; try - "TD003", #Missing issue link on the line following this TODO + "T201", # `print` found + "TD002", # Missing author in TODO; try + "TD003", # Missing issue link on the line following this TODO "TD004", # Missing colon in TODO "TRY003", ] preview = false +select = [ + "ALL", + "D212", # Multi-line docstring summary should start at the first line +] [tool.ruff.lint.isort] known-first-party = ["cf_units"] @@ -233,17 +225,19 @@ convention = "numpy" [tool.repo-review] # These are a list of the currently failing tests: ignore = [ - "PP003", # Does not list wheel as a build-dep - # "PP309", # Filter warnings specified - "GH212", # Require GHA update grouping - "PC170", # Uses PyGrep hooks (only needed if rST present) - "PC180", # Uses a markdown formatter - "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "PP003", # Does not list wheel as a build-dep + "GH212", # Require GHA update grouping + "PC170", # Uses PyGrep hooks (only needed if rST present) + "PC180", # Uses a markdown formatter + "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) ] [tool.mypy] disable_error_code = [ - # TODO: Fix these: + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): "arg-type", "assignment", "attr-defined", @@ -301,7 +295,8 @@ checks = [ # -> No Yields section found "YD01", # Not all docstrings require a "Yields" section. - # Record temporarily ignored checks below; will be reviewed at a later date: + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): "GL08", # The object does not have a docstring "GL09", # Deprecation warning should precede extended From 0a10cabaced807a876687f38f7c5d79dfba012d7 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 12:49:49 +0000 Subject: [PATCH 14/33] Updated Ruff config: - Added: `force-sort-within-sections = true` - Auto updated files resultig from above - file specific ignores --- cf_units/__init__.py | 2 +- cf_units/_udunits2_parser/compile.py | 2 +- cf_units/config.py | 2 +- cf_units/tests/integration/parse/test_graph.py | 2 +- cf_units/tests/test_coding_standards.py | 2 +- cf_units/tests/test_unit.py | 2 +- cf_units/util.py | 2 +- pyproject.toml | 18 ++++++++++++++++++ setup.py | 6 ++---- 9 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cf_units/__init__.py b/cf_units/__init__.py index 3d47cbeb..25c26823 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -12,11 +12,11 @@ """ +from contextlib import contextmanager import copy import locale import math import threading -from contextlib import contextmanager from warnings import warn import cftime diff --git a/cf_units/_udunits2_parser/compile.py b/cf_units/_udunits2_parser/compile.py index faa75efa..27a6a4d0 100644 --- a/cf_units/_udunits2_parser/compile.py +++ b/cf_units/_udunits2_parser/compile.py @@ -16,12 +16,12 @@ # ruff: noqa: E501 import collections +from pathlib import Path import re import shutil import subprocess import sys import urllib.request -from pathlib import Path try: import jinja2 diff --git a/cf_units/config.py b/cf_units/config.py index 2c13d2a4..eac8941b 100644 --- a/cf_units/config.py +++ b/cf_units/config.py @@ -5,8 +5,8 @@ import configparser -import sys from pathlib import Path +import sys from tempfile import NamedTemporaryFile diff --git a/cf_units/tests/integration/parse/test_graph.py b/cf_units/tests/integration/parse/test_graph.py index 24923f91..e69ad8ed 100644 --- a/cf_units/tests/integration/parse/test_graph.py +++ b/cf_units/tests/integration/parse/test_graph.py @@ -4,8 +4,8 @@ # See LICENSE in the root of the repository for full licensing details. # ruff: noqa: E402 -import cf_units._udunits2_parser.graph as g from cf_units._udunits2_parser import parse +import cf_units._udunits2_parser.graph as g def test_Node_attributes(): diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 652a40b2..33d0c1b0 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -3,10 +3,10 @@ # This file is part of cf-units and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -import subprocess from datetime import datetime from fnmatch import fnmatch from pathlib import Path +import subprocess import pytest diff --git a/cf_units/tests/test_unit.py b/cf_units/tests/test_unit.py index 6af60833..92e73d27 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -7,8 +7,8 @@ import copy import datetime as datetime import operator -import re from operator import truediv +import re import cftime import numpy as np diff --git a/cf_units/util.py b/cf_units/util.py index d1828784..804fc32b 100644 --- a/cf_units/util.py +++ b/cf_units/util.py @@ -5,8 +5,8 @@ """Miscellaneous utility functions.""" import abc -import warnings from collections.abc import Hashable +import warnings def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): diff --git a/pyproject.toml b/pyproject.toml index 0b393dc7..600cd0d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,8 +214,26 @@ select = [ ] [tool.ruff.lint.isort] +force-sort-within-sections = true known-first-party = ["cf_units"] +[tool.ruff.lint.per-file-ignores] +# All test scripts +"cf_units/tests/*.py" = [ + # https://docs.astral.sh/ruff/rules/undocumented-public-module/ + "D104", # Missing docstring in public module + "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 + "SLOT000", # Subclasses of `str` should define `__slots__` + "N999", # Invalid module name +] + +"setup.py" = [ + "FBT003", # Boolean positional value in function call + "ICN001", # `numpy` should be imported as `np` +] + [tool.ruff.lint.mccabe] max-complexity = 22 diff --git a/setup.py b/setup.py index 2f7c3587..465a83b4 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,11 @@ All other setup configuration is in `pyproject.toml`. """ -import sys from distutils.sysconfig import get_config_var from os import environ from pathlib import Path from shutil import copy +import sys from setuptools import Command, Extension, setup @@ -133,9 +133,7 @@ def _set_builtin(name, value): library_dirs=library_dirs, libraries=["udunits2"], define_macros=DEFINE_MACROS, - runtime_library_dirs=( - None if sys.platform.startswith("win") else library_dirs - ), + runtime_library_dirs=(None if sys.platform.startswith("win") else library_dirs), ) if cythonize: From 7fc50ed26697fbed94909cd7db25ae4c0d7aae5a Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 12:55:20 +0000 Subject: [PATCH 15/33] Auto-corrections for - Trailing witespace - No neline at end of file - Some Ruff linting --- .git_archival.txt | 2 +- .github/CONTRIBUTING.md | 6 +++--- .github/ISSUE_TEMPLATE/bug-report.md | 8 ++++---- .github/workflows/ci-wheels.yml | 2 +- MANIFEST.in | 2 +- cf_units/_udunits2.pxd | 1 - .../_antlr4_runtime/_antlr4_version.txt | 2 +- cf_units/_udunits2_parser/udunits2Lexer.g4.jinja | 2 +- cf_units/_udunits2_parser/udunits2Parser.g4 | 2 +- .../tests/integration/test__Unit_num2date.py | 4 +--- cf_units/tests/integration/test_num2date.py | 4 +--- cf_units/tests/integration/test_num2pydate.py | 4 +--- cf_units/tests/unit/unit/test_Unit.py | 16 ++++------------ codecov.yml | 2 +- 14 files changed, 21 insertions(+), 36 deletions(-) diff --git a/.git_archival.txt b/.git_archival.txt index 8ff24555..3994ec0a 100644 --- a/.git_archival.txt +++ b/.git_archival.txt @@ -1,4 +1,4 @@ node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true)$ -ref-names: $Format:%D$ \ No newline at end of file +ref-names: $Format:%D$ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index adfb2c4b..a9fac57b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,11 +21,11 @@ Submitting changes 1. Push your branch to your fork of cf-units. 1. Submit your pull request. -1. Note that all authors on pull requests will automatically be asked to sign the +1. Note that all authors on pull requests will automatically be asked to sign the [SciTools Contributor Licence Agreement](https://cla-assistant.io/SciTools/) - (CLA), if they have not already done so. + (CLA), if they have not already done so. 1. Chillax! If in doubt, please post in the -[cf-units Discussions space](https://github.com/SciTools/cf-units/discussions) +[cf-units Discussions space](https://github.com/SciTools/cf-units/discussions) and we'll be happy to help you. diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 9ac1a95d..c911f992 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -13,14 +13,14 @@ assignees: '' ## How to Reproduce Steps to reproduce the behaviour: -1. -2. -3. +1. +2. +3. ## Expected Behaviour -## Environment +## Environment - OS & Version: [e.g., Ubuntu 20.04 LTS] - cf-units Version: [e.g., From the command line run `python -c "import cf_units; print(cf_units.__version__)"`] diff --git a/.github/workflows/ci-wheels.yml b/.github/workflows/ci-wheels.yml index bd94cebc..52d58e1d 100644 --- a/.github/workflows/ci-wheels.yml +++ b/.github/workflows/ci-wheels.yml @@ -104,7 +104,7 @@ jobs: - name: "Building sdist" shell: bash run: | - pipx run build --sdist + pipx run build --sdist - uses: actions/upload-artifact@v4 with: diff --git a/MANIFEST.in b/MANIFEST.in index 312f34cb..c6e68033 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,4 +10,4 @@ recursive-include requirements * recursive-include docs * prune docs/build -exclude cf_units/etc/site.cfg \ No newline at end of file +exclude cf_units/etc/site.cfg diff --git a/cf_units/_udunits2.pxd b/cf_units/_udunits2.pxd index 7ce60cd8..801ca2ea 100644 --- a/cf_units/_udunits2.pxd +++ b/cf_units/_udunits2.pxd @@ -89,4 +89,3 @@ cdef extern from "udunits2.h": double* cv_convert_doubles(cv_converter* converter, double* const in_, size_t count, double* out) void cv_free(cv_converter* conv) - diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt b/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt index 012d0f05..d782fca8 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt +++ b/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt @@ -1 +1 @@ -4.11.1 \ No newline at end of file +4.11.1 diff --git a/cf_units/_udunits2_parser/udunits2Lexer.g4.jinja b/cf_units/_udunits2_parser/udunits2Lexer.g4.jinja index 58ad2a3e..226423ee 100644 --- a/cf_units/_udunits2_parser/udunits2Lexer.g4.jinja +++ b/cf_units/_udunits2_parser/udunits2Lexer.g4.jinja @@ -48,7 +48,7 @@ SHIFT_OP: UNICODE_EXPONENT: // One or more ISO-8859-9 encoded exponent characters ('⁻' | '⁺' | '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹' | '⁰')+ -; +; RAISE : ( '^' diff --git a/cf_units/_udunits2_parser/udunits2Parser.g4 b/cf_units/_udunits2_parser/udunits2Parser.g4 index 014581bf..49c65dfa 100644 --- a/cf_units/_udunits2_parser/udunits2Parser.g4 +++ b/cf_units/_udunits2_parser/udunits2Parser.g4 @@ -36,7 +36,7 @@ power: basic_spec: ID | '(' shift_spec ')' -// Log not yet implemented, but it is supported in UDUNITS2. +// Log not yet implemented, but it is supported in UDUNITS2. // | LOGREF product_spec ')' | number ; diff --git a/cf_units/tests/integration/test__Unit_num2date.py b/cf_units/tests/integration/test__Unit_num2date.py index e97cc238..e0617972 100644 --- a/cf_units/tests/integration/test__Unit_num2date.py +++ b/cf_units/tests/integration/test__Unit_num2date.py @@ -252,9 +252,7 @@ def test_fractional_second_360_day(self): def test_pydatetime_wrong_calendar(self): unit = cf_units.Unit("days since 1970-01-01", calendar="360_day") - with pytest.raises( - ValueError, match="illegal calendar or reference date" - ): + with pytest.raises(ValueError, match="illegal calendar or reference date"): unit.num2date( 1, only_use_cftime_datetimes=False, diff --git a/cf_units/tests/integration/test_num2date.py b/cf_units/tests/integration/test_num2date.py index 8cd9ae4c..f831ff29 100644 --- a/cf_units/tests/integration/test_num2date.py +++ b/cf_units/tests/integration/test_num2date.py @@ -11,9 +11,7 @@ class Test: def test_num2date_wrong_calendar(self): - with pytest.raises( - ValueError, match="illegal calendar or reference date" - ): + with pytest.raises(ValueError, match="illegal calendar or reference date"): num2date( 1, "days since 1970-01-01", diff --git a/cf_units/tests/integration/test_num2pydate.py b/cf_units/tests/integration/test_num2pydate.py index 7067c4c9..fbc51949 100644 --- a/cf_units/tests/integration/test_num2pydate.py +++ b/cf_units/tests/integration/test_num2pydate.py @@ -19,7 +19,5 @@ def test_num2pydate_simple(self): assert isinstance(result, datetime.datetime) def test_num2pydate_wrong_calendar(self): - with pytest.raises( - ValueError, match="illegal calendar or reference date" - ): + with pytest.raises(ValueError, match="illegal calendar or reference date"): num2pydate(1, "days since 1970-01-01", calendar="360_day") diff --git a/cf_units/tests/unit/unit/test_Unit.py b/cf_units/tests/unit/unit/test_Unit.py index dc44d6ff..5d80d75c 100644 --- a/cf_units/tests/unit/unit/test_Unit.py +++ b/cf_units/tests/unit/unit/test_Unit.py @@ -212,9 +212,7 @@ class Test_convert__result_ctype: def setup_method(self): self.initial_dtype = np.float32 - self.degs_array = np.array( - [356.7, 356.8, 356.9], dtype=self.initial_dtype - ) + self.degs_array = np.array([356.7, 356.8, 356.9], dtype=self.initial_dtype) self.deg = cf_units.Unit("degrees") self.rad = cf_units.Unit("radians") @@ -254,15 +252,11 @@ def setup_method(self): ) def test_no_type_conversion(self): - result = self.deg.convert( - self.degs_array, self.rad, ctype=cf_units.FLOAT32 - ) + result = self.deg.convert(self.degs_array, self.rad, ctype=cf_units.FLOAT32) np.testing.assert_array_almost_equal(self.rads_array, result) def test_type_conversion(self): - result = self.deg.convert( - self.degs_array, self.rad, ctype=cf_units.FLOAT64 - ) + result = self.deg.convert(self.degs_array, self.rad, ctype=cf_units.FLOAT64) np.testing.assert_array_almost_equal(self.rads_array, result) @@ -270,9 +264,7 @@ class Test_is_long_time_interval: @staticmethod def test_deprecated(): unit = Unit("seconds since epoch") - with pytest.warns( - DeprecationWarning, match="This method is no longer needed" - ): + with pytest.warns(DeprecationWarning, match="This method is no longer needed"): _ = unit.is_long_time_interval() def test_short_time_interval(self): diff --git a/codecov.yml b/codecov.yml index 99cf0038..fbb15dd2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,6 +5,6 @@ coverage: status: project: default: - target: auto + target: auto threshold: 0.5% # coverage can drop by up to % while still posting success patch: off From 1e98bb502fb769565baca1007825865a3c37a747 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 15:54:08 +0000 Subject: [PATCH 16/33] - Update Ruff version. - Add trailing / to mypy exclude paths --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe09a304..f3151eec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: types: [file, rst] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.7.3" + rev: "v0.7.4" hooks: # Run the linter - id: ruff @@ -78,7 +78,7 @@ repos: - id: mypy additional_dependencies: - 'types-requests' - exclude: 'docs|cf_units/_udunits2_parser' + exclude: 'docs/|cf_units/_udunits2_parser/' - repo: https://github.com/abravalheri/validate-pyproject # More exhaustive than Ruff RUF200. diff --git a/pyproject.toml b/pyproject.toml index 600cd0d5..d6214ff1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -274,8 +274,8 @@ enable_error_code = [ "truthy-bool" ] exclude = [ - "docs", - "cf_units/_udunits2_parser", + "docs/", + "cf_units/_udunits2_parser/", ] ignore_missing_imports = true strict = true From b59fca0277c17c826e4be3bf3063be392a893516 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Fri, 15 Nov 2024 18:05:33 +0000 Subject: [PATCH 17/33] Try different approach to MyPy ignore files in .pre-commit config --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3151eec..4e5dc639 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: - id: mypy additional_dependencies: - 'types-requests' - exclude: 'docs/|cf_units/_udunits2_parser/' + exclude: 'docs/|cf_units/_udunits2_parser/parser/.*\.py|cf_units/_udunits2_parser/_antlr4_runtime/.*\.py' - repo: https://github.com/abravalheri/validate-pyproject # More exhaustive than Ruff RUF200. diff --git a/pyproject.toml b/pyproject.toml index d6214ff1..134364e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,6 +280,8 @@ exclude = [ ignore_missing_imports = true strict = true warn_unreachable = true +warn_unused_ignores = true +warn_unused_configs = true [tool.numpydoc_validation] checks = [ From f014c41047c5aa850485ab1be18448a501653843 Mon Sep 17 00:00:00 2001 From: Chris Bunney <48915820+ukmo-ccbunney@users.noreply.github.com> Date: Sat, 16 Nov 2024 22:52:43 +0000 Subject: [PATCH 18/33] Update pyproject.toml Added extra ignores in mypy for _udunits generated code --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 134364e9..83d65584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -283,6 +283,13 @@ warn_unreachable = true warn_unused_ignores = true warn_unused_configs = true +[[tool.mypy.overrides]] +module = [ + "cf_units/_udunits2_parser/parser.*", + "cf_units/_udunits2_parser/_antlr4_runtime.*", +] +ignore_errors = true + [tool.numpydoc_validation] checks = [ "all", # Enable all numpydoc validation rules, apart from the following: From 7e9a9bb34bbcc3219391682cf12e7cebaf16dadd Mon Sep 17 00:00:00 2001 From: Chris Bunney <48915820+ukmo-ccbunney@users.noreply.github.com> Date: Sun, 17 Nov 2024 13:09:33 +0000 Subject: [PATCH 19/33] Update pyproject.toml Re-added PY005 test to sp-repo-review ignore list. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 83d65584..86331075 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,6 +245,7 @@ convention = "numpy" ignore = [ # TODO: exceptions that still need investigating are below. # Might be fixable, or might become permanent (above): + "PY005", # Has tests folder "PP003", # Does not list wheel as a build-dep "GH212", # Require GHA update grouping "PC170", # Uses PyGrep hooks (only needed if rST present) From 89afe5ae45e1ba279af0803f0abb6cfa6da92640 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 12:58:34 +0000 Subject: [PATCH 20/33] Roll back changes to _antlr4_runtime and add .pre-commit exclusion for this directory --- .pre-commit-config.yaml | 3 +++ cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py | 2 +- cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt | 2 +- .../_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e5dc639..d0645d33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,9 @@ ci: autofix_prs: false autoupdate_commit_msg: "chore: update pre-commit hooks" +exclude: 'cf_units/_udunits2_parser/_antlr4_runtime/' + + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py b/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py index 0a7f62ad..f827ec86 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py +++ b/cf_units/_udunits2_parser/_antlr4_runtime/Recognizer.py @@ -7,7 +7,7 @@ from .RuleContext import RuleContext from .Token import Token -# need forward declaration +# need forward delcaration RecognitionException = None diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt b/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt index d782fca8..012d0f05 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt +++ b/cf_units/_udunits2_parser/_antlr4_runtime/_antlr4_version.txt @@ -1 +1 @@ -4.11.1 +4.11.1 \ No newline at end of file diff --git a/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py b/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py index 31354c24..642239c2 100644 --- a/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py +++ b/cf_units/_udunits2_parser/_antlr4_runtime/error/ErrorStrategy.py @@ -212,7 +212,7 @@ def sync(self, recognizer: Parser): s = recognizer._interp.atn.states[recognizer.state] la = recognizer.getTokenStream().LA(1) - # try cheaper subset first; might get lucky. seems to shave a small bit off + # try cheaper subset first; might get lucky. seems to shave a wee bit off nextTokens = recognizer.atn.nextTokens(s) if la in nextTokens: self.nextTokensContext = None From 2749787e5e2513005bacc2f827bdecdad4030bee Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 13:02:19 +0000 Subject: [PATCH 21/33] Added reference comment to .pre-commit-config.yaml --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0645d33..b6c578a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ # 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 for repo-review ci: From 843e6c2025d5a247d1d3b3bdc739d9ae1623ce6d Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 13:05:26 +0000 Subject: [PATCH 22/33] Removed redundant setting --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 86331075..c6e25812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,6 @@ Issues = "https://github.com/SciTools/cf-units/issues" Documentation = "https://cf-units.readthedocs.io" [tool.codespell] -#ignore-words-list = "Wirth" skip = 'cf_units/_udunits2_parser/parser/*,cf_units/_udunits2_parser/_antlr4_runtime/*' [tool.coverage.run] From 5a955ff4ef4507f532a4fa2d0bb7f6deb8a936df Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 13:08:18 +0000 Subject: [PATCH 23/33] Removed comment --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6e25812..c677db5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,8 +129,6 @@ preview = false [tool.ruff.lint] ignore = [ - # NOTE: Non-permanent exclusions should be added to the ".ruff.toml" file. - # flake8-commas (COM) # https://docs.astral.sh/ruff/rules/#flake8-commas-com "COM812", # Trailing comma missing. From 01fe1b74a18989b000be90a2740f129359de1de4 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 13:26:43 +0000 Subject: [PATCH 24/33] Alphabetise repo-review ignore list --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c677db5c..4dbb0d2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -242,12 +242,12 @@ convention = "numpy" ignore = [ # TODO: exceptions that still need investigating are below. # Might be fixable, or might become permanent (above): - "PY005", # Has tests folder - "PP003", # Does not list wheel as a build-dep "GH212", # Require GHA update grouping + "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) "PC170", # Uses PyGrep hooks (only needed if rST present) "PC180", # Uses a markdown formatter - "MY105", # MyPy enables redundant-expr (TODO: see MyPy ignore below) + "PP003", # Does not list wheel as a build-dep + "PY005", # Has tests folder ] [tool.mypy] From d831bfb04f7cf999018d588248c36649362c7d46 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 13:44:36 +0000 Subject: [PATCH 25/33] Ahphabetised pre-commit hooks, as per template --- .pre-commit-config.yaml | 61 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b6c578a8..8b446319 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,7 @@ ci: exclude: 'cf_units/_udunits2_parser/_antlr4_runtime/' +minimum_pre_commit_version: 1.21.0 repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -39,19 +40,8 @@ repos: # Duplicates Ruff W291 but also works on non-Python files. - id: trailing-whitespace - -- repo: https://github.com/aio-libs/sort-all - rev: v1.3.0 - hooks: - - id: sort-all - types: [file, python] - -- repo: https://github.com/scientific-python/cookie - rev: 2024.08.19 - hooks: - - id: sp-repo-review - additional_dependencies: ["repo-review[cli]"] # TODO: Only neededed if extra dependencies are required - #args: ["--show=errskip"] # show everything for the moment +# Hooks from all other repos +# NOTE : keep these in hook-name (aka 'id') order - repo: https://github.com/adamchainz/blacken-docs rev: 1.19.1 @@ -59,16 +49,6 @@ repos: - id: blacken-docs types: [file, rst] -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.7.4" - hooks: - # Run the linter - - id: ruff - types: [file, python] - args: [--fix, --show-fixes] - # Run the formatter. - - id: ruff-format - - repo: https://github.com/codespell-project/codespell rev: "v2.3.0" hooks: @@ -84,14 +64,37 @@ repos: - 'types-requests' exclude: 'docs/|cf_units/_udunits2_parser/parser/.*\.py|cf_units/_udunits2_parser/_antlr4_runtime/.*\.py' -- repo: https://github.com/abravalheri/validate-pyproject - # More exhaustive than Ruff RUF200. - rev: "v0.23" - hooks: - - id: validate-pyproject - - repo: https://github.com/numpy/numpydoc rev: v1.8.0 hooks: - id: numpydoc-validation types: [file, python] + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.7.4" + hooks: + # Run the linter + - id: ruff + types: [file, python] + args: [--fix, --show-fixes] + # Run the formatter. + - id: ruff-format + +- repo: https://github.com/aio-libs/sort-all + rev: v1.3.0 + hooks: + - id: sort-all + types: [file, python] + +- repo: https://github.com/scientific-python/cookie + rev: 2024.08.19 + hooks: + - id: sp-repo-review + additional_dependencies: ["repo-review[cli]"] # TODO: Only neededed if extra dependencies are required + #args: ["--show=errskip"] # show everything for the moment + +- repo: https://github.com/abravalheri/validate-pyproject + # More exhaustive than Ruff RUF200. + rev: "v0.23" + hooks: + - id: validate-pyproject From 9ab7ec39a759d352b5f1912a6f3d4332f5356d43 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 14:32:51 +0000 Subject: [PATCH 26/33] Added explanation comments to Ruff ignores --- pyproject.toml | 81 +++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4dbb0d2c..65426755 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,68 +141,67 @@ ignore = [ # TODO: exceptions that still need investigating are below. # Might be fixable, or might become permanent (above): - "A001", - "ANN001", - "ANN003", # Missing type annotation for `**kwargs` - "ANN201", - "ANN202", - "ANN204", - "ANN205", - "ANN206", # Missing return type annotation for classmethod + "A001", # builtin-variable-shadowing + "ANN001", # missing-type-function-argument + "ANN003", # missing type annotation for `**kwargs` + "ANN201", # missing-return-type-undocumented-public-function + "ANN202", # missing-return-type-private-function + "ANN204", # missing-return-type-special-method + "ANN205", # missing-return-type-static-method + "ANN206", # missing-return-type-class-method "ARG002", # Unused method argument - "B904", # Raise statements in exception handlers that lack a from clause - "D100", - "D101", - "D102", - "D103", - "D105", - "D205", + "B904", # raise-without-from-inside-except + "D100", # undocumented-public-module + "D101", # undocumented-public-class + "D102", # undocumented-public-method + "D103", # undocumented-public-function + "D105", # undocumented-magic-method + "D205", # blank-line-after-summary "D301", # Use `r"""` if any backslashes in a docstring - "D400", - "D401", - "D404", # First word of the docstring should not be "This" - "DTZ001", - "DTZ006", - "EM101", - "EM102", + "D400", # ends-in-period (docstring) + "D401", # non-imperative-mood + "D404", # first-line-capitalized + "DTZ001", # call-datetime-without-tzinfo + "DTZ006", # call-datetime-fromtimestamp + "EM101", # raw-string-in-exception + "EM102", # f-string-in-exception "F403", # Wildcard imports "F405", # Use of name from wildcard import - "FBT002", + "FBT002", # boolean-default-value-positional-argument "FIX002", # Line contains TODO, consider resolving the issue "FLY002", # Consider f-string instead of string join "INP001", # part of an implicit namespace package. Add an `__init__.py`. - "N801", - "N802", + "N801", # invalid-class-name + "N802", # invalid-function-name "N803", # Argument name should be lowercase "N806", # Variable in function should be lowercase "PGH004", # Use a colon when specifying `noqa` rule codes "PLC0414", # Import alias does not rename original package "PLR0912", # Too many branches - "PLR0913", - "PLR2004", - "PT006", - "PT011", - "PT012", - "PT019", - "PTH123", + "PLR0913", # too-many-argument + "PLR2004", # magic-value-comparison + "PT006", # pytest-parametrize-names-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT019", # pytest-fixture-param-without-value + "PTH123", # builtin-open "RET504", # Unnecessary assignment to before statement - "RUF001", - "RUF003", + "RUF001", # ambiguous-unicode-character-string + "RUF003", # ambiguous-unicode-character-comment "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` - "S101", - "S102", + "S101", # Assert used + "S102", # exec used "S310", # Audit URL open for permitted schemes. - "S603", - "S607", + "S603", # subprocess-without-shell-equals-true + "S607", # start-process-with-partial-path "S701", # By default, jinja2 sets `autoescape` to `False`. "SIM108", # Use ternary operator instead of `if`-`else`-block - "SLF001", + "SLF001", # private-member-access "T201", # `print` found "TD002", # Missing author in TODO; try "TD003", # Missing issue link on the line following this TODO "TD004", # Missing colon in TODO - "TRY003", - + "TRY003", # raise-vanilla-args ] preview = false select = [ From 2fc5e4f437897771016add2020230b0ec3e7cef1 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 14:52:27 +0000 Subject: [PATCH 27/33] Address Ruff A001 errors --- cf_units/tests/test_unit.py | 4 ++-- docs/source/conf.py | 4 +++- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cf_units/tests/test_unit.py b/cf_units/tests/test_unit.py index 92e73d27..079f5441 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -936,8 +936,8 @@ def test_encode_clock(self): def test_decode_time(self): result = cf_units.decode_time(158976000.0 + 43560.0) - year, month, day, hour, min, sec, res = result - assert (year, month, day, hour, min, sec) == (2006, 1, 15, 12, 6, 0) + year, month, day, hour, minute, sec, res = result + assert (year, month, day, hour, minute, sec) == (2006, 1, 15, 12, 6, 0) class TestNumsAndDates: diff --git a/docs/source/conf.py b/docs/source/conf.py index 207142c9..524dc0ef 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,9 @@ master_doc = "index" project = "cf-units" -copyright = "Copyright cf-units contributors" +# Note: `project_copyright` can also be used to avoid Ruff "variable shadowing" +# error; https://github.com/sphinx-doc/sphinx/issues/9845#issuecomment-970391099 +copyright = "Copyright cf-units contributors" # noqa: A001 current_version = metadata.version("cf_units") version = current_version.split("+")[0] diff --git a/pyproject.toml b/pyproject.toml index 65426755..5ef49aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,7 @@ ignore = [ # TODO: exceptions that still need investigating are below. # Might be fixable, or might become permanent (above): - "A001", # builtin-variable-shadowing + #"A001", # builtin-variable-shadowing "ANN001", # missing-type-function-argument "ANN003", # missing type annotation for `**kwargs` "ANN201", # missing-return-type-undocumented-public-function From dcaefcd25cb059181b5eedbfa5aaaf949c18298a Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 15:03:27 +0000 Subject: [PATCH 28/33] Address D301 - Use `r"""` if any backslashes in a docstring --- cf_units/tests/test_coding_standards.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 33d0c1b0..b376062b 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -31,7 +31,7 @@ class TestLicenseHeaders: @staticmethod def whatchanged_parse(whatchanged_output): - """Returns a generator of tuples of data parsed from + r"""Returns a generator of tuples of data parsed from "git whatchanged --pretty='TIME:%at'". The tuples are of the form ``(filename, last_commit_datetime)`` diff --git a/pyproject.toml b/pyproject.toml index 5ef49aac..a0f926b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,7 @@ ignore = [ "D103", # undocumented-public-function "D105", # undocumented-magic-method "D205", # blank-line-after-summary - "D301", # Use `r"""` if any backslashes in a docstring + #"D301", # Use `r"""` if any backslashes in a docstring "D400", # ends-in-period (docstring) "D401", # non-imperative-mood "D404", # first-line-capitalized From c8f90b85ace463cdc5677c75db7d84b17282012b Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 15:56:52 +0000 Subject: [PATCH 29/33] Addressed D404 docstring-starts-with-this --- cf_units/_udunits2_parser/graph.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cf_units/_udunits2_parser/graph.py b/cf_units/_udunits2_parser/graph.py index 4deb2513..c8b82d47 100644 --- a/cf_units/_udunits2_parser/graph.py +++ b/cf_units/_udunits2_parser/graph.py @@ -90,7 +90,7 @@ class Timestamp(Terminal): class Visitor: - """This class may be used to help traversing an expression graph. + """Utiltiy class to help traverse an expression graph. It follows the same pattern as the Python ``ast.NodeVisitor``. Users should typically not need to override either ``visit`` or diff --git a/pyproject.toml b/pyproject.toml index a0f926b9..42b7ccdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,7 +160,7 @@ ignore = [ #"D301", # Use `r"""` if any backslashes in a docstring "D400", # ends-in-period (docstring) "D401", # non-imperative-mood - "D404", # first-line-capitalized + #"D404", # docstring-starts-with-this "DTZ001", # call-datetime-without-tzinfo "DTZ006", # call-datetime-fromtimestamp "EM101", # raw-string-in-exception From 6f01fac1296aca3de2e6dc1df7a9b6f42c6c771a Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 16:34:40 +0000 Subject: [PATCH 30/33] Fixes for "D401", # non-imperative-mood --- cf_units/__init__.py | 24 ++++++++++++------------ cf_units/_udunits2_parser/__init__.py | 2 +- cf_units/_udunits2_parser/graph.py | 2 +- cf_units/config.py | 2 +- cf_units/util.py | 2 +- pyproject.toml | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cf_units/__init__.py b/cf_units/__init__.py index 25c26823..a53bbd6e 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -502,7 +502,7 @@ def num2pydate(time_value, unit, calendar): def as_unit(unit): - """Returns a Unit corresponding to the given unit. + """Return a Unit corresponding to the given unit. .. note:: @@ -869,7 +869,7 @@ def is_time_reference(self): return self.calendar is not None def is_long_time_interval(self): - """Defines whether this unit describes a time unit with a long time + """Define whether this unit describes a time unit with a long time interval ("months" or "years"). These long time intervals *are* supported by `UDUNITS2` but are not supported by `cftime`. This discrepancy means we cannot run self.num2date() on a time unit with @@ -1196,7 +1196,7 @@ def definition(self): return result def offset_by_time(self, origin): - """Returns the time unit offset with respect to the time origin. + """Return the time unit offset with respect to the time origin. Args: @@ -1253,7 +1253,7 @@ def invert(self): return result def root(self, root): - """Returns the given root of the unit. + """Return the given root of the unit. Args: @@ -1298,7 +1298,7 @@ def root(self, root): return result def log(self, base): - """Returns the logarithmic unit corresponding to the given + """Return the logarithmic unit corresponding to the given logarithmic base. Args: @@ -1337,7 +1337,7 @@ def log(self, base): return result def __str__(self): - """Returns a simple string representation of the unit. + """Return a simple string representation of the unit. Returns ------- @@ -1354,7 +1354,7 @@ def __str__(self): return self.origin or self.symbol def __repr__(self): - """Returns a string representation of the unit object. + """Return a string representation of the unit object. Returns ------- @@ -1638,7 +1638,7 @@ def __ne__(self, other): def change_calendar(self, calendar): """ - Returns a new unit with the requested calendar, modifying the + Return a new unit with the requested calendar, modifying the reference date if necessary. Only works with calendars that represent the real world (standard, proleptic_gregorian, julian) and with short time intervals (days or less). @@ -1662,7 +1662,7 @@ def change_calendar(self, calendar): return Unit(new_origin, calendar=calendar) def convert(self, value, other, ctype=FLOAT64, inplace=False): - """Converts a single value or NumPy array of values from the current unit + """Convert a single value or NumPy array of values from the current unit to the other target unit. If the units are not convertible, then no conversion will take place. @@ -1800,7 +1800,7 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): @property def cftime_unit(self): - """Returns a string suitable for passing as a unit to cftime.num2date and + """Return a string suitable for passing as a unit to cftime.num2date and cftime.date2num. """ @@ -1814,7 +1814,7 @@ def cftime_unit(self): return str(self).rstrip(" UTC") def date2num(self, date): - """Returns the numeric time value calculated from the datetime + """Return the numeric time value calculated from the datetime object using the current calendar and unit time reference. The current unit time reference must be of the form: @@ -1863,7 +1863,7 @@ def num2date( only_use_cftime_datetimes=True, only_use_python_datetimes=False, ): - """Returns a datetime-like object calculated from the numeric time + """Return a datetime-like object calculated from the numeric time value using the current calendar and the unit time reference. The current unit time reference must be of the form: diff --git a/cf_units/_udunits2_parser/__init__.py b/cf_units/_udunits2_parser/__init__.py index 059053f0..1de4ed84 100644 --- a/cf_units/_udunits2_parser/__init__.py +++ b/cf_units/_udunits2_parser/__init__.py @@ -169,7 +169,7 @@ def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): def _debug_tokens(unit_string): - """A really handy way of printing the tokens produced for a given input.""" + """A really handy way of printing the tokens produced for a given input.""" # noqa: D401 unit_str = unit_string.strip() lexer = udunits2Lexer(InputStream(unit_str)) stream = CommonTokenStream(lexer) diff --git a/cf_units/_udunits2_parser/graph.py b/cf_units/_udunits2_parser/graph.py index c8b82d47..a24b32f9 100644 --- a/cf_units/_udunits2_parser/graph.py +++ b/cf_units/_udunits2_parser/graph.py @@ -108,7 +108,7 @@ def visit(self, node): return visitor(node) def generic_visit(self, node): - """Called if no explicit visitor function exists for a node. + """Call if no explicit visitor function exists for a node. Can also be called by ``visit_`` implementations if children of the node are to be processed. diff --git a/cf_units/config.py b/cf_units/config.py index eac8941b..41176125 100644 --- a/cf_units/config.py +++ b/cf_units/config.py @@ -12,7 +12,7 @@ # Returns simple string options. def get_option(section, option, default=None): - """Returns the option value for the given section, or the default value + """Return the option value for the given section, or the default value if the section/option is not present. """ diff --git a/cf_units/util.py b/cf_units/util.py index 804fc32b..60eb69d8 100644 --- a/cf_units/util.py +++ b/cf_units/util.py @@ -10,7 +10,7 @@ def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): - """Returns whether two numbers are almost equal, allowing for the + """Return whether two numbers are almost equal, allowing for the finite precision of floating point numbers. .. deprecated:: 3.2.0 diff --git a/pyproject.toml b/pyproject.toml index 42b7ccdb..a065c7a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ ignore = [ "D205", # blank-line-after-summary #"D301", # Use `r"""` if any backslashes in a docstring "D400", # ends-in-period (docstring) - "D401", # non-imperative-mood + #"D401", # non-imperative-mood #"D404", # docstring-starts-with-this "DTZ001", # call-datetime-without-tzinfo "DTZ006", # call-datetime-fromtimestamp From f7281a5f263bdb0982fc32aeeabc65184965b039 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 16:44:25 +0000 Subject: [PATCH 31/33] Addressed "FBT002", # boolean-default-value-positional-argument. - All instances fixed with `noqa: FBT002` as would affect API --- cf_units/__init__.py | 10 +++++----- cf_units/tests/integration/test__Unit_num2date.py | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cf_units/__init__.py b/cf_units/__init__.py index a53bbd6e..ab069173 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -401,8 +401,8 @@ def num2date( time_value, unit, calendar, - only_use_cftime_datetimes=True, - only_use_python_datetimes=False, + only_use_cftime_datetimes=True, # noqa: FBT002 + only_use_python_datetimes=False, # noqa: FBT002 ): """Return datetime encoding of numeric time value (resolution of 1 second). @@ -1661,7 +1661,7 @@ def change_calendar(self, calendar): return Unit(new_origin, calendar=calendar) - def convert(self, value, other, ctype=FLOAT64, inplace=False): + def convert(self, value, other, ctype=FLOAT64, inplace=False): # noqa: FBT002 """Convert a single value or NumPy array of values from the current unit to the other target unit. @@ -1860,8 +1860,8 @@ def date2num(self, date): def num2date( self, time_value, - only_use_cftime_datetimes=True, - only_use_python_datetimes=False, + only_use_cftime_datetimes=True, # noqa: FBT002 + only_use_python_datetimes=False, # noqa: FBT002 ): """Return a datetime-like object calculated from the numeric time value using the current calendar and the unit time reference. diff --git a/cf_units/tests/integration/test__Unit_num2date.py b/cf_units/tests/integration/test__Unit_num2date.py index e0617972..53da89c4 100644 --- a/cf_units/tests/integration/test__Unit_num2date.py +++ b/cf_units/tests/integration/test__Unit_num2date.py @@ -20,7 +20,7 @@ def setup_units(self, calendar): self.uhours = cf_units.Unit("hours since 1970-01-01", calendar) self.udays = cf_units.Unit("days since 1970-01-01", calendar) - def check_dates(self, nums, units, expected, only_cftime=True): + def check_dates(self, nums, units, expected, only_cftime=True): # noqa: FBT002 for num, unit, exp in zip(nums, units, expected, strict=False): res = unit.num2date(num, only_use_cftime_datetimes=only_cftime) assert exp == res diff --git a/pyproject.toml b/pyproject.toml index a065c7a8..5ce9a361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,7 +167,7 @@ ignore = [ "EM102", # f-string-in-exception "F403", # Wildcard imports "F405", # Use of name from wildcard import - "FBT002", # boolean-default-value-positional-argument +# "FBT002", # boolean-default-value-positional-argument "FIX002", # Line contains TODO, consider resolving the issue "FLY002", # Consider f-string instead of string join "INP001", # part of an implicit namespace package. Add an `__init__.py`. From d57a90cdcb2354c5a8ba7e06a9db9bb414c117b0 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 17:05:33 +0000 Subject: [PATCH 32/33] Added noqa exception for FLY002 - alternatives are not great: - f-strings don't really help (it would be a mess) - triple quotes preserve indentation - backslash at end of string for continuation is nasty and requires manually adding \n newlines --- cf_units/_udunits2_parser/compile.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cf_units/_udunits2_parser/compile.py b/cf_units/_udunits2_parser/compile.py index 27a6a4d0..bff66b92 100644 --- a/cf_units/_udunits2_parser/compile.py +++ b/cf_units/_udunits2_parser/compile.py @@ -73,7 +73,7 @@ def fixup_antlr_imports(antlr_file_path: Path, contents: str) -> str: if antlr_file_path.name == "XPathLexer.py": contents = contents.replace( "from antlr4 import *", - "\n".join( + "\n".join( # noqa: FLY002 [ "from antlr4.Lexer import Lexer", "from antlr4.atn.ATNDeserializer import ATNDeserializer", @@ -92,7 +92,7 @@ def fixup_antlr_imports(antlr_file_path: Path, contents: str) -> str: "from antlr4 import CommonTokenStream, DFA, " "PredictionContextCache, " "Lexer, LexerATNSimulator, ParserRuleContext, TerminalNode", - "\n".join( + "\n".join( # noqa: FLY002 [ "from antlr4.Lexer import Lexer", "from antlr4.CommonTokenStream import CommonTokenStream", diff --git a/pyproject.toml b/pyproject.toml index 5ce9a361..33caad84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,7 +169,7 @@ ignore = [ "F405", # Use of name from wildcard import # "FBT002", # boolean-default-value-positional-argument "FIX002", # Line contains TODO, consider resolving the issue - "FLY002", # Consider f-string instead of string join +# "FLY002", # Consider f-string instead of string join "INP001", # part of an implicit namespace package. Add an `__init__.py`. "N801", # invalid-class-name "N802", # invalid-function-name From 2f6736efbfb09a929d6453f210c56fe5350add42 Mon Sep 17 00:00:00 2001 From: "ukmo-chris.bunney" Date: Tue, 19 Nov 2024 17:26:57 +0000 Subject: [PATCH 33/33] Fixes: "EM101" raw-string-in-exception --- cf_units/__init__.py | 42 ++++++++++++++++--------- cf_units/_udunits2_parser/compile.py | 3 +- cf_units/tests/test_coding_standards.py | 3 +- pyproject.toml | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/cf_units/__init__.py b/cf_units/__init__.py index ab069173..05ea6bf4 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -1216,7 +1216,8 @@ def offset_by_time(self, origin): """ if not isinstance(origin, float | int): - raise TypeError("a numeric type for the origin argument is required") + msg = "a numeric type for the origin argument is required" + raise TypeError(msg) try: ut_unit = _ud.offset_by_time(self.ut_unit, origin) except _ud.UdunitsError as exception: @@ -1244,7 +1245,8 @@ def invert(self): if self.is_unknown(): result = self elif self.is_no_unit(): - raise ValueError("Cannot invert a 'no-unit'.") + msg = "Cannot invert a 'no-unit'." + raise ValueError(msg) else: ut_unit = _ud.invert(self.ut_unit) result = Unit._new_from_existing_ut( @@ -1276,11 +1278,13 @@ def root(self, root): """ if round(root) != root: - raise TypeError("An integer for the root argument is required") + msg = "An integer for the root argument is required" + raise TypeError(msg) if self.is_unknown(): result = self elif self.is_no_unit(): - raise ValueError("Cannot take the root of a 'no-unit'.") + msg = "Cannot take the root of a 'no-unit'." + raise ValueError(msg) # only update the unit if it is not scalar elif self == Unit("1"): result = self @@ -1320,12 +1324,14 @@ def log(self, base): if self.is_unknown(): result = self elif self.is_no_unit(): - raise ValueError("Cannot take the logarithm of a 'no-unit'.") + msg = "Cannot take the logarithm of a 'no-unit'." + raise ValueError(msg) else: try: ut_unit = _ud.log(base, self.ut_unit) except TypeError: - raise TypeError("A numeric type for the base argument is required") + msg = "A numeric type for the base argument is required" + raise TypeError(msg) except _ud.UdunitsError as exception: value_err = _ud_value_error( exception, @@ -1378,7 +1384,8 @@ def _offset_common(self, offset): if self.is_unknown(): result = self elif self.is_no_unit(): - raise ValueError("Cannot offset a 'no-unit'.") + msg = "Cannot offset a 'no-unit'." + raise ValueError(msg) else: try: ut_unit = _ud.offset(self.ut_unit, offset) @@ -1540,12 +1547,14 @@ def __pow__(self, power): try: power = float(power) except ValueError: - raise TypeError("A numeric value is required for the power argument.") + msg = "A numeric value is required for the power argument." + raise TypeError(msg) if self.is_unknown(): result = self elif self.is_no_unit(): - raise ValueError("Cannot raise the power of a 'no-unit'.") + msg = "Cannot raise the power of a 'no-unit'." + raise ValueError(msg) elif self == Unit("1"): # 1 ** N -> 1 result = self @@ -1555,7 +1564,8 @@ def __pow__(self, power): # root. elif not math.isclose(power, 0.0) and abs(power) < 1: if not math.isclose(1 / power, round(1 / power)): - raise ValueError("Cannot raise a unit by a decimal.") + msg = "Cannot raise a unit by a decimal." + raise ValueError(msg) root = int(round(1 / power)) result = self.root(root) else: @@ -1652,7 +1662,8 @@ def change_calendar(self, calendar): """ # NOQA E501 if not self.is_time_reference(): - raise ValueError("unit is not a time reference") + msg = "unit is not a time reference" + raise ValueError(msg) ref_date = self.num2date(0) new_ref_date = ref_date.change_calendar(calendar) @@ -1760,11 +1771,12 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): # noqa: FBT002 # with endianness other than native. if result.dtype.byteorder != "=": if inplace: - raise ValueError( + msg = str( "Unable to convert non-native byte ordered " "array in-place. Consider byte-swapping " "first." ) + raise ValueError(msg) result = result.astype(result.dtype.type) # Strict type check of numpy array. if result.dtype.type not in (np.float32, np.float64): @@ -1788,10 +1800,11 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): # noqa: FBT002 result[...] = result_tmp else: if ctype not in _cv_convert_scalar: - raise ValueError( + msg = str( "Invalid target type. Can only " "convert to float or double." ) + raise ValueError(msg) # Utilise global convenience dictionary # _cv_convert_scalar result = _cv_convert_scalar[ctype](ut_converter, result) @@ -1805,7 +1818,8 @@ def cftime_unit(self): """ if self.calendar is None: - raise ValueError("Unit has undefined calendar") + msg = "Unit has undefined calendar" + raise ValueError(msg) # # ensure to strip out non-parsable 'UTC' postfix, which diff --git a/cf_units/_udunits2_parser/compile.py b/cf_units/_udunits2_parser/compile.py index bff66b92..b1950b95 100644 --- a/cf_units/_udunits2_parser/compile.py +++ b/cf_units/_udunits2_parser/compile.py @@ -26,7 +26,8 @@ try: import jinja2 except ImportError: - raise ImportError("Jinja2 needed to compile the grammar.") + msg = "Jinja2 needed to compile the grammar." + raise ImportError(msg) ANTLR_VERSION = "4.11.1" JAR_NAME = f"antlr-{ANTLR_VERSION}-complete.jar" diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index b376062b..c1dbe9b5 100644 --- a/cf_units/tests/test_coding_standards.py +++ b/cf_units/tests/test_coding_standards.py @@ -109,7 +109,8 @@ def test_license_headers(self): failed = True if failed: - raise AssertionError("There were license header failures. See stdout.") + msg = "There were license header failures. See stdout." + raise AssertionError(msg) @pytest.mark.skipif(not IS_GIT_REPO, reason="Not a git repository.") diff --git a/pyproject.toml b/pyproject.toml index 33caad84..3ff09088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,7 +163,7 @@ ignore = [ #"D404", # docstring-starts-with-this "DTZ001", # call-datetime-without-tzinfo "DTZ006", # call-datetime-fromtimestamp - "EM101", # raw-string-in-exception + #"EM101", # raw-string-in-exception "EM102", # f-string-in-exception "F403", # Wildcard imports "F405", # Use of name from wildcard import