Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jinja2/loaders.py: 26%

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

270 statements  

1"""API and implementations for loading templates from different data 

2sources. 

3""" 

4 

5import importlib.util 

6import os 

7import posixpath 

8import sys 

9import typing as t 

10import weakref 

11import zipimport 

12from collections import abc 

13from hashlib import sha1 

14from importlib import import_module 

15from types import ModuleType 

16 

17from .exceptions import TemplateNotFound 

18from .utils import internalcode 

19 

20if t.TYPE_CHECKING: 

21 from .environment import Environment 

22 from .environment import Template 

23 

24 

25def split_template_path(template: str) -> list[str]: 

26 """Split a path into segments and perform a sanity check. If it detects 

27 '..' in the path it will raise a `TemplateNotFound` error. 

28 """ 

29 pieces = [] 

30 for piece in template.split("/"): 

31 if ( 

32 os.sep in piece 

33 or (os.path.altsep and os.path.altsep in piece) 

34 or piece == os.path.pardir 

35 ): 

36 raise TemplateNotFound(template) 

37 elif piece and piece != ".": 

38 pieces.append(piece) 

39 return pieces 

40 

41 

42class BaseLoader: 

43 """Baseclass for all loaders. Subclass this and override `get_source` to 

44 implement a custom loading mechanism. The environment provides a 

45 `get_template` method that calls the loader's `load` method to get the 

46 :class:`Template` object. 

47 

48 A very basic example for a loader that looks up templates on the file 

49 system could look like this:: 

50 

51 from jinja2 import BaseLoader, TemplateNotFound 

52 from os.path import join, exists, getmtime 

53 

54 class MyLoader(BaseLoader): 

55 

56 def __init__(self, path): 

57 self.path = path 

58 

59 def get_source(self, environment, template): 

60 path = join(self.path, template) 

61 if not exists(path): 

62 raise TemplateNotFound(template) 

63 mtime = getmtime(path) 

64 with open(path) as f: 

65 source = f.read() 

66 return source, path, lambda: mtime == getmtime(path) 

67 """ 

68 

69 #: if set to `False` it indicates that the loader cannot provide access 

70 #: to the source of templates. 

71 #: 

72 #: .. versionadded:: 2.4 

73 has_source_access = True 

74 

75 def get_source( 

76 self, environment: "Environment", template: str 

77 ) -> tuple[str, str | None, t.Callable[[], bool] | None]: 

78 """Get the template source, filename and reload helper for a template. 

79 It's passed the environment and template name and has to return a 

80 tuple in the form ``(source, filename, uptodate)`` or raise a 

81 `TemplateNotFound` error if it can't locate the template. 

82 

83 The source part of the returned tuple must be the source of the 

84 template as a string. The filename should be the name of the 

85 file on the filesystem if it was loaded from there, otherwise 

86 ``None``. The filename is used by Python for the tracebacks 

87 if no loader extension is used. 

88 

89 The last item in the tuple is the `uptodate` function. If auto 

90 reloading is enabled it's always called to check if the template 

91 changed. No arguments are passed so the function must store the 

92 old state somewhere (for example in a closure). If it returns `False` 

93 the template will be reloaded. 

94 """ 

95 if not self.has_source_access: 

96 raise RuntimeError( 

97 f"{type(self).__name__} cannot provide access to the source" 

98 ) 

99 raise TemplateNotFound(template) 

100 

101 def list_templates(self) -> list[str]: 

102 """Iterates over all templates. If the loader does not support that 

103 it should raise a :exc:`TypeError` which is the default behavior. 

104 """ 

105 raise TypeError("this loader cannot iterate over all templates") 

106 

107 @internalcode 

108 def load( 

109 self, 

110 environment: "Environment", 

111 name: str, 

112 globals: t.MutableMapping[str, t.Any] | None = None, 

113 ) -> "Template": 

114 """Loads a template. This method looks up the template in the cache 

115 or loads one by calling :meth:`get_source`. Subclasses should not 

116 override this method as loaders working on collections of other 

117 loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) 

118 will not call this method but `get_source` directly. 

119 """ 

120 code = None 

121 if globals is None: 

122 globals = {} 

123 

124 # first we try to get the source for this template together 

125 # with the filename and the uptodate function. 

126 source, filename, uptodate = self.get_source(environment, name) 

127 

128 # try to load the code from the bytecode cache if there is a 

129 # bytecode cache configured. 

130 bcc = environment.bytecode_cache 

131 if bcc is not None: 

132 bucket = bcc.get_bucket(environment, name, filename, source) 

133 code = bucket.code 

134 

135 # if we don't have code so far (not cached, no longer up to 

136 # date) etc. we compile the template 

137 if code is None: 

138 code = environment.compile(source, name, filename) 

139 

140 # if the bytecode cache is available and the bucket doesn't 

141 # have a code so far, we give the bucket the new code and put 

142 # it back to the bytecode cache. 

143 if bcc is not None and bucket.code is None: 

144 bucket.code = code 

145 bcc.set_bucket(bucket) 

146 

147 return environment.template_class.from_code( 

148 environment, code, globals, uptodate 

149 ) 

150 

151 

152class FileSystemLoader(BaseLoader): 

153 """Load templates from a directory in the file system. 

154 

155 The path can be relative or absolute. Relative paths are relative to 

156 the current working directory. 

157 

158 .. code-block:: python 

159 

160 loader = FileSystemLoader("templates") 

161 

162 A list of paths can be given. The directories will be searched in 

163 order, stopping at the first matching template. 

164 

165 .. code-block:: python 

166 

167 loader = FileSystemLoader(["/override/templates", "/default/templates"]) 

168 

169 :param searchpath: A path, or list of paths, to the directory that 

170 contains the templates. 

171 :param encoding: Use this encoding to read the text from template 

172 files. 

173 :param followlinks: Follow symbolic links in the path. 

174 

175 .. versionchanged:: 2.8 

176 Added the ``followlinks`` parameter. 

177 """ 

178 

179 def __init__( 

180 self, 

181 searchpath: t.Union[ 

182 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]] 

183 ], 

184 encoding: str = "utf-8", 

185 followlinks: bool = False, 

186 ) -> None: 

187 if not isinstance(searchpath, abc.Iterable) or isinstance(searchpath, str): 

188 searchpath = [searchpath] 

189 

190 self.searchpath = [os.fspath(p) for p in searchpath] 

191 self.encoding = encoding 

192 self.followlinks = followlinks 

193 

194 def get_source( 

195 self, environment: "Environment", template: str 

196 ) -> tuple[str, str, t.Callable[[], bool]]: 

197 pieces = split_template_path(template) 

198 

199 for searchpath in self.searchpath: 

200 # Use posixpath even on Windows to avoid "drive:" or UNC 

201 # segments breaking out of the search directory. 

202 filename = posixpath.join(searchpath, *pieces) 

203 

204 if os.path.isfile(filename): 

205 break 

206 else: 

207 plural = "path" if len(self.searchpath) == 1 else "paths" 

208 paths_str = ", ".join(repr(p) for p in self.searchpath) 

209 raise TemplateNotFound( 

210 template, 

211 f"{template!r} not found in search {plural}: {paths_str}", 

212 ) 

213 

214 with open(filename, encoding=self.encoding) as f: 

215 contents = f.read() 

216 

217 mtime = os.path.getmtime(filename) 

218 

219 def uptodate() -> bool: 

220 try: 

221 return os.path.getmtime(filename) == mtime 

222 except OSError: 

223 return False 

224 

225 # Use normpath to convert Windows altsep to sep. 

226 return contents, os.path.normpath(filename), uptodate 

227 

228 def list_templates(self) -> list[str]: 

229 found = set() 

