diff --git a/readme.ipynb b/readme.ipynb index 913ec77..e570dbd 100644 --- a/readme.ipynb +++ b/readme.ipynb @@ -125,6 +125,64 @@ " import readme" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parameterize Notebooks\n", + "\n", + "Literal ast statements are converted to notebooks parameters.\n", + "\n", + "In `readme`, `foo` is a parameter because it may be evaluated with ast.literal_val" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + " from importnb import Parameterize\n", + " f = Parameterize(readme)\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The parameterized module is a callable that evaluates with different literal statements." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + " assert callable(f)\n", + " f.__signature__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " assert f().foo == 42\n", + " assert f(foo='importnb').foo == 'importnb'" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -178,15 +236,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Setuptools\n", - "\n", - "`importnb` provides a setuptool command that will place notebooks in a source distribution. In setuptools, update the command classs with\n", + "### Setup\n", "\n", - " from importnb.utils.setup import build_ipynb\n", - " setup(\n", - " ...,\n", - " cmdclass=dict(build_py=build_ipynb)\n", - " ...,)" + "To package notebooks add `recursive-include package_name *.ipynb`" ] }, { @@ -240,9 +292,59 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "src/notebooks/capture.ipynb\n", + "src/notebooks/compiler_ipython.ipynb\n", + "src/notebooks/compiler_python.ipynb\n", + "src/notebooks/decoder.ipynb\n", + "src/notebooks/exporter.ipynb\n", + "src/notebooks/loader.ipynb\n", + "src/notebooks/parameterize.ipynb\n", + "src/notebooks/utils/__init__.ipynb\n", + "src/notebooks/utils/ipython.ipynb\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "test_import (src.importnb.tests.test_unittests.TestContext) ... " + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "src/notebooks/utils/pytest_plugin.ipynb\n", + "src/notebooks/utils/setup.ipynb\n", + "src/notebooks/utils/watch.ipynb\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "ok\n", + "test_reload_with_context (src.importnb.tests.test_unittests.TestContext) ... ok\n", + "test_failure (src.importnb.tests.test_unittests.TestExtension) ... expected failure\n", + "test_import (src.importnb.tests.test_unittests.TestExtension) ... ok\n", + "test_exception (src.importnb.tests.test_unittests.TestPartial) ... ok\n", + "test_traceback (src.importnb.tests.test_unittests.TestPartial) ... ok\n", + "test_imports (src.importnb.tests.test_unittests.TestRemote) ... skipped 'requires IP'\n", + "\n", + "----------------------------------------------------------------------\n", + "Ran 7 tests in 2.020s\n", + "\n", + "OK (skipped=1, expected failures=1)\n" + ] + } + ], "source": [ " if __name__ == '__main__':\n", " from pathlib import Path\n", @@ -270,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -291,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ diff --git a/readme.md b/readme.md index 6ee3f57..3f5a70d 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,37 @@ The context manager is required to `reload` a module. import readme ``` +# Parameterize Notebooks + +Literal ast statements are converted to notebooks parameters. + +In `readme`, `foo` is a parameter because it may be evaluated with ast.literal_val + + +```python + from importnb import Parameterize + f = Parameterize(readme) + +``` + +The parameterized module is a callable that evaluates with different literal statements. + + +```python + assert callable(f) + f.__signature__ +``` + + + + + + + + + assert f().foo == 42 + assert f(foo='importnb').foo == 'importnb' + ## Integrations @@ -100,15 +131,9 @@ When the default extension is loaded any notebook can be run from the command li `importnb` installs a pytest plugin when it is setup. Any notebook obeying the py.test discovery conventions can be used in to pytest. _This is great because notebooks are generally your first test._ -### Setuptools - -`importnb` provides a setuptool command that will place notebooks in a source distribution. In setuptools, update the command classs with +### Setup - from importnb.utils.setup import build_ipynb - setup( - ..., - cmdclass=dict(build_py=build_ipynb) - ...,) +To package notebooks add `recursive-include package_name *.ipynb` ### [Watchdog](https://github.com/gorakhargosh/watchdog/tree/master/src/watchdog/tricks) @@ -159,6 +184,38 @@ For example, create a file called `tricks.yaml` containing ``` + src/notebooks/capture.ipynb + src/notebooks/compiler_ipython.ipynb + src/notebooks/compiler_python.ipynb + src/notebooks/decoder.ipynb + src/notebooks/exporter.ipynb + src/notebooks/loader.ipynb + src/notebooks/parameterize.ipynb + src/notebooks/utils/__init__.ipynb + src/notebooks/utils/ipython.ipynb + src/notebooks/utils/pytest_plugin.ipynb + + test_import (src.importnb.tests.test_unittests.TestContext) ... + + + src/notebooks/utils/setup.ipynb + src/notebooks/utils/watch.ipynb + + + ok + test_reload_with_context (src.importnb.tests.test_unittests.TestContext) ... ok + test_failure (src.importnb.tests.test_unittests.TestExtension) ... expected failure + test_import (src.importnb.tests.test_unittests.TestExtension) ... ok + test_exception (src.importnb.tests.test_unittests.TestPartial) ... ok + test_traceback (src.importnb.tests.test_unittests.TestPartial) ... ok + test_imports (src.importnb.tests.test_unittests.TestRemote) ... skipped 'requires IP' + + ---------------------------------------------------------------------- + Ran 7 tests in 2.025s + + OK (skipped=1, expected failures=1) + + ### Format the Github markdown files diff --git a/src/importnb/__init__.py b/src/importnb/__init__.py index 071aba9..d8f4e26 100644 --- a/src/importnb/__init__.py +++ b/src/importnb/__init__.py @@ -1,4 +1,5 @@ -__all__ = 'Notebook', 'Partial', 'reload', 'load_ipython_extension', 'unload_ipython_extension' +__all__ = 'Notebook', 'Partial', 'reload', 'Parameterize', 'Lazy' -from .loader import Notebook, Partial, load_ipython_extension, unload_ipython_extension, reload -from . import utils \ No newline at end of file +from .loader import Notebook, Partial, load_ipython_extension, unload_ipython_extension, reload, Lazy +from .parameterize import Parameterize +from . import utils diff --git a/src/importnb/capture.py b/src/importnb/capture.py index ec6b210..82d65f6 100644 --- a/src/importnb/capture.py +++ b/src/importnb/capture.py @@ -2,18 +2,21 @@ from .utils import export, __IPYTHON__ except: from utils import export, __IPYTHON__ -__all__ = 'capture_output', +__all__ = "capture_output", if __IPYTHON__: from IPython.utils.capture import capture_output else: from contextlib import redirect_stdout, ExitStack from io import StringIO + try: from contextlib import redirect_stderr except: import sys + class redirect_stderr: + def __init__(self, new_target): self._new_target = new_target self._old_targets = [] @@ -26,8 +29,8 @@ def __enter__(self): def __exit__(self, exctype, excinst, exctb): sys.stderr = self._old_targets.pop() - class capture_output(ExitStack): + def __init__(self, stdout=True, stderr=True, display=True): self.stdout = stdout self.stderr = stderr @@ -36,30 +39,31 @@ def __init__(self, stdout=True, stderr=True, display=True): def __enter__(self): super().__enter__() - stdout = stderr = outputs = None - if self.stdout: + stdout = stderr = outputs = None + if self.stdout: stdout = StringIO() self.enter_context(redirect_stdout(stdout)) - if self.stderr: + if self.stderr: stderr = StringIO() self.enter_context(redirect_stderr(stderr)) return CapturedIO(stdout, stderr, outputs) class CapturedIO: + def __init__(self, stdout=None, stderr=None, outputs=None): self._stdout = stdout self._stderr = stderr self.outputs = outputs @property - def stdout(self): return self._stdout and self._stdout.getvalue() or '' + def stdout(self): + return self._stdout and self._stdout.getvalue() or "" @property - def stderr(self): return self._stderr and self._stderr.getvalue() or '' - - + def stderr(self): + return self._stderr and self._stderr.getvalue() or "" -if __name__ == '__main__': - export('capture.ipynb', '../importnb/capture.py') - __import__('doctest').testmod() +if __name__ == "__main__": + export("capture.ipynb", "../importnb/capture.py") + __import__("doctest").testmod() diff --git a/src/importnb/loader.py b/src/importnb/loader.py index 333333f..7779dc8 100644 --- a/src/importnb/loader.py +++ b/src/importnb/loader.py @@ -1,23 +1,27 @@ try: from .exporter import Compile, AST - from .utils import __IPYTHON__, export + from .utils import __IPYTHON__ from .capture import capture_output except: from exporter import Compile, AST - from utils import __IPYTHON__, export + from utils import __IPYTHON__ from capture import capture_output import inspect, sys from importlib.machinery import SourceFileLoader -try: + +try: from importlib._bootstrap_external import FileFinder except: - #python 3.4 + # python 3.4 from importlib.machinery import FileFinder +from functools import partialmethod from importlib import reload -from traceback import print_exc +from traceback import print_exc, format_exc +from warnings import warn from contextlib import contextmanager, ExitStack -__all__ = 'Notebook', 'Partial', 'reload', +__all__ = "Notebook", "Partial", "reload", "Lazy" + @contextmanager def modify_file_finder_details(): @@ -29,26 +33,30 @@ def modify_file_finder_details(): for id, hook in enumerate(sys.path_hooks): try: closure = inspect.getclosurevars(hook).nonlocals - except TypeError: continue - if issubclass(closure['cls'], FileFinder): + except TypeError: + continue + if issubclass(closure["cls"], FileFinder): sys.path_hooks.pop(id) - details = list(closure['loader_details']) + details = list(closure["loader_details"]) yield details break sys.path_hooks.insert(id, FileFinder.path_hook(*details)) sys.path_importer_cache.clear() + def add_path_hooks(loader: SourceFileLoader, extensions, *, position=0, lazy=False): """Update the FileFinder loader in sys.path_hooks to accomodate a {loader} with the {extensions}""" with modify_file_finder_details() as details: try: from importlib.util import LazyLoader - if lazy: + + if lazy: loader = LazyLoader.factory(loader) except: ImportWarning("""LazyLoading is only available in > Python 3.5""") details.insert(position, (loader, extensions)) + def remove_one_path_hook(loader): with modify_file_finder_details() as details: _details = list(details) @@ -58,64 +66,109 @@ def remove_one_path_hook(loader): details.pop(ct) break + def lazy_loader_cls(loader): """Extract the loader contents of a lazy loader in the import path.""" if not isinstance(loader, type) and callable(loader): - return inspect.getclosurevars(loader).nonlocals.get('cls', loader) + return inspect.getclosurevars(loader).nonlocals.get("cls", loader) return loader -e = ExitStack() -class Notebook(SourceFileLoader, ExitStack): +class ImportNbException(BaseException): + """ImportNbException allows all exceptions to be raised, a null except statement always passes.""" + + +class Notebook(SourceFileLoader, capture_output): """A SourceFileLoader for notebooks that provides line number debugginer in the JSON source.""" - EXTENSION_SUFFIXES = '.ipynb', + EXTENSION_SUFFIXES = ".ipynb", def __init__( - self, fullname=None, path=None, *, stdout=False, stderr=False, display=False, lazy=False - ): + self, + fullname=None, + path=None, + *, + stdout=False, + stderr=False, + display=False, + lazy=False, + exceptions=(ImportNbException,) + ): SourceFileLoader.__init__(self, fullname, path) - ExitStack.__init__(self) - self._stdout, self._stderr, self._display = stdout, stderr, display + capture_output.__init__(self, stdout=stdout, stderr=stderr, display=display) self._lazy = lazy + self._exceptions = exceptions - def __enter__(self, position=0): + def __enter__(self, position=0): add_path_hooks(type(self), self.EXTENSION_SUFFIXES, position=position, lazy=self._lazy) - if self._capture: - stack = super().__enter__() - return stack.enter_context(capture_output( - stdout=self._stdout, stderr=self._stderr, display=self._display - )) + return super().__enter__() - @property - def _capture(self): return any((self._stdout, self._stderr, self._display)) - def __exit__(self, *excepts): - remove_one_path_hook(type(self)) - - if self._capture: super().__exit__(*excepts) + def exec_module(self, module): + """All exceptions specific in the context.""" + try: + super().exec_module(module) + module.__exception__ = None + except self._exceptions as e: + """Display a message if an error is escaped.""" + module.__exception__ = e + warn( + ".".join( + [ + """{name} was partially imported with a {error}""".format( + error=type(e), name=module.__name__ + ), + "=" * 10, + format_exc(), + ] + ) + ) + + def __exit__(self, *excepts): + remove_one_path_hook(type(self)), super().__exit__(*excepts) def source_to_code(Notebook, data, path): - with __import__('io').BytesIO(data) as stream: + with __import__("io").BytesIO(data) as stream: return Compile().from_file(stream, filename=Notebook.path, name=Notebook.name) + class Partial(Notebook): - def exec_module(loader, module): - try: super().exec_module(module) - except BaseException as exception: - try: - raise ImportWarning("""{name} from {file} failed to load completely.""".format( - name=module.__name__, file=module.__file__ - )) - except ImportWarning as error: - if not loader._stderr: print_exc() - module.__exception__ = exception - return module - -def load_ipython_extension(ip=None): + """A partial import tool for notebooks. + + Sometimes notebooks don't work, but there may be useful code! + + with Partial(): + import Untitled as nb + assert nb.__exception__ + + if isinstance(nb.__exception__, AssertionError): + print("There was a False assertion.") + + Partial is useful in logging specific debugging approaches to the exception. + """ + __init__ = partialmethod(Notebook.__init__, exceptions=(BaseException,)) + + +class Lazy(Notebook): + """A lazy importer for notebooks. For long operations and a lot of data, the lazy importer delays imports until + an attribute is accessed the first time. + + with Lazy(): + import Untitled as nb + """ + __init__ = partialmethod(Notebook.__init__, lazy=True) + + +def load_ipython_extension(ip=None): add_path_hooks(Notebook, Notebook.EXTENSION_SUFFIXES) -def unload_ipython_extension(ip=None): + + +def unload_ipython_extension(ip=None): remove_one_path_hook(Notebook) -if __name__ == '__main__': - export('loader.ipynb', '../importnb/loader.py') - __import__('doctest').testmod() +if __name__ == "__main__": + try: + from .utils import export + except: + from utils import export + export("loader.ipynb", "../importnb/loader.py") + __import__("doctest").testmod() diff --git a/src/importnb/parameterize.py b/src/importnb/parameterize.py new file mode 100644 index 0000000..5510031 --- /dev/null +++ b/src/importnb/parameterize.py @@ -0,0 +1,158 @@ +try: + from .loader import Notebook +except: + from loader import Notebook +from inspect import getsource + +from types import ModuleType + +from ast import ( + NodeTransformer, + parse, + Assign, + literal_eval, + dump, + fix_missing_locations, + Str, + Tuple, +) + + +class FreeStatement(NodeTransformer): + + def __init__(self, params=None, globals=None): + self.params = params if params is not None else [] + self.globals = globals if globals is not None else {} + + visit_Module = NodeTransformer.generic_visit + + def visit_Assign(FreeStatement, node): + if len(node.targets): + try: + if not getattr(node.targets[0], "id", "_").startswith("_"): + FreeStatement.globals[node.targets[0].id] = literal_eval(node.value) + return + except: + assert True, """The target can not will not literally evaluate.""" + return node + + def generic_visit(self, node): + return node + + def __call__(FreeStatement, nodes): + return FreeStatement.globals, fix_missing_locations(FreeStatement.visit(nodes)) + + +def combine_input_strings(nb): + cells = nb["cells"] + new_cells = [] + for cell in cells: + if cell["cell_type"] == "code": + source = cell["source"] + if isinstance(source, list): + cell["source"] = "".join(source) + + new_cells.append(cell) + nb["cells"] = new_cells + return nb + + +class Parameterize: + """Parameterize takes a module, filename, or notebook dictionary and returns callable object that parameterizes the notebook module. + + f = Parameterize('parameterize.ipynb') + """ + + def __init__(self, object=None): + from importnb.capture import capture_output + from pathlib import Path + from json import load, loads + + self.object = object + + self.__file__ = None + + if isinstance(object, ModuleType): + self.__file__ = object.__file__ + object = loads(getsource(object)) + + if isinstance(object, str): + self.__file__ = object + with open(object) as f: + self.__notebook__ = load(f) + elif isinstance(object, dict): + self.__notebook__ = object + else: + raise ValueError("object must be a module, file string, or dict.") + + self.__notebook__ = combine_input_strings(self.__notebook__) + + with capture_output(stdout=False, stderr=False) as output: + self.__variables__, self.__ast__ = FreeStatement()( + AST().from_notebook_node(self.__notebook__) + ) + self.__output__ = output + self.__signature__ = self.vars_to_sig(**self.__variables__) + # Parameterize.__doc__ = docify(Parameterize.__notebook__) + + def __call__(self, **dict): + self = __import__("copy").copy(self) + self.__dict__.update(self.__variables__) + self.__dict__.update(dict) + exec(AST(filename=self.__file__).compile(self.__ast__), *[self.__dict__] * 2) + return self + + def interact(Parameterize): + """Use the ipywidgets.interact to explore the parameterized notebook.""" + return __import__("ipywidgets").interact(Parameterize) + + @staticmethod + def vars_to_sig(**vars): + """Create a signature for a dictionary of names.""" + from inspect import Parameter, Signature + + return Signature( + [Parameter(str, Parameter.KEYWORD_ONLY, default=vars[str]) for str in vars] + ) + + +try: + from importnb.loader import AST +except: + from importnb.loader import AST + +import sys + +param = "xyz" +extraparam = 42 + +"""Parameters are not created when literal_eval fails.""" +noparam0 = Parameterize + +"""Multiple target assignments are ignored.""" +noparam1, noparam2 = "xyz", 42 + +__test__ = dict( + imports=""" + >>> assert callable(f) + """, + default=""" + >>> default = f() + >>> assert default.param == default.noparam1 == 'xyz' and default.noparam2 == 42 + >>> assert all(str not in default.__signature__.parameters for str in ('noparam', 'noparam1', 'noparam2')) + """, + reuse=""" + >>> new = f(param=10) + >>> assert new.param is 10 and new.extraparam is 42""", +) +if __name__ == "__main__": + f = Parameterize(globals().get("__file__", "parameterize.ipynb")) + __import__("doctest").testmod(verbose=1) + +if __name__ == "__main__": + try: + from .utils import export + except: + from utils import export + export("parameterize.ipynb", "../importnb/parameterize.py") + __import__("doctest").testmod() diff --git a/src/importnb/tests/pyimport.py b/src/importnb/tests/pyimport.py new file mode 100644 index 0000000..61fb01a --- /dev/null +++ b/src/importnb/tests/pyimport.py @@ -0,0 +1,4 @@ +from importnb import Notebook + +with Notebook(): + import foobar \ No newline at end of file diff --git a/src/importnb/tests/test_importnb.ipynb b/src/importnb/tests/test_importnb.ipynb index e99f438..db34b74 100644 --- a/src/importnb/tests/test_importnb.ipynb +++ b/src/importnb/tests/test_importnb.ipynb @@ -14,10 +14,10 @@ } ], "source": [ - "from importnb import Notebook, reload\n", + "from importnb import Notebook, reload, Lazy, Partial, load_ipython_extension, unload_ipython_extension\n", "from nbformat import v4\n", "from pathlib import Path\n", - "import shutil, os, functools\n", + "import shutil, os, functools, sys\n", "from pytest import fixture, mark\n", "from importnb.utils import __IPYTHON__\n", "import warnings\n", @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -60,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -83,12 +83,12 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@fixture\n", - "def clean_up_file(single_file, request):\n", + "def nb(single_file, request):\n", " def clean_sys():\n", " import sys\n", " del sys.modules['foobar']\n", @@ -105,11 +105,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def test_single_file_with_context(clean_up_file):\n", + "def test_single_file_with_context(nb):\n", " with Notebook():\n", " import foobar\n", " assert foobar.foo == 42 and foobar.bar == 100\n", @@ -119,11 +119,11 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def test_single_file_with_context(clean_up_file):\n", + "def test_single_file_with_context(nb):\n", " with Notebook():\n", " import foobar\n", " assert foobar.foo == 42 and foobar.bar == 100\n", @@ -133,29 +133,56 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "def test_single_file_with_capture(clean_up_file):\n", - " # I don't think i can test stderr with pytest\n", - " with Notebook(stdout=True, stderr=True) as out:\n", + "def test_parameterize(nb):\n", + " with Notebook():\n", " import foobar\n", + " \n", + " from importnb.parameterize import Parameterize\n", + " \n", + " f = Parameterize(foobar)\n", + " \n", + " foobar = f()\n", + " \n", " assert foobar.foo == 42 and foobar.bar == 100\n", - " assert out.stdout\n", - "# assert out.stderr\n", - " assert out.stdout == \"42\\n\"\n", " \n", + " foobar = f(foo=\"something\", bar=0)\n", + " assert foobar.foo == \"something\" and foobar.bar == 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def test_python_file(nb):\n", + " with Notebook(stdout=True) as out:\n", + " from pyimport import foobar\n", + " assert foobar.foo == 42 and foobar.bar == 100\n", + " assert out.stdout == \"42\\n\"\n", " validate_reload(foobar)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import sys" + "def test_single_file_with_capture(nb):\n", + " # I don't think i can test stderr with pytest\n", + " with Notebook(stdout=True, stderr=True) as out:\n", + " import foobar\n", + " assert foobar.foo == 42 and foobar.bar == 100\n", + " assert out.stdout\n", + "# assert out.stderr\n", + " assert out.stdout == \"42\\n\"\n", + " \n", + " validate_reload(foobar)" ] }, { @@ -173,15 +200,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@mark.skipif(sys.version_info.minor==4, reason=\"\"\"Requires > python 3.5\"\"\")\n", - "@mark.skipif(not __IPYTHON__, reason=\"\"\"need to capture the output\"\"\")\n", - "def test_single_file_with_lazy(clean_up_file):\n", - " from IPython.utils.capture import capture_output\n", - " with Notebook(lazy=True), capture_output() as out:\n", + "def test_single_file_with_lazy(nb):\n", + " from importnb.capture import capture_output \n", + " with Lazy() as out:\n", " import foobar\n", " assert not out.stdout\n", " with capture_output() as out:\n", @@ -199,7 +225,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -223,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -241,7 +267,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -278,28 +304,19 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "from importnb import load_ipython_extension, unload_ipython_extension" - ] - }, - { - "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "@fixture\n", - "def extension(clean_up_file, request):\n", + "def extension(nb, request):\n", " load_ipython_extension()\n", " request.addfinalizer(unload_ipython_extension)" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -310,7 +327,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -336,7 +353,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -352,7 +369,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -371,16 +388,7 @@ }, { "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "from importnb import Partial" - ] - }, - { - "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/src/importnb/tests/test_module.ipynb b/src/importnb/tests/test_module.ipynb new file mode 100644 index 0000000..54257ef --- /dev/null +++ b/src/importnb/tests/test_module.ipynb @@ -0,0 +1,49 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from importnb import *" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def test_all():\n", + " \n", + " assert all(\n", + " object in globals()\n", + " for object in (\n", + " 'Notebook', 'Partial', 'reload', 'Parameterize', 'Lazy'\n", + " ))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "p6", + "language": "python", + "name": "other-env" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/importnb/tests/test_unittests.ipynb b/src/importnb/tests/test_unittests.ipynb index 7916cd4..11351d4 100644 --- a/src/importnb/tests/test_unittests.ipynb +++ b/src/importnb/tests/test_unittests.ipynb @@ -18,6 +18,7 @@ " \n", " try:\n", " from importnb import *\n", + " from importnb import load_ipython_extension, unload_ipython_extension\n", " except:\n", " # install importnb if it doesn't exist, this makes the tests work on binder.\n", " __import__('subprocess').check_call(\"pip install importnb\".split())\n", @@ -28,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -37,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -85,10 +86,6 @@ " assert loader.test is 42\n", " assert isinstance(loader, __import__('types').ModuleType)\n", " \n", - " @skipUnless(not IPY, \"\"\"importnb is probably installed\"\"\")\n", - " @expectedFailure\n", - " def test_reload_without_context(Test):\n", - " reload(Test.loader)\n", " \n", " def test_reload_with_context(Test):\n", " with Notebook(): assert reload(Test.loader)" @@ -96,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -124,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -144,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -171,13 +168,13 @@ "output_type": "stream", "text": [ "unittests.ipynb:5: ImportWarning: can't resolve package from __spec__ or __package__, falling back on __name__ and __path__\n", - "..sunittests.ipynb:17: ImportWarning: can't resolve package from __spec__ or __package__, falling back on __name__ and __path__\n", + "..unittests.ipynb:17: ImportWarning: can't resolve package from __spec__ or __package__, falling back on __name__ and __path__\n", "xunittests.ipynb:6: ImportWarning: can't resolve package from __spec__ or __package__, falling back on __name__ and __path__\n", "...s\n", "----------------------------------------------------------------------\n", - "Ran 8 tests in 2.019s\n", + "Ran 7 tests in 2.022s\n", "\n", - "OK (skipped=2, expected failures=1)\n" + "OK (skipped=1, expected failures=1)\n" ] } ], @@ -196,19 +193,94 @@ "output_type": "stream", "text": [ "\u001b[1m============================= test session starts ==============================\u001b[0m\n", - "platform darwin -- Python 3.5.4, pytest-3.5.1, py-1.5.3, pluggy-0.6.0\n", + "platform darwin -- Python 3.5.4, pytest-3.5.1, py-1.5.3, pluggy-0.6.0 -- /Users/tonyfast/anaconda/bin/python\n", + "cachedir: ../../../.pytest_cache\n", "benchmark: 3.1.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)\n", - "rootdir: /Users/tonyfast/importnb, inifile:\n", - "plugins: ignore-flaky-0.1.1, benchmark-3.1.1, importnb-0.2.5\n", - "collected 16 items \u001b[0m\n", + "rootdir: /Users/tonyfast/importnb, inifile: tox.ini\n", + "plugins: ignore-flaky-0.1.1, cov-2.5.1, benchmark-3.1.1, importnb-0.2.9\n", + "collected 19 items \u001b[0m\n", + "\n", + "test_importnb.ipynb::test_single_file_with_context \u001b[32mPASSED\u001b[0m\u001b[36m [ 5%]\u001b[0m\n", + "test_importnb.ipynb::test_python_file \u001b[32mPASSED\u001b[0m\u001b[36m [ 10%]\u001b[0m\n", + "test_importnb.ipynb::test_single_file_with_capture \u001b[32mPASSED\u001b[0m\u001b[36m [ 15%]\u001b[0m\n", + "test_importnb.ipynb::test_capturer \u001b[32mPASSED\u001b[0m\u001b[36m [ 21%]\u001b[0m\n", + "test_importnb.ipynb::test_single_file_with_lazy \u001b[32mPASSED\u001b[0m\u001b[36m [ 26%]\u001b[0m\n", + "test_importnb.ipynb::test_single_file_without_context \u001b[33mxfail\u001b[0m\u001b[36m [ 31%]\u001b[0m\n", + "test_importnb.ipynb::test_single_file_relative \u001b[33mxfail\u001b[0m\u001b[36m [ 36%]\u001b[0m\n", + "test_importnb.ipynb::test_single_with_extension \u001b[32mPASSED\u001b[0m\u001b[36m [ 42%]\u001b[0m\n", + "test_importnb.ipynb::test_package \u001b[32mPASSED\u001b[0m\u001b[36m [ 47%]\u001b[0m\n", + "test_importnb.ipynb::test_package_failure \u001b[33mxfail\u001b[0m\u001b[36m [ 52%]\u001b[0m\n", + "test_importnb.ipynb::test_package_failure_partial \u001b[32mPASSED\u001b[0m\u001b[36m [ 57%]\u001b[0m\n", + "test_module.ipynb::test_all \u001b[32mPASSED\u001b[0m\u001b[36m [ 63%]\u001b[0m\n", + "test_unittests.ipynb::TestContext::test_import \u001b[32mPASSED\u001b[0m\u001b[36m [ 68%]\u001b[0m\n", + "test_unittests.ipynb::TestContext::test_reload_with_context \u001b[32mPASSED\u001b[0m\u001b[36m [ 73%]\u001b[0m\n", + "test_unittests.ipynb::TestRemote::test_imports \u001b[33mSKIPPED\u001b[0m\u001b[36m [ 78%]\u001b[0m\n", + "test_unittests.ipynb::TestExtension::test_failure \u001b[33mxfail\u001b[0m\u001b[36m [ 84%]\u001b[0m\n", + "test_unittests.ipynb::TestExtension::test_import \u001b[32mPASSED\u001b[0m\u001b[36m [ 89%]\u001b[0m\n", + "test_unittests.ipynb::TestPartial::test_exception \u001b[32mPASSED\u001b[0m\u001b[36m [ 94%]\u001b[0m\n", + "test_unittests.ipynb::TestPartial::test_traceback \u001b[32mPASSED\u001b[0m\u001b[36m [100%]\u001b[0m\n", + "\n", + "---------- coverage: platform darwin, python 3.5.4-final-0 -----------\n", + "Name Stmts Miss Cover\n", + "-------------------------------------------------------------------------------\n", + "/Users/tonyfast/importnb/src/importnb/__init__ 4 4 0%\n", + "/Users/tonyfast/importnb/src/importnb/_version 1 1 0%\n", + "/Users/tonyfast/importnb/src/importnb/capture 51 34 33%\n", + "/Users/tonyfast/importnb/src/importnb/compiler_ipython 27 27 0%\n", + "/Users/tonyfast/importnb/src/importnb/compiler_python 34 30 12%\n", + "/Users/tonyfast/importnb/src/importnb/decoder 20 9 55%\n", + "/Users/tonyfast/importnb/src/importnb/exporter 55 41 25%\n", + "/Users/tonyfast/importnb/src/importnb/loader 92 47 49%\n", + "/Users/tonyfast/importnb/src/importnb/parameterize 89 48 46%\n", + "/Users/tonyfast/importnb/src/importnb/utils/__init__ 22 22 0%\n", + "/Users/tonyfast/importnb/src/importnb/utils/ipython 42 42 0%\n", + "/Users/tonyfast/importnb/src/importnb/utils/pytest_plugin 25 16 36%\n", + "/Users/tonyfast/importnb/src/importnb/utils/setup 56 56 0%\n", + "/Users/tonyfast/importnb/src/importnb/utils/watch 20 20 0%\n", + "failure 1 0 100%\n", + "import_this 1 0 100%\n", + "pyimport 3 0 100%\n", + "test_importnb 1 0 100%\n", + "test_module 1 0 100%\n", + "test_unittests 1 0 100%\n", + "-------------------------------------------------------------------------------\n", + "TOTAL 546 397 27%\n", + "\n", + "\n", + "\u001b[33m=============================== warnings summary ===============================\u001b[0m\n", + "src/importnb/tests/test_importnb.ipynb::test_package_failure_partial\n", + " /Users/tonyfast/importnb/src/importnb/loader.py:120: UserWarning: a_test_package.failure was partially imported with a .==========.Traceback (most recent call last):\n", + " File \"/Users/tonyfast/importnb/src/importnb/loader.py\", line 108, in exec_module\n", + " super().exec_module(module)\n", + " File \"\", line 697, in exec_module\n", + " File \"\", line 222, in _call_with_frames_removed\n", + " File \"/Users/tonyfast/importnb/src/importnb/tests/a_test_package/failure.ipynb\", line 10, in \n", + " \"assert False\\n\",\n", + " AssertionError\n", + " \n", + " format_exc(),\n", "\n", - "test_importnb.ipynb .sxx..x.\u001b[36m [ 50%]\u001b[0m\n", - "test_unittests.ipynb ....xsF.\u001b[36m [100%]\u001b[0m\n", + "src/importnb/tests/test_unittests.ipynb::TestPartial::test_exception\n", + " /Users/tonyfast/importnb/src/importnb/loader.py:120: UserWarning: failure was partially imported with a .==========.Traceback (most recent call last):\n", + " File \"/Users/tonyfast/importnb/src/importnb/tests/test_unittests.ipynb\", line 104, in setUp\n", + " \" from . import failure\\n\",\n", + " ImportError: attempted relative import with no known parent package\n", + " \n", + " During handling of the above exception, another exception occurred:\n", + " \n", + " Traceback (most recent call last):\n", + " File \"/Users/tonyfast/importnb/src/importnb/loader.py\", line 108, in exec_module\n", + " super().exec_module(module)\n", + " File \"\", line 697, in exec_module\n", + " File \"\", line 222, in _call_with_frames_removed\n", + " File \"/Users/tonyfast/importnb/src/importnb/tests/failure.ipynb\", line 22, in \n", + " \" assert False\\n\",\n", + " AssertionError\n", + " \n", + " format_exc(),\n", "\n", - "=================================== FAILURES ===================================\n", - "\u001b[1m\u001b[31m__________________________ TestExtension.test_failure __________________________\u001b[0m\n", - "Unexpected success\n", - "\u001b[1m\u001b[31m=========== 1 failed, 9 passed, 2 skipped, 4 xfailed in 2.59 seconds ===========\u001b[0m\n" + "-- Docs: http://doc.pytest.org/en/latest/warnings.html\n", + "\u001b[1m\u001b[33m========= 14 passed, 1 skipped, 4 xfailed, 2 warnings in 2.89 seconds ==========\u001b[0m\n" ] } ], diff --git a/src/importnb/utils/__init__.py b/src/importnb/utils/__init__.py index 13832dc..a86312a 100644 --- a/src/importnb/utils/__init__.py +++ b/src/importnb/utils/__init__.py @@ -18,8 +18,12 @@ from importnb.compiler_python import ScriptExporter -def export(src, dst): - Path(dst).write_text(ScriptExporter().from_filename(src)[0]) +def export(src, dst, columns=100): + try: + from black import format_str + except: + format_str = lambda str, int: str + Path(dst).write_text(format_str(ScriptExporter().from_filename(src)[0], columns)) if __name__ == "__main__": diff --git a/src/notebooks/loader.ipynb b/src/notebooks/loader.ipynb index 4c58e88..16fbccf 100644 --- a/src/notebooks/loader.ipynb +++ b/src/notebooks/loader.ipynb @@ -17,11 +17,11 @@ "source": [ " try:\n", " from .exporter import Compile, AST\n", - " from .utils import __IPYTHON__, export\n", + " from .utils import __IPYTHON__\n", " from .capture import capture_output\n", " except:\n", " from exporter import Compile, AST\n", - " from utils import __IPYTHON__, export\n", + " from utils import __IPYTHON__\n", " from capture import capture_output\n", " import inspect, sys\n", " from importlib.machinery import SourceFileLoader\n", @@ -30,11 +30,13 @@ " except:\n", " #python 3.4\n", " from importlib.machinery import FileFinder\n", + " from functools import partialmethod\n", " from importlib import reload\n", - " from traceback import print_exc\n", + " from traceback import print_exc, format_exc\n", + " from warnings import warn\n", " from contextlib import contextmanager, ExitStack\n", " \n", - " __all__ = 'Notebook', 'Partial', 'reload'," + " __all__ = 'Notebook', 'Partial', 'reload', 'Lazy'" ] }, { @@ -133,39 +135,55 @@ "The way the context manager works it is difficult to attach contexts to each module." ] }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + " class ImportNbException(BaseException):\n", + " \"\"\"ImportNbException allows all exceptions to be raised, a null except statement always passes.\"\"\"" + ] + }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - " class Notebook(SourceFileLoader, ExitStack):\n", + " class Notebook(SourceFileLoader, capture_output):\n", " \"\"\"A SourceFileLoader for notebooks that provides line number debugginer in the JSON source.\"\"\"\n", " EXTENSION_SUFFIXES = '.ipynb',\n", " \n", " def __init__(\n", - " self, fullname=None, path=None, *, stdout=False, stderr=False, display=False, lazy=False\n", + " self, fullname=None, path=None, *, \n", + " stdout=False, stderr=False, display=False, \n", + " lazy=False, exceptions=(ImportNbException,)\n", " ): \n", " SourceFileLoader.__init__(self, fullname, path)\n", - " ExitStack.__init__(self)\n", - " self._stdout, self._stderr, self._display = stdout, stderr, display\n", + " capture_output.__init__(self, stdout=stdout, stderr=stderr, display=display)\n", " self._lazy = lazy\n", + " self._exceptions = exceptions\n", " \n", " def __enter__(self, position=0): \n", " add_path_hooks(type(self), self.EXTENSION_SUFFIXES, position=position, lazy=self._lazy)\n", - " if self._capture:\n", - " stack = super().__enter__()\n", - " return stack.enter_context(capture_output(\n", - " stdout=self._stdout, stderr=self._stderr, display=self._display\n", - " ))\n", - " \n", - " @property\n", - " def _capture(self): return any((self._stdout, self._stderr, self._display))\n", - " \n", - " def __exit__(self, *excepts): \n", - " remove_one_path_hook(type(self))\n", + " return super().__enter__()\n", " \n", - " if self._capture: super().__exit__(*excepts)\n", + " def exec_module(self, module):\n", + " \"\"\"All exceptions specific in the context.\"\"\"\n", + " try:\n", + " super().exec_module(module)\n", + " module.__exception__ = None\n", + " except self._exceptions as e:\n", + " \"\"\"Display a message if an error is escaped.\"\"\"\n", + " module.__exception__ = e\n", + " warn('.'.join([\n", + " \"\"\"{name} was partially imported with a {error}\"\"\".format(\n", + " error = type(e), name=module.__name__\n", + " ),\n", + " \"=\"*10, format_exc()]))\n", + " \n", + " def __exit__(self, *excepts): remove_one_path_hook(type(self)), super().__exit__(*excepts)\n", " \n", " def source_to_code(Notebook, data, path):\n", " with __import__('io').BytesIO(data) as stream:\n", @@ -185,18 +203,37 @@ "metadata": {}, "outputs": [], "source": [ - " class Partial(Notebook):\n", - " def exec_module(loader, module):\n", - " try: super().exec_module(module)\n", - " except BaseException as exception:\n", - " try: \n", - " raise ImportWarning(\"\"\"{name} from {file} failed to load completely.\"\"\".format(\n", - " name=module.__name__, file=module.__file__\n", - " ))\n", - " except ImportWarning as error:\n", - " if not loader._stderr: print_exc()\n", - " module.__exception__ = exception\n", - " return module" + " class Partial(Notebook): \n", + " \"\"\"A partial import tool for notebooks.\n", + " \n", + " Sometimes notebooks don't work, but there may be useful code!\n", + " \n", + " with Partial():\n", + " import Untitled as nb\n", + " assert nb.__exception__\n", + " \n", + " if isinstance(nb.__exception__, AssertionError):\n", + " print(\"There was a False assertion.\")\n", + " \n", + " Partial is useful in logging specific debugging approaches to the exception.\n", + " \"\"\"\n", + " __init__ = partialmethod(Notebook.__init__, exceptions=(BaseException,))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + " class Lazy(Notebook): \n", + " \"\"\"A lazy importer for notebooks. For long operations and a lot of data, the lazy importer delays imports until \n", + " an attribute is accessed the first time.\n", + " \n", + " with Lazy():\n", + " import Untitled as nb\n", + " \"\"\"\n", + " __init__ = partialmethod(Notebook.__init__, lazy=True)" ] }, { @@ -208,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -227,23 +264,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": { "scrolled": false }, "outputs": [], "source": [ " if __name__ == '__main__':\n", + " try:\n", + " from .utils import export\n", + " except:\n", + " from utils import export\n", " export('loader.ipynb', '../importnb/loader.py')\n", " __import__('doctest').testmod()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/src/notebooks/parameterize.ipynb b/src/notebooks/parameterize.ipynb new file mode 100644 index 0000000..817fa7c --- /dev/null +++ b/src/notebooks/parameterize.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + " try:\n", + " from .loader import Notebook\n", + " except:\n", + " from loader import Notebook\n", + " from inspect import getsource\n", + " \n", + " from types import ModuleType" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Are single target `ast.Expr` that will `ast.literal_eval` is a possible parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + " from ast import NodeTransformer, parse, Assign, literal_eval, dump, fix_missing_locations, Str, Tuple\n", + " class FreeStatement(NodeTransformer):\n", + " def __init__(self, params=None, globals=None):\n", + " self.params = params if params is not None else []\n", + " self.globals = globals if globals is not None else {}\n", + " \n", + " visit_Module = NodeTransformer.generic_visit\n", + " \n", + " def visit_Assign(FreeStatement, node):\n", + " if len(node.targets):\n", + " try:\n", + " if not getattr(node.targets[0], 'id', '_').startswith('_'):\n", + " FreeStatement.globals[node.targets[0].id] = literal_eval(node.value)\n", + " return \n", + " except: assert True, \"\"\"The target can not will not literally evaluate.\"\"\"\n", + " return node\n", + " \n", + " def generic_visit(self, node): return node\n", + " \n", + " def __call__(FreeStatement, nodes): return FreeStatement.globals, fix_missing_locations(FreeStatement.visit(nodes))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# `Parameterize` notebooks\n", + "\n", + "`Parameterize` is callable version of a notebook. It uses `pidgin` to load the `NotebookNode` and evaluates the `FreeStatement`s to discover the signature." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + " def combine_input_strings(nb):\n", + " cells = nb['cells']\n", + " new_cells = []\n", + " for cell in cells:\n", + " if cell['cell_type'] == 'code':\n", + " source = cell['source']\n", + " if isinstance(source, list):\n", + " cell['source'] = ''.join(source)\n", + " \n", + " new_cells.append(cell)\n", + " nb['cells'] = new_cells\n", + " return nb" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + " class Parameterize:\n", + " \"\"\"Parameterize takes a module, filename, or notebook dictionary and returns callable object that parameterizes the notebook module.\n", + " \n", + " f = Parameterize('parameterize.ipynb')\n", + " \"\"\"\n", + " def __init__(\n", + " self, object=None\n", + " ):\n", + " from importnb.capture import capture_output\n", + " from pathlib import Path\n", + " from json import load, loads\n", + " self.object = object\n", + "\n", + " self.__file__ = None\n", + " \n", + " if isinstance(object, ModuleType):\n", + " self.__file__ = object.__file__\n", + " object = loads(getsource(object))\n", + " \n", + " \n", + " if isinstance(object, str):\n", + " self.__file__ = object\n", + " with open(object) as f: \n", + " self.__notebook__ = load(f)\n", + " elif isinstance(object, dict):\n", + " self.__notebook__ = object\n", + " else: raise ValueError(\"object must be a module, file string, or dict.\")\n", + " \n", + " \n", + " self.__notebook__ = combine_input_strings(self.__notebook__)\n", + " \n", + " with capture_output(stdout=False, stderr=False) as output:\n", + " self.__variables__, self.__ast__ = \\\n", + " FreeStatement()(AST().from_notebook_node(self.__notebook__))\n", + " self.__output__ = output\n", + " self.__signature__ = self.vars_to_sig(**self.__variables__)\n", + " # Parameterize.__doc__ = docify(Parameterize.__notebook__)\n", + "\n", + " def __call__(self, **dict):\n", + " self = __import__('copy').copy(self)\n", + " self.__dict__.update(self.__variables__)\n", + " self.__dict__.update(dict)\n", + " exec(AST(filename=self.__file__).compile(self.__ast__), *[self.__dict__]*2)\n", + " return self\n", + " \n", + " def interact(Parameterize): \n", + " \"\"\"Use the ipywidgets.interact to explore the parameterized notebook.\"\"\"\n", + " return __import__('ipywidgets').interact(Parameterize)\n", + " \n", + " @staticmethod\n", + " def vars_to_sig(**vars):\n", + " \"\"\"Create a signature for a dictionary of names.\"\"\"\n", + " from inspect import Parameter, Signature\n", + " return Signature([Parameter(str, Parameter.KEYWORD_ONLY, default = vars[str]) for str in vars])\n", + " \n", + " try: from importnb.loader import AST\n", + " except: from importnb.loader import AST" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Examples that do work" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + " import sys\n", + " \n", + " param = 'xyz'\n", + " extraparam = 42" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Examples that do *not* work" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + " \"\"\"Parameters are not created when literal_eval fails.\"\"\"\n", + " noparam0 = Parameterize\n", + " \n", + " \"\"\"Multiple target assignments are ignored.\"\"\"\n", + " noparam1, noparam2 = 'xyz', 42" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Developer" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying:\n", + " default = f()\n", + "Expecting nothing\n", + "ok\n", + "Trying:\n", + " assert default.param == default.noparam1 == 'xyz' and default.noparam2 == 42\n", + "Expecting nothing\n", + "ok\n", + "Trying:\n", + " assert all(str not in default.__signature__.parameters for str in ('noparam', 'noparam1', 'noparam2'))\n", + "Expecting nothing\n", + "ok\n", + "Trying:\n", + " assert callable(f)\n", + "Expecting nothing\n", + "ok\n", + "Trying:\n", + " new = f(param=10)\n", + "Expecting nothing\n", + "ok\n", + "Trying:\n", + " assert new.param is 10 and new.extraparam is 42\n", + "Expecting nothing\n", + "ok\n", + "13 items had no tests:\n", + " __main__\n", + " __main__.FreeStatement\n", + " __main__.FreeStatement.__call__\n", + " __main__.FreeStatement.__init__\n", + " __main__.FreeStatement.generic_visit\n", + " __main__.FreeStatement.visit_Assign\n", + " __main__.FreeStatement.visit_FunctionDef\n", + " __main__.Parameterize\n", + " __main__.Parameterize.__call__\n", + " __main__.Parameterize.__init__\n", + " __main__.Parameterize.interact\n", + " __main__.Parameterize.vars_to_sig\n", + " __main__.combine_input_strings\n", + "3 items passed all tests:\n", + " 3 tests in __main__.__test__.default\n", + " 1 tests in __main__.__test__.imports\n", + " 2 tests in __main__.__test__.reuse\n", + "6 tests in 16 items.\n", + "6 passed and 0 failed.\n", + "Test passed.\n" + ] + } + ], + "source": [ + " __test__ = dict(\n", + " imports=\"\"\"\n", + " >>> assert callable(f)\n", + " \"\"\",\n", + " default=\"\"\"\n", + " >>> default = f()\n", + " >>> assert default.param == default.noparam1 == 'xyz' and default.noparam2 == 42\n", + " >>> assert all(str not in default.__signature__.parameters for str in ('noparam', 'noparam1', 'noparam2'))\n", + " \"\"\",\n", + " reuse=\"\"\"\n", + " >>> new = f(param=10)\n", + " >>> assert new.param is 10 and new.extraparam is 42\"\"\",\n", + " )\n", + " if __name__ == '__main__':\n", + " f = Parameterize(globals().get('__file__', 'parameterize.ipynb'))\n", + " __import__('doctest').testmod(verbose=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + " if __name__ == '__main__':\n", + " try:\n", + " from .utils import export\n", + " except:\n", + " from utils import export\n", + " export('parameterize.ipynb', '../importnb/parameterize.py')\n", + " __import__('doctest').testmod()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "p6", + "language": "python", + "name": "other-env" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + }, + "toc": { + "colors": { + "hover_highlight": "#DAA520", + "running_highlight": "#FF0000", + "selected_highlight": "#FFD700" + }, + "moveMenuLeft": true, + "nav_menu": { + "height": "30px", + "width": "252px" + }, + "navigate_menu": true, + "number_sections": true, + "sideBar": true, + "threshold": 4, + "toc_cell": false, + "toc_section_display": "block", + "toc_window_display": false, + "widenNotebook": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/notebooks/utils/__init__.ipynb b/src/notebooks/utils/__init__.ipynb index 3c731f6..9c43ed6 100644 --- a/src/notebooks/utils/__init__.ipynb +++ b/src/notebooks/utils/__init__.ipynb @@ -6,15 +6,15 @@ "metadata": {}, "outputs": [], "source": [ - "__IPYTHON__ = False\n", + " __IPYTHON__ = False\n", "\n", - "try:\n", - " from IPython import get_ipython\n", - " if not get_ipython(): ...\n", - " else: __IPYTHON__ = True\n", - "except: ...\n", + " try:\n", + " from IPython import get_ipython\n", + " if not get_ipython(): ...\n", + " else: __IPYTHON__ = True\n", + " except: ...\n", "\n", - "from pathlib import Path" + " from pathlib import Path" ] }, { @@ -23,10 +23,10 @@ "metadata": {}, "outputs": [], "source": [ - "try:\n", - " from ..compiler_python import ScriptExporter \n", - "except:\n", - " from importnb.compiler_python import ScriptExporter" + " try:\n", + " from ..compiler_python import ScriptExporter \n", + " except:\n", + " from importnb.compiler_python import ScriptExporter" ] }, { @@ -35,8 +35,13 @@ "metadata": {}, "outputs": [], "source": [ - "def export(src, dst):\n", - " Path(dst).write_text(ScriptExporter().from_filename(src)[0])" + "def export(src, dst, columns=100):\n", + " try:\n", + " from black import format_str\n", + " except:\n", + " format_str = lambda str, int: str\n", + " Path(dst).write_text(\n", + " format_str(ScriptExporter().from_filename(src)[0], columns))" ] }, { diff --git a/src/notebooks/utils/__init__.py b/src/notebooks/utils/__init__.py index b1e50a8..a86312a 100644 --- a/src/notebooks/utils/__init__.py +++ b/src/notebooks/utils/__init__.py @@ -2,22 +2,30 @@ try: from IPython import get_ipython - if not get_ipython(): ... - else: __IPYTHON__ = True -except: ... + + if not get_ipython(): + ... + else: + __IPYTHON__ = True +except: + ... from pathlib import Path try: from ..compiler_python import ScriptExporter - except: from importnb.compiler_python import ScriptExporter -def export(src, dst): - Path(dst).write_text(ScriptExporter().from_filename(src)[0]) -if __name__ == '__main__': +def export(src, dst, columns=100): + try: + from black import format_str + except: + format_str = lambda str, int: str + Path(dst).write_text(format_str(ScriptExporter().from_filename(src)[0], columns)) + - export('__init__.ipynb', '../../importnb/utils/__init__.py') - export('__init__.ipynb', '__init__.py') \ No newline at end of file +if __name__ == "__main__": + export("__init__.ipynb", "../../importnb/utils/__init__.py") + export("__init__.ipynb", "__init__.py") diff --git a/src/notebooks/utils/ipython.ipynb b/src/notebooks/utils/ipython.ipynb index 0e4fdf4..b8fe705 100644 --- a/src/notebooks/utils/ipython.ipynb +++ b/src/notebooks/utils/ipython.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython import paths, get_ipython" + " from IPython import paths, get_ipython" ] }, { @@ -15,8 +15,8 @@ "metadata": {}, "outputs": [], "source": [ - "from pathlib import Path\n", - "import json" + " from pathlib import Path\n", + " import json" ] }, { @@ -25,9 +25,9 @@ "metadata": {}, "outputs": [], "source": [ - "def get_config():\n", - " ip = get_ipython()\n", - " return Path(ip.profile_dir.location if ip else paths.locate_profile()) / \"ipython_config.json\" " + " def get_config():\n", + " ip = get_ipython()\n", + " return Path(ip.profile_dir.location if ip else paths.locate_profile()) / \"ipython_config.json\" " ] }, { @@ -36,21 +36,21 @@ "metadata": {}, "outputs": [], "source": [ - "def load_config():\n", - " location = get_config()\n", - " try:\n", - " with location.open() as file: \n", - " config = json.load(file)\n", - " except (FileNotFoundError, json.JSONDecodeError):\n", - " config = {}\n", + " def load_config():\n", + " location = get_config()\n", + " try:\n", + " with location.open() as file: \n", + " config = json.load(file)\n", + " except (FileNotFoundError, json.JSONDecodeError):\n", + " config = {}\n", + "\n", + " if 'InteractiveShellApp' not in config:\n", + " config['InteractiveShellApp'] = {}\n", "\n", - " if 'InteractiveShellApp' not in config:\n", - " config['InteractiveShellApp'] = {}\n", + " if 'extensions' not in config['InteractiveShellApp']:\n", + " config['InteractiveShellApp']['extensions'] = []\n", "\n", - " if 'extensions' not in config['InteractiveShellApp']:\n", - " config['InteractiveShellApp']['extensions'] = []\n", - " \n", - " return config, location" + " return config, location" ] }, { @@ -59,14 +59,14 @@ "metadata": {}, "outputs": [], "source": [ - "def install(ip=None):\n", - " config, location = load_config()\n", - " \n", - " if 'importnb' not in config['InteractiveShellApp']['extensions']:\n", - " config['InteractiveShellApp']['extensions'].append('importnb.utils.ipython')\n", - " \n", - " with location.open('w') as file: \n", - " json.dump(config, file)" + " def install(ip=None):\n", + " config, location = load_config()\n", + "\n", + " if 'importnb' not in config['InteractiveShellApp']['extensions']:\n", + " config['InteractiveShellApp']['extensions'].append('importnb.utils.ipython')\n", + "\n", + " with location.open('w') as file: \n", + " json.dump(config, file)" ] }, { @@ -75,9 +75,9 @@ "metadata": {}, "outputs": [], "source": [ - "def installed():\n", - " config = load_config()\n", - " return 'importnb.utils.ipython' in config.get('InteractiveShellApp', {}).get('extensions', [])" + " def installed():\n", + " config = load_config()\n", + " return 'importnb.utils.ipython' in config.get('InteractiveShellApp', {}).get('extensions', [])" ] }, { @@ -86,14 +86,14 @@ "metadata": {}, "outputs": [], "source": [ - "def uninstall(ip=None):\n", - " config, location = load_config()\n", - " \n", - " config['InteractiveShellApp']['extensions'] = [\n", - " ext for ext in config['InteractiveShellApp']['extensions'] if ext != 'importnb.utils.ipython'\n", - " ]\n", - " \n", - " with location.open('w') as file: json.dump(config, file)" + " def uninstall(ip=None):\n", + " config, location = load_config()\n", + "\n", + " config['InteractiveShellApp']['extensions'] = [\n", + " ext for ext in config['InteractiveShellApp']['extensions'] if ext != 'importnb.utils.ipython'\n", + " ]\n", + "\n", + " with location.open('w') as file: json.dump(config, file)" ] }, { @@ -102,9 +102,9 @@ "metadata": {}, "outputs": [], "source": [ - "def load_ipython_extension(ip):\n", - " from ..loader import Notebook\n", - " Notebook().__enter__(position=-1)" + " def load_ipython_extension(ip):\n", + " from ..loader import Notebook\n", + " Notebook().__enter__(position=-1)" ] }, { @@ -115,13 +115,13 @@ }, "outputs": [], "source": [ - "if __name__ == '__main__':\n", - " try:\n", - " from .compiler_python import ScriptExporter\n", - " except:\n", - " from importnb.compiler_python import ScriptExporter\n", - " from pathlib import Path\n", - " Path('../../importnb/utils/ipython.py').write_text(ScriptExporter().from_filename('ipython.ipynb')[0])" + " if __name__ == '__main__':\n", + " try:\n", + " from .compiler_python import ScriptExporter\n", + " except:\n", + " from importnb.compiler_python import ScriptExporter\n", + " from pathlib import Path\n", + " Path('../../importnb/utils/ipython.py').write_text(ScriptExporter().from_filename('ipython.ipynb')[0])" ] }, { diff --git a/src/notebooks/utils/pytest_plugin.ipynb b/src/notebooks/utils/pytest_plugin.ipynb index 2bf617d..695a9bd 100644 --- a/src/notebooks/utils/pytest_plugin.ipynb +++ b/src/notebooks/utils/pytest_plugin.ipynb @@ -6,46 +6,46 @@ "metadata": {}, "outputs": [], "source": [ - "import pytest\n", - "try:\n", - " from .loader import Notebook\n", - "except:\n", - " from importnb.loader import Notebook\n", + " import pytest\n", + " try:\n", + " from .loader import Notebook\n", + " except:\n", + " from importnb.loader import Notebook\n", + "\n", + " loader = Notebook\n", "\n", - "loader = Notebook\n", + " def pytest_collect_file(parent, path):\n", + " if path.ext in ('.ipynb', '.py'):\n", + " if not parent.session.isinitpath(path):\n", + " for pat in parent.config.getini('python_files'):\n", + " if path.fnmatch(pat.rstrip('.py') + path.ext):\n", + " break\n", + " else:\n", + " return\n", + " return Module(path, parent)\n", "\n", - "def pytest_collect_file(parent, path):\n", - " if path.ext in ('.ipynb', '.py'):\n", - " if not parent.session.isinitpath(path):\n", - " for pat in parent.config.getini('python_files'):\n", - " if path.fnmatch(pat.rstrip('.py') + path.ext):\n", - " break\n", - " else:\n", - " return\n", - " return Module(path, parent)\n", - " \n", - "class Module(pytest.Module):\n", - " def collect(self):\n", - " global loader\n", - " with loader(): \n", - " return super().collect()" + " class Module(pytest.Module):\n", + " def collect(self):\n", + " global loader\n", + " with loader(): \n", + " return super().collect()" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": { "scrolled": false }, "outputs": [], "source": [ - "if __name__ == '__main__':\n", - " try:\n", - " from .compiler_python import ScriptExporter\n", - " except:\n", - " from importnb.compiler_python import ScriptExporter\n", - " from pathlib import Path\n", - " Path('../../importnb/utils/pytest_plugin.py').write_text(ScriptExporter().from_filename('pytest_plugin.ipynb')[0])" + " if __name__ == '__main__':\n", + " try:\n", + " from .compiler_python import ScriptExporter\n", + " except:\n", + " from importnb.compiler_python import ScriptExporter\n", + " from pathlib import Path\n", + " Path('../../importnb/utils/pytest_plugin.py').write_text(ScriptExporter().from_filename('pytest_plugin.ipynb')[0])" ] }, { diff --git a/src/notebooks/utils/setup.ipynb b/src/notebooks/utils/setup.ipynb index af09ffe..6563401 100644 --- a/src/notebooks/utils/setup.ipynb +++ b/src/notebooks/utils/setup.ipynb @@ -6,10 +6,10 @@ "metadata": {}, "outputs": [], "source": [ - "try: \n", - " from .. import exporter\n", - "except:\n", - " import importnb.exporter" + " try: \n", + " from .. import exporter\n", + " except:\n", + " import importnb.exporter" ] }, { @@ -36,73 +36,73 @@ "metadata": {}, "outputs": [], "source": [ - "from setuptools.command.build_py import build_py\n", - "import sys, os\n", - "from pathlib import Path\n", - "import importlib\n", + " from setuptools.command.build_py import build_py\n", + " import sys, os\n", + " from pathlib import Path\n", + " import importlib\n", "\n", - "class build_ipynb(build_py):\n", - " \"\"\"Should really use manifest.in\n", - " \n", - " Lazy import build_ipynb in your setup.\n", - " \n", - " class BuildWithNotebooks(setuptools.command.build_py.build_py):\n", - " def __new__(cls, distribution):\n", - " from importnb.utils.setup import build_ipynb\n", - " return build_ipynb(distribution)\n", - " setup_args.update(cmdclass=dict(build_py=BuildWithNotebooks))\n", - " \"\"\"\n", - " def get_module_outfile(self, build_dir, package, module):\n", - " module_mapper = {module[1]: module[2] for module in self.find_all_modules()}\n", - " outfile_path = [build_dir] + list(package) + [module_mapper[module]]\n", - " return os.path.join(*outfile_path)\n", - " \n", - " def find_package_modules(self, package, package_dir):\n", - " from glob import glob\n", - " self.check_package(package, package_dir)\n", - " module_files = glob(os.path.join(package_dir, \"*.py\"))\n", - " modules = []\n", - " setup_script = os.path.abspath(self.distribution.script_name)\n", + " class build_ipynb(build_py):\n", + " \"\"\"Should really use manifest.in\n", "\n", - " for f in module_files + glob(os.path.join(package_dir, \"*.ipynb\")):\n", - " abs_f = os.path.abspath(f)\n", - " if abs_f != setup_script:\n", - " module = os.path.splitext(os.path.basename(f))[0]\n", - " modules.append((package, module, f))\n", - " else:\n", - " self.debug_print(\"excluding %s\" % setup_script)\n", - " return modules\n", + " Lazy import build_ipynb in your setup.\n", "\n", - " def find_modules(self):\n", - " packages, modules = {}, []\n", + " class BuildWithNotebooks(setuptools.command.build_py.build_py):\n", + " def __new__(cls, distribution):\n", + " from importnb.utils.setup import build_ipynb\n", + " return build_ipynb(distribution)\n", + " setup_args.update(cmdclass=dict(build_py=BuildWithNotebooks))\n", + " \"\"\"\n", + " def get_module_outfile(self, build_dir, package, module):\n", + " module_mapper = {module[1]: module[2] for module in self.find_all_modules()}\n", + " outfile_path = [build_dir] + list(package) + [module_mapper[module]]\n", + " return os.path.join(*outfile_path)\n", "\n", - " for module in self.py_modules:\n", - " path = module.split('.')\n", - " package = '.'.join(path[0:-1])\n", - " module_base = path[-1]\n", + " def find_package_modules(self, package, package_dir):\n", + " from glob import glob\n", + " self.check_package(package, package_dir)\n", + " module_files = glob(os.path.join(package_dir, \"*.py\"))\n", + " modules = []\n", + " setup_script = os.path.abspath(self.distribution.script_name)\n", "\n", - " try:\n", - " (package_dir, checked) = packages[package]\n", - " except KeyError:\n", - " package_dir = self.get_package_dir(package)\n", - " checked = 0\n", + " for f in module_files + glob(os.path.join(package_dir, \"*.ipynb\")):\n", + " abs_f = os.path.abspath(f)\n", + " if abs_f != setup_script:\n", + " module = os.path.splitext(os.path.basename(f))[0]\n", + " modules.append((package, module, f))\n", + " else:\n", + " self.debug_print(\"excluding %s\" % setup_script)\n", + " return modules\n", "\n", - " if not checked:\n", - " init_py = self.check_package(package, package_dir)\n", - " packages[package] = (package_dir, 1)\n", - " if init_py:\n", - " modules.append((package, \"__init__\", init_py))\n", + " def find_modules(self):\n", + " packages, modules = {}, []\n", "\n", - " module_file = os.path.join(package_dir, module_base + \".ipynb\")\n", - " \n", - " if Path(module_file).exists():\n", - " modules.append((package, module_base, str(module_file)))\n", - " else:\n", - " module_file = str(Path(module_file).with_suffix('.py'))\n", - " if self.check_module(module, module_file):\n", - " modules.append((package, module_base, str(module_file))) \n", + " for module in self.py_modules:\n", + " path = module.split('.')\n", + " package = '.'.join(path[0:-1])\n", + " module_base = path[-1]\n", + "\n", + " try:\n", + " (package_dir, checked) = packages[package]\n", + " except KeyError:\n", + " package_dir = self.get_package_dir(package)\n", + " checked = 0\n", + "\n", + " if not checked:\n", + " init_py = self.check_package(package, package_dir)\n", + " packages[package] = (package_dir, 1)\n", + " if init_py:\n", + " modules.append((package, \"__init__\", init_py))\n", + "\n", + " module_file = os.path.join(package_dir, module_base + \".ipynb\")\n", + "\n", + " if Path(module_file).exists():\n", + " modules.append((package, module_base, str(module_file)))\n", + " else:\n", + " module_file = str(Path(module_file).with_suffix('.py'))\n", + " if self.check_module(module, module_file):\n", + " modules.append((package, module_base, str(module_file))) \n", "\n", - " return modules " + " return modules " ] }, { diff --git a/src/notebooks/utils/watch.ipynb b/src/notebooks/utils/watch.ipynb index d385e24..075243f 100644 --- a/src/notebooks/utils/watch.ipynb +++ b/src/notebooks/utils/watch.ipynb @@ -21,8 +21,8 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "from watchdog.tricks import ShellCommandTrick" + " import os\n", + " from watchdog.tricks import ShellCommandTrick" ] }, { @@ -31,19 +31,19 @@ "metadata": {}, "outputs": [], "source": [ - "class ModuleTrick(ShellCommandTrick):\n", - " \"\"\"ModuleTrick is a watchdog trick that \"\"\"\n", - " def __init__(self, **kwargs):\n", - " super().__init__(**kwargs)\n", - " self._ignore_patterns = self._ignore_patterns or []\n", - " self._ignore_patterns.extend((\n", - " '*-checkpoint.ipynb', '*.~*'\n", - " ))\n", - " def on_any_event(self, event):\n", - " try:\n", - " event.dest_path = event.src_path.lstrip('.').lstrip(os.sep).rstrip('.ipynb').rstrip('.py').replace(os.sep, '.')\n", - " super().on_any_event(event)\n", - " except AttributeError: ..." + " class ModuleTrick(ShellCommandTrick):\n", + " \"\"\"ModuleTrick is a watchdog trick that \"\"\"\n", + " def __init__(self, **kwargs):\n", + " super().__init__(**kwargs)\n", + " self._ignore_patterns = self._ignore_patterns or []\n", + " self._ignore_patterns.extend((\n", + " '*-checkpoint.ipynb', '*.~*'\n", + " ))\n", + " def on_any_event(self, event):\n", + " try:\n", + " event.dest_path = event.src_path.lstrip('.').lstrip(os.sep).rstrip('.ipynb').rstrip('.py').replace(os.sep, '.')\n", + " super().on_any_event(event)\n", + " except AttributeError: ..." ] }, { @@ -52,13 +52,13 @@ "metadata": {}, "outputs": [], "source": [ - "if __name__ == '__main__':\n", - " try:\n", - " from .compiler_python import ScriptExporter\n", - " except:\n", - " from importnb.compiler_python import ScriptExporter\n", - " from pathlib import Path\n", - " Path('../../importnb/utils/watch.py').write_text(ScriptExporter().from_filename('watch.ipynb')[0])" + " if __name__ == '__main__':\n", + " try:\n", + " from .compiler_python import ScriptExporter\n", + " except:\n", + " from importnb.compiler_python import ScriptExporter\n", + " from pathlib import Path\n", + " Path('../../importnb/utils/watch.py').write_text(ScriptExporter().from_filename('watch.ipynb')[0])" ] }, {