Repository: itamarst/eliot

Issue:
Make eliot._parse public so eliot-tree isn't depending on private API
`eliot-tree` depends on `eliot._parse`, which is private. Really, it's a stable API, and it should be useful to others as well.

So:

1. Make it public.
2. Add backwards compat `eliot._parse` for old eliot-tree installs.
3. Document it, at least a little.

CC @jonathanj 

Discussion:

Patch:
diff --git a/docs/source/news.rst b/docs/source/news.rst
index bb2ba9b..b0409df 100644
--- a/docs/source/news.rst
+++ b/docs/source/news.rst
@@ -13,6 +13,7 @@ Features:
 * NumPy integers, floats, bools and arrays are now automatically serialized to JSON, via a new default JSON encoder (``eliot.json.EliotJSONEncoder``).
 * Dask support: replace ``dask.compute()`` with ``eliot.dask.compute_with_trace()`` to automatically preserve Eliot context for ``Bag`` and ``Delayed`` Dask computations. See :ref:`dask_usage` for details.
 * New decorator, ``@eliot.log_call``, which automatically creates an action that starts when function is called and ends when it returns. See :ref:`log_call decorator`.
+* A parser library for parsing serialized Eliot JSON messages into a tree of Python objects. See :ref:`parsing_logs` for details.
 
 Testing features:
 
diff --git a/docs/source/reading/reading.rst b/docs/source/reading/reading.rst
index 1312135..89a0ab9 100644
--- a/docs/source/reading/reading.rst
+++ b/docs/source/reading/reading.rst
@@ -41,3 +41,24 @@ For example, here's how you'd extract a particular field with `jq`_:
 
 .. _eliot-tree: https://github.com/jonathanj/eliottree
 .. _jq: https://stedolan.github.io/jq/
+
+
+.. _parsing_logs:
+
+Parsing Logs
+------------
+
+Eliot also includes a parser for parsing logs into Python objects:
+
+.. code-block:: python
+
+   import json
+   from eliot.parse import Parser
+
+   def load_messages(logfile_path):
+      for line in open(logfile_path):
+          yield json.loads(line)
+       
+   def parse(logfile_path):
+       for task in Parser.parse_stream(load_messages(logfile_path)):
+           print("Root action type is", task.root().action_type)
diff --git a/eliot/__init__.py b/eliot/__init__.py
index 2d998fc..d21ecbb 100644
--- a/eliot/__init__.py
+++ b/eliot/__init__.py
@@ -47,6 +47,18 @@ add_destinations = Logger._destinations.add
 remove_destination = removeDestination
 add_global_fields = addGlobalFields
 
+# Backwards compatibility for old versions of eliot-tree, which rely on
+# eliot._parse:
+def _parse_compat():
+    # Force eliot.parse to be imported in way that works with old Python:
+    from .parse import Parser
+    del Parser
+    import sys
+    sys.modules["eliot._parse"] = sys.modules["eliot.parse"]
+    return sys.modules["eliot.parse"]
+_parse = _parse_compat()
+del _parse_compat
+
 
 __all__ = [
     "Message",
@@ -83,7 +95,11 @@ __all__ = [
     "to_file",
     "log_call",
     "__version__",
+
+    # Backwards compat for eliot-tree:
+    "_parse",
 ]
 
+
 __version__ = get_versions()['version']
 del get_versions
diff --git a/eliot/_parse.py b/eliot/parse.py
similarity index 98%
rename from eliot/_parse.py
rename to eliot/parse.py
index 91474d0..bca580c 100644
--- a/eliot/_parse.py
+++ b/eliot/parse.py
@@ -181,3 +181,6 @@ class Parser(PClass):
                 yield task
         for task in parser.incomplete_tasks():
             yield task
+
+
+__all__ = ["Parser", "Task", "TaskLevel", "WrittenMessage", "WrittenAction"]


Test patch:
diff --git a/eliot/tests/corotests.py b/eliot/tests/corotests.py
index de844c8..78e3501 100644
--- a/eliot/tests/corotests.py
+++ b/eliot/tests/corotests.py
@@ -10,7 +10,7 @@ from threading import Thread
 from unittest import TestCase
 
 from ..testing import capture_logging
-from .._parse import Parser
+from ..parse import Parser
 from .. import start_action
 from .._action import _ExecutionContext, _context, use_asyncio_context
 from .._asyncio import AsyncioContext
diff --git a/eliot/tests/test_action.py b/eliot/tests/test_action.py
index 86eb5e1..85b91ed 100644
--- a/eliot/tests/test_action.py
+++ b/eliot/tests/test_action.py
@@ -44,7 +44,7 @@ from .._message import (
 from .._output import MemoryLogger
 from .._validation import ActionType, Field, _ActionSerializers
 from ..testing import assertContainsFields, capture_logging
-from .._parse import Parser
+from ..parse import Parser
 from .. import (
     _action,
     add_destination,
diff --git a/eliot/tests/test_parse.py b/eliot/tests/test_parse.py
index a24ad00..77410ee 100644
--- a/eliot/tests/test_parse.py
+++ b/eliot/tests/test_parse.py
@@ -16,7 +16,7 @@ from pyrsistent import PClass, field, pvector_field
 
 from .. import start_action, Message
 from ..testing import MemoryLogger
-from .._parse import Task, Parser
+from ..parse import Task, Parser
 from .._message import (
     WrittenMessage,
     MESSAGE_TYPE_FIELD,
@@ -295,3 +295,15 @@ class ParserTests(TestCase):
                 if m is not None]))
         assertCountEqual(
             self, all_tasks, [parse_to_task(msgs) for msgs in all_messages])
+
+
+class BackwardsCompatibility(TestCase):
+    """Tests for backwards compatibility."""
+
+    def test_imports(self):
+        """Old ways of importing still work."""
+        import eliot._parse
+        from eliot import _parse
+        import eliot.parse
+        self.assertIs(eliot.parse, eliot._parse)
+        self.assertIs(_parse, eliot.parse)