230 for searchpath in self.searchpath: 

231 walk_dir = os.walk(searchpath, followlinks=self.followlinks) 

232 for dirpath, _, filenames in walk_dir: 

233 for filename in filenames: 

234 template = ( 

235 os.path.join(dirpath, filename)[len(searchpath) :] 

236 .strip(os.sep) 

237 .replace(os.sep, "/") 

238 ) 

239 if template[:2] == "./": 

240 template = template[2:] 

241 if template not in found: 

242 found.add(template) 

243 return sorted(found) 

244 

245 

246if sys.version_info >= (3, 13): 

247 

248 def _get_zipimporter_files(z: t.Any) -> dict[str, object]: 

249 try: 

250 get_files = z._get_files 

251 except AttributeError as e: 

252 raise TypeError( 

253 "This zip import does not have the required metadata to list templates." 

254 ) from e 

255 return get_files() 

256 

257else: 

258 

259 def _get_zipimporter_files(z: t.Any) -> dict[str, object]: 

260 try: 

261 files = z._files 

262 except AttributeError as e: 

263 raise TypeError( 

264 "This zip import does not have the required metadata to list templates." 

265 ) from e 

266 return files # type: ignore[no-any-return] 

267 

268 

269class PackageLoader(BaseLoader): 

270 """Load templates from a directory in a Python package. 

271 

272 :param package_name: Import name of the package that contains the 

273 template directory. 

274 :param package_path: Directory within the imported package that 

275 contains the templates. 

276 :param encoding: Encoding of template files. 

277 

278 The following example looks up templates in the ``pages`` directory 

279 within the ``project.ui`` package. 

280 

281 .. code-block:: python 

282 

283 loader = PackageLoader("project.ui", "pages") 

284 

285 Only packages installed as directories (standard pip behavior) or 

286 zip/egg files (less common) are supported. The Python API for 

287 introspecting data in packages is too limited to support other 

288 installation methods the way this loader requires. 

289 

290 There is limited support for :pep:`420` namespace packages. The 

291 template directory is assumed to only be in one namespace 

292 contributor. Zip files contributing to a namespace are not 

293 supported. 

294 

295 .. versionchanged:: 3.0 

296 No longer uses ``setuptools`` as a dependency. 

297 

298 .. versionchanged:: 3.0 

299 Limited PEP 420 namespace package support. 

300 """ 

301 

302 def __init__( 

303 self, 

304 package_name: str, 

305 package_path: "str" = "templates", 

306 encoding: str = "utf-8", 

307 ) -> None: 

308 package_path = os.path.normpath(package_path).rstrip(os.sep) 

309 

310 # normpath preserves ".", which isn't valid in zip paths. 

311 if package_path == os.path.curdir: 

312 package_path = "" 

313 elif package_path[:2] == os.path.curdir + os.sep: 

314 package_path = package_path[2:] 

315 

316 self.package_path = package_path 

317 self.package_name = package_name 

318 self.encoding = encoding 

319 

320 # Make sure the package exists. This also makes namespace 

321 # packages work, otherwise get_loader returns None. 

322 import_module(package_name) 

323 spec = importlib.util.find_spec(package_name) 

324 assert spec is not None, "An import spec was not found for the package." 

325 loader = spec.loader 

326 assert loader is not None, "A loader was not found for the package." 

327 self._loader = loader 

328 self._archive = None 

329 

330 if isinstance(loader, zipimport.zipimporter): 

331 self._archive = loader.archive 

332 pkgdir = next(iter(spec.submodule_search_locations)) # type: ignore 

333 template_root = os.path.join(pkgdir, package_path).rstrip(os.sep) 

334 else: 

335 roots: list[str] = [] 

336 

337 # One element for regular packages, multiple for namespace 

338 # packages, or None for single module file. 

339 if spec.submodule_search_locations: 

340 roots.extend(spec.submodule_search_locations) 

341 # A single module file, use the parent directory instead. 

