Skip to content

Tidy up the propagation of exceptions from UDUNITS2 #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2019

Conversation

pelson
Copy link
Member

@pelson pelson commented Jan 21, 2019

Modernises the exception raising when propagating from udunits2.

In python 3 we have a syntax to allow us to control whether to show multiple tracebacks (super useful for the case of exceptions being raised in multiple threads). This functionality is the default in python 3. Essentially, the following code:

def foo():
    raise ValueError('Inside foo')

def bar():
    try:
        foo()
    except Exception as err:
        raise ValueError('bar')

bar()
$ python2 raise_from.py 
Traceback (most recent call last):
  File "raise_from.py", line 15, in <module>
    bar()
  File "raise_from.py", line 11, in bar
    raise ValueError('bar')
ValueError: bar
python3 raise_from.py 
Traceback (most recent call last):
  File "raise_from.py", line 9, in bar
    foo()
  File "raise_from.py", line 2, in foo
    raise ValueError('Inside foo')
ValueError: Inside foo

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "raise_from.py", line 15, in <module>
    bar()
  File "raise_from.py", line 11, in bar
    raise ValueError('bar')
ValueError: bar

The same is true for existing cf_units:

python2 -c "import cf_units as u; u.Unit('2^m')"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "cf_units/__init__.py", line 836, in __init__
    self._propogate_error('Failed to parse unit "%s"' % unit, e)
  File "cf_units/__init__.py", line 871, in _propogate_error
    raise ValueError('[%s] %s%s' % (ud_err.status_msg(), msg, error_msg))
ValueError: [UT_SUCCESS] Failed to parse unit "2^m"
python3 -c "import cf_units as u; u.Unit('2^m')"
Traceback (most recent call last):
  File "/Users/pelson/dev/scitools/cf-units/cf_units/__init__.py", line 834, in __init__
    ut_unit = _ud.parse(_ud_system, unit.encode('ascii'), UT_ASCII)
  File "cf_units/_udunits2.pyx", line 268, in cf_units._udunits2.parse
    return wrap_unit(system, cunit)
  File "cf_units/_udunits2.pyx", line 123, in cf_units._udunits2.wrap_unit
    _raise_error()
  File "cf_units/_udunits2.pyx", line 196, in cf_units._udunits2._raise_error
    raise UdunitsError(status, errnum)
cf_units._udunits2.UdunitsError: UT_SUCCESS

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/pelson/dev/scitools/cf-units/cf_units/__init__.py", line 836, in __init__
    self._propogate_error('Failed to parse unit "%s"' % unit, e)
  File "/Users/pelson/dev/scitools/cf-units/cf_units/__init__.py", line 871, in _propogate_error
    raise ValueError('[%s] %s%s' % (ud_err.status_msg(), msg, error_msg))
ValueError: [UT_SUCCESS] Failed to parse unit "2^m"

With this change we now get good exceptions in both python2 and python3:

python2 -c "import cf_units as u; u.Unit('2^m')"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "cf_units/__init__.py", line 853, in __init__
    six.raise_from(value_error, None)
  File "/Users/pelson/dev/scitools/cf-units/env_py27/lib/python2.7/site-packages/six.py", line 737, in raise_from
    raise value
ValueError: [UT_SUCCESS] Failed to parse unit "2^m"
$ python3 -c "import cf_units as u; u.Unit('2^m')"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/pelson/dev/scitools/cf-units/cf_units/__init__.py", line 853, in __init__
    six.raise_from(value_error, None)
  File "<string>", line 3, in raise_from
ValueError: [UT_SUCCESS] Failed to parse unit "2^m"

@coveralls
Copy link

coveralls commented Jan 21, 2019

Coverage Status

Coverage increased (+3.4%) to 91.435% when pulling 683dde7 on pelson:raise_from into 659e994 on SciTools:master.

@pelson
Copy link
Member Author

pelson commented Jan 22, 2019

This PR now depends upon #135, as I added a few unicode tests on Unit.format in order to assure myself that the exception was not being raised in any situation.

@bjlittle
Copy link
Member

@pelson Rebase after recent merges?

@bjlittle bjlittle self-assigned this Jan 22, 2019
@pelson
Copy link
Member Author

pelson commented Jan 22, 2019

@bjlittle, let's get #137 in first, then I'll rebase this.

…we don't have multiple tracebacks in python 3.

Also improved coverage of Unit.log, which was highlighted by this change.

Bolster the unit tests to ensure that some of the exception handling is redundant.
@pelson
Copy link
Member Author

pelson commented Jan 24, 2019

OK, now rebased @bjlittle. Take it away.

@@ -189,6 +190,18 @@ def test_format_unit_definition(self):
u = Unit('watt')
self.assertEqual(u.format(unit.UT_DEFINITION), 'm2.kg.s-3')

def test_format_multiple_options(self):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this was tested anywhere before!

u = Unit('watt')
self.assertEqual(
u.format([unit.UT_NAMES, unit.UT_DEFINITION, unit.UT_UTF8]),
u'meter²·kilogram·second⁻³')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty cool eh?


def test_format_latin1(self):
u = Unit('m2')
self.assertEqual(u.format(unit.UT_LATIN1), u'm²')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a coverage perspective, LATIN1 == ISO8859-1

def test_negative(self):
u = Unit('hPa')
msg = re.escape("Failed to calculate logorithmic base of Unit('hPa')")
with six.assertRaisesRegex(self, ValueError, msg):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No previous test of this.

self._propogate_error('Failed to format %r' % self, e)
return str(result.decode(endocing_str))
encoding_str = _encoding_lookup[encoding]
result = _ud.format(self.ut_unit, bitmask)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @pelson, removed unnecessary defensive try...except.

There were no test cases that raised this exception for _ud.format.

except _ud.UdunitsError as e:
self._propogate_error('Failed to invert %r' % self, e)
calendar = None
ut_unit = _ud.invert(self.ut_unit)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @pelson, removed unnecessary defensive try...except.

There were no test cases that raised this exception for _ud.invert.

@bjlittle
Copy link
Member

Lovely, thanks @pelson 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants