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/.pre-commit-config.yaml b/.pre-commit-config.yaml index eea99b2b..8b446319 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,16 @@ # 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: + autofix_prs: false + autoupdate_commit_msg: "chore: update pre-commit hooks" + +exclude: 'cf_units/_udunits2_parser/_antlr4_runtime/' + +minimum_pre_commit_version: 1.21.0 + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -16,21 +25,23 @@ 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 - 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 @@ -38,6 +49,27 @@ repos: - id: blacken-docs types: [file, rst] +- repo: https://github.com/codespell-project/codespell + rev: "v2.3.0" + hooks: + - 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 + additional_dependencies: + - 'types-requests' + exclude: 'docs/|cf_units/_udunits2_parser/parser/.*\.py|cf_units/_udunits2_parser/_antlr4_runtime/.*\.py' + +- 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: @@ -47,3 +79,22 @@ repos: 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 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/__init__.py b/cf_units/__init__.py index b5dddaa7..05ea6bf4 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. @@ -13,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 @@ -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 @@ -409,11 +401,10 @@ 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). + """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. + """Return 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 + """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 @@ -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. + """Return 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,23 @@ 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" - ) + 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: - 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: @@ -1277,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( @@ -1286,14 +1255,14 @@ def invert(self): return result def root(self, root): - """ - Returns the given root of the unit. + """Return the given root of the unit. Args: * root (int): Value by which the unit root is taken. - Returns: + Returns + ------- None. For example: @@ -1309,40 +1278,39 @@ 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 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. + """Return 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: @@ -1356,31 +1324,29 @@ 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, - 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. + """Return a simple string representation of the unit. - Returns: + Returns + ------- string. For example: @@ -1394,10 +1360,10 @@ 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: + Returns + ------- string. For example: @@ -1411,17 +1377,15 @@ 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): 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) @@ -1446,7 +1410,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 +1432,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 +1442,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 +1453,8 @@ def __mul__(self, other): * other (int/float/string/Unit): Multiplication scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1507,8 +1469,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 +1479,8 @@ def __div__(self, other): * other (int/float/string/Unit): Division scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1533,8 +1495,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 +1505,8 @@ def __truediv__(self, other): * other (int/float/string/Unit): Division scale factor or unit. - Returns: + Returns + ------- Unit. For example: @@ -1559,8 +1521,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 +1532,8 @@ def __pow__(self, power): * power (int/float): Value by which the unit power is raised. - Returns: + Returns + ------- Unit. For example: @@ -1585,55 +1547,55 @@ 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 + # 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)): + msg = "Cannot raise a unit by a decimal." + raise ValueError(msg) + 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 +1625,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: @@ -1686,7 +1648,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). @@ -1700,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) @@ -1709,9 +1672,8 @@ 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 + 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. If the units are not convertible, then no conversion will take place. @@ -1732,7 +1694,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 +1753,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, @@ -1810,13 +1771,13 @@ def convert(self, value, other, ctype=FLOAT64, inplace=False): # 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." ) - else: - result = result.astype(result.dtype.type) + 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): raise TypeError( @@ -1828,43 +1789,37 @@ 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: 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) 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 + """Return a string suitable for passing as a unit to cftime.num2date and cftime.date2num. """ 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 @@ -1873,8 +1828,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: @@ -1894,7 +1848,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: @@ -1919,11 +1874,10 @@ 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 ): - """ - 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: @@ -1958,7 +1912,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 +1937,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.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/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..1de4ed84 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.""" # noqa: D401 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/compile.py b/cf_units/_udunits2_parser/compile.py index 304fb033..b1950b95 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: @@ -17,17 +16,18 @@ # 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 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" @@ -47,9 +47,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" @@ -76,7 +74,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", @@ -95,7 +93,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", @@ -218,14 +216,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..a24b32f9 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. + """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 @@ -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. + """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/_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/config.py b/cf_units/config.py index 0c970d90..41176125 100644 --- a/cf_units/config.py +++ b/cf_units/config.py @@ -5,15 +5,14 @@ import configparser -import sys from pathlib import Path +import sys from tempfile import NamedTemporaryFile # 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/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/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/integration/test__Unit_num2date.py b/cf_units/tests/integration/test__Unit_num2date.py index e97cc238..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 @@ -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/test_coding_standards.py b/cf_units/tests/test_coding_standards.py index 760f5b4a..c1dbe9b5 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 @@ -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 + 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)`` @@ -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,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.") @@ -138,10 +133,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 +146,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 +163,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 +188,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..079f5441 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -2,16 +2,13 @@ # # 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 import operator -import re from operator import truediv +import re import cftime import numpy as np @@ -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 ) @@ -943,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: @@ -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/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/cf_units/util.py b/cf_units/util.py index 8977c351..60eb69d8 100644 --- a/cf_units/util.py +++ b/cf_units/util.py @@ -2,19 +2,15 @@ # # 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 from collections.abc import Hashable +import warnings 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 @@ -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/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 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 2b2ac024..3ff09088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,9 @@ Discussions = "https://github.com/SciTools/cf-units/discussions" Issues = "https://github.com/SciTools/cf-units/issues" Documentation = "https://cf-units.readthedocs.io" +[tool.codespell] +skip = 'cf_units/_udunits2_parser/parser/*,cf_units/_udunits2_parser/_antlr4_runtime/*' + [tool.coverage.run] branch = true plugins = [ @@ -95,10 +98,16 @@ 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" +filterwarnings = [ + "error", + "default:This method is no longer needed:DeprecationWarning", # Added for known warnings +] minversion = "6.0" testpaths = "cf_units" +xfail_strict = true [tool.setuptools.packages.find] include = ["cf_units"] @@ -109,33 +118,118 @@ 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.format] +preview = false [tool.ruff.lint] +ignore = [ + # flake8-commas (COM) + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # Trailing comma missing. + "COM819", # Trailing comma prohibited. + + # flake8-implicit-str-concat (ISC) + # https://docs.astral.sh/ruff/rules/single-line-implicit-string-concatenation/ + # NOTE: This rule may cause conflicts when used with "ruff format". + "ISC001", # Implicitly concatenate string literals on one line. + + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + #"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-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", # ends-in-period (docstring) + #"D401", # non-imperative-mood + #"D404", # docstring-starts-with-this + "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", # 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", # 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", # 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", # ambiguous-unicode-character-string + "RUF003", # ambiguous-unicode-character-comment + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "S101", # Assert used + "S102", # exec used + "S310", # Audit URL open for permitted schemes. + "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", # 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", # raise-vanilla-args +] +preview = false select = [ - # pyflakes - "F", - # pycodestyle - "E", - "W", - # flake8-bugbear - "B", - # flake8-comprehensions - "C4", - # isort - "I", - # pyupgrade - "UP", -] -ignore = ["B904", "F403", "F405"] + "ALL", + "D212", # Multi-line docstring summary should start at the first line +] [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 @@ -145,22 +239,110 @@ 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) - "RF001", # Uses RUFF + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "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 + "PP003", # Does not list wheel as a build-dep + "PY005", # Has tests folder +] + +[tool.mypy] +disable_error_code = [ + # TODO: exceptions that still need investigating are below. + # Might be fixable, or might become permanent (above): + "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/", +] +ignore_missing_imports = true +strict = true +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: + + # -> 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. + + # 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 + + "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") + + "RT03", # Return value has no description + + "RT05", # Return value description should finish with "." + +] +exclude = [ + '\.__eq__$', + '\.__ne__$', + '\.__repr__$', ] 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: