Bug 1422302 - Create python/mozterm for sharing terminal blessings across modules r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 04 Dec 2017 09:38:24 -0500
changeset 395099 ea36374ea21af2a2b677645116e4a86e3745fe99
parent 395098 613018fe3f8196184c5d6f2d69d9e1d2ae1d19fe
child 395100 7d01faca3c8eff3300cb08f75d53cf5f99bb5c28
push id56597
push userahalberstadt@mozilla.com
push dateTue, 05 Dec 2017 19:19:11 +0000
treeherderautoland@7d01faca3c8e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1422302
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1422302 - Create python/mozterm for sharing terminal blessings across modules r=gps 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