Repository: itamarst/eliot

Issue:
Hypothesis tests have issues (running too slowly? Flaky?)
E.g. https://travis-ci.org/itamarst/eliot/jobs/490759458

Discussion:

Patch:
diff --git a/benchmarks/serialization.py b/benchmarks/serialization.py
index d2baa87..453fe24 100644
--- a/benchmarks/serialization.py
+++ b/benchmarks/serialization.py
@@ -6,6 +6,8 @@ realistic manner. That is, mesages are logged in context of a message with a
 small number of fields.
 """
 
+from __future__ import unicode_literals
+
 import time
 
 from eliot import Message, start_action
diff --git a/docs/source/news.rst b/docs/source/news.rst
index b0409df..d501e84 100644
--- a/docs/source/news.rst
+++ b/docs/source/news.rst
@@ -1,6 +1,13 @@
 What's New
 ==========
 
+1.7.0
+^^^^^
+
+Features:
+
+* Generating messages is much faster.
+
 1.6.0
 ^^^^^
 
diff --git a/eliot/_action.py b/eliot/_action.py
index 9549ca2..09ed9b8 100644
--- a/eliot/_action.py
+++ b/eliot/_action.py
@@ -20,10 +20,9 @@ from pyrsistent import (
     PClass,
     optional,
     pmap_field,
-    pvector_field,
     pvector, )
 from boltons.funcutils import wraps
-from six import text_type as unicode, integer_types, PY3
+from six import text_type as unicode, PY3
 
 from ._message import (
     Message,
@@ -108,7 +107,7 @@ def use_asyncio_context():
     _context.get_sub_context = AsyncioContext().get_stack
 
 
-class TaskLevel(PClass):
+class TaskLevel(object):
     """
     The location of a message within the tree of actions of a task.
 
@@ -117,23 +116,45 @@ class TaskLevel(PClass):
         3]} indicates this is the third message within an action which is
         the second item in the task.
     """
+    def __init__(self, level):
+        self._level = level
 
-    level = pvector_field(integer_types)
+    def as_list(self):
+        """Return the current level.
 
-    # PClass really ought to provide this ordering facility for us:
-    # tobgu/pyrsistent#45.
+        @return: List of integers.
+        """
+        return self._level[:]
+
+    # Backwards compatibility:
+    @property
+    def level(self):
+        return pvector(self._level)
 
     def __lt__(self, other):
-        return self.level < other.level
+        return self._level < other._level
 
     def __le__(self, other):
-        return self.level <= other.level
+        return self._level <= other._level
 
     def __gt__(self, other):
-        return self.level > other.level
+        return self._level > other._level
 
     def __ge__(self, other):
-        return self.level >= other.level
+        return self._level >= other._level
+
+    def __eq__(self, other):
+        if other.__class__ != TaskLevel:
+            return False
+        return self._level == other._level
+
+    def __ne__(self, other):
+        if other.__class__ != TaskLevel:
+            return True
+        return self._level != other._level
+
+    def __hash__(self):
+        return hash(tuple(self._level))
 
     @classmethod
     def fromString(cls, string):
@@ -152,7 +173,7 @@ class TaskLevel(PClass):
 
         @return: L{unicode} representation of the L{TaskLevel}.
         """
-        return "/" + "/".join(map(unicode, self.level))
+        return "/" + "/".join(map(unicode, self._level))
 
     def next_sibling(self):
         """
@@ -161,7 +182,9 @@ class TaskLevel(PClass):
 
         @return: L{TaskLevel} which follows this one.
         """
-        return TaskLevel(level=self.level.set(-1, self.level[-1] + 1))
+        new_level = self._level[:]
+        new_level[-1] += 1
+        return TaskLevel(level=new_level)
 
     def child(self):
         """
@@ -169,7 +192,9 @@ class TaskLevel(PClass):
 
         @return: L{TaskLevel} which is the first child of this one.
         """
-        return TaskLevel(level=self.level.append(1))
+        new_level = self._level[:]
+        new_level.append(1)
+        return TaskLevel(level=new_level)
 
     def parent(self):
         """
@@ -178,9 +203,9 @@ class TaskLevel(PClass):
 
         @return: L{TaskLevel} which is the parent of this one.
         """
-        if not self.level:
+        if not self._level:
             return None
-        return TaskLevel(level=self.level[:-1])
+        return TaskLevel(level=self._level[:-1])
 
     def is_sibling_of(self, task_level):
         """
diff --git a/eliot/_message.py b/eliot/_message.py
index 60c61a5..320daee 100644
--- a/eliot/_message.py
+++ b/eliot/_message.py
@@ -9,7 +9,7 @@ from uuid import uuid4
 
 from six import text_type as unicode
 
-from pyrsistent import PClass, thaw, pmap_field, pmap
+from pyrsistent import PClass, pmap_field
 
 MESSAGE_TYPE_FIELD = 'message_type'
 TASK_UUID_FIELD = 'task_uuid'
@@ -55,7 +55,7 @@ class Message(object):
 
         The keyword arguments will become contents of the L{Message}.
         """
