diff --git a/docs/source/news.rst b/docs/source/news.rst index e563c7b..fb7575a 100644 --- a/docs/source/news.rst +++ b/docs/source/news.rst @@ -11,6 +11,7 @@ Documentation: Features: * Generating messages is much faster. +* Eliot now works with PyInstaller. Thanks to Jean-Paul Calderone for the bug report. Fixes issue #386. * The testing infrastructure now has slightly more informative error messages. Thanks to Jean-Paul Calderone for the bug report. Fixes issue #373. * Added lower-level testing infrastructure—``eliot.testing.swap_logger`` and ``eliot.testing.check_for_errors``—which is useful for cases when the ``@capture_logging`` decorator is insufficient. For example, test methods that are async, or return Twisted ``Deferred``. See the :doc:`testing documentation` for details. Thanks to Jean-Paul Calderone for the feature request. Fixes #364. * ``eliot.ValidationError``, as raised by e.g. ``capture_logging``, is now part of the public API. Fixed issue #146. diff --git a/eliot/_traceback.py b/eliot/_traceback.py index 39287d8..4dad7fd 100644 --- a/eliot/_traceback.py +++ b/eliot/_traceback.py @@ -54,7 +54,11 @@ def _get_traceback_no_io(): """ Return a version of L{traceback} that doesn't do I/O. """ - module = load_module(str("_traceback_no_io"), traceback) + try: + module = load_module(str("_traceback_no_io"), traceback) + except NotImplementedError: + # Can't fix the I/O problem, oh well: + return traceback class FakeLineCache(object): def checkcache(self, *args, **kwargs): diff --git a/eliot/_util.py b/eliot/_util.py index 420bc4c..b8f20fa 100644 --- a/eliot/_util.py +++ b/eliot/_util.py @@ -4,9 +4,10 @@ from __future__ import unicode_literals +import sys from types import ModuleType -from six import exec_, text_type as unicode +from six import exec_, text_type as unicode, PY3 def safeunicode(o): @@ -52,9 +53,17 @@ def load_module(name, original_module): @return: A new, distinct module. """ module = ModuleType(name) - path = original_module.__file__ - if path.endswith(".pyc") or path.endswith(".pyo"): - path = path[:-1] - with open(path) as f: - exec_(f.read(), module.__dict__, module.__dict__) + if PY3: + import importlib.util + spec = importlib.util.find_spec(original_module.__name__) + source = spec.loader.get_code(original_module.__name__) + else: + if getattr(sys, "frozen", False): + raise NotImplementedError("Can't load modules on Python 2 with PyInstaller") + path = original_module.__file__ + if path.endswith(".pyc") or path.endswith(".pyo"): + path = path[:-1] + with open(path) as f: + source = f.read() + exec_(source, module.__dict__, module.__dict__) return module diff --git a/eliot/prettyprint.py b/eliot/prettyprint.py index 81284bc..6f17945 100644 --- a/eliot/prettyprint.py +++ b/eliot/prettyprint.py @@ -33,8 +33,13 @@ def _nicer_unicode_repr(o, original_repr=repr): else: return original_repr(o) - pprint = load_module(b"unicode_pprint", pprint) - pprint.repr = _nicer_unicode_repr + try: + pprint = load_module(b"unicode_pprint", pprint) + pprint.repr = _nicer_unicode_repr + except NotImplementedError: + # Oh well won't have nicer output. + import pprint + # Fields that all Eliot messages are expected to have: REQUIRED_FIELDS = {TASK_LEVEL_FIELD, TASK_UUID_FIELD, TIMESTAMP_FIELD} diff --git a/eliot/tests/test_pyinstaller.py b/eliot/tests/test_pyinstaller.py new file mode 100644 index 0000000..f80d2cd --- /dev/null +++ b/eliot/tests/test_pyinstaller.py @@ -0,0 +1,34 @@ +"""Test for pyinstaller compatibility.""" + +from __future__ import absolute_import + +from unittest import TestCase, SkipTest +from tempfile import mkdtemp, NamedTemporaryFile +from subprocess import check_call, CalledProcessError +import os + +from six import PY2 +if PY2: + FileNotFoundError = OSError + + +class PyInstallerTests(TestCase): + """Make sure PyInstaller doesn't break Eliot.""" + + def setUp(self): + try: + check_call(["pyinstaller", "--help"]) + except (CalledProcessError, FileNotFoundError): + raise SkipTest("Can't find pyinstaller.") + + def test_importable(self): + """The Eliot package can be imported inside a PyInstaller packaged binary.""" + output_dir = mkdtemp() + with NamedTemporaryFile(mode="w") as f: + f.write("import eliot; import eliot.prettyprint\n") + f.flush() + check_call( + ["pyinstaller", "--distpath", output_dir, + "-F", "-n", "importeliot", f.name] + ) + check_call([os.path.join(output_dir, "importeliot")]) diff --git a/tox.ini b/tox.ini index 5189222..88ff638 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ basepython = pypy [testenv:py27] deps = cffi + pyinstaller==3.3 basepython = python2.7 [testenv:py34] @@ -38,6 +39,7 @@ deps = numpy [testenv:py36] basepython = python3.6 deps = cffi + pyinstaller [testenv:py37] basepython = python3.7