Skip to content

Commit

Permalink
Update log rendering in kernel (#68)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jld23 authored Aug 20, 2020
1 parent ed63dce commit 093d794
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 65 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ __pycache__
build/
dist/
MANIFEST
**/*checkpoint.ipynb
**/*.ipynb
**/*.sas7bcat
141 changes: 86 additions & 55 deletions sas_kernel/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@
# Create Logger
import logging

from typing import Tuple

from metakernel import MetaKernel
from sas_kernel import __version__
from sas_kernel.version import __version__
from IPython.display import HTML
# color syntax for the SASLog
from saspy.SASLogLexer import SASLogStyle, SASLogLexer
from pygments.formatters import HtmlFormatter
from pygments import highlight

logger = logging.getLogger('')
# create a logger to output messages to the Jupyter console
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARN)
console = logging.StreamHandler()
console.setFormatter(logging.Formatter('%(name)-12s: %(message)s'))
logger.addHandler(console)

logger.debug("sanity check")
class SASKernel(MetaKernel):
"""
SAS Kernel for Jupyter implementation. This module relies on SASPy
Expand Down Expand Up @@ -82,8 +85,50 @@ def _start_sas(self):
self.mva = saspy.SASsession(kernel=self)
except:
self.mva = None

def _colorize_log(self, log: str) -> str:
"""
takes a SAS log (str) and then looks for errors.
Returns a tuple of error count, list of error messages
"""
regex_note = r"(?m)(^NOTE.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
regex_warn = r"(?m)(^WARNING.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"
regex_error = r"(?m)(^ERROR.*((\n|\t|\n\t)[ ]([^WEN].*)(.*))*)"

sub_note = "\x1b[38;5;21m\\1\x1b[0m"
sub_warn = "\x1b[38;5;2m\\1\x1b[0m"
sub_error = "\x1B[1m\x1b[38;5;9m\\1\x1b[0m\x1b[0m"
color_pattern = [
(regex_error, sub_error),
(regex_note, sub_note),
(regex_warn, sub_warn)
]
colored_log = log
for pat, sub in color_pattern:
colored_log = re.sub(pat, sub, colored_log)

return colored_log


def _is_error_log(self, log: str) -> Tuple:
"""
takes a SAS log (str) and then looks for errors.
Returns a tuple of error count, list of error messages
"""
lines = re.split(r'[\n]\s*', log)
error_count = 0
error_log_msg_list = []
error_log_line_list = []
for index, line in enumerate(lines):
#logger.debug("line:{}".format(line))
if line.startswith('ERROR'):
error_count +=1
error_log_msg_list.append(line)
error_log_line_list.append(index)
return (error_count, error_log_msg_list, error_log_line_list)


def _which_display(self, log: str, output: str) -> HTML:
def _which_display(self, log: str, output: str = '') -> str:
"""
Determines if the log or lst should be returned as the results for the cell based on parsing the log
looking for errors and the presence of lst output.
Expand All @@ -93,40 +138,27 @@ def _which_display(self, log: str, output: str) -> HTML:
:return: The correct results based on log and lst
:rtype: str
"""
lines = re.split(r'[\n]\s*', log)
i = 0
elog = []
for line in lines:
i += 1
e = []
if line.startswith('ERROR'):
logger.debug("In ERROR Condition")
e = lines[(max(i - 15, 0)):(min(i + 16, len(lines)))]
elog = elog + e
tlog = '\n'.join(elog)
logger.debug("elog count: " + str(len(elog)))
logger.debug("tlog: " + str(tlog))

color_log = highlight(log, SASLogLexer(), HtmlFormatter(full=True, style=SASLogStyle, lineseparator="<br>"))
error_count, msg_list, error_line_list = self._is_error_log(log)

# store the log for display in the showSASLog nbextension
self.cachedlog = color_log
# Are there errors in the log? if show the lines on each side of the error
if len(elog) == 0 and len(output) > self.lst_len: # no error and LST output
debug1 = 1
logger.debug("DEBUG1: " + str(debug1) + " no error and LST output ")
return HTML(output)
elif len(elog) == 0 and len(output) <= self.lst_len: # no error and no LST
debug1 = 2
logger.debug("DEBUG1: " + str(debug1) + " no error and no LST")
return HTML(color_log)
elif len(elog) > 0 and len(output) <= self.lst_len: # error and no LST
debug1 = 3
logger.debug("DEBUG1: " + str(debug1) + " error and no LST")
return HTML(color_log)
else: # errors and LST
debug1 = 4
logger.debug("DEBUG1: " + str(debug1) + " errors and LST")
return HTML(color_log + output)
self.cachedlog = self._colorize_log(log)

if error_count == 0 and len(output) > self.lst_len: # no error and LST output
return self.Display(HTML(output))

elif error_count > 0 and len(output) > self.lst_len: # errors and LST
#filter log to lines around first error
# by default get 5 lines on each side of the first Error message.
# to change that modify the values in {} below
regex_around_error = r"(.*)(.*\n){6}^ERROR(.*\n){6}"
# Extract the first match +/- 5 lines
e_log = re.search(regex_around_error, log, re.MULTILINE).group()
assert error_count == len(error_line_list), "Error count and count of line number don't match"
return self.Error_display(msg_list[0], print(self._colorize_log(e_log)), HTML(output))

# for everything else return the log
return self.Print(self._colorize_log(log))

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

# If no mva session start a session
if self.mva is None:
self._allow_stdin = True
self._start_sas()

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

if code.startswith('Obfuscated SAS Code'):
logger.debug("decoding string")
tmp1 = code.split()
decode = base64.b64decode(tmp1[-1])
code = decode.decode('utf-8')

# This block uses special strings submitted by the Jupyter notebook extensions
if code.startswith('showSASLog_11092015') == False and code.startswith("CompleteshowSASLog_11092015") == False:
logger.debug("code type: " + str(type(code)))
logger.debug("code length: " + str(len(code)))
Expand All @@ -166,17 +195,20 @@ def do_execute_direct(self, code: str, silent: bool = False) -> [str, dict]:
print(res['LOG'], '\n' "Restarting SAS session on your behalf")
self.do_shutdown(True)
return res['LOG']

# Parse the log to check for errors
error_count, error_log_msg, _ = self._is_error_log(res['LOG'])

if error_count > 0 and len(res['LST']) <= self.lst_len:
return(self.Error(error_log_msg[0], print(self._colorize_log(res['LOG']))))

return self._which_display(res['LOG'], res['LST'])

output = res['LST']
log = res['LOG']
return self._which_display(log, output)
elif code.startswith("CompleteshowSASLog_11092015") == True and code.startswith('showSASLog_11092015') == False:
full_log = highlight(self.mva.saslog(), SASLogLexer(),
HtmlFormatter(full=True, style=SASLogStyle, lineseparator="<br>",
title="Full SAS Log"))
return full_log.replace('\n', ' ')
return (self.Print(self._colorize_log(self.mva.saslog())))
else:
return self.cachedlog.replace('\n', ' ')
return (self.Print(self._colorize_log(self.cachedlog)))


def get_completions(self, info):
"""
Expand Down Expand Up @@ -279,5 +311,4 @@ def do_shutdown(self, restart):
if __name__ == '__main__':
from ipykernel.kernelapp import IPKernelApp
from .kernel import SASKernel
from sas_kernel import __version__
IPKernelApp.launch_instance(kernel_class=SASKernel)
9 changes: 2 additions & 7 deletions sas_kernel/magics/log_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
# limitations under the License.
#
from metakernel import Magic
from IPython.display import HTML
from pygments import highlight
from saspy.SASLogLexer import SASLogStyle, SASLogLexer
from pygments.formatters import HtmlFormatter

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


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

def register_magics(kernel):
kernel.register_magics(logMagic)
Expand Down
2 changes: 1 addition & 1 deletion sas_kernel/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
# limitations under the License.
#

__version__ = '2.2.0'
__version__ = '2.3.0'
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ def run(self):
packages=find_packages(),
cmdclass={'install': InstallWithKernelspec},
package_data={'': ['*.js', '*.md', '*.yaml', '*.css'], 'sas_kernel': ['data/*.json', 'data/*.png']},
install_requires=['saspy>=2.2.7', 'pygments', "metakernel>=0.18.0", "jupyter_client >=4.4.0",
"ipython>=4.0.0"
install_requires=['saspy>=3', "metakernel>=0.24.0", "jupyter_client >=4.4.0",
"ipython>=5.0.0"
],
classifiers=['Framework :: IPython',
'License :: OSI Approved :: Apache Software License',
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: System :: Shells"]
)

0 comments on commit 093d794

Please sign in to comment.