Bug 1422302 - Create python/mozterm for sharing terminal blessings across modules draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 04 Dec 2017 09:38:24 -0500
changeset 707734 0cce8b5be51ca7d705b457dc08170af0394eaecb
parent 707503 b4cef8d1dff06a1ec2b9bb17211c0c3c7f5b76fa
child 707735 4291222f6cbf037d9fd5104287b880e4488e3278
push id92201
push userahalberstadt@mozilla.com
push dateTue, 05 Dec 2017 18:53:12 +0000
bugs1422302
milestone59.0a1
Bug 1422302 - Create python/mozterm for sharing terminal blessings across modules This is a new module that will provide a place to store some common abstractions around the 'blessings' module. The main entrypoint is: from mozterm import Terminal term = Terminal() If blessings is available, this will return a blessings.Terminal() object. If it isn't available, or something went wrong on import, this will return a NullTerminal() object, which is a drop-in replacement that does no formatting. MozReview-Commit-ID: 6c63svm4tM5
build/virtualenv_packages.txt
python/moz.build
python/mozlint/mozlint/formatters/stylish.py
python/mozterm/mozterm/__init__.py
python/mozterm/mozterm/terminal.py
python/mozterm/test/python.ini
python/mozterm/test/test_terminal.py
taskcluster/ci/source-test/python.yml
tools/lint/flake8.yml
tools/tryselect/selectors/fuzzy.py
--- a/build/virtualenv_packages.txt
+++ b/build/virtualenv_packages.txt
@@ -1,12 +1,13 @@
 mozilla.pth:python/mach
 mozilla.pth:python/mozboot
 mozilla.pth:python/mozbuild
 mozilla.pth:python/mozlint
+mozilla.pth:python/mozterm
 mozilla.pth:python/mozversioncontrol
 mozilla.pth:third_party/python/blessings
 mozilla.pth:third_party/python/compare-locales
 mozilla.pth:third_party/python/configobj
 mozilla.pth:third_party/python/cram
 mozilla.pth:third_party/python/dlmanager
 mozilla.pth:third_party/python/fluent
 mozilla.pth:third_party/python/futures
--- a/python/moz.build
+++ b/python/moz.build
@@ -35,16 +35,17 @@ SPHINX_PYTHON_PACKAGE_DIRS += [
 ]
 
 SPHINX_TREES['mach'] = 'mach/docs'
 
 PYTHON_UNITTEST_MANIFESTS += [
     'mach/mach/test/python.ini',
     'mozbuild/dumbmake/test/python.ini',
     'mozlint/test/python.ini',
+    'mozterm/test/python.ini',
     'mozversioncontrol/test/python.ini',
 ]
 
 if CONFIG['MOZ_BUILD_APP']:
     PYTHON_UNITTEST_MANIFESTS += [
         'mozbuild/mozbuild/test/python.ini',
         'mozbuild/mozpack/test/python.ini',
     ]
--- a/python/mozlint/mozlint/formatters/stylish.py
+++ b/python/mozlint/mozlint/formatters/stylish.py
@@ -1,37 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
-from ..result import ResultContainer
-
-try:
-    import blessings
-except ImportError:
-    blessings = None
-
+from mozterm import Terminal
 
-class NullTerminal(object):
-    """Replacement for `blessings.Terminal()` that does no formatting."""
-    class NullCallableString(unicode):
-        """A dummy callable Unicode stolen from blessings"""
-        def __new__(cls):
-            new = unicode.__new__(cls, u'')
-            return new
-
-        def __call__(self, *args):
-            if len(args) != 1 or isinstance(args[0], int):
-                return u''
-            return args[0]
-
-    def __getattr__(self, attr):
-        return self.NullCallableString()
+from ..result import ResultContainer
 
 
 class StylishFormatter(object):
     """Formatter based on the eslint default."""
 
     # Colors later on in the list are fallbacks in case the terminal
     # doesn't support colors earlier in the list.
     # See http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html
@@ -40,21 +20,18 @@ class StylishFormatter(object):
         'red': [1],
         'yellow': [3],
         'brightred': [9, 1],
         'brightyellow': [11, 3],
     }
     fmt = "  {c1}{lineno}{column}  {c2}{level}{normal}  {message}  {c1}{rule}({linter}){normal}"
     fmt_summary = "{t.bold}{c}\u2716 {problem} ({error}, {warning}{failure}){t.normal}"
 
-    def __init__(self, disable_colors=None):
-        if disable_colors or not blessings:
-            self.term = NullTerminal()
-        else:
-            self.term = blessings.Terminal()
+    def __init__(self, disable_colors=False):
+        self.term = Terminal(disable_styling=disable_colors)
         self.num_colors = self.term.number_of_colors
 
     def color(self, color):
         for num in self._colors[color]:
             if num < self.num_colors:
                 return self.term.color(num)
         return ''
 
