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