342 elif spec.origin is not None: 

343 roots.append(os.path.dirname(spec.origin)) 

344 

345 if not roots: 

346 raise ValueError( 

347 f"The {package_name!r} package was not installed in a" 

348 " way that PackageLoader understands." 

349 ) 

350 

351 for root in roots: 

352 root = os.path.join(root, package_path) 

353 

354 if os.path.isdir(root): 

355 template_root = root 

356 break 

357 else: 

358 raise ValueError( 

359 f"PackageLoader could not find a {package_path!r} directory" 

360 f" in the {package_name!r} package." 

361 ) 

362 

363 self._template_root = template_root 

364 

365 def get_source( 

366 self, environment: "Environment", template: str 

367 ) -> tuple[str, str, t.Callable[[], bool] | None]: 

368 # Use posixpath even on Windows to avoid "drive:" or UNC 

369 # segments breaking out of the search directory. Use normpath to 

370 # convert Windows altsep to sep. 

371 p = os.path.normpath( 

372 posixpath.join(self._template_root, *split_template_path(template)) 

373 ) 

374 up_to_date: t.Callable[[], bool] | None 

375 

376 if self._archive is None: 

377 # Package is a directory. 

378 if not os.path.isfile(p): 

379 raise TemplateNotFound(template) 

380 

381 with open(p, "rb") as f: 

382 source = f.read() 

383 

384 mtime = os.path.getmtime(p) 

385 

386 def up_to_date() -> bool: 

387 return os.path.isfile(p) and os.path.getmtime(p) == mtime 

388 

389 else: 

390 # Package is a zip file. 

391 try: 

392 source = self._loader.get_data(p) # type: ignore 

393 except OSError as e: 

394 raise TemplateNotFound(template) from e 

395 

396 # Could use the zip's mtime for all template mtimes, but 

397 # would need to safely reload the module if it's out of 

398 # date, so just report it as always current. 

399 up_to_date = None 

400 

401 return source.decode(self.encoding), p, up_to_date 

402 

403 def list_templates(self) -> list[str]: 

404 results: list[str] = [] 

405 

406 if self._archive is None: 

407 # Package is a directory. 

408 offset = len(self._template_root) 

409 

410 for dirpath, _, filenames in os.walk(self._template_root): 

411 dirpath = dirpath[offset:].lstrip(os.sep) 

412 results.extend( 

413 os.path.join(dirpath, name).replace(os.sep, "/") 

414 for name in filenames 

415 ) 

416 else: 

417 files = _get_zipimporter_files(self._loader) 

418 

419 # Package is a zip file. 

420 prefix = self._template_root[len(self._archive) :].lstrip(os.sep) + os.sep 

421 offset = len(prefix) 

422 

423 for name in files: 

424 # Find names under the templates directory that aren't directories. 

425 if name.startswith(prefix) and name[-1] != os.sep: 

426 results.append(name[offset:].replace(os.sep, "/")) 

427 

428 results.sort() 

429 return results 

430 

431 

432class DictLoader(BaseLoader): 

433 """Loads a template from a Python dict mapping template names to 

434 template source. This loader is useful for unittesting: 

435 

436 >>> loader = DictLoader({'index.html': 'source here'}) 

437 

438 Because auto reloading is rarely useful this is disabled by default. 

439 """ 

440 

441 def __init__(self, mapping: t.Mapping[str, str]) -> None: 

442 self.mapping = mapping 

443 

444 def get_source( 

445 self, environment: "Environment", template: str 

446 ) -> tuple[str, None, t.Callable[[], bool]]: 

447 if template in self.mapping: 

448 source = self.mapping[template] 

449 return source, None, lambda: source == self.mapping.get(template) 

450 raise TemplateNotFound(template) 

451 

452 def list_templates(self) -> list[str]: 

453 return sorted(self.mapping) 

454 

455 

456class FunctionLoader(BaseLoader): 

