Coverage for /home/tbone/.local/share/hatch/env/virtual/importnb-aVRh-lqt/test.stdlib/lib/python3.9/site-packages/importnb/loader.py: 91%
207 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-03 09:31 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-03 09:31 -0700
1# coding: utf-8
2"""# `loader`
4Combine the __import__ finder with the loader.
5"""
8import ast
9from dataclasses import asdict, dataclass, field
10import sys
11import re
12import textwrap
13from types import ModuleType
14from functools import partial
15from importlib import reload
16from importlib.machinery import ModuleSpec, SourceFileLoader
18from pathlib import Path
20from . import is_ipython, get_ipython
21from .decoder import LineCacheNotebookDecoder, quote
22from .docstrings import update_docstring
23from .finder import FuzzyFinder, get_loader_details, get_loader_index
25from importlib._bootstrap import _requires_builtin
26from importlib._bootstrap_external import decode_source, FileFinder
27from importlib._bootstrap import _init_module_attrs
28from importlib.util import LazyLoader, find_spec
31_GTE38 = sys.version_info.major == 3 and sys.version_info.minor >= 8
33if is_ipython():
34 import IPython
35 from IPython.core.inputsplitter import IPythonInputSplitter
37 dedent = IPythonInputSplitter(
38 line_input_checker=False,
39 physical_line_transforms=[
40 IPython.core.inputsplitter.leading_indent(),
41 IPython.core.inputsplitter.ipy_prompt(),
42 IPython.core.inputsplitter.cellmagic(end_on_blank_line=False),
43 ],
44 ).transform_cell
45else:
47 def dedent(body):
48 from textwrap import dedent, indent
50 if MAGIC.match(body):
51 return indent(body, "# ")
52 return dedent(body)
55__all__ = "Notebook", "reload"
58MAGIC = re.compile("^\s*%{2}", re.MULTILINE)
61@dataclass
62class Interface:
63 name: str = None
64 path: str = None
65 lazy: bool = False
66 include_fuzzy_finder: bool = True
68 include_markdown_docstring: bool = True
69 only_defs: bool = False
70 no_magic: bool = False
71 _loader_hook_position: int = field(default=0, repr=False)
73 def __new__(cls, name=None, path=None, **kwargs):
74 kwargs.update(name=name, path=path)
75 self = super().__new__(cls)
76 self.__init__(**kwargs)
77 return self
80class BaseLoader(Interface, SourceFileLoader):
81 """The simplest implementation of a Notebook Source File Loader."""
83 @property
84 def loader(self):
85 """Create a lazy loader source file loader."""
86 loader = type(self)
87 if self.lazy and (sys.version_info.major, sys.version_info.minor) != (3, 4):
88 loader = LazyLoader.factory(loader)
89 # Strip the leading underscore from slots
90 params = asdict(self)
91 params.pop("name")
92 params.pop("path")
93 return partial(loader, **params)
95 @property
96 def finder(self):
97 """Permit fuzzy finding of files with special characters."""
98 return self.include_fuzzy_finder and FuzzyFinder or FileFinder
100 def translate(self, source):
101 if self.path and self.path.endswith(".ipynb"):
102 return LineCacheNotebookDecoder(
103 code=self.code, raw=self.raw, markdown=self.markdown
104 ).decode(source, self.path)
105 return self.code(source)
107 def get_data(self, path):
108 """Needs to return the string source for the module."""
109 return self.translate(self.decode())
111 def create_module(self, spec):
112 module = ModuleType(str(spec.name))
113 _init_module_attrs(spec, module)
114 if self.name:
115 module.__name__ = self.name
116 if getattr(spec, "alias", None):
117 # put a fuzzy spec on the modules to avoid re importing it.
118 # there is a funky trick you do with the fuzzy finder where you
119 # load multiple versions with different finders.
121 sys.modules[spec.alias] = module
122 module.get_ipython = get_ipython
123 return module
125 def decode(self):
126 return decode_source(super().get_data(self.path))
128 def code(self, str):
129 return dedent(str)
131 def markdown(self, str):
132 return quote(str)
134 def raw(self, str):
135 return comment(str)
137 def visit(self, node):
138 return node
140 @classmethod
141 @_requires_builtin
142 def is_package(cls, fullname):
143 """Return False as built-in modules are never packages."""
144 if "." not in fullname:
145 return True
146 return super().is_package(fullname)
148 get_source = get_data
150 def __enter__(self):
151 path_id, loader_id, details = get_loader_index(".py")
152 for _, e in details:
153 if all(map(e.__contains__, self.extensions)):
154 self._loader_hook_position = None
155 return self
156 else:
157 self._loader_hook_position = loader_id + 1
158 details.insert(self._loader_hook_position, (self.loader, self.extensions))
159 sys.path_hooks[path_id] = self.finder.path_hook(*details)
160 sys.path_importer_cache.clear()
161 return self
163 def __exit__(self, *excepts):
164 if self._loader_hook_position is not None:
165 path_id, details = get_loader_details()
166 details.pop(self._loader_hook_position)
167 sys.path_hooks[path_id] = self.finder.path_hook(*details)
168 sys.path_importer_cache.clear()
171class FileModuleSpec(ModuleSpec):
172 def __init__(self, *args, **kwargs):
173 super().__init__(*args, **kwargs)
174 self._set_fileattr = True
177def comment(str):
178 return textwrap.indent(str, "# ")
181class DefsOnly(ast.NodeTransformer):
182 INCLUDE = ast.Import, ast.ImportFrom, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef
184 def visit_Module(self, node):
185 args = ([x for x in node.body if isinstance(x, self.INCLUDE)],)
186 if _GTE38:
187 args += (node.type_ignores,)
188 return ast.Module(*args)
191class Notebook(BaseLoader):
192 """Notebook is a user friendly file finder and module loader for notebook source code.
194 > Remember, restart and run all or it didn't happen.
196 Notebook provides several useful options.
198 * Lazy module loading. A module is executed the first time it is used in a script.
199 """
201 extensions = (".ipy", ".ipynb")
203 def parse(self, nodes):
204 return ast.parse(nodes, self.path)
206 def visit(self, nodes):
207 if self.only_defs:
208 nodes = DefsOnly().visit(nodes)
209 return nodes
211 def code(self, str):
212 if self.no_magic:
213 if MAGIC.match(str):
214 return comment(str)
215 return super().code(str)
217 def source_to_code(self, nodes, path, *, _optimize=-1):
218 """* Convert the current source to ast
219 * Apply ast transformers.
220 * Compile the code."""
221 if not isinstance(nodes, ast.Module):
222 nodes = self.parse(nodes)
223 if self.include_markdown_docstring:
224 nodes = update_docstring(nodes)
225 return super().source_to_code(
226 ast.fix_missing_locations(self.visit(nodes)), path, _optimize=_optimize
227 )
229 @classmethod
230 def load_file(cls, filename, main=True, **kwargs):
231 """Import a notebook as a module from a filename.
233 dir: The directory to load the file from.
234 main: Load the module in the __main__ context.
236 > assert Notebook.load('loader.ipynb')
237 """
238 name = main and "__main__" or filename
239 loader = cls(name, str(filename), **kwargs)
240 spec = FileModuleSpec(name, loader, origin=loader.path)
241 module = loader.create_module(spec)
242 loader.exec_module(module)
243 return module
245 @classmethod
246 def load_module(cls, module, main=False, **kwargs):
247 """Import a notebook as a module.
249 dir: The directory to load the file from.
250 main: Load the module in the __main__ context.
252 > assert Notebook.load('loader.ipynb')
253 """
254 from runpy import _run_module_as_main, run_module
256 with cls() as loader:
257 if main:
258 return _dict_module(_run_module_as_main(module))
259 else:
260 spec = find_spec(module)
262 m = spec.loader.create_module(spec)
263 spec.loader.exec_module(m)
264 return m
266 @classmethod
267 def load_argv(cls, argv=None, *, parser=None):
268 import sys
269 from sys import path
271 if parser is None:
272 parser = cls.get_argparser()
274 if argv is None:
275 from sys import argv
277 argv = argv[1:]
279 if isinstance(argv, str):
280 from shlex import split
282 argv = split(argv)
284 ns, unknown = parser.parse_known_args(argv)
286 if ns.code:
287 return cls.load_code(" ".join(ns.args))
289 n = ns.args and ns.args[0] or sys.argv[0]
291 sys.argv = [n] + unknown
292 if ns.module:
293 path.insert(0, ns.dir) if ns.dir else ... if "" in path else path.insert(0, "")
294 return cls.load_module(n, main=True)
295 elif ns.args:
296 L = len(ns.args)
297 if L > 1:
298 raise ValueError(f"Expected one file to execute, but received {L}.")
300 if ns.dir:
301 n = str(Path(ns.dir) / n)
302 return cls.load_file(n)
303 else:
304 parser.print_help()
306 @classmethod
307 def load_code(cls, code, mod_name=None, script_name=None, main=False):
308 from runpy import _run_module_code
310 self = cls()
311 name = main and "__main__" or mod_name or "<markdown code>"
313 return _dict_module(
314 _run_module_code(self.translate(code), mod_name=name, script_name=script_name)
315 )
317 @staticmethod
318 def get_argparser(parser=None):
319 from argparse import ArgumentParser, REMAINDER
321 if parser is None:
322 parser = ArgumentParser("importnb", description="run notebooks as python code")
323 parser.add_argument(
324 "args", help="the file [default], module or code to execute", nargs=REMAINDER
325 )
326 parser.add_argument("-f", "--file", action="store_false", help="load a file")
327 parser.add_argument("-m", "--module", action="store_true", help="run args as a module")
328 parser.add_argument("-c", "--code", action="store_true", help="run args as code")
329 parser.add_argument("-d", "--dir", help="the directory path to run in.")
330 return parser
333def _dict_module(ns):
334 m = ModuleType(ns.get("__name__"), ns.get("__doc__"))
335 m.__dict__.update(ns)
336 return m