Repository: bokeh/bokeh

Issue:
Add nodejs and npmjs version numbers to `bokeh info`
Followup on issue #5645.


Discussion:

Resolving patch:
diff --git a/bokeh/command/subcommands/info.py b/bokeh/command/subcommands/info.py
--- a/bokeh/command/subcommands/info.py
+++ b/bokeh/command/subcommands/info.py
@@ -38,9 +38,18 @@
 
 from bokeh import __version__
 from bokeh.settings import settings
+from bokeh.util.compiler import nodejs_version, npmjs_version
 
 from ..subcommand import Subcommand
 
+def ipython_version():
+    try:
+        import IPython
+    except ImportError:
+        return None
+    else:
+        return IPython.__version__
+
 class Info(Subcommand):
     ''' Subcommand to print information about Bokeh and Bokeh server configuration.
 
@@ -63,12 +72,11 @@ def invoke(self, args):
         if args.static:
             print(settings.bokehjsdir())
         else:
-            try:
-                import IPython
-                ipy_version = IPython.__version__
-            except ImportError:
-                ipy_version = "Not installed"
+            if_installed = lambda version_or_none: version_or_none or "(not installed)"
+
             print("Python version      :  %s" % sys.version.split('\n')[0])
-            print("IPython version     :  %s" % ipy_version)
+            print("IPython version     :  %s" % if_installed(ipython_version()))
             print("Bokeh version       :  %s" % __version__)
             print("BokehJS static path :  %s" % settings.bokehjsdir())
+            print("node.js version     :  %s" % if_installed(nodejs_version()))
+            print("npm version         :  %s" % if_installed(npmjs_version()))
diff --git a/bokeh/util/compiler.py b/bokeh/util/compiler.py
--- a/bokeh/util/compiler.py
+++ b/bokeh/util/compiler.py
@@ -122,37 +122,45 @@ def _detect_nodejs():
         return None
 
 _nodejs = _detect_nodejs()
+_npmjs = None if _nodejs is None else join(dirname(_nodejs), "npm")
 
-def _run_nodejs(script, input):
+def _run(app, argv, input=None):
     if _nodejs is None:
         raise RuntimeError('node.js is needed to allow compilation of custom models ' +
                            '("conda install -c bokeh nodejs" or follow https://nodejs.org/en/download/)')
 
-    proc = Popen([_nodejs, script], stdout=PIPE, stderr=PIPE, stdin=PIPE)
-    (stdout, errout) = proc.communicate(input=json.dumps(input).encode())
+    proc = Popen([app] + argv, stdout=PIPE, stderr=PIPE, stdin=PIPE)
+    (stdout, errout) = proc.communicate(input=None if input is None else json.dumps(input).encode())
 
     if proc.returncode != 0:
         raise RuntimeError(errout)
     else:
-        return AttrDict(json.loads(stdout.decode()))
+        return stdout.decode()
 
-def _run_npm(argv):
-    if _nodejs is None:
-        raise RuntimeError('node.js is needed to allow compilation of custom models ' +
-                           '("conda install -c bokeh nodejs" or follow https://nodejs.org/en/download/)')
+def _run_nodejs(argv, input=None):
+    return _run(_nodejs, argv, input)
 
-    _npm = join(dirname(_nodejs), "npm")
-    proc = Popen([_npm] + argv, stdout=PIPE, stderr=PIPE, stdin=PIPE)
-    (_stdout, errout) = proc.communicate()
+def _run_npmjs(argv, input=None):
+    return _run(_npmjs, argv, input)
 
-    if proc.returncode != 0:
-        raise RuntimeError(errout)
-    else:
+def _version(run_app):
+    try:
+        version = run_app(["--version"])
+    except RuntimeError:
         return None
+    else:
+        return version.strip()
+
+def nodejs_version():
+    return _version(_run_nodejs)
+
+def npmjs_version():
+    return _version(_run_npmjs)
 
 def nodejs_compile(code, lang="javascript", file=None):
     compilejs_script = join(bokehjs_dir, "js", "compile.js")
-    return _run_nodejs(compilejs_script, dict(code=code, lang=lang, file=file))
+    output = _run_nodejs([compilejs_script], dict(code=code, lang=lang, file=file))
+    return AttrDict(json.loads(output))
 
 class Implementation(object):
 
@@ -295,7 +303,7 @@ def gen_custom_models_static():
 
     if dependencies:
         dependencies = sorted(dependencies, key=lambda name_version: name_version[0])
-        _run_npm(["install"] + [ name + "@" + version for (name, version) in dependencies ])
+        _run_npmjs(["install"] + [ name + "@" + version for (name, version) in dependencies ])
 
     for model in ordered_models:
         impl = model.implementation


Test patch:
diff --git a/bokeh/command/subcommands/tests/test_info.py b/bokeh/command/subcommands/tests/test_info.py
--- a/bokeh/command/subcommands/tests/test_info.py
+++ b/bokeh/command/subcommands/tests/test_info.py
@@ -30,12 +30,14 @@ def test_run(capsys):
     main(["bokeh", "info"])
     out, err = capsys.readouterr()
     lines = out.split("\n")
-    assert len(lines) == 5
+    assert len(lines) == 7
     assert lines[0].startswith("Python version")
     assert lines[1].startswith("IPython version")
     assert lines[2].startswith("Bokeh version")
     assert lines[3].startswith("BokehJS static")
-    assert lines[4] == ""
+    assert lines[4].startswith("node.js version")
+    assert lines[5].startswith("npm version")
+    assert lines[6] == ""
     assert err == ""
 
 def test_run_static(capsys):