# ------------------------------------------------------------------------------ # 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 . # ------------------------------------------------------------------------------ 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 as 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 as 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 self.iter in context: 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] # ------------------------------------------------------------------------------