457 """A loader that is passed a function which does the loading. The 

458 function receives the name of the template and has to return either 

459 a string with the template source, a tuple in the form ``(source, 

460 filename, uptodatefunc)`` or `None` if the template does not exist. 

461 

462 >>> def load_template(name): 

463 ... if name == 'index.html': 

464 ... return '...' 

465 ... 

466 >>> loader = FunctionLoader(load_template) 

467 

468 The `uptodatefunc` is a function that is called if autoreload is enabled 

469 and has to return `True` if the template is still up to date. For more 

470 details have a look at :meth:`BaseLoader.get_source` which has the same 

471 return value. 

472 """ 

473 

474 def __init__( 

475 self, 

476 load_func: t.Callable[ 

477 [str], 

478 str | tuple[str, str | None, t.Callable[[], bool] | None] | None, 

479 ], 

480 ) -> None: 

481 self.load_func = load_func 

482 

483 def get_source( 

484 self, environment: "Environment", template: str 

485 ) -> tuple[str, str | None, t.Callable[[], bool] | None]: 

486 rv = self.load_func(template) 

487 

488 if rv is None: 

489 raise TemplateNotFound(template) 

490 

491 if isinstance(rv, str): 

492 return rv, None, None 

493 

494 return rv 

495 

496 

497class PrefixLoader(BaseLoader): 

498 """A loader that is passed a dict of loaders where each loader is bound 

499 to a prefix. The prefix is delimited from the template by a slash per 

500 default, which can be changed by setting the `delimiter` argument to 

501 something else:: 

502 

503 loader = PrefixLoader({ 

504 'app1': PackageLoader('mypackage.app1'), 

505 'app2': PackageLoader('mypackage.app2') 

506 }) 

507 

508 By loading ``'app1/index.html'`` the file from the app1 package is loaded, 

509 by loading ``'app2/index.html'`` the file from the second. 

510 """ 

511 

512 def __init__( 

513 self, mapping: t.Mapping[str, BaseLoader], delimiter: str = "/" 

514 ) -> None: 

515 self.mapping = mapping 

516 self.delimiter = delimiter 

517 

518 def get_loader(self, template: str) -> tuple[BaseLoader, str]: 

519 try: 

520 prefix, name = template.split(self.delimiter, 1) 

521 loader = self.mapping[prefix] 

522 except (ValueError, KeyError) as e: 

523 raise TemplateNotFound(template) from e 

524 return loader, name 

525 

526 def get_source( 

527 self, environment: "Environment", template: str 

528 ) -> tuple[str, str | None, t.Callable[[], bool] | None]: 

529 loader, name = self.get_loader(template) 

530 try: 

531 return loader.get_source(environment, name) 

532 except TemplateNotFound as e: 

533 # re-raise the exception with the correct filename here. 

534 # (the one that includes the prefix) 

535 raise TemplateNotFound(template) from e 

536 

537 @internalcode 

538 def load( 

539 self, 

540 environment: "Environment", 

541 name: str, 

542 globals: t.MutableMapping[str, t.Any] | None = None, 

543 ) -> "Template": 

544 loader, local_name = self.get_loader(name) 

545 try: 

546 return loader.load(environment, local_name, globals) 

547 except TemplateNotFound as e: 

548 # re-raise the exception with the correct filename here. 

549 # (the one that includes the prefix) 

550 raise TemplateNotFound(name) from e 

551 

552 def list_templates(self) -> list[str]: 

553 result = [] 

554 for prefix, loader in self.mapping.items(): 

555 for template in loader.list_templates(): 

556 result.append(prefix + self.delimiter + template) 

557 return result 

558 

559 

560class ChoiceLoader(BaseLoader): 

