Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nbconvert/exporters/templateexporter.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

332 statements  

1"""This module defines TemplateExporter, a highly configurable converter 

2that uses Jinja2 to export notebook files into different formats. 

3""" 

4 

5# Copyright (c) IPython Development Team. 

6# Distributed under the terms of the Modified BSD License. 

7from __future__ import annotations 

8 

9import html 

10import json 

11import os 

12import typing as t 

13import uuid 

14import warnings 

15from pathlib import Path 

16 

17from jinja2 import ( 

18 BaseLoader, 

19 ChoiceLoader, 

20 DictLoader, 

21 Environment, 

22 FileSystemLoader, 

23 TemplateNotFound, 

24) 

25from jupyter_core.paths import jupyter_path 

26from nbformat import NotebookNode 

27from traitlets import Bool, Dict, HasTraits, List, Unicode, default, observe, validate 

28from traitlets.config import Config 

29from traitlets.utils.importstring import import_item 

30 

31from nbconvert import filters 

32 

33from .exporter import Exporter 

34 

35# Jinja2 extensions to load. 

36JINJA_EXTENSIONS = ["jinja2.ext.loopcontrols"] 

37 

38ROOT = os.path.dirname(__file__) 

39DEV_MODE = os.path.exists(os.path.join(ROOT, "../../.git")) 

40 

41 

42default_filters = { 

43 "indent": filters.indent, 

44 "markdown2html": filters.markdown2html, 

45 "markdown2asciidoc": filters.markdown2asciidoc, 

46 "ansi2html": filters.ansi2html, 

47 "filter_data_type": filters.DataTypeFilter, 

48 "get_lines": filters.get_lines, 

49 "highlight2html": filters.Highlight2HTML, 

50 "highlight2latex": filters.Highlight2Latex, 

51 "ipython2python": filters.ipython2python, 

52 "posix_path": filters.posix_path, 

53 "markdown2latex": filters.markdown2latex, 

54 "markdown2rst": filters.markdown2rst, 

55 "comment_lines": filters.comment_lines, 

56 "strip_ansi": filters.strip_ansi, 

57 "strip_dollars": filters.strip_dollars, 

58 "strip_files_prefix": filters.strip_files_prefix, 

59 "html2text": filters.html2text, 

60 "add_anchor": filters.add_anchor, 

61 "ansi2latex": filters.ansi2latex, 

62 "wrap_text": filters.wrap_text, 

63 "escape_latex": filters.escape_latex, 

64 "citation2latex": filters.citation2latex, 

65 "path2url": filters.path2url, 

66 "add_prompts": filters.add_prompts, 

67 "ascii_only": filters.ascii_only, 

68 "prevent_list_blocks": filters.prevent_list_blocks, 

69 "get_metadata": filters.get_metadata, 

70 "convert_pandoc": filters.convert_pandoc, 

71 "json_dumps": json.dumps, 

72 # For removing any HTML 

73 "escape_html": lambda s: html.escape(str(s)), 

74 "escape_html_keep_quotes": lambda s: html.escape(str(s), quote=False), 

75 "escape_html_script": lambda s: s.replace("/", "\\/"), 

76 # For sanitizing HTML for any XSS 

77 "clean_html": filters.clean_html, 

78 "strip_trailing_newline": filters.strip_trailing_newline, 

79 "text_base64": filters.text_base64, 

80} 

81 

82 

83# copy of https://github.com/jupyter/jupyter_server/blob/b62458a7f5ad6b5246d2f142258dedaa409de5d9/jupyter_server/config_manager.py#L19 

84def recursive_update(target, new): 

85 """Recursively update one dictionary using another. 

86 None values will delete their keys. 

87 """ 

88 for k, v in new.items(): 

89 if isinstance(v, dict): 

90 if k not in target: 

91 target[k] = {} 

92 recursive_update(target[k], v) 

93 if not target[k]: 

