Anonymous avatar Anonymous committed 6830e9a

Improve infix and augmented infix operator handling (fixes #12)

- Make augmented infix operators like += (__iadd__) return self instead of a
new dingus.
- Record calls to infix and augmented infix operators.

Comments (0)

Files changed (2)

         self._remove_child_if_exists(child_name)
         self._existing_or_new_child(child_name, value)
 
-    def _create_operator(name):
+    def _create_infix_operator(name):
         def operator_fn(self, other):
-            return self._existing_or_new_child(name)
+            return_value = self._existing_or_new_child(name)
+            self._log_call(name, (other,), {}, return_value)
+            return return_value
         operator_fn.__name__ = name
         return operator_fn
 
-    def _operators():
-        operator_names = ['add', 'and', 'div', 'lshift', 'mod', 'mul', 'or',
-                          'pow', 'rshift', 'sub', 'xor']
-        reverse_operator_names = ['r%s' % name for name in operator_names]
-        for operator_name in operator_names + reverse_operator_names:
+    _BASE_OPERATOR_NAMES = ['add', 'and', 'div', 'lshift', 'mod', 'mul', 'or',
+                            'pow', 'rshift', 'sub', 'xor']
+
+    def _infix_operator_names(base_operator_names):
+        # This function has to have base_operator_names passed in because
+        # Python's scoping rules prevent it from seeing the class-level
+        # _BASE_OPERATOR_NAMES.
+
+        reverse_operator_names = ['r%s' % name for name in base_operator_names]
+        for operator_name in base_operator_names + reverse_operator_names:
             operator_fn_name = '__%s__' % operator_name
             yield operator_fn_name
 
-    # Define each operator
-    for operator_fn_name in _operators():
-        exec('%s = _create_operator("%s")' % (operator_fn_name,
+    # Define each infix operator
+    for operator_fn_name in _infix_operator_names(_BASE_OPERATOR_NAMES):
+        exec('%s = _create_infix_operator("%s")' % (operator_fn_name,
                                               operator_fn_name))
 
+    def _augmented_operator_names(base_operator_names):
+        # Augmented operators are things like +=. They behavior differently
+        # than normal infix operators because they return self instead of a
+        # new object.
+
+        return ['__i%s__' % operator_name
+                for operator_name in base_operator_names]
+
+    def _create_augmented_operator(name):
+        def operator_fn(self, other):
+            return_value = self
+            self._log_call(name, (other,), {}, return_value)
+            return return_value
+        operator_fn.__name__ = name
+        return operator_fn
+
+    # Define each augmenting operator
+    for operator_fn_name in _augmented_operator_names(_BASE_OPERATOR_NAMES):
+        exec('%s = _create_augmented_operator("%s")' % (operator_fn_name,
+                                                        operator_fn_name))
+
     def __str__(self):
         return '<Dingus %s>' % self._full_name
     __repr__ = __str__

tests/test_dingus.py

         assert_raises(AttributeError, lambda: Dingus().__getnewargs__)
 
 
-class WhenApplyingBinaryOperators:
-    operator_names = ['add', 'and_', 'div', 'lshift', 'mod', 'mul', 'or_',
-                      'pow', 'rshift', 'sub', 'xor']
+INFIX_OPERATORS = ['add', 'and_', 'div', 'lshift', 'mod', 'mul', 'or_',
+                   'pow', 'rshift', 'sub', 'xor']
+
+
+class WhenApplyingInfixOperators:
+    def __init__(self):
+        self.operators = [getattr(operator, operator_name)
+                          for operator_name in INFIX_OPERATORS]
 
     def assert_returns_new_dingus(self, op):
         left, right = Dingus.many(2)
         assert result is not left and result is not right
 
     def should_always_return_new_dingus(self):
-        for operator_name in self.operator_names:
-            op = getattr(operator, operator_name)
-            yield self.assert_returns_new_dingus, op
+        for operator in self.operators:
+            yield self.assert_returns_new_dingus, operator
+
+    def should_record_call(self):
+        for operator in self.operators:
+            left, right = Dingus.many(2)
+            operator(left, right)
+            operator_name_without_mangling = operator.__name__.replace('_', '')
+            magic_method_name = '__%s__' % operator_name_without_mangling
+            yield assert_call_was_logged, left, magic_method_name, right
+
+
+class WhenApplyingAugmentedOperators:
+    AUGMENTED_OPERATORS = ['i%s' % operator_name.replace('_', '')
+                           for operator_name in INFIX_OPERATORS]
+
+    def __init__(self):
+        self.operators = [getattr(operator, operator_name)
+                          for operator_name in self.AUGMENTED_OPERATORS]
+
+    def assert_returns_same_dingus(self, op):
+        left, right = Dingus.many(2)
+        result = op(left, right)
+        assert result is left
+
+    def should_always_return_same_dingus(self):
+        for operator in self.operators:
+            yield self.assert_returns_same_dingus, operator
+
+    def should_record_call(self):
+        for operator in self.operators:
+            left, right = Dingus.many(2)
+            operator(left, right)
+            magic_method_name = '__%s__' % operator.__name__
+            yield assert_call_was_logged, left, magic_method_name, right
+
+
+def assert_call_was_logged(dingus, method_name, *args):
+    assert dingus.calls(method_name, *args).once()
 
 
 class WhenComputingLength:
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.