Coverage for /home/tbone/.local/share/hatch/env/virtual/importnb-aVRh-lqt/test-legacy.interactive/lib/python3.9/site-packages/importnb/loader.py: 92%

1# coding: utf-8

2"""# `loader`

3

4 Combine the __import__ finder with the loader.

5"""

6

7

8 import ast

9 from dataclasses import asdict, dataclass, field

10 import sys

11 import re

12 import textwrap

13 from types import ModuleType

14 from functools import partial

15 from importlib import reload

16 from importlib.machinery import ModuleSpec, SourceFileLoader

17

18 from pathlib import Path

19

20 from . import is_ipython, get_ipython

21 from .decoder import LineCacheNotebookDecoder, quote

22 from .docstrings import update_docstring

23 from .finder import FuzzyFinder, get_loader_details, get_loader_index

24

25 from importlib._bootstrap import _requires_builtin

26 from importlib._bootstrap_external import decode_source, FileFinder

27 from importlib._bootstrap import _init_module_attrs

28 from importlib.util import LazyLoader, find_spec

29

30

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

32

33 if 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

45 else:

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

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

59

60

61@dataclass

62 class 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

80 class 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

171 class FileModuleSpec(ModuleSpec):

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

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

174 self._set_fileattr = True

175

176

177 def comment(str):

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

179

180

181 class 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

191 class 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

333 def _dict_module(ns):

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

335 m.__dict__.update(ns)

336 return m