1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
|
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy import Object
from appy.pod import PodError
from appy.shared.utils import Traceback
from appy.pod.elements import *
# ------------------------------------------------------------------------------
EVAL_ERROR = 'Error while evaluating expression "%s". %s'
FROM_EVAL_ERROR = 'Error while evaluating the expression "%s" defined in the ' \
'"from" part of a statement. %s'
WRONG_SEQ_TYPE = 'Expression "%s" is not iterable.'
TABLE_NOT_ONE_CELL = "The table you wanted to populate with '%s' " \
"can\'t be dumped with the '-' option because it has " \
"more than one cell in it."
# ------------------------------------------------------------------------------
class BufferAction:
'''Abstract class representing a action (=statement) that must be performed
on the content of a buffer (if, for...).'''
def __init__(self, name, buffer, expr, elem, minus, source, fromExpr):
self.name = name # Actions may be named. Currently, the name of an
# action is only used for giving a name to "if" actions; thanks to this
# name, "else" actions that are far away may reference their "if".
self.buffer = buffer # The object of the action
self.expr = expr # Python expression to evaluate (may be None in the
# case of a NullAction or ElseAction, for example)
self.elem = elem # The element within the buffer that is the object
# of the action.
self.minus = minus # If True, the main elem(s) must not be dumped
self.source = source # If 'buffer', we must dump the (evaluated) buffer
# content. If 'from', we must dump what comes from the 'from' part of
# the action (='fromExpr')
self.fromExpr = fromExpr
# Several actions may co-exist for the same buffer, as a chain of
# BufferAction instances, defined via the following attribute.
self.subAction = None
def getExceptionLine(self, e):
'''Gets the line describing exception p_e, containing the exception
class, message and line number.'''
return '%s: %s' % (e.__class__.__name__, str(e))
def manageError(self, result, context, errorMessage):
'''Manage the encountered error: dump it into the buffer or raise an
exception.'''
if self.buffer.env.raiseOnError:
if not self.buffer.pod:
# Add in the error message the line nb where the errors occurs
# within the PX.
locator = self.buffer.env.parser.locator
# The column number may not be given
col = locator.getColumnNumber()
if col == None: col = ''
else: col = ', column %d' % col
errorMessage += ' (line %s%s)' % (locator.getLineNumber(), col)
# Integrate the traceback (at least, it last lines)
errorMessage += '\n' + Traceback.get(4)
raise Exception(errorMessage)
# Create a temporary buffer to dump the error. If I reuse this buffer to
# dump the error (what I did before), and we are, at some depth, in a
# for loop, this buffer will contain the error message and not the
# content to repeat anymore. It means that this error will also show up
# for every subsequent iteration.
tempBuffer = self.buffer.clone()
PodError.dump(tempBuffer, errorMessage, withinElement=self.elem)
tempBuffer.evaluate(result, context)
def _evalExpr(self, expr, context):
'''Evaluates p_expr with p_context. p_expr can contain an error expr,
in the form "someExpr|errorExpr". If it is the case, if the "normal"
expr raises an error, the "error" expr is evaluated instead.'''
if '|' not in expr:
res = eval(expr, context)
else:
expr, errorExpr = expr.rsplit('|', 1)
try:
res = eval(expr, context)
except Exception:
res = eval(errorExpr, context)
return res
def evaluateExpression(self, result, context, expr):
'''Evaluates expression p_expr with the current p_context. Returns a
tuple (result, errorOccurred).'''
try:
res = self._evalExpr(expr, context)
error = False
except Exception, e:
res = None
errorMessage = EVAL_ERROR % (expr, self.getExceptionLine(e))
self.manageError(result, context, errorMessage)
error = True
return res, error
def execute(self, result, context):
'''Executes this action given some p_context and add the result to
p_result.'''
# Check that if minus is set, we have an element which can accept it
if self.minus and isinstance(self.elem, Table) and \
(not self.elem.tableInfo.isOneCell()):
self.manageError(result, context, TABLE_NOT_ONE_CELL % self.expr)
else:
error = False
# Evaluate self.expr in eRes
eRes = None
if self.expr:
eRes,error = self.evaluateExpression(result, context, self.expr)
if not error:
# Trigger action-specific behaviour
self.do(result, context, eRes)
def evaluateBuffer(self, result, context):
if self.source == 'buffer':
self.buffer.evaluate(result, context, removeMainElems=self.minus)
else:
# Evaluate self.fromExpr in feRes
feRes = None
error = False
try:
feRes = eval(self.fromExpr, context)
except Exception, e:
msg = FROM_EVAL_ERROR% (self.fromExpr, self.getExceptionLine(e))
self.manageError(result, context, msg)
error = True
if not error:
result.write(feRes)
def addSubAction(self, action):
'''Adds p_action as a sub-action of this action.'''
if not self.subAction:
self.subAction = action
else:
self.subAction.addSubAction(action)
class IfAction(BufferAction):
'''Action that determines if we must include the content of the buffer in
the result or not.'''
def do(self, result, context, exprRes):
if exprRes:
if self.subAction:
self.subAction.execute(result, context)
else:
self.evaluateBuffer(result, context)
else:
if self.buffer.isMainElement(Cell.OD):
# Don't leave the current row with a wrong number of cells
result.dumpElement(Cell.OD.elem)
class ElseAction(IfAction):
'''Action that is linked to a previous "if" action. In fact, an "else"
action works exactly like an "if" action, excepted that instead of
defining a conditional expression, it is based on the negation of the
conditional expression of the last defined "if" action.'''
def __init__(self, name, buff, expr, elem, minus, src, fromExpr, ifAction):
IfAction.__init__(self, name, buff, None, elem, minus, src, fromExpr)
self.ifAction = ifAction
def do(self, result, context, exprRes):
# This action is executed if the tied "if" action is not executed.
ifAction = self.ifAction
iRes,error = ifAction.evaluateExpression(result, context, ifAction.expr)
IfAction.do(self, result, context, not iRes)
class ForAction(BufferAction):
'''Actions that will include the content of the buffer as many times as
specified by the action parameters.'''
def __init__(self, name, buff, expr, elem, minus, iter, src, fromExpr):
BufferAction.__init__(self, name, buff, expr, elem, minus, src,fromExpr)
self.iter = iter # Name of the iterator variable used in the each loop
def initialiseLoop(self, context, elems):
'''Initialises information about the loop, before entering into it. It
is possible that this loop overrides an outer loop whose iterator
has the same name. This method returns a tuple
(loop, outerOverriddenLoop).'''
# The "loop" object, made available in the POD context, contains info
# about all currently walked loops. For every walked loop, a specific
# object, le'ts name it curLoop, accessible at getattr(loop, self.iter),
# stores info about its status:
# * curLoop.length gives the total number of walked elements withhin
# the loop
# * curLoop.nb gives the index (starting at 0) if the currently
# walked element.
# * curLoop.first is True if the currently walked element is the
# first one.
# * curLoop.last is True if the currently walked element is the
# last one.
# * curLoop.odd is True if the currently walked element is odd
# * curLoop.even is True if the currently walked element is even
# For example, if you have a "for" statement like this:
# for elem in myListOfElements
# Within the part of the ODT document impacted by this statement, you
# may access to:
# * loop.elem.length to know the total length of myListOfElements
# * loop.elem.nb to know the index of the current elem within
# myListOfElements.
if 'loop' not in context:
context['loop'] = Object()
try:
total = len(elems)
except Exception:
total = 0
curLoop = Object(length=total)
# Does this loop overrides an outer loop whose iterator has the same
# name ?
outerLoop = None
if hasattr(context['loop'], self.iter):
outerLoop = getattr(context['loop'], self.iter)
# Put this loop in the global object "loop".
setattr(context['loop'], self.iter, curLoop)
return curLoop, outerLoop
def do(self, result, context, elems):
'''Performs the "for" action. p_elems is the list of elements to
walk, evaluated from self.expr.'''
# Check p_exprRes type
try:
# All "iterable" objects are OK
iter(elems)
except TypeError:
self.manageError(result, context, WRONG_SEQ_TYPE % self.expr)
return
# Remember variable hidden by iter if any
hasHiddenVariable = False
if context.has_key(self.iter):
hiddenVariable = context[self.iter]
hasHiddenVariable = True
# In the case of cells, initialize some values
isCell = False
if isinstance(self.elem, Cell):
isCell = True
if 'columnsRepeated' in context:
nbOfColumns = sum(context['columnsRepeated'])
customColumnsRepeated = True
else:
nbOfColumns = self.elem.tableInfo.nbOfColumns
customColumnsRepeated = False
initialColIndex = self.elem.colIndex
currentColIndex = initialColIndex
rowAttributes = self.elem.tableInfo.curRowAttrs
# If p_elems is empty, dump an empty cell to avoid having the wrong
# number of cells for the current row.
if not elems:
result.dumpElement(Cell.OD.elem)
# Enter the "for" loop
loop, outerLoop = self.initialiseLoop(context, elems)
i = -1
for item in elems:
i += 1
loop.nb = i
loop.first = i == 0
loop.last = i == (loop.length-1)
loop.even = (i%2)==0
loop.odd = not loop.even
context[self.iter] = item
# Cell: add a new row if we are at the end of a row
if isCell and (currentColIndex == nbOfColumns):
result.dumpEndElement(Row.OD.elem)
result.dumpStartElement(Row.OD.elem, rowAttributes)
currentColIndex = 0
# If a sub-action is defined, execute it
if self.subAction:
self.subAction.execute(result, context)
else:
# Evaluate the buffer directly
self.evaluateBuffer(result, context)
# Cell: increment the current column index
if isCell:
currentColIndex += 1
# Cell: leave the last row with the correct number of cells, excepted
# if the user has specified himself "columnsRepeated": it is his
# responsibility to produce the correct number of cells.
if isCell and elems and not customColumnsRepeated:
wrongNbOfCells = (currentColIndex-1) - initialColIndex
if wrongNbOfCells < 0: # Too few cells for last row
for i in range(abs(wrongNbOfCells)):
context[self.iter] = ''
self.buffer.evaluate(result, context, subElements=False)
# This way, the cell is dumped with the correct styles
elif wrongNbOfCells > 0: # Too many cells for last row
# Finish current row
nbOfMissingCells = 0
if currentColIndex < nbOfColumns:
nbOfMissingCells = nbOfColumns - currentColIndex
context[self.iter] = ''
for i in range(nbOfMissingCells):
self.buffer.evaluate(result, context, subElements=False)
result.dumpEndElement(Row.OD.elem)
# Create additional row with remaining cells
result.dumpStartElement(Row.OD.elem, rowAttributes)
nbOfRemainingCells = wrongNbOfCells + nbOfMissingCells
nbOfMissingCellsLastLine = nbOfColumns - nbOfRemainingCells
context[self.iter] = ''
for i in range(nbOfMissingCellsLastLine):
self.buffer.evaluate(result, context, subElements=False)
# Delete the current loop object and restore the overridden one if any
try:
delattr(context['loop'], self.iter)
except AttributeError:
pass
if outerLoop:
setattr(context['loop'], self.iter, outerLoop)
# Restore hidden variable if any
if hasHiddenVariable:
context[self.iter] = hiddenVariable
else:
if elems:
if self.iter in context: # May not be the case on error
del context[self.iter]
class NullAction(BufferAction):
'''Action that does nothing. Used in conjunction with a "from" clause, it
allows to insert in a buffer arbitrary odt content.'''
def do(self, result, context, exprRes):
self.evaluateBuffer(result, context)
class VariablesAction(BufferAction):
'''Action that allows to define a set of variables somewhere in the
template.'''
def __init__(self, name, buff, elem, minus, variables, src, fromExpr):
# We do not use the default Buffer.expr attribute for storing the Python
# expression, because here we will have several expressions, one for
# every defined variable.
BufferAction.__init__(self,name, buff, None, elem, minus, src, fromExpr)
# Definitions of variables: ~[(s_name, s_expr)]~
self.variables = variables
def do(self, result, context, exprRes):
'''Evaluate the variables' expressions: because there are several
expressions, we do not use the standard, single-expression-minded
BufferAction code for evaluating our expressions.
We remember the names and values of the variables that we will hide
in the context: after execution of this buffer we will restore those
values.
'''
hidden = None
for name, expr in self.variables:
# Evaluate variable expression in vRes
vRes, error = self.evaluateExpression(result, context, expr)
if error: return
# Replace the value of global variables
if name.startswith('@'):
context[name[1:]] = vRes
continue
# Remember the variable previous value if already in the context
if name in context:
if not hidden:
hidden = {name: context[name]}
else:
hidden[name] = context[name]
# Store the result into the context
context[name] = vRes
# If a sub-action is defined, execute it
if self.subAction:
self.subAction.execute(result, context)
else:
# Evaluate the buffer directly
self.evaluateBuffer(result, context)
# Restore hidden variables if any
if hidden: context.update(hidden)
# Delete not-hidden variables
for name, expr in self.variables:
if name.startswith('@'): continue
if hidden and (name in hidden): continue
del context[name]
# ------------------------------------------------------------------------------
|