Skip to content

Commit 093d794

Browse files
authored
Update log rendering in kernel (#68)
* restructure kernel to include errors and txt. * update .gitignore * bump dependencies in setup * remove HTML output for logs * remove pygments and fix logic error in log test. * adjust log colors to better match * bump version; remove pygments dependency * remove pygemnts dependency. * update regex to fix issues Tom found
1 parent ed63dce commit 093d794

File tree

5 files changed

+95
-65
lines changed

5 files changed

+95
-65
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ __pycache__
33
build/
44
dist/
55
MANIFEST
6+
**/*checkpoint.ipynb
7+
**/*.ipynb
8+
**/*.sas7bcat

sas_kernel/kernel.py

Lines changed: 86 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,20 @@
2020
# Create Logger
2121
import logging
2222

23+
from typing import Tuple
24+
2325
from metakernel import MetaKernel
24-
from sas_kernel import __version__
26+
from sas_kernel.version import __version__
2527
from IPython.display import HTML
26-
# color syntax for the SASLog
27-
from saspy.SASLogLexer import SASLogStyle, SASLogLexer
28-
from pygments.formatters import HtmlFormatter
29-
from pygments import highlight
3028

31-
logger = logging.getLogger('')
29+
# create a logger to output messages to the Jupyter console
30+
logger = logging.getLogger(__name__)
3231
logger.setLevel(logging.WARN)
32+
console = logging.StreamHandler()
33+
console.setFormatter(logging.Formatter('%(name)-12s: %(message)s'))
34+
logger.addHandler(console)
3335

36+
logger.debug("sanity check")
3437
class SASKernel(MetaKernel):
3538
"""
3639
SAS Kernel for Jupyter implementation. This module relies on SASPy
@@ -82,8 +85,50 @@ def _start_sas(self):
8285
self.mva = saspy.SASsession(kernel=self)
8386
except:
8487
self.mva = None
88+
89+
def _colorize_log(self, log: str) -> str:
90+
"""
91+
takes a SAS log (str) and then looks for errors.
92+
Returns a tuple of error count, list of error messages
93+
"""
94+
regex_note = r"(?m)(^NOTE.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
95+
regex_warn = r"(?m)(^WARNING.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
96+
regex_error = r"(?m)(^ERROR.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
97+
98+
sub_note = "\x1b[38;5;21m\\1\x1b[0m"
99+
sub_warn = "\x1b[38;5;2m\\1\x1b[0m"
100+
sub_error = "\x1B[1m\x1b[38;5;9m\\1\x1b[0m\x1b[0m"
101+
color_pattern = [
102+
(regex_error, sub_error),
103+
(regex_note, sub_note),
104+
(regex_warn, sub_warn)
105+
]
106+
colored_log = log
107+
for pat, sub in color_pattern:
108+
colored_log = re.sub(pat, sub, colored_log)
109+
110+
return colored_log
111+
112+
113+
def _is_error_log(self, log: str) -> Tuple:
114+
"""
115+
takes a SAS log (str) and then looks for errors.
116+
Returns a tuple of error count, list of error messages
117+
"""
118+
lines = re.split(r'[\n]\s*', log)
119+
error_count = 0
120+
error_log_msg_list = []
121+
error_log_line_list = []
122+
for index, line in enumerate(lines):
123+
#logger.debug("line:{}".format(line))
124+
if line.startswith('ERROR'):
125+
error_count +=1
126+
error_log_msg_list.append(line)
127+
error_log_line_list.append(index)
128+
return (error_count, error_log_msg_list, error_log_line_list)
129+
85130

86-
def _which_display(self, log: str, output: str) -> HTML:
131+
def _which_display(self, log: str, output: str = '') -> str:
87132
"""
88133
Determines if the log or lst should be returned as the results for the cell based on parsing the log
89134
looking for errors and the presence of lst output.
@@ -93,40 +138,27 @@ def _which_display(self, log: str, output: str) -> HTML:
93138
:return: The correct results based on log and lst
94139
:rtype: str
95140
"""
96-
lines = re.split(r'[\n]\s*', log)
97-
i = 0
98-
elog = []
99-
for line in lines:
100-
i += 1
101-
e = []
102-
if line.startswith('ERROR'):
103-
logger.debug("In ERROR Condition")
104-
e = lines[(max(i - 15, 0)):(min(i + 16, len(lines)))]
105-
elog = elog + e
106-
tlog = '\n'.join(elog)
107-
logger.debug("elog count: " + str(len(elog)))
108-
logger.debug("tlog: " + str(tlog))
109-
110-
color_log = highlight(log, SASLogLexer(), HtmlFormatter(full=True, style=SASLogStyle, lineseparator="<br>"))
141+
error_count, msg_list, error_line_list = self._is_error_log(log)
142+
111143
# store the log for display in the showSASLog nbextension
112-
self.cachedlog = color_log
113-
# Are there errors in the log? if show the lines on each side of the error
114-
if len(elog) == 0 and len(output) > self.lst_len: # no error and LST output
115-
debug1 = 1
116-
logger.debug("DEBUG1: " + str(debug1) + " no error and LST output ")
117-
return HTML(output)
118-
elif len(elog) == 0 and len(output) <= self.lst_len: # no error and no LST
119-
debug1 = 2
120-
logger.debug("DEBUG1: " + str(debug1) + " no error and no LST")
121-
return HTML(color_log)
122-
elif len(elog) > 0 and len(output) <= self.lst_len: # error and no LST
123-
debug1 = 3
124-
logger.debug("DEBUG1: " + str(debug1) + " error and no LST")
125-
return HTML(color_log)
126-
else: # errors and LST
127-
debug1 = 4
128-
logger.debug("DEBUG1: " + str(debug1) + " errors and LST")
129-
return HTML(color_log + output)
144+
self.cachedlog = self._colorize_log(log)
145+
146+
if error_count == 0 and len(output) > self.lst_len: # no error and LST output
147+
return self.Display(HTML(output))
148+
149+
elif error_count > 0 and len(output) > self.lst_len: # errors and LST
150+
#filter log to lines around first error
151+
# by default get 5 lines on each side of the first Error message.
152+
# to change that modify the values in {} below
153+
regex_around_error = r"(.*)(.*\n){6}^ERROR(.*\n){6}"
154+
155+
# Extract the first match +/- 5 lines
156+
e_log = re.search(regex_around_error, log, re.MULTILINE).group()
157+
assert error_count == len(error_line_list), "Error count and count of line number don't match"
158+
return self.Error_display(msg_list[0], print(self._colorize_log(e_log)), HTML(output))
159+
160+
# for everything else return the log
161+
return self.Print(self._colorize_log(log))
130162

131163
def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
132164
"""
@@ -140,19 +172,16 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
140172
return {'status': 'ok', 'execution_count': self.execution_count,
141173
'payload': [], 'user_expressions': {}}
142174

175+
# If no mva session start a session
143176
if self.mva is None:
144177
self._allow_stdin = True
145178
self._start_sas()
146179

180+
# This code is now handeled in saspy will remove in future version
147181
if self.lst_len < 0:
148182
self._get_lst_len()
149183

150-
if code.startswith('Obfuscated SAS Code'):
151-
logger.debug("decoding string")
152-
tmp1 = code.split()
153-
decode = base64.b64decode(tmp1[-1])
154-
code = decode.decode('utf-8')
155-
184+
# This block uses special strings submitted by the Jupyter notebook extensions
156185
if code.startswith('showSASLog_11092015') == False and code.startswith("CompleteshowSASLog_11092015") == False:
157186
logger.debug("code type: " + str(type(code)))
158187
logger.debug("code length: " + str(len(code)))
@@ -166,17 +195,20 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
166195
print(res['LOG'], '\n' "Restarting SAS session on your behalf")
167196
self.do_shutdown(True)
168197
return res['LOG']
198+
199+
# Parse the log to check for errors
200+
error_count, error_log_msg, _ = self._is_error_log(res['LOG'])
201+
202+
if error_count > 0 and len(res['LST']) <= self.lst_len:
203+
return(self.Error(error_log_msg[0], print(self._colorize_log(res['LOG']))))
204+
205+
return self._which_display(res['LOG'], res['LST'])
169206

170-
output = res['LST']
171-
log = res['LOG']
172-
return self._which_display(log, output)
173207
elif code.startswith("CompleteshowSASLog_11092015") == True and code.startswith('showSASLog_11092015') == False:
174-
full_log = highlight(self.mva.saslog(), SASLogLexer(),
175-
HtmlFormatter(full=True, style=SASLogStyle, lineseparator="<br>",
176-
title="Full SAS Log"))
177-
return full_log.replace('\n', ' ')
208+
return (self.Print(self._colorize_log(self.mva.saslog())))
178209
else:
179-
return self.cachedlog.replace('\n', ' ')
210+
return (self.Print(self._colorize_log(self.cachedlog)))
211+
180212

181213
def get_completions(self, info):
182214
"""
@@ -279,5 +311,4 @@ def do_shutdown(self, restart):
279311
if __name__ == '__main__':
280312
from ipykernel.kernelapp import IPKernelApp
281313
from .kernel import SASKernel
282-
from sas_kernel import __version__
283314
IPKernelApp.launch_instance(kernel_class=SASKernel)

sas_kernel/magics/log_magic.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
# limitations under the License.
1515
#
1616
from metakernel import Magic
17-
from IPython.display import HTML
18-
from pygments import highlight
19-
from saspy.SASLogLexer import SASLogStyle, SASLogLexer
20-
from pygments.formatters import HtmlFormatter
2117

2218
class logMagic(Magic):
2319
def __init__(self, *args, **kwargs):
@@ -31,7 +27,7 @@ def line_showLog(self):
3127
if self.kernel.mva is None:
3228
print("Can't show log because no session exists")
3329
else:
34-
return self.kernel.Display(HTML(self.kernel.cachedlog))
30+
return self.kernel._which_display(self.kernel.cachedlog)
3531

3632

3733
def line_showFullLog(self):
@@ -43,8 +39,7 @@ def line_showFullLog(self):
4339
self.kernel._allow_stdin = True
4440
self.kernel._start_sas()
4541
print("Session Started probably not the log you want")
46-
full_log = highlight(self.kernel.mva.saslog(), SASLogLexer(), HtmlFormatter(full=True, style=SASLogStyle, lineseparator="<br>"))
47-
return self.kernel.Display(HTML(full_log))
42+
return self.kernel._which_display(self.kernel.mva.saslog())
4843

4944
def register_magics(kernel):
5045
kernel.register_magics(logMagic)

sas_kernel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
# limitations under the License.
1515
#
1616

17-
__version__ = '2.2.0'
17+
__version__ = '2.3.0'

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ def run(self):
5454
packages=find_packages(),
5555
cmdclass={'install': InstallWithKernelspec},
5656
package_data={'': ['*.js', '*.md', '*.yaml', '*.css'], 'sas_kernel': ['data/*.json', 'data/*.png']},
57-
install_requires=['saspy>=2.2.7', 'pygments', "metakernel>=0.18.0", "jupyter_client >=4.4.0",
58-
"ipython>=4.0.0"
57+
install_requires=['saspy>=3', "metakernel>=0.24.0", "jupyter_client >=4.4.0",
58+
"ipython>=5.0.0"
5959
],
6060
classifiers=['Framework :: IPython',
6161
'License :: OSI Approved :: Apache Software License',
6262
"Programming Language :: Python :: 3.4",
6363
"Programming Language :: Python :: 3.5",
6464
"Programming Language :: Python :: 3.6",
6565
"Programming Language :: Python :: 3.7",
66+
"Programming Language :: Python :: 3.8",
6667
"Topic :: System :: Shells"]
6768
)

0 commit comments

Comments
 (0)