Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
ocefpaf committed Nov 21, 2024
1 parent 7a2c0a7 commit 815aca6
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 37 deletions.
19 changes: 9 additions & 10 deletions compliance_checker/cf/cf_1_6.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
grid_mapping_dict16,
)
from compliance_checker.cf.cf_base import CFNCCheck, appendix_a_base
from compliance_checker.cfunits import Unit

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -811,7 +812,7 @@ def _check_valid_cf_units(self, ds, variable_name):
)

try:
units_conv = cfutil._units(units)
units_conv = Unit(units)
except ValueError:
valid_units.messages.append(
f'Unit string "{units}" is not recognized by UDUnits',
Expand All @@ -827,7 +828,7 @@ def _check_valid_cf_units(self, ds, variable_name):
# being expressed as "s"/seconds
if standard_name not in {"time", "forecast_reference_time"}:
valid_units.assert_true(
units_conv.is_convertible_to(cfutil._units(reference)),
units_conv.is_convertible(Unit(reference)),
f'Units "{units}" for variable '
f"{variable_name} must be convertible to "
f'canonical units "{reference}"',
Expand Down Expand Up @@ -1493,8 +1494,7 @@ def check_latitude(self, ds):
# check that the units aren't in east and north degrees units,
# but are convertible to angular units
allowed_units.assert_true(
units not in e_n_units
and cfutil._units(units) == cfutil._units("degree"),
units not in e_n_units and Unit(units) == Unit("degree"),
f"Grid latitude variable '{latitude}' should use degree equivalent units without east or north components. "
f"Current units are {units}",
)
Expand Down Expand Up @@ -1603,8 +1603,7 @@ def check_longitude(self, ds):
# check that the units aren't in east and north degrees units,
# but are convertible to angular units
allowed_units.assert_true(
units not in e_n_units
and cfutil._units(units) == cfutil._units("degree"),
units not in e_n_units and Unit(units) == Unit("degree"),
f"Grid longitude variable '{longitude}' should use degree equivalent units without east or north components. "
f"Current units are {units}",
)
Expand Down Expand Up @@ -2844,13 +2843,13 @@ def _cell_measures_core(self, ds, var, external_set, variable_template):
f'cell_methods attribute with a measure type of "{cell_measure_type}".'
)
try:
cell_measure_units = cfutil._units(cell_measure_var.units)
cell_measure_units = Unit(cell_measure_var.units)
except ValueError:
valid = False
reasoning.append(conversion_failure_msg)
else:
if not cell_measure_units.is_convertible_to(
cfutil._units(f"m{exponent}"),
if not cell_measure_units.is_convertible(
Unit(f"m{exponent}"),
):
valid = False
reasoning.append(conversion_failure_msg)
Expand Down Expand Up @@ -3044,7 +3043,7 @@ def _check_cell_methods_paren_info(self, paren_contents, var):

# then the units
try:
cfutil._units(interval_matches.group("interval_units"))
Unit(interval_matches.group("interval_units"))
except ValueError:
valid_info.messages.append(
'§7.3.3 {}:cell_methods interval units "{}" is not parsable by UDUNITS.'.format(
Expand Down
10 changes: 5 additions & 5 deletions compliance_checker/cf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from pkgutil import get_data

import requests
from cf_units import Unit
from lxml import etree
from netCDF4 import Dataset

from compliance_checker.cfutil import _units, units_convertible
from compliance_checker.cfunits import Unit
from compliance_checker.cfutil import units_convertible

# copied from paegan
# paegan may depend on these later
Expand Down Expand Up @@ -321,15 +321,15 @@ def create_cached_data_dir():

def units_known(units):
try:
_units(units)
Unit(units)
except ValueError:
return False
return True


def units_temporal(units):
try:
u = _units(units)
u = Unit(units)
except ValueError:
return False
# IMPLEMENTATION CONFORMANCE REQUIRED 4.4 1/3
Expand All @@ -338,7 +338,7 @@ def units_temporal(units):
# IMPLEMENTATION CONFORMANCE REQUIRED 4.4 3/3
# check that reference time seconds is not greater than or
# equal to 60
return u.is_time_reference
return u.is_time_reference()


def find_coord_vars(ncds):
Expand Down
90 changes: 90 additions & 0 deletions compliance_checker/cfunits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from pyudunits2 import UnitSystem, UnresolvableUnitException

try:
import cf_units
except ImportError:
cf_units = False


class PyUdunits2:
"""Workaround for the differences in pyudunits2 and cf-units.
NB: Some of these may change and/or get implemented upstream. Pyudunits2 is new and in-flux.
1/4 Raise the same ValueError to match cf-unit errors.
2/4 Creates an empty unit from None to mimic cf-unit's Unit('unknown')
3/4 Add a definition object that is ust units.expanded()
"""

def __init__(self, units):
"""Keep unit system so we can convert from string later."""
self.ut_system = UnitSystem.from_udunits2_xml()

if units is None:
units = ""

try:
self.units = self.ut_system.unit(units)
except (SyntaxError, UnresolvableUnitException) as err:
raise ValueError from err
self.definition = self.units.expanded()

def __eq__(self, other):
return self.units == other

def is_dimensionless(self):
return self.units.is_dimensionless()

def is_convertible(self, other):
if isinstance(other, str):
other = self.ut_system.unit(other)
elif isinstance(other, (PyUdunits2)):
other = other.units
else:
msg = f"Expected valid unit string or pyudunits2 unit object. Got {other}."
raise ValueError(msg)

# FIXME: cf-units Workaround 1/4 -> cf_units.Unit(None) -> Unit('unknown').
if "" in (self.units.expanded(), other.expanded()):
return False

convertible = self.units.is_convertible_to(other)
# FIXME: cf-units Workaround 2/4 -> time is not convertible to time reference.

# Both are time reference confirm.
if _is_time_reference(self.units) and _is_time_reference(other):
convertible = True
# One is time, the other is not, change it to False.
if sum((_is_time_reference(self.units), _is_time_reference(other))) == 1:
convertible = False

return convertible

def is_time_reference(self):
return _is_time_reference(self.units)


def _is_time_reference(self):
# FIXME: cf-units Workaround 4/4 -> cf_units can differentiante between time reference and time units.
is_time_reference = False
try:
if hasattr(self._definition, "shift_from"):
is_time_reference = True
except KeyError:
# FIXME: hasattr should return None in that case.
# pyudunits2/_expr_graph.py:27, in Node.__getattr__(self, name)
# 25 def __getattr__(self, name):
# 26 # Allow the dictionary to raise KeyError if the key doesn't exist.
# ---> 27 return self._attrs[name]
# KeyError: 'shift_from'
pass
return is_time_reference


if cf_units:
PyUdunits2 = cf_units.Unit


class Unit(PyUdunits2):
def __init__(self, units):
super().__init__(units)
23 changes: 5 additions & 18 deletions compliance_checker/cfutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from functools import lru_cache, partial
from importlib.resources import files

from cf_units import Unit
from compliance_checker.cfunits import Unit

_UNITLESS_DB = None
_SEA_NAMES = None
Expand Down Expand Up @@ -111,7 +111,7 @@ def is_dimensionless_standard_name(standard_name_table, standard_name):
f".//entry[@id='{standard_name}']",
)
if found_standard_name is not None:
canonical_units = _units(
canonical_units = Unit(
found_standard_name.find("canonical_units").text,
)
return canonical_units.is_dimensionless()
Expand Down Expand Up @@ -2038,22 +2038,9 @@ def units_convertible(units1, units2):
:param str units1: A string representing the units
:param str units2: A string representing the units
"""
convertible = False
try:
u1 = _units(units1)
u2 = _units(units2)
u1 = Unit(units1)
u2 = Unit(units2)
except ValueError:
return False
# FIXME: Workaround for unknown units in cf_units.
if "" in (u1.expanded(), u2.expanded()):
return False

convertible = u1.is_convertible_to(u2)
# FIXME: Workaround for is_time_reference vs time in cf_units.
# Both are time reference confirm.
if u1.is_time_reference and u2.is_time_reference:
convertible = True
# One is time, the other is not, change it to False.
if sum((u1.is_time_reference, u2.is_time_reference)) == 1:
convertible = False
return convertible
return u1.is_convertible(u2)
8 changes: 4 additions & 4 deletions compliance_checker/ioos.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
)
from compliance_checker.cf import util as cf_util # not to be confused with cfutil.py
from compliance_checker.cf.cf import CF1_6Check, CF1_7Check
from compliance_checker.cfunits import Unit
from compliance_checker.cfutil import (
_units,
get_geophysical_variables,
get_instrument_variables,
get_z_variables,
Expand Down Expand Up @@ -1378,12 +1378,12 @@ def check_vertical_coordinates(self, ds):
"fathom",
)
unit_def_set = {
_units(unit_str).expanded() for unit_str in expected_unit_strs
Unit(unit_str).definition for unit_str in expected_unit_strs
}

try:
units = _units(units_str)
pass_stat = units.expanded() in unit_def_set
units = Unit(units_str)
pass_stat = units.definition in unit_def_set
# unknown unit not convertible to UDUNITS
except ValueError:
pass_stat = False
Expand Down

0 comments on commit 815aca6

Please sign in to comment.