1"""
2Various richly-typed exceptions, that also help us deal with string formatting
3in python where it's easier.
4
5By putting the formatting in `__str__`, we also avoid paying the cost for
6users who silence the exceptions.
7"""
8from .._utils import set_module
9
10def _unpack_tuple(tup):
11 if len(tup) == 1:
12 return tup[0]
13 else:
14 return tup
15
16
17def _display_as_base(cls):
18 """
19 A decorator that makes an exception class look like its base.
20
21 We use this to hide subclasses that are implementation details - the user
22 should catch the base type, which is what the traceback will show them.
23
24 Classes decorated with this decorator are subject to removal without a
25 deprecation warning.
26 """
27 assert issubclass(cls, Exception)
28 cls.__name__ = cls.__base__.__name__
29 return cls
30
31
32class UFuncTypeError(TypeError):
33 """ Base class for all ufunc exceptions """
34 def __init__(self, ufunc):
35 self.ufunc = ufunc
36
37
38@_display_as_base
39class _UFuncNoLoopError(UFuncTypeError):
40 """ Thrown when a ufunc loop cannot be found """
41 def __init__(self, ufunc, dtypes):
42 super().__init__(ufunc)
43 self.dtypes = tuple(dtypes)
44
45 def __str__(self):
46 return (
47 "ufunc {!r} did not contain a loop with signature matching types "
48 "{!r} -> {!r}"
49 ).format(
50 self.ufunc.__name__,
51 _unpack_tuple(self.dtypes[:self.ufunc.nin]),
52 _unpack_tuple(self.dtypes[self.ufunc.nin:])
53 )
54
55
56@_display_as_base
57class _UFuncBinaryResolutionError(_UFuncNoLoopError):
58 """ Thrown when a binary resolution fails """
59 def __init__(self, ufunc, dtypes):
60 super().__init__(ufunc, dtypes)
61 assert len(self.dtypes) == 2
62
63 def __str__(self):
64 return (
65 "ufunc {!r} cannot use operands with types {!r} and {!r}"
66 ).format(
67 self.ufunc.__name__, *self.dtypes
68 )
69
70
71@_display_as_base
72class _UFuncCastingError(UFuncTypeError):
73 def __init__(self, ufunc, casting, from_, to):
74 super().__init__(ufunc)
75 self.casting = casting
76 self.from_ = from_
77 self.to = to
78
79
80@_display_as_base
81class _UFuncInputCastingError(_UFuncCastingError):
82 """ Thrown when a ufunc input cannot be casted """
83 def __init__(self, ufunc, casting, from_, to, i):
84 super().__init__(ufunc, casting, from_, to)
85 self.in_i = i
86
87 def __str__(self):
88 # only show the number if more than one input exists
89 i_str = "{} ".format(self.in_i) if self.ufunc.nin != 1 else ""
90 return (
91 "Cannot cast ufunc {!r} input {}from {!r} to {!r} with casting "
92 "rule {!r}"
93 ).format(
94 self.ufunc.__name__, i_str, self.from_, self.to, self.casting
95 )
96
97
98@_display_as_base
99class _UFuncOutputCastingError(_UFuncCastingError):
100 """ Thrown when a ufunc output cannot be casted """
101 def __init__(self, ufunc, casting, from_, to, i):
102 super().__init__(ufunc, casting, from_, to)
103 self.out_i = i
104
105 def __str__(self):
106 # only show the number if more than one output exists
107 i_str = "{} ".format(self.out_i) if self.ufunc.nout != 1 else ""
108 return (
109 "Cannot cast ufunc {!r} output {}from {!r} to {!r} with casting "
110 "rule {!r}"
111 ).format(
112 self.ufunc.__name__, i_str, self.from_, self.to, self.casting
113 )
114
115
116@_display_as_base
117class _ArrayMemoryError(MemoryError):
118 """ Thrown when an array cannot be allocated"""
119 def __init__(self, shape, dtype):
120 self.shape = shape
121 self.dtype = dtype
122
123 @property
124 def _total_size(self):
125 num_bytes = self.dtype.itemsize
126 for dim in self.shape:
127 num_bytes *= dim
128 return num_bytes
129
130 @staticmethod
131 def _size_to_string(num_bytes):
132 """ Convert a number of bytes into a binary size string """
133
134 # https://en.wikipedia.org/wiki/Binary_prefix
135 LOG2_STEP = 10
136 STEP = 1024
137 units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
138
139 unit_i = max(num_bytes.bit_length() - 1, 1) // LOG2_STEP
140 unit_val = 1 << (unit_i * LOG2_STEP)
141 n_units = num_bytes / unit_val
142 del unit_val
143
144 # ensure we pick a unit that is correct after rounding
145 if round(n_units) == STEP:
146 unit_i += 1
147 n_units /= STEP
148
149 # deal with sizes so large that we don't have units for them
150 if unit_i >= len(units):
151 new_unit_i = len(units) - 1
152 n_units *= 1 << ((unit_i - new_unit_i) * LOG2_STEP)
153 unit_i = new_unit_i
154
155 unit_name = units[unit_i]
156 # format with a sensible number of digits
157 if unit_i == 0:
158 # no decimal point on bytes
159 return '{:.0f} {}'.format(n_units, unit_name)
160 elif round(n_units) < 1000:
161 # 3 significant figures, if none are dropped to the left of the .
162 return '{:#.3g} {}'.format(n_units, unit_name)
163 else:
164 # just give all the digits otherwise
165 return '{:#.0f} {}'.format(n_units, unit_name)
166
167 def __str__(self):
168 size_str = self._size_to_string(self._total_size)
169 return (
170 "Unable to allocate {} for an array with shape {} and data type {}"
171 .format(size_str, self.shape, self.dtype)
172 )