94 # Prune empty subdicts 

95 del target[k] 

96 

97 elif v is None: 

98 target.pop(k, None) 

99 

100 else: 

101 target[k] = v 

102 return target # return for convenience 

103 

104 

105# define function at the top level to avoid pickle errors 

106def deprecated(msg): 

107 """Emit a deprecation warning.""" 

108 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

109 

110 

111class ExtensionTolerantLoader(BaseLoader): 

112 """A template loader which optionally adds a given extension when searching. 

113 

114 Constructor takes two arguments: *loader* is another Jinja loader instance 

115 to wrap. *extension* is the extension, which will be added to the template 

116 name if finding the template without it fails. This should include the dot, 

117 e.g. '.tpl'. 

118 """ 

119 

120 def __init__(self, loader, extension): 

121 """Initialize the loader.""" 

122 self.loader = loader 

123 self.extension = extension 

124 

125 def get_source(self, environment, template): 

126 """Get the source for a template.""" 

127 try: 

128 return self.loader.get_source(environment, template) 

129 except TemplateNotFound: 

130 if template.endswith(self.extension): 

131 raise TemplateNotFound(template) from None 

132 return self.loader.get_source(environment, template + self.extension) 

133 

134 def list_templates(self): 

135 """List available templates.""" 

136 return self.loader.list_templates() 

137 

138 

139class TemplateExporter(Exporter): 

140 """ 

141 Exports notebooks into other file formats. Uses Jinja 2 templating engine 

142 to output new formats. Inherit from this class if you are creating a new 

143 template type along with new filters/preprocessors. If the filters/ 

144 preprocessors provided by default suffice, there is no need to inherit from 

145 this class. Instead, override the template_file and file_extension 

146 traits via a config file. 

147 

148 Filters available by default for templates: 

149 

150 {filters} 

151 """ 

152 

153 # finish the docstring 

154 __doc__ = ( 

155 __doc__.format(filters="- " + "\n - ".join(sorted(default_filters.keys()))) 

156 if __doc__ 

157 else None 

158 ) 

159 

160 _template_cached = None 

161 

162 def _invalidate_template_cache(self, change=None): 

163 self._template_cached = None 

164 

165 @property 

166 def template(self): 

167 if self._template_cached is None: 

168 self._template_cached = self._load_template() 

169 return self._template_cached 

170 

171 _environment_cached = None 

172 

173 def _invalidate_environment_cache(self, change=None): 

174 self._environment_cached = None 

175 self._invalidate_template_cache() 

176 

177 @property 

178 def environment(self): 

179 if self._environment_cached is None: 

180 self._environment_cached = self._create_environment() 

181 return self._environment_cached 

182 

183 @property 

184 def default_config(self): 

185 c = Config( 

186 { 

187 "RegexRemovePreprocessor": {"enabled": True}, 

188 "TagRemovePreprocessor": {"enabled": True}, 

189 } 

190 ) 

191 if super().default_config: 

192 c2 = super().default_config.copy() 

193 c2.merge(c) 

194 c = c2 

195 return c 

196 

197 template_name = Unicode(help="Name of the template to use").tag( 

198 config=True, affects_template=True 

199 ) 

200 

201 template_file = Unicode(None, allow_none=True, help="Name of the template file to use").tag( 

202 config=True, affects_template=True 

203 ) 

204 

205 raw_template = Unicode("", help="raw template string").tag(affects_environment=True) 

206 

207 enable_async = Bool(False, help="Enable Jinja async template execution").tag( 

208 affects_environment=True 

209 ) 

210 

211 _last_template_file = "" 

212 _raw_template_key = "<memory>" 

213 

214 @validate("template_name") 

215 def _template_name_validate(self, change): 

216 template_name = change["value"] 

217 if template_name and template_name.endswith(".tpl"): 

