Skip to content

Commit 218e7de

Browse files
d-w-moorealanking
authored andcommitted
[#400] more advanced iRODSException representation
The most notable change here is that we now include the OS/Errno information in the __repr__ result for any exception class based on iRODSException.
1 parent e6b8038 commit 218e7de

3 files changed

Lines changed: 117 additions & 5 deletions

File tree

irods/exception.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44

55
from __future__ import absolute_import
6-
import six
6+
from __future__ import print_function
7+
import errno
78
import numbers
9+
import os
10+
import six
11+
import sys
812

913

1014
class PycommandsException(Exception):
@@ -61,14 +65,72 @@ class MultipleResultsFound(QueryException):
6165

6266
class iRODSExceptionMeta(type):
6367
codes = {}
64-
68+
positive_code_error_message = "For {name}, a positive code of {attrs[code]} was declared."
6569
def __init__(self, name, bases, attrs):
6670
if 'code' in attrs:
71+
if attrs['code'] > 0:
72+
print(self.positive_code_error_message.format(**locals()), file = sys.stderr)
73+
exit(1)
6774
iRODSExceptionMeta.codes[attrs['code']] = self
6875

6976

77+
class Errno:
78+
"""Encapsulates an integer error code from the operating system
79+
and provides a text representation.
80+
"""
81+
def __init__(self,arg0,*_):
82+
"""Initializes an object with an integer error code arg0.
83+
Further arguments are optional and ignored.
84+
"""
85+
self.int_code = arg0
86+
87+
def __repr__(self):
88+
e = self.int_code
89+
try:
90+
return self.__class__.__name__ + repr(tuple([e, errno.errorcode[e], os.strerror(e)]))
91+
except:
92+
# The errno code is unrecognized, so fall through to default representation.
93+
pass
94+
return self.__class__.__name__ + repr(tuple([e,]))
95+
96+
def __int__(self):
97+
return self.int_code
98+
99+
70100
class iRODSException(six.with_metaclass(iRODSExceptionMeta, Exception)):
71-
pass
101+
"""An exception that originates from a server error.
102+
Exception classes that are derived from this base and represent a concrete error, should
103+
store a unique error code (X*1000) in their 'code' attribute, where X < 0.
104+
"""
105+
106+
def __init__(self,*arg):
107+
explicit_errno = None
108+
argl = list(arg)
109+
110+
# For consistency with the text representation, allow initialization with an Errno object
111+
# at the end of the argument list.
112+
# Example: err = UNIX_FILE_OPEN_ERR('message', Errno(13))
113+
# err_copy = eval(repr(err))
114+
if hasattr(self.__class__,'code'):
115+
if argl and isinstance (argl[-1],Errno):
116+
explicit_errno = argl.pop()
117+
118+
super(iRODSException,self).__init__(*argl)
119+
120+
# To properly represent the Errno instance, the "code" attribute should be (-errno) plus the thousands
121+
# attribute stored in the class's code attribute. Because Python honors the instance "code" attribute
122+
# over the class "code" attribute, the following does _not_ modify the class:
123+
if explicit_errno:
124+
self.code -= int(explicit_errno)
125+
126+
def __repr__(self):
127+
args = tuple(self.args)
128+
code = getattr(self,'code',None)
129+
if code is not None:
130+
os_err = abs(code) % 1000
131+
if os_err: args += (Errno(os_err),)
132+
return self.__class__.__name__ + repr(args)
133+
72134

73135

74136
def nominal_code( the_code, THRESHOLD = 1000 ):

irods/test/exception_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#! /usr/bin/env python
2+
from __future__ import print_function
3+
from __future__ import absolute_import
4+
from datetime import datetime
5+
import errno
6+
import os
7+
import sys
8+
import unittest
9+
import irods.test.helpers as helpers
10+
import irods.exception
11+
12+
13+
class TestException(unittest.TestCase):
14+
15+
def setUp(self):
16+
# open the session (per-test)
17+
self.sess = helpers.make_session()
18+
19+
def tearDown(self):
20+
# close the session (per-test)
21+
self.sess.cleanup()
22+
23+
def test_400(self):
24+
ses = self.sess
25+
data = ''
26+
try:
27+
seed = helpers.my_function_name() + ":" + str(datetime.now())
28+
data = helpers.home_collection(ses) + "/" + helpers.unique_name(seed,'data')
29+
exc = None
30+
with helpers.create_simple_resc(self,vault_path = '/home') as resc_name:
31+
try:
32+
# iRODS will attempt to make a data object where it doesn't have permission
33+
ses.data_objects.create(data,resource = resc_name)
34+
except Exception as e:
35+
exc = e
36+
self.assertIsNotNone(exc, 'Creating data object in unwriteable vault did not raise an error as it should.')
37+
excep_repr = repr(exc)
38+
errno_object = irods.exception.Errno(errno.EACCES)
39+
errno_repr = repr(errno_object)
40+
self.assertRegexpMatches(errno_repr,r'\bErrno\b')
41+
self.assertRegexpMatches(errno_repr,'''['"]{msg}['"]'''.format(msg = os.strerror(errno.EACCES)))
42+
self.assertIn( errno_repr, excep_repr)
43+
finally:
44+
if ses.data_objects.exists(data):
45+
ses.data_objects.unlink(data,force = True)
46+
47+
if __name__ == '__main__':
48+
# let the tests find the parent irods lib
49+
sys.path.insert(0, os.path.abspath('../..'))
50+
unittest.main()

irods/test/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,15 +193,15 @@ def make_flat_test_dir(dir_path, file_count=10, file_size=1024):
193193
f.write(os.urandom(file_size))
194194

195195
@contextlib.contextmanager
196-
def create_simple_resc (self, rescName = None):
196+
def create_simple_resc (self, rescName = None, vault_path = ''):
197197
if not rescName:
198198
rescName = 'simple_resc_' + unique_name (my_function_name() + '_simple_resc', datetime.datetime.now())
199199
created = False
200200
try:
201201
self.sess.resources.create(rescName,
202202
'unixfilesystem',
203203
host = self.sess.host,
204-
path = '/tmp/' + rescName)
204+
path = vault_path or '/tmp/' + rescName)
205205
created = True
206206
yield rescName
207207
finally:

0 commit comments

Comments
 (0)