diff --git a/nbsmoke/lint/magics/__init__.py b/nbsmoke/lint/magics/__init__.py index 150e005..5583495 100644 --- a/nbsmoke/lint/magics/__init__.py +++ b/nbsmoke/lint/magics/__init__.py @@ -30,9 +30,6 @@ from . import holoviews_support from . import builtins_support -# as noted in that file, shouldn't be there but should just be replaced iwth something better anyway -_Unavailable = holoviews_support._Unavailable - # TODO: still nede to investigate the following line magics (and what # does ipython convert do to them?): # @@ -170,11 +167,10 @@ def _get_parser(line): # yuck return parsers[None](line) # default/no magic def _call_a_handler(handlers, magic): - handler = handlers[magic.name] - if isinstance(handler, _Unavailable): - # this is a bit rubbish. and how to reconstruct original error? - raise Exception("nbsmoke can't process the following '%s' magic without additional dependencies:\n %s\n. Error was:\n %s"%(magic.name, magic.line, handler.e.msg)) - return handler(magic) + try: + return handlers[magic.name](magic) + except ImportError as e: + raise ImportError("nbsmoke can't process the following '%s' magic without additional dependencies:\n %s\n. Error was:\n %s"%(magic.name, magic.line, e)) def _process_magics(magic, other_magic_handlers, ignored_magics, blacklisted_magics, simple_magics=None): if simple_magics is None: diff --git a/nbsmoke/lint/magics/holoviews_support.py b/nbsmoke/lint/magics/holoviews_support.py index 555d39a..d35cc2b 100644 --- a/nbsmoke/lint/magics/holoviews_support.py +++ b/nbsmoke/lint/magics/holoviews_support.py @@ -6,103 +6,102 @@ from itertools import groupby -# placeholder for unavailable imports - should do something better (also, not specific to hv) -class _Unavailable: - def __init__(self,e): - self.e = e +IGNORED_LINE_MAGICS = ['output'] +IGNORED_CELL_MAGICS = ['output'] try: - from holoviews.util.parser import OptsSpec -except ImportError as e: - OptsSpec = _Unavailable(e) + import numpy # noqa: Some bad numpy/pytest interaction. Without importing numpy here (so it happens when nbsmoke is imported, i.e. at plugin load time, + # we get the traceback in https://github.com/numpy/numpy/issues/14012 (no idea if same cause) +except ImportError: + pass -IGNORED_LINE_MAGICS = ['output'] -IGNORED_CELL_MAGICS = ['output'] +def _make_optsspec(): + from holoviews.util.parser import OptsSpec + + class NbSmokeOptsSpec(OptsSpec): + + @classmethod + def _hvparse(cls, line, ns={}): + """ + Parse an options specification, returning a dictionary with + path keys and {'plot':, 'style':} values. + """ + parses = [p for p in cls.opts_spec.scanString(line)] + if len(parses) != 1: + raise SyntaxError("Invalid specification syntax.") + else: + e = parses[0][2] + processed = line[:e] + if (processed.strip() != line.strip()): + raise SyntaxError("Failed to parse remainder of string: %r" % line[e:]) + + grouped_paths = cls._group_paths_without_options(cls.opts_spec.parseString(line)) + things = [] + for pathspecs, group in grouped_paths: + + # normalization = cls.process_normalization(group) + # if normalization is not None: + # options['norm'] = normalization + + if 'plot_options' in group: + plotopts = group['plot_options'][0] + opts = cls.todict(plotopts, 'brackets', ns=ns) + things+=opts + + if 'style_options' in group: + styleopts = group['style_options'][0] + opts = cls.todict(styleopts, 'parens', ns=ns) + things+=opts + + return things + + @classmethod + def _hvtodict(cls, parseresult, mode='parens', ns={}): + """ + Helper function to return dictionary given the parse results + from a pyparsing.nestedExpr object (containing keywords). + + The ns is a dynamic namespace (typically the IPython Notebook + namespace) used to update the class-level namespace. + """ + grouped = [] + things = [] + tokens = cls.collect_tokens(parseresult, mode) + # Group tokens without '=' and append to last token containing '=' + for group in groupby(tokens, lambda el: '=' in el): + (val, items) = group + if val is True: + grouped += list(items) + if val is False: + elements =list(items) + # Assume anything before ) or } can be joined with commas + # (e.g tuples with spaces in them) + joiner=',' if any(((')' in el) or ('}' in el)) + for el in elements) else '' + grouped[-1] += joiner + joiner.join(elements) + + for keyword in grouped: + # Tuple ('a', 3) becomes (,'a',3) and '(,' is never valid + # Same for some of the other joining errors corrected here + for (fst,snd) in [('(,', '('), ('{,', '{'), ('=,','='), + (',:',':'), (':,', ':'), (',,', ','), + (',.', '.')]: + keyword = keyword.replace(fst, snd) + + things.append('dict(%s)' % keyword) + + return things + + return NbSmokeOptsSpec + def opts_handler(magic): """Given an opts magic, return line of python suitable for pyflakes.""" - string_of_magic_args = magic.python - return " ; ".join(OptsSpec.parse(string_of_magic_args)) + " # noqa: here to use names for original %s magic: %s"%(magic.__class__.__name__, magic.python) + NbSmokeOptsSpec = _make_optsspec() + string_of_magic_args = magic.python + return " ; ".join(NbSmokeOptsSpec.parse(string_of_magic_args)) + " # noqa: here to use names for original %s magic: %s"%(magic.__class__.__name__, magic.python) -if isinstance(OptsSpec, _Unavailable): - opts_handler = OptsSpec # noqa: opts_handler unusable cell_magic_handlers = {'opts': opts_handler} line_magic_handlers = {'opts': opts_handler} - - -def _hvparse(cls, line, ns={}): - """ - Parse an options specification, returning a dictionary with - path keys and {'plot':, 'style':} values. - """ - parses = [p for p in cls.opts_spec.scanString(line)] - if len(parses) != 1: - raise SyntaxError("Invalid specification syntax.") - else: - e = parses[0][2] - processed = line[:e] - if (processed.strip() != line.strip()): - raise SyntaxError("Failed to parse remainder of string: %r" % line[e:]) - - grouped_paths = cls._group_paths_without_options(cls.opts_spec.parseString(line)) - things = [] - for pathspecs, group in grouped_paths: - -# normalization = cls.process_normalization(group) -# if normalization is not None: -# options['norm'] = normalization - - if 'plot_options' in group: - plotopts = group['plot_options'][0] - opts = cls.todict(plotopts, 'brackets', ns=ns) - things+=opts - - if 'style_options' in group: - styleopts = group['style_options'][0] - opts = cls.todict(styleopts, 'parens', ns=ns) - things+=opts - - return things - - -def _hvtodict(cls, parseresult, mode='parens', ns={}): - """ - Helper function to return dictionary given the parse results - from a pyparsing.nestedExpr object (containing keywords). - - The ns is a dynamic namespace (typically the IPython Notebook - namespace) used to update the class-level namespace. - """ - grouped = [] - things = [] - tokens = cls.collect_tokens(parseresult, mode) - # Group tokens without '=' and append to last token containing '=' - for group in groupby(tokens, lambda el: '=' in el): - (val, items) = group - if val is True: - grouped += list(items) - if val is False: - elements =list(items) - # Assume anything before ) or } can be joined with commas - # (e.g tuples with spaces in them) - joiner=',' if any(((')' in el) or ('}' in el)) - for el in elements) else '' - grouped[-1] += joiner + joiner.join(elements) - - for keyword in grouped: - # Tuple ('a', 3) becomes (,'a',3) and '(,' is never valid - # Same for some of the other joining errors corrected here - for (fst,snd) in [('(,', '('), ('{,', '{'), ('=,','='), - (',:',':'), (':,', ':'), (',,', ','), - (',.', '.')]: - keyword = keyword.replace(fst, snd) - - things.append('dict(%s)' % keyword) - - return things - -if OptsSpec is not None: - OptsSpec.parse = classmethod(_hvparse) - OptsSpec.todict = classmethod(_hvtodict) diff --git a/tests/test_lint_magics.py b/tests/test_lint_magics.py index d0c413e..c1b2e98 100644 --- a/tests/test_lint_magics.py +++ b/tests/test_lint_magics.py @@ -428,15 +428,16 @@ def test_optional_import_warn(testdir): testdir.makefile('.ipynb', testing123=nb_optional_dep) ### # what a hack + def not_clever_magics_handler(magic): + raise ImportError("Amazing dependency is missing") import nbsmoke.lint.magics as M - from collections import namedtuple - M.other_line_magic_handlers['clever_magic'] = M._Unavailable(namedtuple("SomeError", "msg")("A terrible error.")) + M.other_line_magic_handlers['clever_magic'] = not_clever_magics_handler ### result = testdir.runpytest(*lint_args) assert result.ret == 1 result.stdout.re_match_lines_random( - [".*Exception: nbsmoke can't process the following 'clever_magic' magic without additional dependencies.*", - ".*A terrible error.*"]) + [".*ImportError: nbsmoke can't process the following 'clever_magic' magic without additional dependencies.*", + ".*Amazing dependency is missing.*"]) def test_lint_line_magics_indent(testdir):