1## 2# .python.element 3## 4import os 5from abc import ABCMeta, abstractproperty, abstractmethod 6from .string import indent 7from .decorlib import propertydoc 8 9class RecursiveFactor(Exception): 10 'Raised when a factor is ultimately composed of itself' 11 pass 12 13class Element(object, metaclass = ABCMeta): 14 """ 15 The purpose of an element is to provide a general mechanism for specifying 16 the factors that composed an object. Factors are designated using an 17 ordered set of strings referencing those significant attributes on the object. 18 19 Factors are important for PG-API as it provides the foundation for 20 collecting the information about the state of the interface that ultimately 21 led up to an error. 22 23 Traceback: 24 ... 25 postgresql.exceptions.*: <message> 26 CODE: XX000 27 CURSOR: <cursor_id> 28 parameters: (p1, p2, ...) 29 STATEMENT: <statement_id> <parameter info> 30 ... 31 string: 32 <query body> 33 SYMBOL: get_types 34 LIBRARY: catalog 35 ... 36 CONNECTION: 37 <backend_id> <socket information> 38 CONNECTOR: [Host] 39 IRI: pq://user@localhost:5432/database 40 DRIVER: postgresql.driver.pq3 41 """ 42 43 @propertydoc 44 @abstractproperty 45 def _e_label(self) -> str: 46 """ 47 Single-word string describing the kind of element. 48 49 For instance, `postgresql.api.Statement`'s _e_label is 'STATEMENT'. 50 51 Usually, this is set directly on the class itself, and is a shorter 52 version of the class's name. 53 """ 54 55 @propertydoc 56 @abstractproperty 57 def _e_factors(self) -> (): 58 """ 59 The attribute names of the objects that contributed to the creation of 60 this object. 61 62 The ordering is significant. The first factor is the prime factor. 63 """ 64 65 @abstractmethod 66 def _e_metas(self) -> [(str, object)]: 67 """ 68 Return an iterable to key-value pairs that provide useful descriptive 69 information about an attribute. 70 71 Factors on metas are not checked. They are expected to be primitives. 72 73 If there are no metas, the str() of the object will be used to represent 74 it. 75 """ 76 77class ElementSet(Element, set): 78 """ 79 An ElementSet is a set of Elements that can be used as an individual factor. 80 81 In situations where a single factor is composed of multiple elements where 82 each has no significance over the other, this Element can be used represent 83 that fact. 84 85 Importantly, it provides the set metadata so that the appropriate information 86 will be produced in element tracebacks. 87 """ 88 _e_label = 'SET' 89 _e_factors = () 90 __slots__ = () 91 92 def _e_metas(self): 93 yield (None, len(self)) 94 for x in self: 95 yield (None, '--') 96 yield (None, format_element(x)) 97 98def prime_factor(obj): 99 'get the primary factor on the `obj`, returns None if none.' 100 f = getattr(obj, '_e_factors', None) 101 if f: 102 return f[0], getattr(obj, f[0], None) 103 104def prime_factors(obj): 105 """ 106 Yield out the sequence of primary factors of the given object. 107 """ 108 visited = set((obj,)) 109 ef = getattr(obj, '_e_factors', None) 110 if not ef: 111 return 112 fn = ef[0] 113 e = getattr(obj, fn, None) 114 if e in visited: 115 raise RecursiveFactor(obj, e) 116 visited.add(e) 117 yield fn, e 118 119 while e is not None: 120 ef = getattr(obj, '_e_factors', None) 121 fn = ef[0] 122 e = getattr(e, fn, None) 123 if e in visited: 124 raise RecursiveFactor(obj, e) 125 visited.add(e) 126 yield fn, e 127 128def format_element(obj, coverage = ()): 129 'format the given element with its factors and metadata into a readable string' 130 # if it's not an Element, all there is to return is str(obj) 131 if obj in coverage: 132 raise RecursiveFactor(coverage) 133 coverage = coverage + (obj,) 134 135 if not isinstance(obj, Element): 136 if obj is None: 137 return 'None' 138 return str(obj) 139 140 # The description of `obj` is built first. 141 142 # formal element, get metas first. 143 nolead = False 144 metas = [] 145 for key, val in obj._e_metas(): 146 m = "" 147 if val is None: 148 sval = 'None' 149 else: 150 sval = str(val) 151 152 pre = ' ' 153 if key is not None: 154 m += key + ':' 155 if (len(sval) > 70 or os.linesep in sval): 156 pre = os.linesep 157 sval = indent(sval) 158 else: 159 # if the key is None, it is intended to be inlined. 160 nolead = True 161 pre = '' 162 m += pre + sval.rstrip() 163 metas.append(m) 164 165 factors = [] 166 for att in obj._e_factors[1:]: 167 m = "" 168 f = getattr(obj, att) 169 # if the object has a label, use the label 170 m += att + ':' 171 sval = format_element(f, coverage = coverage) 172 if len(sval) > 70 or os.linesep in sval: 173 m += os.linesep + indent(sval) 174 else: 175 m += ' ' + sval 176 factors.append(m) 177 178 mtxt = os.linesep.join(metas) 179 ftxt = os.linesep.join(factors) 180 if mtxt: 181 mtxt = indent(mtxt) 182 if ftxt: 183 ftxt = indent(ftxt) 184 s = mtxt + ftxt 185 if nolead is True: 186 # metas started with a `None` key. 187 s = ' ' + s.lstrip() 188 else: 189 s = os.linesep + s 190 s = obj._e_label + ':' + s.rstrip() 191 192 # and resolve the next prime 193 pf = prime_factor(obj) 194 if pf is not None: 195 factor_name, prime = pf 196 factor = format_element(prime, coverage = coverage) 197 if getattr(prime, '_e_label', None) is not None: 198 # if the factor has a label, then it will be 199 # included in the format_element output, and 200 # thus factor_name is not needed. 201 factor_name = '' 202 else: 203 factor_name += ':' 204 if len(factor) > 70 or os.linesep in factor: 205 factor = os.linesep + indent(factor) 206 else: 207 factor_name += ' ' 208 s += os.linesep + factor_name + factor 209 return s 210