bebraw
1/10/2010 - 12:37 PM

assignment.py

import re

class Assignment:

    def matches(self, expression):
        '''
        >>> assignment = Assignment()

        >>> assignment.matches('a=5')
        True

        >>> assignment.matches('  a  =  5   ')
        True

        >>> assignment.matches('5=5')
        False

        Extra whitespace in name
        >>> assignment.matches('bar bar = 5')
        False

        Assignment of a variable's value
        >>> assignment.matches('a=b')
        True

        Underscore is allowed in the beginning
        >>> assignment.matches('_fooDOO=bar')
        True

        >>> assignment.matches('a=b+5')
        True

        Multiple assignment
        >>> assignment.matches('a=b=5')
        True

        Multipart
        >>> assignment.matches('a, b = 5, 10')
        True

        >>> assignment.matches('a, b, c = 5, 10')
        False

        Multiple assignment and multipart
        >>> assignment.matches('a, b = d, e = 10, 13')
        True

        Disallow minus in front of a name because "foo - -FOO" is
        ambiguous
        >>> assignment.matches('-FOO=b')
        False
        
        >>> assignment.matches('a, -b = 5, 10')
        False

        Disallow minus in middle of a name because "foo-doo" is
        ambiguous
        >>> assignment.matches('foo-doo=b')
        False

        Don't match if missing =
        >>> assignment.matches('a + b')
        False

        Don't match empty
        >>> assignment.matches('')
        False

        - expansion
        >>> assignment.matches('FOO-=b')
        True

        + expansion
        >>> assignment.matches('FOO+=b')
        True

        * expansion
        >>> assignment.matches('FOO*=b')
        True

        / expansion
        >>> assignment.matches('FOO/=b')
        True

        % expansion
        >>> assignment.matches('FOO%=b')
        True
        '''
        parts = self._split_expression(expression)

        if len(parts) < 2:
            return False

        first_segments_len = len(parts[0].segments)
        for part in parts:
            if len(part.segments) != first_segments_len:
                return False

        if len(parts) == 2 and len(parts[0].segments) == 1:
            l_part = parts[0].segments[0]

            if not l_part.is_valid():
                return False
        else:
            for part in parts[:-1]:
                for segment in part.segments:
                    if not segment.is_valid():
                        return False

        return True

    def execute(self, expression, commands, variables):
        '''
        >>> from mock import Mock
        >>> from py.test import raises

        >>> python = Mock()

        >>> commands = Mock()
        >>> commands.find.return_value = python

        >>> assignment = Assignment()

        Simple assignment
        >>> python.execute.return_value = 5
        >>> variables = {}
        >>> assignment.execute('a=5', commands, variables)
        >>> variables
        {'a': 5}

        Multipart
        >>> python.execute.return_value = 5
        >>> variables = {}
        >>> assignment.execute('a=b=5', commands, variables)
        >>> variables
        {'a': 5, 'b': 5}

        Multiple assignment
        TODO: check type conversion (int to str?)
        >>> def execute(expression, variables):
        ...     return expression
        >>> python.execute = execute
        >>> variables = {}
        >>> assignment.execute('a, b = 5, 10', commands, variables)
        >>> variables
        {'a': '5', 'b': '10'}

        Multiple assignment and multipart
        TODO: check type conversion (int to str?)
        >>> def execute(expression, variables):
        ...     return expression
        >>> python.execute = execute
        >>> variables = {'a': 5, 'b': 6}
        >>> assignment.execute('a, b = c, d = 5, 10', commands, variables)
        >>> variables
        {'a': '5', 'c': '5', 'b': '10', 'd': '10'}

        Assignment of the value of another variable
        >>> python.execute = Mock()
        >>> python.execute.return_value = 10
        >>> variables = {'a': 10}
        >>> assignment.execute('b=a', commands, variables)
        >>> variables
        {'a': 10, 'b': 10}

        Assignment of a result
        >>> python.execute.return_value = 15
        >>> variables = {'a': 10}
        >>> assignment.execute('b=a+5', commands, variables)
        >>> variables
        {'a': 10, 'b': 15}

        - expansion
        >>> python.execute.return_value = 5
        >>> variables = {'a': 10}
        >>> assignment.execute('a-=5', commands, variables)
        >>> variables
        {'a': 5}

        + expansion
        >>> python.execute.return_value = 15
        >>> variables = {'a': 10}
        >>> assignment.execute('a+=5', commands, variables)
        >>> variables
        {'a': 15}

        * expansion
        >>> python.execute.return_value = 50
        >>> variables = {'a': 10}
        >>> assignment.execute('a*=5', commands, variables)
        >>> variables
        {'a': 50}

        / expansion
        >>> python.execute.return_value = 2
        >>> variables = {'a': 10}
        >>> assignment.execute('a/=5', commands, variables)
        >>> variables
        {'a': 2}

        % expansion
        >>> python.execute.return_value = 0
        >>> variables = {'a': 10}
        >>> assignment.execute('a%=5', commands, variables)
        >>> variables
        {'a': 0}

        Assignment of an invalid variable
        >>> python.execute = Mock()
        >>> python.execute.side_effect = NameError("name 'b' is not defined")
        >>> variables = {}
        >>> exception = raises(NameError, assignment.execute, 'a=b', commands,
        ...     variables)
        >>> exception.typename
        'NameError'
        >>> variables
        {}

        Assignment of an invalid variable - Multipart case
        >>> python.execute = Mock()
        >>> python.execute.side_effect = NameError("name 'c' is not defined")
        >>> variables = {}
        >>> exception = raises(NameError, assignment.execute, 'a=b=c',
        ...     commands, variables)
        >>> exception.typename
        'NameError'
        >>> variables
        {}

        Assignment of an invalid variable - Multiple assignment case
        >>> python.execute = Mock()
        >>> python.execute.side_effect = NameError("name 'c' is not defined")
        >>> variables = {}
        >>> exception = raises(NameError, assignment.execute, 'a,b=c,d',
        ...     commands, variables)
        >>> exception.typename
        'NameError'
        >>> variables
        {}
        '''
        def set_variables(l_part, r_part):
            python = commands.find('python')

            for l_segment, r_segment in zip(l_part.segments, r_part.segments):
                if r_segment in variables:
                    variables[l_segment] = variables[r_segment]
                else:
                    variables[l_segment] = python.execute(r_segment, variables)

        parts = self._split_expression(expression)

        if len(parts) == 2:
            if len(parts[1].segments) == 1:
                parts[1].expand(parts[0])

            ret = set_variables(parts[0], parts[1])

            return ret
        else:
            for l_part in parts[:-1]:
                ret = set_variables(l_part, parts[-1])

                if ret:
                    return ret

    def _split_expression(self, expression):
        class Segment(str):
            def is_valid(self):
                return re.match('^[a-zA-Z_]{1}[a-zA-Z_]*$', self)

        class Part:
            def __init__(self, value):
                value = value.strip()

                if len(value) > 0 and value[-1] in ('+', '-', '*', '/', '%'):
                    self.expansion = value[-1]
                    self.segments = (Segment(value[:-1]), )
                else:
                    self.expansion = None
                    self.segments = (Segment(value), )

            def expand(self, l_part):
                expansion = l_part.expansion

                if expansion:
                    self.segments = (l_part.segments[0] + expansion + \
                        self.segments[0], )

        class SegmentedPart:
            def __init__(self, raw_part):
                segments = raw_part.split(',')

                self.segments = []
                for segment in segments:
                    self.segments.append(Segment(segment.strip()))

        expression = expression.strip()
        parts = expression.split('=')

        ret = []
        for part in parts:
            if part.count(',') > 0:
                ret.append(SegmentedPart(part))
            else:
                ret.append(Part(part))

        return ret

if __name__ == "__main__":
    import doctest
    doctest.testmod()