Skip to content

Commit 333a490

Browse files
kinowmr-c
authored andcommitted
Use patched sphinxcontrib.runcmd to execute the commands.
1 parent e1c01c4 commit 333a490

File tree

4 files changed

+176
-2
lines changed

4 files changed

+176
-2
lines changed

cwl/sphinx/__init__.py

Whitespace-only changes.

cwl/sphinx/runcmd.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import csv
2+
import re
3+
import shlex
4+
import subprocess
5+
import sys
6+
7+
from docutils.parsers.rst import directives
8+
from sphinx.directives import code
9+
10+
""""
11+
Patched version of https://github.com/sphinx-contrib/sphinxcontrib-runcmd
12+
with default values to avoid having to re-type in every page. Also
13+
prepends commands with a value (``$``), see https://github.com/invenia/sphinxcontrib-runcmd/issues/1.
14+
Finally, it also checks if the command is ``cwltool``, and if then
15+
tries to remove any paths from the command-line (not the logs).
16+
"""
17+
18+
__version__ = "0.2.0"
19+
20+
# CONSTANTS
21+
RE_SPLIT = re.compile(r"(?P<pattern>.*)(?<!\\)/(?P<replacement>.*)")
22+
23+
24+
# These classes were in the .util module of the original directive.
25+
# TODO: PATCHED
26+
class _Singleton(type):
27+
_instances = {}
28+
29+
def __call__(cls, *args, **kwargs):
30+
if cls not in cls._instances:
31+
cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
32+
return cls._instances[cls]
33+
34+
35+
class Singleton(_Singleton("SingletonMeta", (object,), {})):
36+
pass
37+
38+
39+
class CMDCache(Singleton):
40+
cache = {}
41+
42+
def get(self, cmd, working_directory):
43+
h = hash(cmd)
44+
if h in self.cache:
45+
return self.cache[h]
46+
else:
47+
result = run_command(cmd, working_directory)
48+
self.cache[h] = result
49+
return result
50+
51+
52+
def run_command(command, working_directory):
53+
true_cmd = shlex.split(command)
54+
try:
55+
# The subprocess Popen function takes a ``cwd`` argument that
56+
# conveniently changes the working directory to run the command.
57+
#
58+
# Furthermore, we also patched the stderr to redirect to STDOUT,
59+
# so that stderr and stdout appear in order, as you would see in
60+
# a terminal.
61+
# TODO: PATCHED
62+
subp = subprocess.Popen(
63+
true_cmd,
64+
cwd=working_directory,
65+
stdout=subprocess.PIPE,
66+
stderr=subprocess.STDOUT
67+
)
68+
except Exception as e:
69+
out = ""
70+
err = e
71+
else:
72+
out, err = subp.communicate()
73+
encoding = sys.getfilesystemencoding()
74+
out = out.decode(encoding, "replace").rstrip()
75+
# The stderr is now combined with stdout.
76+
# err = err.decode(encoding, "replace").rstrip()
77+
78+
if err and err != "":
79+
print("Error in runcmd: {}".format(err))
80+
out = "{}\n{}".format(out, err)
81+
82+
return out
83+
84+
85+
class RunCmdDirective(code.CodeBlock):
86+
has_content = False
87+
final_argument_whitespace = False
88+
required_arguments = 1
89+
optional_arguments = 99
90+
91+
option_spec = {
92+
# code.CodeBlock option_spec
93+
"linenos": directives.flag,
94+
"dedent": int,
95+
"lineno-start": int,
96+
"emphasize-lines": directives.unchanged_required,
97+
"caption": directives.unchanged_required,
98+
"class": directives.class_option,
99+
"name": directives.unchanged,
100+
# RunCmdDirective option_spec
101+
"syntax": directives.unchanged,
102+
"replace": directives.unchanged,
103+
"prompt": directives.flag,
104+
"dedent-output": int,
105+
# TODO: PATCHED
106+
"working-directory": directives.unchanged
107+
}
108+
109+
def run(self):
110+
# Grab a cache singleton instance
111+
cache = CMDCache()
112+
113+
# The examples in our User Guide are stored in ``src/_includes/cwl``.
114+
# For convenience, instead of including that in every command, we
115+
# allow the directive to receive a working directory, so that we
116+
# change to that working directory before running the desired command.
117+
# The working directory is omitted from the final output.
118+
# TODO: PATCHED
119+
working_directory = self.options.get('working-directory', 'src/_includes/cwl/')
120+
if working_directory == '':
121+
# subprocess default value, so that we can disable it if needed.
122+
working_directory = None
123+
124+
# Get the command output
125+
command = " ".join(self.arguments)
126+
output = cache.get(command, working_directory)
127+
128+
# Grab our custom commands
129+
syntax = self.options.get("syntax", "bash") # TODO: PATCHED
130+
# N.B. ``cwltool`` by default emits output with colors, unless disabled
131+
# via a command line flag. There is no way to disable via env-vars
132+
# so we replace it via regexes.
133+
# Source: https://superuser.com/questions/380772/removing-ansi-color-codes-from-text-stream
134+
replace = self.options.get("replace", '"\\[[0-9;]+m/","\\x1b/"') # TODO: PATCHED
135+
reader = csv.reader([replace], delimiter=",", escapechar="\\")
136+
# prompt = "prompt" in self.options
137+
# We patched this so that the prompt is displayed by default, similar
138+
# to how ``{code-block} console`` works.
139+
prompt = True # TODO: PATCHED
140+
dedent_output = self.options.get("dedent-output", 0)
141+
142+
# Dedent the output if required
143+
if dedent_output > 0:
144+
output = "\n".join([x[dedent_output:] for x in output.split("\n")])
145+
146+
# Add the prompt to our output if required
147+
if prompt:
148+
output = "$ {}\n{}".format(command, output)
149+
150+
# Do our "replace" syntax on the command output
151+
for items in reader:
152+
for regex in items:
153+
if regex != "":
154+
match = RE_SPLIT.match(regex)
155+
p = match.group("pattern")
156+
# Let's unescape the escape chars here as we don't need them to be
157+
# escaped in the replacement at this point
158+
r = match.group("replacement").replace("\\", "")
159+
output = re.sub(p, r, output)
160+
161+
# Set up our arguments to run the CodeBlock parent run function
162+
self.arguments[0] = syntax
163+
self.content = [output]
164+
node = super(RunCmdDirective, self).run()
165+
166+
return node
167+
168+
169+
def setup(app):
170+
app.add_directive("runcmd", RunCmdDirective)
171+
172+
return {"version": __version__}

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ packages = find_namespace:
3232
include_package_data = True
3333
python_requires = >=3.6
3434
install_requires =
35+
cwl-utils==0.*
3536
myst-parser==0.*
3637
pydata-sphinx-theme==0.*
3738
sphinx==5.*
3839
sphinx-reredirects==0.1.*
39-
cwl-utils==0.*
40+
sphinxcontrib-runcmd==0.2.*
4041

4142
[options.packages.find]
4243
include = cwl*

src/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
extensions = [
4040
'myst_parser',
4141
'sphinx.ext.graphviz',
42-
'sphinx_reredirects'
42+
'sphinx_reredirects',
43+
'cwl.sphinx.runcmd'
4344
]
4445

4546
# myst-parser settings

0 commit comments

Comments
 (0)