new file mode 100644
--- /dev/null
+++ b/python/mozterm/mozterm/__init__.py
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from __future__ import absolute_import, unicode_literals
+
+from .terminal import Terminal, NullTerminal  # noqa
new file mode 100644
--- /dev/null
+++ b/python/mozterm/mozterm/terminal.py
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, unicode_literals
+
+import os
+import sys
+
+
+class NullTerminal(object):
+    """Replacement for `blessings.Terminal()` that does no formatting."""
+    number_of_colors = 0
+    width = 0
+    height = 0
+
+    def __init__(self, stream=None, **kwargs):
+        self.stream = stream or sys.__stdout__
+        try:
+            self.is_a_tty = os.isatty(self.stream.fileno())
+        except:
+            self.is_a_tty = False
+
+    class NullCallableString(unicode):
+        """A dummy callable Unicode stolen from blessings"""
+        def __new__(cls):
+            new = unicode.__new__(cls, '')
+            return new
+
+        def __call__(self, *args):
+            if len(args) != 1 or isinstance(args[0], int):
+                return ''
+            return args[0]
+
+    def __getattr__(self, attr):
+        return self.NullCallableString()
+
+
+def Terminal(raises=False, disable_styling=False, **kwargs):
+    if disable_styling:
+        return NullTerminal(**kwargs)
+
+    try:
+        import blessings
+    except Exception:
+        if raises:
+            raise
+        return NullTerminal(**kwargs)
+    return blessings.Terminal(**kwargs)
new file mode 100644
--- /dev/null
+++ b/python/mozterm/test/python.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+subsuite = mozterm
+
+[test_terminal.py]
new file mode 100644
--- /dev/null
+++ b/python/mozterm/test/test_terminal.py
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, unicode_literals
+
+import os
+import sys
+
+import mozunit
+import pytest
+
+from mozterm import Terminal, NullTerminal
+
+
+def test_terminal():
+    blessings = pytest.importorskip('blessings')
+    term = Terminal()
+    assert isinstance(term, blessings.Terminal)
+
+    term = Terminal(disable_styling=True)
+    assert isinstance(term, NullTerminal)
+
+    del sys.modules['blessings']
+    orig = sys.path[:]
+    for path in orig:
+        if 'blessings' in path:
+            sys.path.remove(path)
+
+    term = Terminal()
+    assert isinstance(term, NullTerminal)
+
+    with pytest.raises(ImportError):
+        term = Terminal(raises=True)
+
+    sys.path = orig
+
+
+def test_null_terminal():
+    term = NullTerminal()
+    assert term.red("foo") == "foo"
+    assert term.red == ""
+    assert term.color(1) == ""
+    assert term.number_of_colors == 0
+    assert term.width == 0
+    assert term.height == 0
+    assert term.is_a_tty == os.isatty(sys.stdout.fileno())
+
+
+if __name__ == '__main__':
+    mozunit.main()
--- a/taskcluster/ci/source-test/python.yml
+++ b/taskcluster/ci/source-test/python.yml
@@ -123,16 +123,32 @@ mozlint:
                 docker-image: {in-tree: "lint"}
                 max-run-time: 3600
     run:
         mach: python-test --subsuite mozlint
     when:
         files-changed:
             - 'python/mozlint/**'
 
+mozterm:
+    description: python/mozterm unit tests
+    platform: linux64/opt
+    treeherder:
+        symbol: py(term)
+    worker:
+        by-platform:
+            linux64.*:
+                docker-image: {in-tree: "lint"}
+                max-run-time: 3600
+    run:
+        mach: python-test --subsuite mozterm
+    when:
+        files-changed:
+            - 'python/mozterm/**'
+
 mozversioncontrol:
     description: python/mozversioncontrol unit tests
     platform: linux64/opt
     treeherder:
         symbol: py(vcs)
     worker:
         by-platform:
             linux64.*:
--- a/tools/lint/flake8.yml
+++ b/tools/lint/flake8.yml
@@ -7,16 +7,17 @@ flake8:
         - configure.py
         - config/check_macroassembler_style.py
         - config/mozunit.py
         - layout/tools/reftest
         - python/mach
         - python/mach_commands.py
         - python/mozboot
         - python/mozlint
+        - python/mozterm
         - python/mozversioncontrol
         - security/manager
         - taskcluster
         - testing/firefox-ui
         - testing/mach_commands.py
         - testing/marionette/client
         - testing/marionette/harness
         - testing/marionette/puppeteer
--- a/tools/tryselect/selectors/fuzzy.py
+++ b/tools/tryselect/selectors/fuzzy.py
@@ -6,28 +6,25 @@ from __future__ import absolute_import, 
 
 import os
 import platform
 import subprocess
 import sys
 from distutils.spawn import find_executable
 
 from mozboot.util import get_state_dir
+from mozterm import Terminal
 
 from .. import preset as pset
 from ..cli import BaseTryParser
 from ..tasks import generate_tasks
 from ..vcs import VCSHelper
 
-try:
-    import blessings
-    terminal = blessings.Terminal()
-except ImportError:
-    from mozlint.formatters.stylish import NullTerminal
-    terminal = NullTerminal()
+terminal = Terminal()
+
 
 FZF_NOT_FOUND = """
 Could not find the `fzf` binary.
 
 The `mach try fuzzy` command depends on fzf. Please install it following the
 appropriate instructions for your platform:
 
     https://github.com/junegunn/fzf#installation