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

1# coding: utf-8 

2"""# `loader` 

3 

4Combine the __import__ finder with the loader. 

5""" 

6 

7 

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 

17 

18from pathlib import Path 

19 

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 

24 

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 

29 

30 

31_GTE38 = sys.version_info.major == 3 and sys.version_info.minor >= 8 

32 

33if is_ipython(): 

34 import IPython 

35 from IPython.core.inputsplitter import IPythonInputSplitter 

36 

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: 

46 

47 def dedent(body): 

48 from textwrap import dedent, indent 

49 

50 if MAGIC.match(body): 

51 return indent(body, "# ") 

52 return dedent(body) 

53 

54 

55__all__ = "Notebook", "reload" 

56 

57 

58MAGIC = re.compile("^\s*%{2}", re.MULTILINE) 

59 

60 

61@dataclass 

62class Interface: 

63 name: str = None 

64 path: str = None 

65 lazy: bool = False 

66 include_fuzzy_finder: bool = True 

67 

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) 

72 

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 

78 

79 

80class BaseLoader(Interface, SourceFileLoader): 

81 """The simplest implementation of a Notebook Source File Loader.""" 

82 

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) 

94 

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 

99 

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) 

106 

107 def get_data(self, path): 

108 """Needs to return the string source for the module.""" 

109 return self.translate(self.decode()) 

110 

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. 

120 

121 sys.modules[spec.alias] = module 

122 module.get_ipython = get_ipython 

123 return module 

124 

125 def decode(self): 

126 return decode_source(super().get_data(self.path)) 

127 

128 def code(self, str): 

129 return dedent(str) 

130 

131 def markdown(self, str): 

132 return quote(str) 

133 

134 def raw(self, str): 

135 return comment(str) 

136 

137 def visit(self, node): 

138 return node 

139 

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) 

147 

148 get_source = get_data 

149 

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 

162 

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() 

169 

170 

171class FileModuleSpec(ModuleSpec): 

172 def __init__(self, *args, **kwargs): 

173 super().__init__(*args, **kwargs) 

174 self._set_fileattr = True 

175 

176 

177def comment(str): 

178 return textwrap.indent(str, "# ") 

179 

180 

181class DefsOnly(ast.NodeTransformer): 

182 INCLUDE = ast.Import, ast.ImportFrom, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef 

183 

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) 

189 

190 

191class Notebook(BaseLoader): 

192 """Notebook is a user friendly file finder and module loader for notebook source code. 

193 

194 > Remember, restart and run all or it didn't happen. 

195 

196 Notebook provides several useful options. 

197 

198 * Lazy module loading. A module is executed the first time it is used in a script. 

199 """ 

200 

201 extensions = (".ipy", ".ipynb") 

202 

203 def parse(self, nodes): 

204 return ast.parse(nodes, self.path) 

205 

206 def visit(self, nodes): 

207 if self.only_defs: 

208 nodes = DefsOnly().visit(nodes) 

209 return nodes 

210 

211 def code(self, str): 

212 if self.no_magic: 

213 if MAGIC.match(str): 

214 return comment(str) 

215 return super().code(str) 

216 

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 ) 

228 

229 @classmethod 

230 def load_file(cls, filename, main=True, **kwargs): 

231 """Import a notebook as a module from a filename. 

232 

233 dir: The directory to load the file from. 

234 main: Load the module in the __main__ context. 

235 

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 

244 

245 @classmethod 

246 def load_module(cls, module, main=False, **kwargs): 

247 """Import a notebook as a module. 

248 

249 dir: The directory to load the file from. 

250 main: Load the module in the __main__ context. 

251 

252 > assert Notebook.load('loader.ipynb') 

253 """ 

254 from runpy import _run_module_as_main, run_module 

255 

256 with cls() as loader: 

257 if main: 

258 return _dict_module(_run_module_as_main(module)) 

259 else: 

260 spec = find_spec(module) 

261 

262 m = spec.loader.create_module(spec) 

263 spec.loader.exec_module(m) 

264 return m 

265 

266 @classmethod 

267 def load_argv(cls, argv=None, *, parser=None): 

268 import sys 

269 from sys import path 

270 

271 if parser is None: 

272 parser = cls.get_argparser() 

273 

274 if argv is None: 

275 from sys import argv 

276 

277 argv = argv[1:] 

278 

279 if isinstance(argv, str): 

280 from shlex import split 

281 

282 argv = split(argv) 

283 

284 ns, unknown = parser.parse_known_args(argv) 

285 

286 if ns.code: 

287 return cls.load_code(" ".join(ns.args)) 

288 

289 n = ns.args and ns.args[0] or sys.argv[0] 

290 

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}.") 

299 

300 if ns.dir: 

301 n = str(Path(ns.dir) / n) 

302 return cls.load_file(n) 

303 else: 

304 parser.print_help() 

305 

306 @classmethod 

307 def load_code(cls, code, mod_name=None, script_name=None, main=False): 

308 from runpy import _run_module_code 

309 

310 self = cls() 

311 name = main and "__main__" or mod_name or "<markdown code>" 

312 

313 return _dict_module( 

314 _run_module_code(self.translate(code), mod_name=name, script_name=script_name) 

315 ) 

316 

317 @staticmethod 

318 def get_argparser(parser=None): 

319 from argparse import ArgumentParser, REMAINDER 

320 

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 

331 

332 

333def _dict_module(ns): 

334 m = ModuleType(ns.get("__name__"), ns.get("__doc__")) 

335 m.__dict__.update(ns) 

336 return m