-        _class.new(**fields).write()
+        _class(fields).write()
 
     def __init__(self, contents, serializer=None):
         """
@@ -70,7 +70,7 @@ class Message(object):
             L{eliot.Logger} may choose to serialize the message. If you're
             using L{eliot.MessageType} this will be populated for you.
         """
-        self._contents = pmap(contents)
+        self._contents = contents.copy()
         self._serializer = serializer
 
     def bind(self, **fields):
@@ -78,13 +78,15 @@ class Message(object):
         Return a new L{Message} with this message's contents plus the
         additional given bindings.
         """
-        return Message(self._contents.update(fields), self._serializer)
+        contents = self._contents.copy()
+        contents.update(fields)
+        return Message(contents, self._serializer)
 
     def contents(self):
         """
         Return a copy of L{Message} contents.
         """
-        return dict(self._contents)
+        return self._contents.copy()
 
     def _timestamp(self):
         """
@@ -110,7 +112,7 @@ class Message(object):
             task_level = [1]
         else:
             task_uuid = action._identification[TASK_UUID_FIELD]
-            task_level = thaw(action._nextTaskLevel().level)
+            task_level = action._nextTaskLevel().as_list()
         timestamp = self._timestamp()
         new_values = {
             TIMESTAMP_FIELD: timestamp,
@@ -121,7 +123,8 @@ class Message(object):
             "message_type" not in self._contents
         ):
             new_values["message_type"] = ""
-        return self._contents.update(new_values)
+        new_values.update(self._contents)
+        return new_values
 
     def write(self, logger=None, action=None):
         """
@@ -141,7 +144,7 @@ class Message(object):
         if logger is None:
             logger = _output._DEFAULT_LOGGER
         logged_dict = self._freeze(action=action)
-        logger.write(dict(logged_dict), self._serializer)
+        logger.write(logged_dict, self._serializer)
 
 
 class WrittenMessage(PClass):
diff --git a/eliot/_output.py b/eliot/_output.py
index 087ce05..da83f22 100644
--- a/eliot/_output.py
+++ b/eliot/_output.py
@@ -218,7 +218,7 @@ class Logger(object):
                             self._safeUnicodeDictionary(dictionary)
                         }
                     )
-                    self._destinations.send(dict(msg._freeze()))
+                    self._destinations.send(msg._freeze())
                 except:
                     # Nothing we can do here, raising exception to caller will
                     # break business logic, better to have that continue to
diff --git a/setup.py b/setup.py
index b9b9bba..9f76f6f 100644
--- a/setup.py
+++ b/setup.py
@@ -62,7 +62,8 @@ setup(
             "sphinx",
             "sphinx_rtd_theme",
             "flake8",
-            "yapf"
+            "yapf",
+            "pytest"
         ]
     },
     entry_points={


Test patch:
diff --git a/eliot/tests/__init__.py b/eliot/tests/__init__.py
index 0bd69e7..052ada6 100644
--- a/eliot/tests/__init__.py
+++ b/eliot/tests/__init__.py
@@ -1,3 +1,8 @@
 """
 Tests for the eliot package.
 """
+
+# Increase hypothesis deadline so we don't time out on PyPy:
+from hypothesis import settings
+settings.register_profile("eliot", deadline=1000)
+settings.load_profile("eliot")
diff --git a/eliot/tests/test_action.py b/eliot/tests/test_action.py
index dc1113b..500004d 100644
--- a/eliot/tests/test_action.py
+++ b/eliot/tests/test_action.py
@@ -960,6 +960,37 @@ class TaskLevelTests(TestCase):
             )
         )
 
+    def test_equality(self):
+        """
+        L{TaskChild} correctly implements equality and hashing.
+        """
+        a = TaskLevel(level=[1, 2])
+        a2 = TaskLevel(level=[1, 2])
+        b = TaskLevel(level=[2, 999])
+        self.assertTrue(
+            all(
+                [
+                    a == a2,
+                    a2 == a,
+                    a != b,
+                    b != a,
+                    not b == a,
+                    not a == b,
+                    not a == 1,
+                    a != 1,
+                    hash(a) == hash(a2),
+                    hash(b) != hash(a)
+                ]
+            )
+        )
+
+    def test_as_list(self):
+        """
+        L{TaskChild.as_list} returns the level.
+        """
+        self.assertEqual(TaskLevel(level=[1, 2, 3]).as_list(),
+                         [1, 2, 3])
+
     @given(lists(task_level_indexes))
     def test_parent_of_child(self, base_task_level):
         """