218 warnings.warn( 

219 f"5.x style template name passed '{self.template_name}'. Use --template-name for the template directory with a index.<ext>.j2 file and/or --template-file to denote a different template.", 

220 DeprecationWarning, 

221 stacklevel=2, 

222 ) 

223 directory, self.template_file = os.path.split(self.template_name) 

224 if directory: 

225 directory, template_name = os.path.split(directory) 

226 if directory and os.path.isabs(directory): 

227 self.extra_template_basedirs = [directory] 

228 return template_name 

229 

230 @observe("template_file") 

231 def _template_file_changed(self, change): 

232 new = change["new"] 

233 if new == "default": 

234 self.template_file = self.default_template # type:ignore[attr-defined] 

235 return 

236 # check if template_file is a file path 

237 # rather than a name already on template_path 

238 full_path = os.path.abspath(new) 

239 if os.path.isfile(full_path): 

240 directory, self.template_file = os.path.split(full_path) 

241 self.extra_template_paths = [directory, *self.extra_template_paths] 

242 # While not strictly an invalid template file name, the extension hints that there isn't a template directory involved 

243 if self.template_file and self.template_file.endswith(".tpl"): 

244 warnings.warn( 

245 f"5.x style template file passed '{new}'. Use --template-name for the template directory with a index.<ext>.j2 file and/or --template-file to denote a different template.", 

246 DeprecationWarning, 

247 stacklevel=2, 

248 ) 

249 

250 @default("template_file") 

251 def _template_file_default(self): 

252 if self.template_extension: 

253 return "index" + self.template_extension 

254 return None 

255 

256 @observe("raw_template") 

257 def _raw_template_changed(self, change): 

258 if not change["new"]: 

259 self.template_file = self._last_template_file 

260 self._invalidate_template_cache() 

261 

262 template_paths = List(["."]).tag(config=True, affects_environment=True) 

263 extra_template_basedirs = List(Unicode()).tag(config=True, affects_environment=True) 

264 extra_template_paths = List(Unicode()).tag(config=True, affects_environment=True) 

265 

266 @default("extra_template_basedirs") 

267 def _default_extra_template_basedirs(self): 

268 return [os.getcwd()] 

269 

270 # Extension that the template files use. 

271 template_extension = Unicode().tag(config=True, affects_environment=True) 

272 

273 template_data_paths = List( 

274 jupyter_path("nbconvert", "templates"), help="Path where templates can be installed too." 

275 ).tag(affects_environment=True) 

276 

277 @default("template_extension") 

278 def _template_extension_default(self): 

279 if self.file_extension: 

280 return self.file_extension + ".j2" 

281 return self.file_extension 

282 

283 exclude_input = Bool( 

284 False, help="This allows you to exclude code cell inputs from all templates if set to True." 

285 ).tag(config=True) 

286 

287 exclude_input_prompt = Bool( 

288 False, help="This allows you to exclude input prompts from all templates if set to True." 

289 ).tag(config=True) 

290 

291 exclude_output = Bool( 

292 False, 

293 help="This allows you to exclude code cell outputs from all templates if set to True.", 

294 ).tag(config=True) 

295 

296 exclude_output_prompt = Bool( 

297 False, help="This allows you to exclude output prompts from all templates if set to True." 

298 ).tag(config=True) 

299 

300 exclude_output_stdin = Bool( 

301 True, 

302 help="This allows you to exclude output of stdin stream from lab template if set to True.", 

303 ).tag(config=True) 

304 

305 exclude_code_cell = Bool( 

306 False, help="This allows you to exclude code cells from all templates if set to True." 

307 ).tag(config=True) 

308 

309 exclude_markdown = Bool( 

310 False, help="This allows you to exclude markdown cells from all templates if set to True." 

311 ).tag(config=True) 

312 

313 exclude_raw = Bool( 

314 False, help="This allows you to exclude raw cells from all templates if set to True." 

315 ).tag(config=True) 

316 

