Skip to content

Commit

Permalink
some refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
ocefpaf committed Nov 14, 2024
1 parent 5165974 commit 9f15ece
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 77 deletions.
2 changes: 1 addition & 1 deletion compliance_checker/cf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,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
113 changes: 113 additions & 0 deletions compliance_checker/cfunits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
try:
from cf_units import Unit
except ImportError:
cf_units = None
Unit = object
import functools

from pyudunits2 import UnitSystem, UnresolvableUnitException


class CFUnitMixin:
"""Standardize the methods from cf-untis and pyudunits2 used in compliance-checker."""

def is_convertible_to(self, other):
return self.is_convertible(other)

def expand_definition(self):
return self.definition

def has_shift_from(self):
return self.is_time_reference()


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


class Udunits2(CFUnitMixin, PyUdunits2):
def __init__(self, units):
super().__init__(units)


class CFUnits(CFUnitMixin, Unit):
def __init__(self, units):
super().__init__(units)


@functools.lru_cache(128)
def _units(units):
if cf_units is not None:
return CFUnits(units)
return Udunits2(units)
78 changes: 4 additions & 74 deletions compliance_checker/cfutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from importlib_resources import files

from compliance_checker.cfunits import _units

_UNITLESS_DB = None
_SEA_NAMES = None

Expand Down Expand Up @@ -2029,89 +2031,17 @@ def guess_feature_type(nc, variable):
return "reduced-grid"


class _CCUnit:
def __init__(self, units):
import cf_units

self.u = cf_units.Unit(units)
self.is_time_reference = self.u.is_time_reference()

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

def is_convertible_to(self, other):
if isinstance(other, str):
ret = self.u.is_convertible(other)
else:
ret = self.u.is_convertible(other.u)
return ret

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

def expanded(self):
return self.u.definition


def _units(units: str):
"""PLACEHOLDER."""
# FIXME: Make cf_units optional in a more elegant way when pyudunits2 is release.
try:
return _CCUnit(units)
except ImportError:
from pyudunits2 import UnitSystem, UnresolvableUnitException

ut_system = UnitSystem.from_udunits2_xml()

# FIXME: cf_units.Unit(None) -> Unit('unknown')
if units is None:
units = ""

# FIXME: cf_units raised only ValueError
try:
u = ut_system.unit(units)
except (SyntaxError, UnresolvableUnitException) as err:
raise ValueError from err
# FIXME: cf_units defined .is_time_reference for time reference units.
u.is_time_reference = False
try:
if hasattr(u._definition, "shift_from"):
u.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 u


def units_convertible(units1, units2, reftimeistime=True):
def units_convertible(units1, units2):
"""
Return True if a Unit representing the string units1 can be converted
to a Unit representing the string units2, else False.
: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)
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_to(u2)
4 changes: 2 additions & 2 deletions compliance_checker/ioos.py
Original file line number Diff line number Diff line change
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
_units(unit_str).expand_definition() for unit_str in expected_unit_strs
}

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

0 comments on commit 9f15ece

Please sign in to comment.