Skip to content

Update log rendering in kernel #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
)