317 exclude_unknown = Bool( 

318 False, help="This allows you to exclude unknown cells from all templates if set to True." 

319 ).tag(config=True) 

320 

321 extra_loaders: List[t.Any] = List( 

322 help="Jinja loaders to find templates. Will be tried in order " 

323 "before the default FileSystem ones.", 

324 ).tag(affects_environment=True) 

325 

326 filters = Dict( 

327 help="""Dictionary of filters, by name and namespace, to add to the Jinja 

328 environment.""" 

329 ).tag(config=True, affects_environment=True) 

330 

331 raw_mimetypes = List( 

332 Unicode(), help="""formats of raw cells to be included in this Exporter's output.""" 

333 ).tag(config=True) 

334 

335 @default("raw_mimetypes") 

336 def _raw_mimetypes_default(self): 

337 return [self.output_mimetype, ""] 

338 

339 # TODO: passing config is wrong, but changing this revealed more complicated issues 

340 def __init__(self, config=None, **kw): 

341 """ 

342 Public constructor 

343 

344 Parameters 

345 ---------- 

346 config : config 

347 User configuration instance. 

348 extra_loaders : list[of Jinja Loaders] 

349 ordered list of Jinja loader to find templates. Will be tried in order 

350 before the default FileSystem ones. 

351 template_file : str (optional, kw arg) 

352 Template to use when exporting. 

353 """ 

354 super().__init__(config=config, **kw) 

355 

356 self.observe( 

357 self._invalidate_environment_cache, list(self.traits(affects_environment=True)) 

358 ) 

359 self.observe(self._invalidate_template_cache, list(self.traits(affects_template=True))) 

360 

361 def _load_template(self): 

362 """Load the Jinja template object from the template file 

363 

364 This is triggered by various trait changes that would change the template. 

365 """ 

366 

367 # this gives precedence to a raw_template if present 

368 with self.hold_trait_notifications(): 

369 if self.template_file and (self.template_file != self._raw_template_key): 

370 self._last_template_file = self.template_file 

371 if self.raw_template: 

372 self.template_file = self._raw_template_key 

373 

374 if not self.template_file: 

375 msg = "No template_file specified!" 

376 raise ValueError(msg) 

377 

378 # First try to load the 

379 # template by name with extension added, then try loading the template 

380 # as if the name is explicitly specified. 

381 template_file = self.template_file 

382 self.log.debug("Attempting to load template %s", template_file) 

383 self.log.debug(" template_paths: %s", os.pathsep.join(self.template_paths)) 

384 return self.environment.get_template(template_file) 

385 

386 def from_filename( # type:ignore[override] 

387 self, filename: str, resources: dict[str, t.Any] | None = None, **kw: t.Any 

388 ) -> tuple[str, dict[str, t.Any]]: 

389 """Convert a notebook from a filename.""" 

390 return super().from_filename(filename, resources, **kw) # type:ignore[return-value] 

391 

392 def from_file( # type:ignore[override] 

393 self, file_stream: t.Any, resources: dict[str, t.Any] | None = None, **kw: t.Any 

394 ) -> tuple[str, dict[str, t.Any]]: 

395 """Convert a notebook from a file.""" 

396 return super().from_file(file_stream, resources, **kw) # type:ignore[return-value] 

397 

398 def from_notebook_node( # type:ignore[override] 

399 self, nb: NotebookNode, resources: dict[str, t.Any] | None = None, **kw: t.Any 

400 ) -> tuple[str, dict[str, t.Any]]: 

401 """ 

402 Convert a notebook from a notebook node instance. 

403 

404 Parameters 

405 ---------- 

406 nb : :class:`~nbformat.NotebookNode` 

407 Notebook node 

408 resources : dict 

409 Additional resources that can be accessed read/write by 

410 preprocessors and filters. 

411 """ 

412 nb_copy, resources = super().from_notebook_node(nb, resources, **kw) 

413 resources.setdefault("raw_mimetypes", self.raw_mimetypes) 

