diff --git a/.travis.yml b/.travis.yml index 4b66e1e2..2089ebc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: python python: - 2.7 - #- 3.4 + - 3.4 install: # Install miniconda diff --git a/cf_units/cf_units.py b/cf_units/cf_units.py index 295b9823..90c39588 100644 --- a/cf_units/cf_units.py +++ b/cf_units/cf_units.py @@ -26,6 +26,8 @@ """ from __future__ import (absolute_import, division, print_function) +from six.moves import range, zip +import six from contextlib import contextmanager import copy @@ -39,7 +41,7 @@ import numpy as np from . import config -from . import util +from .util import approx_equal __all__ = ['CALENDAR_STANDARD', @@ -329,13 +331,13 @@ _ut_scale.argtypes = [ctypes.c_double, ctypes.c_void_p] _ut_scale.restype = ctypes.c_void_p - # convenience dictionary for the Unit convert method + # Convenience dictionary for the Unit convert method. _cv_convert_scalar = {FLOAT32: _cv_convert_float, FLOAT64: _cv_convert_double} _cv_convert_array = {FLOAT32: _cv_convert_floats, FLOAT64: _cv_convert_doubles} _numpy2ctypes = {np.float32: FLOAT32, np.float64: FLOAT64} - _ctypes2numpy = {v: k for k, v in _numpy2ctypes.iteritems()} + _ctypes2numpy = {v: k for k, v in _numpy2ctypes.items()} @contextmanager @@ -369,7 +371,7 @@ def suppress_errors(): if _ud_system is None: _alt_xml_path = os.path.join(sys.prefix, 'share', 'udunits', 'udunits2.xml') - _ud_system = _ut_read_xml(_alt_xml_path) + _ud_system = _ut_read_xml(_alt_xml_path.encode()) if not _ud_system: _status_msg = 'UNKNOWN' _error_msg = '' @@ -748,7 +750,7 @@ def num2date(time_value, unit, calendar): ######################################################################## def _Unit(category, ut_unit, calendar=None, origin=None): - unit = util._OrderedHashable.__new__(Unit) + unit = _OrderedHashable.__new__(Unit) unit._init(category, ut_unit, calendar, origin) return unit @@ -769,7 +771,7 @@ def as_unit(unit): result = unit else: result = None - use_cache = isinstance(unit, basestring) or unit is None + use_cache = isinstance(unit, six.string_types) or unit is None if use_cache: result = _CACHE.get(unit) if result is None: @@ -825,7 +827,10 @@ def is_vertical(unit): return as_unit(unit).is_vertical() -class Unit(util._OrderedHashable): +from .util import _OrderedHashable + + +class Unit(_OrderedHashable): """ A class to represent S.I. units and support common operations to manipulate such units in a consistent manner as per UDUNITS-2. @@ -840,7 +845,46 @@ class Unit(util._OrderedHashable): This class also supports time and calendar defintion and manipulation. """ - # Declare the attribute names relevant to the _OrderedHashable behaviour. + def _init_from_tuple(self, values): + for name, value in zip(self._names, values): + object.__setattr__(self, name, value) + + def _as_tuple(self): + return tuple(getattr(self, name) for name in self._names) + + # Provide hash semantics + + def _identity(self): + return self._as_tuple() + + def __hash__(self): + return hash(self._identity()) + + def __eq__(self, other): + return (isinstance(other, type(self)) and + self._identity() == other._identity()) + + def __ne__(self, other): + # Since we've defined __eq__ we should also define __ne__. + return not self == other + + # Provide default ordering semantics + + def __lt__(self, other): + return self._identity() < other._identity() + + # Prevent attribute updates + + def __setattr__(self, name, value): + raise AttributeError('Instances of %s are immutable' % + type(self).__name__) + + def __delattr__(self, name): + raise AttributeError('Instances of %s are immutable' % + type(self).__name__) + + # Declare the attribute names relevant to the ordered and hashable + # behaviour. _names = ('category', 'ut_unit', 'calendar', 'origin') category = None @@ -934,14 +978,14 @@ def __init__(self, unit, calendar=None): unit = _NO_UNIT_STRING else: category = _CATEGORY_UDUNIT - ut_unit = _ut_parse(_ud_system, unit, UT_ASCII) + ut_unit = _ut_parse(_ud_system, unit.encode('ascii'), UT_ASCII) # _ut_parse returns 0 on failure if ut_unit is None: self._raise_error('Failed to parse unit "%s"' % unit) if _OP_SINCE in unit.lower(): if calendar is None: calendar_ = CALENDAR_GREGORIAN - elif isinstance(calendar, basestring): + elif isinstance(calendar, six.string_types): if calendar.lower() in CALENDARS: calendar_ = calendar.lower() else: @@ -951,7 +995,7 @@ def __init__(self, unit, calendar=None): msg = 'Expected string-like calendar argument, got {!r}.' raise TypeError(msg.format(type(calendar))) - self._init(category, ut_unit, calendar_, unit) + self._init_from_tuple((category, ut_unit, calendar_, unit,)) def _raise_error(self, msg): """ @@ -1028,7 +1072,7 @@ def is_time(self): if self.is_unknown() or self.is_no_unit(): result = False else: - day = _ut_get_unit_by_name(_ud_system, 'day') + day = _ut_get_unit_by_name(_ud_system, b'day') result = _ut_are_convertible(self.ut_unit, day) != 0 return result @@ -1054,10 +1098,10 @@ def is_vertical(self): if self.is_unknown() or self.is_no_unit(): result = False else: - bar = _ut_get_unit_by_name(_ud_system, 'bar') + bar = _ut_get_unit_by_name(_ud_system, b'bar') result = _ut_are_convertible(self.ut_unit, bar) != 0 if not result: - meter = _ut_get_unit_by_name(_ud_system, 'meter') + meter = _ut_get_unit_by_name(_ud_system, b'meter') result = _ut_are_convertible(self.ut_unit, meter) != 0 return result @@ -1286,7 +1330,7 @@ def format(self, option=None): ctypes.sizeof(string_buffer), bitmask) if depth < 0: self._raise_error('Failed to format %r' % self) - return string_buffer.value + return str(string_buffer.value.decode('ascii')) @property def name(self): @@ -1386,7 +1430,7 @@ def offset_by_time(self, origin): """ - if not isinstance(origin, (int, float, long)): + if not isinstance(origin, (float, six.integer_types)): raise TypeError('a numeric type for the origin argument is' ' required') ut_unit = _ut_offset_by_time(self.ut_unit, ctypes.c_double(origin)) @@ -1429,7 +1473,7 @@ def root(self, root): Args: - * root (int/long): Value by which the unit root is taken. + * root (int): Value by which the unit root is taken. Returns: None. @@ -1449,7 +1493,7 @@ def root(self, root): try: root = ctypes.c_int(root) except TypeError: - raise TypeError('An int or long type for the root argument' + raise TypeError('An int type for the root argument' ' is required') if self.is_unknown(): @@ -1475,7 +1519,7 @@ def log(self, base): Args: - * base (int/float/long): Value of the logorithmic base. + * base (int/float): Value of the logorithmic base. Returns: None. @@ -1614,7 +1658,7 @@ def __mul__(self, other): Args: - * other (int/float/long/string/Unit): Multiplication scale + * other (int/float/string/Unit): Multiplication scale factor or unit. Returns: @@ -1641,7 +1685,7 @@ def __div__(self, other): Args: - * other (int/float/long/string/Unit): Division scale factor or unit. + * other (int/float/string/Unit): Division scale factor or unit. Returns: Unit. @@ -1667,7 +1711,7 @@ def __truediv__(self, other): Args: - * other (int/float/long/string/Unit): Division scale factor or unit. + * other (int/float/string/Unit): Division scale factor or unit. Returns: Unit. @@ -1694,7 +1738,7 @@ def __pow__(self, power): Args: - * power (int/float/long): Value by which the unit power is raised. + * power (int/float): Value by which the unit power is raised. Returns: Unit. @@ -1725,15 +1769,15 @@ def __pow__(self, power): # 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 util.approx_equal(power, 0.0) and abs(power) < 1: - if not util.approx_equal(1 / power, round(1 / power)): + if not approx_equal(power, 0.0) and abs(power) < 1: + if not approx_equal(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 util.approx_equal(power, round(power)): + if not approx_equal(power, round(power)): msg = 'Cannot raise a unit by a decimal (got %s).' % power raise ValueError(msg) power = int(round(power)) @@ -1746,7 +1790,7 @@ def __pow__(self, power): def _identity(self): # Redefine the comparison/hash/ordering identity as used by - # util._OrderedHashable. + # _OrderedHashable. return (self.name, self.calendar) def __eq__(self, other): @@ -1814,7 +1858,7 @@ def convert(self, value, other, ctype=FLOAT64): Args: - * value (int/float/long/numpy.ndarray): + * value (int/float/numpy.ndarray): Value/s to be converted. * other (string/Unit): Target unit to convert to. @@ -1832,8 +1876,8 @@ def convert(self, value, other, ctype=FLOAT64): >>> import numpy as np >>> c = cf_units.Unit('deg_c') >>> f = cf_units.Unit('deg_f') - >>> print(c.convert(0, f)) - 32.0 + >>> c.convert(0, f) + 31.999999999999886 >>> c.convert(0, f, cf_units.FLOAT32) 32.0 >>> a64 = np.arange(10, dtype=np.float64) @@ -1881,8 +1925,8 @@ def convert(self, value, other, ctype=FLOAT64): if issubclass(value_copy.dtype.type, np.integer): value_copy = value_copy.astype( _ctypes2numpy[ctype]) - # strict type check of numpy array - if value_copy.dtype.type not in _numpy2ctypes.keys(): + # Strict type check of numpy array. + if value_copy.dtype.type not in _numpy2ctypes: raise TypeError( "Expect a numpy array of '%s' or '%s'" % tuple(sorted(_numpy2ctypes.keys()))) @@ -1895,7 +1939,7 @@ def convert(self, value, other, ctype=FLOAT64): value_copy.size, pointer) result = value_copy else: - if ctype not in _cv_convert_scalar.keys(): + if ctype not in _cv_convert_scalar: raise ValueError('Invalid target type. Can only ' 'convert to float or double.') # Utilise global convenience dictionary diff --git a/cf_units/config.py b/cf_units/config.py index a2b97ee8..8bf2e01b 100644 --- a/cf_units/config.py +++ b/cf_units/config.py @@ -18,11 +18,15 @@ from __future__ import (absolute_import, division, print_function) -import ConfigParser +try: + import ConfigParser as configparser +except ImportError: + import configparser + import os.path # Load the optional "site.cfg" file if it exists. -config = ConfigParser.SafeConfigParser() +config = configparser.SafeConfigParser() # Returns simple string options. @@ -44,5 +48,5 @@ def get_option(section, option, default=None): CONFIG_PATH = os.path.join(ROOT_PATH, 'etc') # Load the optional "site.cfg" file if it exists. -config = ConfigParser.SafeConfigParser() +config = configparser.SafeConfigParser() config.read([os.path.join(CONFIG_PATH, 'site.cfg')]) diff --git a/cf_units/tests/test_unit.py b/cf_units/tests/test_unit.py index 9c0cbfab..75f0c551 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -26,6 +26,11 @@ import datetime as datetime import operator +try: + from operator import truediv +except ImportError: + from operator import div as truediv + import numpy as np from cf_units import cf_units as unit @@ -244,7 +249,7 @@ def test_offset_pass_1(self): def test_offset_pass_2(self): u = Unit("meter") - self.assertEqual(u + 1000L, "m @ 1000") + self.assertEqual(u + 1000, "m @ 1000") class TestOffsetByTime(unittest.TestCase): @@ -376,7 +381,7 @@ def test_multiply_pass_1(self): def test_multiply_pass_2(self): u = Unit("amp") - self.assertEqual((u * 1000L).format(), "1000 A") + self.assertEqual((u * 1000).format(), "1000 A") def test_multiply_pass_3(self): u = Unit("amp") @@ -387,7 +392,7 @@ def test_multiply_pass_3(self): class TestDivide(unittest.TestCase): def test_divide_fail_0(self): u = Unit("watts") - self.assertRaises(ValueError, operator.div, u, "naughty") + self.assertRaises(ValueError, truediv, u, "naughty") def test_divide_fail_1(self): u = Unit('unknown') @@ -398,14 +403,14 @@ def test_divide_fail_1(self): def test_divide_fail_3(self): u = Unit('unknown') v = Unit('no unit') - self.assertRaises(ValueError, operator.div, u, v) - self.assertRaises(ValueError, operator.div, v, u) + self.assertRaises(ValueError, truediv, u, v) + self.assertRaises(ValueError, truediv, v, u) def test_divide_fail_5(self): u = Unit('meters') v = Unit('no unit') - self.assertRaises(ValueError, operator.div, u, v) - self.assertRaises(ValueError, operator.div, v, u) + self.assertRaises(ValueError, truediv, u, v) + self.assertRaises(ValueError, truediv, v, u) def test_divide_pass_0(self): u = Unit("watts") @@ -417,7 +422,7 @@ def test_divide_pass_1(self): def test_divide_pass_2(self): u = Unit("watts") - self.assertEqual((u / 1000L).format(), "0.001 W") + self.assertEqual((u / 1000).format(), "0.001 W") def test_divide_pass_3(self): u = Unit("watts") @@ -434,7 +439,7 @@ def test_power(self): self.assertRaises(TypeError, operator.pow, u, Unit('no unit')) self.assertEqual(u ** 2, Unit('A^2')) self.assertEqual(u ** 3.0, Unit('A^3')) - self.assertEqual(u ** 4L, Unit('A^4')) + self.assertEqual(u ** 4, Unit('A^4')) self.assertRaises(ValueError, operator.pow, u, 2.4) u = Unit("m^2") @@ -447,7 +452,7 @@ def test_power_unknown(self): self.assertRaises(TypeError, operator.pow, u, Unit('m')) self.assertEqual(u ** 2, Unit('unknown')) self.assertEqual(u ** 3.0, Unit('unknown')) - self.assertEqual(u ** 4L, Unit('unknown')) + self.assertEqual(u ** 4, Unit('unknown')) def test_power_nounit(self): u = Unit('no unit') diff --git a/cf_units/util.py b/cf_units/util.py index 285dcf31..772be8b1 100644 --- a/cf_units/util.py +++ b/cf_units/util.py @@ -21,6 +21,7 @@ from __future__ import (absolute_import, division, print_function) +from six import with_metaclass import abc import collections @@ -82,7 +83,8 @@ def __new__(cls, name, bases, namespace): cls, name, bases, namespace) -class _OrderedHashable(collections.Hashable): +class _OrderedHashable(with_metaclass(_MetaOrderedHashable, + collections.Hashable)): """ Convenience class for creating "immutable", hashable, and ordered classes. @@ -100,64 +102,4 @@ class _OrderedHashable(collections.Hashable): its attributes are themselves hashable. """ - - # The metaclass adds default __init__ methods when appropriate. - __metaclass__ = _MetaOrderedHashable - - @abc.abstractproperty - def _names(self): - """ - Override this attribute to declare the names of all the attributes - relevant to the hash/comparison semantics. - - """ - pass - - def _init_from_tuple(self, values): - for name, value in zip(self._names, values): - object.__setattr__(self, name, value) - - def __repr__(self): - class_name = type(self).__name__ - attributes = ', '.join('%s=%r' % (name, value) - for (name, value) - in zip(self._names, self._as_tuple())) - return '%s(%s)' % (class_name, attributes) - - def _as_tuple(self): - return tuple(getattr(self, name) for name in self._names) - - # Prevent attribute updates - - def __setattr__(self, name, value): - raise AttributeError('Instances of %s are immutable' % - type(self).__name__) - - def __delattr__(self, name): - raise AttributeError('Instances of %s are immutable' % - type(self).__name__) - - # Provide hash semantics - - def _identity(self): - return self._as_tuple() - - def __hash__(self): - return hash(self._identity()) - - def __eq__(self, other): - return (isinstance(other, type(self)) and - self._identity() == other._identity()) - - def __ne__(self, other): - # Since we've defined __eq__ we should also define __ne__. - return not self == other - - # Provide default ordering semantics - - def __cmp__(self, other): - if isinstance(other, _OrderedHashable): - result = cmp(self._identity(), other._identity()) - else: - result = NotImplemented - return result + pass diff --git a/conda-requirements.txt b/conda-requirements.txt index 68920c52..5d9e862d 100644 --- a/conda-requirements.txt +++ b/conda-requirements.txt @@ -1,4 +1,5 @@ # Mandatory dependencies. +six netcdf4 udunits=2.*