561 """This loader works like the `PrefixLoader` just that no prefix is 

562 specified. If a template could not be found by one loader the next one 

563 is tried. 

564 

565 >>> loader = ChoiceLoader([ 

566 ... FileSystemLoader('/path/to/user/templates'), 

567 ... FileSystemLoader('/path/to/system/templates') 

568 ... ]) 

569 

570 This is useful if you want to allow users to override builtin templates 

571 from a different location. 

572 """ 

573 

574 def __init__(self, loaders: t.Sequence[BaseLoader]) -> None: 

575 self.loaders = loaders 

576 

577 def get_source( 

578 self, environment: "Environment", template: str 

579 ) -> tuple[str, str | None, t.Callable[[], bool] | None]: 

580 for loader in self.loaders: 

581 try: 

582 return loader.get_source(environment, template) 

583 except TemplateNotFound: 

584 pass 

585 raise TemplateNotFound(template) 

586 

587 @internalcode 

588 def load( 

589 self, 

590 environment: "Environment", 

591 name: str, 

592 globals: t.MutableMapping[str, t.Any] | None = None, 

593 ) -> "Template": 

594 for loader in self.loaders: 

595 try: 

596 return loader.load(environment, name, globals) 

597 except TemplateNotFound: 

598 pass 

599 raise TemplateNotFound(name) 

600 

601 def list_templates(self) -> list[str]: 

602 found = set() 

603 for loader in self.loaders: 

604 found.update(loader.list_templates()) 

605 return sorted(found) 

606 

607 

608class _TemplateModule(ModuleType): 

609 """Like a normal module but with support for weak references""" 

610 

611 

612class ModuleLoader(BaseLoader): 

613 """This loader loads templates from precompiled templates. 

614 

615 Example usage: 

616 

617 >>> loader = ModuleLoader('/path/to/compiled/templates') 

618 

619 Templates can be precompiled with :meth:`Environment.compile_templates`. 

620 """ 

621 

622 has_source_access = False 

623 

624 def __init__( 

625 self, 

626 path: t.Union[ 

627 str, "os.PathLike[str]", t.Sequence[t.Union[str, "os.PathLike[str]"]] 

628 ], 

629 ) -> None: 

630 package_name = f"_jinja2_module_templates_{id(self):x}" 

631 

632 # create a fake module that looks for the templates in the 

633 # path given. 

634 mod = _TemplateModule(package_name) 

635 

636 if not isinstance(path, abc.Iterable) or isinstance(path, str): 

637 path = [path] 

638 

639 mod.__path__ = [os.fspath(p) for p in path] 

640 

641 sys.modules[package_name] = weakref.proxy( 

642 mod, lambda x: sys.modules.pop(package_name, None) 

643 ) 

644 

645 # the only strong reference, the sys.modules entry is weak 

646 # so that the garbage collector can remove it once the 

647 # loader that created it goes out of business. 

648 self.module = mod 

649 self.package_name = package_name 

650 

651 @staticmethod 

652 def get_template_key(name: str) -> str: 

653 return "tmpl_" + sha1(name.encode("utf-8")).hexdigest() 

654 

655 @staticmethod 

656 def get_module_filename(name: str) -> str: 

657 return ModuleLoader.get_template_key(name) + ".py" 

658 

659 @internalcode 

660 def load( 

661 self, 

662 environment: "Environment", 

663 name: str, 

664 globals: t.MutableMapping[str, t.Any] | None = None, 

665 ) -> "Template": 

666 key = self.get_template_key(name) 

667 module = f"{self.package_name}.{key}" 

668 mod = getattr(self.module, module, None) 

669 

670 if mod is None: 

671 try: 

672 mod = __import__(module, None, None, ["root"]) 

673 except ImportError as e: 

674 raise TemplateNotFound(name) from e 

675 

676 # remove the entry from sys.modules, we only want the attribute 

677 # on the module object we have stored on the loader. 

678 sys.modules.pop(module, None) 

679 

680 if globals is None: 

681 globals = {} 

682 

683 return environment.template_class.from_module_dict( 

684 environment, mod.__dict__, globals 

685 )