414 resources.setdefault("output_mimetype", self.output_mimetype) 

415 resources["global_content_filter"] = { 

416 "include_code": not self.exclude_code_cell, 

417 "include_markdown": not self.exclude_markdown, 

418 "include_raw": not self.exclude_raw, 

419 "include_unknown": not self.exclude_unknown, 

420 "include_input": not self.exclude_input, 

421 "include_output": not self.exclude_output, 

422 "include_output_stdin": not self.exclude_output_stdin, 

423 "include_input_prompt": not self.exclude_input_prompt, 

424 "include_output_prompt": not self.exclude_output_prompt, 

425 "no_prompt": self.exclude_input_prompt and self.exclude_output_prompt, 

426 } 

427 

428 # Top level variables are passed to the template_exporter here. 

429 output = self.template.render(nb=nb_copy, resources=resources) 

430 output = output.lstrip("\r\n") 

431 return output, resources 

432 

433 def _register_filter(self, environ, name, jinja_filter): 

434 """ 

435 Register a filter. 

436 A filter is a function that accepts and acts on one string. 

437 The filters are accessible within the Jinja templating engine. 

438 

439 Parameters 

440 ---------- 

441 name : str 

442 name to give the filter in the Jinja engine 

443 filter : filter 

444 """ 

445 if jinja_filter is None: 

446 msg = "filter" 

447 raise TypeError(msg) 

448 isclass = isinstance(jinja_filter, type) 

449 constructed = not isclass 

450 

451 # Handle filter's registration based on it's type 

452 if constructed and isinstance(jinja_filter, (str,)): 

453 # filter is a string, import the namespace and recursively call 

454 # this register_filter method 

455 filter_cls = import_item(jinja_filter) 

456 return self._register_filter(environ, name, filter_cls) 

457 

458 if constructed and callable(jinja_filter): 

459 # filter is a function, no need to construct it. 

460 environ.filters[name] = jinja_filter 

461 return jinja_filter 

462 

463 if isclass and issubclass(jinja_filter, HasTraits): 

464 # filter is configurable. Make sure to pass in new default for 

465 # the enabled flag if one was specified. 

466 filter_instance = jinja_filter(parent=self) 

467 self._register_filter(environ, name, filter_instance) 

468 return None 

469 

470 if isclass: 

471 # filter is not configurable, construct it 

472 filter_instance = jinja_filter() 

473 self._register_filter(environ, name, filter_instance) 

474 return None 

475 

476 # filter is an instance of something without a __call__ 

477 # attribute. 

478 msg = "filter" 

479 raise TypeError(msg) 

480 

481 def register_filter(self, name, jinja_filter): 

482 """ 

483 Register a filter. 

484 A filter is a function that accepts and acts on one string. 

485 The filters are accessible within the Jinja templating engine. 

486 

487 Parameters 

488 ---------- 

489 name : str 

490 name to give the filter in the Jinja engine 

491 filter : filter 

492 """ 

493 return self._register_filter(self.environment, name, jinja_filter) 

494 

495 def default_filters(self): 

496 """Override in subclasses to provide extra filters. 

497 

498 This should return an iterable of 2-tuples: (name, class-or-function). 

499 You should call the method on the parent class and include the filters 

500 it provides. 

501 

502 If a name is repeated, the last filter provided wins. Filters from 

503 user-supplied config win over filters provided by classes. 

504 """ 

505 return default_filters.items() 

506 

507 def _create_environment(self): 

508 """ 

509 Create the Jinja templating environment. 

510 """ 

511 paths = self.template_paths 

512 self.log.debug("Template paths:\n\t%s", "\n\t".join(paths)) 

513 

514 loaders = [ 

515 *self.extra_loaders, 

516 ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), 

517 DictLoader({self._raw_template_key: self.raw_template}), 

518 ] 

519 environment = Environment( # noqa: S701 

520 loader=ChoiceLoader(loaders), 

521 extensions=JINJA_EXTENSIONS, 

522 enable_async=self.enable_async, 

523 ) 

524 

525 environment.globals["uuid4"] = uuid.uuid4 

526 

527 # Add default filters to the Jinja2 environment 

528 for key, value in self.default_filters(): 

529 self._register_filter(environment, key, value) 

530 

531 # Load user filters. Overwrite existing filters if need be. 

532 if self.filters: 

533 for key, user_filter in self.filters.items(): 

534 self._register_filter(environment, key, user_filter) 

535 

536 return environment 

537 

538 def _init_preprocessors(self): 

539 super()._init_preprocessors() 

540 conf = self._get_conf() 

541 preprocessors = conf.get("preprocessors", {}) 

542 # preprocessors is a dict for three reasons 

543 # * We rely on recursive_update, which can only merge dicts, lists will be overwritten 

544 # * We can use the key with numerical prefixing to guarantee ordering (/etc/*.d/XY-file style) 

545 # * We can disable preprocessors by overwriting the value with None 

546 for _, preprocessor in sorted(preprocessors.items(), key=lambda x: x[0]): 

547 if preprocessor is not None: 

548 kwargs = preprocessor.copy() 

549 preprocessor_cls = kwargs.pop("type") 

550 preprocessor_cls = import_item(preprocessor_cls) 

551 if preprocessor_cls.__name__ in self.config: 

552 kwargs.update(self.config[preprocessor_cls.__name__]) 

553 preprocessor = preprocessor_cls(**kwargs) # noqa: PLW2901 

554 self.register_preprocessor(preprocessor) 

555 

556 def _get_conf(self): 

557 conf: dict[str, t.Any] = {} # the configuration once all conf files are merged 

558 for path in map(Path, self.template_paths): 

559 conf_path = path / "conf.json" 

560 try: 

561 conf_path_exists = conf_path.exists() 

562 except PermissionError: 

563 # for Python <3.14 

564 pass 

565 else: 

566 if conf_path_exists: 

567 with conf_path.open() as f: 

568 conf = recursive_update(conf, json.load(f)) 

569 return conf 

570 

571 @default("template_paths") 

572 def _template_paths(self, prune=True, root_dirs=None): 

573 paths = [] 

574 root_dirs = self.get_prefix_root_dirs() 

575 template_names = self.get_template_names() 

576 for template_name in template_names: 

577 for base_dir in self.extra_template_basedirs: 

578 path = os.path.join(base_dir, template_name) 

579 try: 

580 if not prune or os.path.exists(path): 

581 paths.append(path) 

582 except PermissionError: 

583 pass 

584 for root_dir in root_dirs: 

585 base_dir = os.path.join(root_dir, "nbconvert", "templates") 

586 path = os.path.join(base_dir, template_name) 

587 try: 

588 if not prune or os.path.exists(path): 

589 paths.append(path) 

590 except PermissionError: 

591 pass 

592 

593 for root_dir in root_dirs: 

594 # we include root_dir for when we want to be very explicit, e.g. 

595 # {% extends 'nbconvert/templates/classic/base.html' %} 

596 paths.append(root_dir) 

597 # we include base_dir for when we want to be explicit, but less than root_dir, e.g. 

598 # {% extends 'classic/base.html' %} 

599 base_dir = os.path.join(root_dir, "nbconvert", "templates") 

600 paths.append(base_dir) 

601 

602 compatibility_dir = os.path.join(root_dir, "nbconvert", "templates", "compatibility") 

603 paths.append(compatibility_dir) 

604 

605 additional_paths = [] 

606 for path in self.template_data_paths: 

607 if not prune or os.path.exists(path): 

608 additional_paths.append(path) 

609 

610 return paths + self.extra_template_paths + additional_paths 

611 

612 @classmethod 

613 def get_compatibility_base_template_conf(cls, name): 

614 """Get the base template config.""" 

615 # Hard-coded base template confs to use for backwards compatibility for 5.x-only templates 

616 if name == "display_priority": 

617 return {"base_template": "base"} 

618 if name == "full": 

619 return {"base_template": "classic", "mimetypes": {"text/html": True}} 

620 return None 

621 

622 def get_template_names(self): 

623 """Finds a list of template names where each successive template name is the base template""" 

624 template_names = [] 

625 root_dirs = self.get_prefix_root_dirs() 

626 base_template: str | None = self.template_name 

627 merged_conf: dict[str, t.Any] = {} # the configuration once all conf files are merged 

628 while base_template is not None: 

629 template_names.append(base_template) 

630 conf: dict[str, t.Any] = {} 

631 found_at_least_one = False 

632 for base_dir in self.extra_template_basedirs: 

633 template_dir = os.path.join(base_dir, base_template) 

634 if os.path.exists(template_dir): 

635 found_at_least_one = True 

636 conf_file = os.path.join(template_dir, "conf.json") 

637 if os.path.exists(conf_file): 

638 with open(conf_file) as f: 

639 conf = recursive_update(json.load(f), conf) 

640 for root_dir in root_dirs: 

641 template_dir = os.path.join(root_dir, "nbconvert", "templates", base_template) 

642 if os.path.exists(template_dir): 

643 found_at_least_one = True 

644 conf_file = os.path.join(template_dir, "conf.json") 

645 if os.path.exists(conf_file): 

646 with open(conf_file) as f: 

647 conf = recursive_update(json.load(f), conf) 

648 if not found_at_least_one: 

649 # Check for backwards compatibility template names 

650 for root_dir in root_dirs: 

651 compatibility_file = base_template + ".tpl" 

652 compatibility_path = os.path.join( 

653 root_dir, "nbconvert", "templates", "compatibility", compatibility_file 

654 ) 

655 if os.path.exists(compatibility_path): 

656 found_at_least_one = True 

657 warnings.warn( 

658 f"5.x template name passed '{self.template_name}'. Use 'lab' or 'classic' for new template usage.", 

659 DeprecationWarning, 

660 stacklevel=2, 

661 ) 

662 self.template_file = compatibility_file 

663 conf = self.get_compatibility_base_template_conf(base_template) 

664 self.template_name = t.cast(str, conf.get("base_template")) 

665 break 

666 if not found_at_least_one: 

667 paths = "\n\t".join(root_dirs) 

668 msg = f"No template sub-directory with name {base_template!r} found in the following paths:\n\t{paths}" 

669 raise ValueError(msg) 

670 merged_conf = recursive_update(dict(conf), merged_conf) 

671 base_template = t.cast(t.Any, conf.get("base_template")) 

672 conf = merged_conf 

673 mimetypes = [mimetype for mimetype, enabled in conf.get("mimetypes", {}).items() if enabled] 

674 if self.output_mimetype and self.output_mimetype not in mimetypes and mimetypes: 

675 supported_mimetypes = "\n\t".join(mimetypes) 

676 msg = f"Unsupported mimetype {self.output_mimetype!r} for template {self.template_name!r}, mimetypes supported are: \n\t{supported_mimetypes}" 

677 raise ValueError(msg) 

678 return template_names 

679 

680 def get_prefix_root_dirs(self): 

681 """Get the prefix root dirs.""" 

682 # We look at the usual jupyter locations, and for development purposes also 

683 # relative to the package directory (first entry, meaning with highest precedence) 

684 root_dirs = [] 

685 if DEV_MODE: 

686 root_dirs.append(os.path.abspath(os.path.join(ROOT, "..", "..", "share", "jupyter"))) 

687 root_dirs.extend(jupyter_path()) 

688 return root_dirs 

689 

690 def _init_resources(self, resources): 

691 resources = super()._init_resources(resources) 

692 resources["deprecated"] = deprecated 

693 return resources