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

255 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:15 +0000

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

2sources. 

3""" 

4import importlib.util 

5import os 

6import posixpath 

7import sys 

8import typing as t 

9import weakref 

10import zipimport 

11from collections import abc 

12from hashlib import sha1 

13from importlib import import_module 

14from types import ModuleType 

15 

16from .exceptions import TemplateNotFound 

17from .utils import internalcode 

18 

19if t.TYPE_CHECKING: 

20 from .environment import Environment 

21 from .environment import Template 

22 

23 

24def split_template_path(template: str) -> t.List[str]: 

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

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

27 """ 

28 pieces = [] 

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

30 if ( 

31 os.sep in piece 

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

33 or piece == os.path.pardir 

34 ): 

35 raise TemplateNotFound(template) 

36 elif piece and piece != ".": 

37 pieces.append(piece) 

38 return pieces 

39 

40 

41class BaseLoader: 

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

43 implement a custom loading mechanism. The environment provides a 

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

45 :class:`Template` object. 

46 

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

48 system could look like this:: 

49 

50 from jinja2 import BaseLoader, TemplateNotFound 

51 from os.path import join, exists, getmtime 

52 

53 class MyLoader(BaseLoader): 

54 

55 def __init__(self, path): 

56 self.path = path 

57 

58 def get_source(self, environment, template): 

59 path = join(self.path, template) 

60 if not exists(path): 

61 raise TemplateNotFound(template) 

62 mtime = getmtime(path) 

63 with open(path) as f: 

64 source = f.read() 

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

66 """ 

67 

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

69 #: to the source of templates. 

70 #: 

71 #: .. versionadded:: 2.4 

72 has_source_access = True 

73 

74 def get_source( 

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

76 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: 

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

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

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

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

81 

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

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

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

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

86 if no loader extension is used. 

87 

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

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

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

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

92 the template will be reloaded. 

93 """ 

94 if not self.has_source_access: 

95 raise RuntimeError( 

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

97 ) 

98 raise TemplateNotFound(template) 

99 

100 def list_templates(self) -> t.List[str]: 

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

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

103 """ 

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

105 

106 @internalcode 

107 def load( 

108 self, 

109 environment: "Environment", 

110 name: str, 

111 globals: t.Optional[t.MutableMapping[str, t.Any]] = None, 

112 ) -> "Template": 

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

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

115 override this method as loaders working on collections of other 

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

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

118 """ 

119 code = None 

120 if globals is None: 

121 globals = {} 

122 

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

124 # with the filename and the uptodate function. 

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

126 

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

128 # bytecode cache configured. 

129 bcc = environment.bytecode_cache 

130 if bcc is not None: 

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

132 code = bucket.code 

133 

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

135 # date) etc. we compile the template 

136 if code is None: 

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

138 

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

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

141 # it back to the bytecode cache. 

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

143 bucket.code = code 

144 bcc.set_bucket(bucket) 

145 

146 return environment.template_class.from_code( 

147 environment, code, globals, uptodate 

148 ) 

149 

150 

151class FileSystemLoader(BaseLoader): 

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

153 

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

155 the current working directory. 

156 

157 .. code-block:: python 

158 

159 loader = FileSystemLoader("templates") 

160 

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

162 order, stopping at the first matching template. 

163 

164 .. code-block:: python 

165 

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

167 

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

169 contains the templates. 

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

171 files. 

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

173 

174 .. versionchanged:: 2.8 

175 Added the ``followlinks`` parameter. 

176 """ 

177 

178 def __init__( 

179 self, 

180 searchpath: t.Union[ 

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

182 ], 

183 encoding: str = "utf-8", 

184 followlinks: bool = False, 

185 ) -> None: 

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

187 searchpath = [searchpath] 

188 

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

190 self.encoding = encoding 

191 self.followlinks = followlinks 

192 

193 def get_source( 

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

195 ) -> t.Tuple[str, str, t.Callable[[], bool]]: 

196 pieces = split_template_path(template) 

197 

198 for searchpath in self.searchpath: 

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

200 # segments breaking out of the search directory. 

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

202 

203 if os.path.isfile(filename): 

204 break 

205 else: 

206 raise TemplateNotFound(template) 

207 

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

209 contents = f.read() 

210 

211 mtime = os.path.getmtime(filename) 

212 

213 def uptodate() -> bool: 

214 try: 

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

216 except OSError: 

217 return False 

218 

219 # Use normpath to convert Windows altsep to sep. 

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

221 

222 def list_templates(self) -> t.List[str]: 

223 found = set() 

224 for searchpath in self.searchpath: 

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

226 for dirpath, _, filenames in walk_dir: 

227 for filename in filenames: 

228 template = ( 

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

230 .strip(os.sep) 

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

232 ) 

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

234 template = template[2:] 

235 if template not in found: 

236 found.add(template) 

237 return sorted(found) 

238 

239 

240class PackageLoader(BaseLoader): 

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

242 

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

244 template directory. 

245 :param package_path: Directory within the imported package that 

246 contains the templates. 

247 :param encoding: Encoding of template files. 

248 

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

250 within the ``project.ui`` package. 

251 

252 .. code-block:: python 

253 

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

255 

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

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

258 introspecting data in packages is too limited to support other 

259 installation methods the way this loader requires. 

260 

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

262 template directory is assumed to only be in one namespace 

263 contributor. Zip files contributing to a namespace are not 

264 supported. 

265 

266 .. versionchanged:: 3.0 

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

268 

269 .. versionchanged:: 3.0 

270 Limited PEP 420 namespace package support. 

271 """ 

272 

273 def __init__( 

274 self, 

275 package_name: str, 

276 package_path: "str" = "templates", 

277 encoding: str = "utf-8", 

278 ) -> None: 

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

280 

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

282 if package_path == os.path.curdir: 

283 package_path = "" 

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

285 package_path = package_path[2:] 

286 

287 self.package_path = package_path 

288 self.package_name = package_name 

289 self.encoding = encoding 

290 

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

292 # packages work, otherwise get_loader returns None. 

293 import_module(package_name) 

294 spec = importlib.util.find_spec(package_name) 

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

296 loader = spec.loader 

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

298 self._loader = loader 

299 self._archive = None 

300 template_root = None 

301 

302 if isinstance(loader, zipimport.zipimporter): 

303 self._archive = loader.archive 

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

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

306 else: 

307 roots: t.List[str] = [] 

308 

309 # One element for regular packages, multiple for namespace 

310 # packages, or None for single module file. 

311 if spec.submodule_search_locations: 

312 roots.extend(spec.submodule_search_locations) 

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

314 elif spec.origin is not None: 

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

316 

317 for root in roots: 

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

319 

320 if os.path.isdir(root): 

321 template_root = root 

322 break 

323 

324 if template_root is None: 

325 raise ValueError( 

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

327 " way that PackageLoader understands." 

328 ) 

329 

330 self._template_root = template_root 

331 

332 def get_source( 

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

334 ) -> t.Tuple[str, str, t.Optional[t.Callable[[], bool]]]: 

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

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

337 # convert Windows altsep to sep. 

338 p = os.path.normpath( 

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

340 ) 

341 up_to_date: t.Optional[t.Callable[[], bool]] 

342 

343 if self._archive is None: 

344 # Package is a directory. 

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

346 raise TemplateNotFound(template) 

347 

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

349 source = f.read() 

350 

351 mtime = os.path.getmtime(p) 

352 

353 def up_to_date() -> bool: 

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

355 

356 else: 

357 # Package is a zip file. 

358 try: 

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

360 except OSError as e: 

361 raise TemplateNotFound(template) from e 

362 

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

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

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

366 up_to_date = None 

367 

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

369 

370 def list_templates(self) -> t.List[str]: 

371 results: t.List[str] = [] 

372 

373 if self._archive is None: 

374 # Package is a directory. 

375 offset = len(self._template_root) 

376 

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

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

379 results.extend( 

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

381 for name in filenames 

382 ) 

383 else: 

384 if not hasattr(self._loader, "_files"): 

385 raise TypeError( 

386 "This zip import does not have the required" 

387 " metadata to list templates." 

388 ) 

389 

390 # Package is a zip file. 

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

392 offset = len(prefix) 

393 

394 for name in self._loader._files.keys(): 

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

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

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

398 

399 results.sort() 

400 return results 

401 

402 

403class DictLoader(BaseLoader): 

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

405 template source. This loader is useful for unittesting: 

406 

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

408 

409 Because auto reloading is rarely useful this is disabled per default. 

410 """ 

411 

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

413 self.mapping = mapping 

414 

415 def get_source( 

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

417 ) -> t.Tuple[str, None, t.Callable[[], bool]]: 

418 if template in self.mapping: 

419 source = self.mapping[template] 

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

421 raise TemplateNotFound(template) 

422 

423 def list_templates(self) -> t.List[str]: 

424 return sorted(self.mapping) 

425 

426 

427class FunctionLoader(BaseLoader): 

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

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

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

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

432 

433 >>> def load_template(name): 

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

435 ... return '...' 

436 ... 

437 >>> loader = FunctionLoader(load_template) 

438 

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

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

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

442 return value. 

443 """ 

444 

445 def __init__( 

446 self, 

447 load_func: t.Callable[ 

448 [str], 

449 t.Optional[ 

450 t.Union[ 

451 str, t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]] 

452 ] 

453 ], 

454 ], 

455 ) -> None: 

456 self.load_func = load_func 

457 

458 def get_source( 

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

460 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: 

461 rv = self.load_func(template) 

462 

463 if rv is None: 

464 raise TemplateNotFound(template) 

465 

466 if isinstance(rv, str): 

467 return rv, None, None 

468 

469 return rv 

470 

471 

472class PrefixLoader(BaseLoader): 

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

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

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

476 something else:: 

477 

478 loader = PrefixLoader({ 

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

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

481 }) 

482 

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

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

485 """ 

486 

487 def __init__( 

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

489 ) -> None: 

490 self.mapping = mapping 

491 self.delimiter = delimiter 

492 

493 def get_loader(self, template: str) -> t.Tuple[BaseLoader, str]: 

494 try: 

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

496 loader = self.mapping[prefix] 

497 except (ValueError, KeyError) as e: 

498 raise TemplateNotFound(template) from e 

499 return loader, name 

500 

501 def get_source( 

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

503 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: 

504 loader, name = self.get_loader(template) 

505 try: 

506 return loader.get_source(environment, name) 

507 except TemplateNotFound as e: 

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

509 # (the one that includes the prefix) 

510 raise TemplateNotFound(template) from e 

511 

512 @internalcode 

513 def load( 

514 self, 

515 environment: "Environment", 

516 name: str, 

517 globals: t.Optional[t.MutableMapping[str, t.Any]] = None, 

518 ) -> "Template": 

519 loader, local_name = self.get_loader(name) 

520 try: 

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

522 except TemplateNotFound as e: 

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

524 # (the one that includes the prefix) 

525 raise TemplateNotFound(name) from e 

526 

527 def list_templates(self) -> t.List[str]: 

528 result = [] 

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

530 for template in loader.list_templates(): 

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

532 return result 

533 

534 

535class ChoiceLoader(BaseLoader): 

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

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

538 is tried. 

539 

540 >>> loader = ChoiceLoader([ 

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

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

543 ... ]) 

544 

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

546 from a different location. 

547 """ 

548 

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

550 self.loaders = loaders 

551 

552 def get_source( 

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

554 ) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]: 

555 for loader in self.loaders: 

556 try: 

557 return loader.get_source(environment, template) 

558 except TemplateNotFound: 

559 pass 

560 raise TemplateNotFound(template) 

561 

562 @internalcode 

563 def load( 

564 self, 

565 environment: "Environment", 

566 name: str, 

567 globals: t.Optional[t.MutableMapping[str, t.Any]] = None, 

568 ) -> "Template": 

569 for loader in self.loaders: 

570 try: 

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

572 except TemplateNotFound: 

573 pass 

574 raise TemplateNotFound(name) 

575 

576 def list_templates(self) -> t.List[str]: 

577 found = set() 

578 for loader in self.loaders: 

579 found.update(loader.list_templates()) 

580 return sorted(found) 

581 

582 

583class _TemplateModule(ModuleType): 

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

585 

586 

587class ModuleLoader(BaseLoader): 

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

589 

590 Example usage: 

591 

592 >>> loader = ChoiceLoader([ 

593 ... ModuleLoader('/path/to/compiled/templates'), 

594 ... FileSystemLoader('/path/to/templates') 

595 ... ]) 

596 

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

598 """ 

599 

600 has_source_access = False 

601 

602 def __init__( 

603 self, 

604 path: t.Union[ 

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

606 ], 

607 ) -> None: 

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

609 

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

611 # path given. 

612 mod = _TemplateModule(package_name) 

613 

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

615 path = [path] 

616 

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

618 

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

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

621 ) 

622 

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

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

625 # loader that created it goes out of business. 

626 self.module = mod 

627 self.package_name = package_name 

628 

629 @staticmethod 

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

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

632 

633 @staticmethod 

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

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

636 

637 @internalcode 

638 def load( 

639 self, 

640 environment: "Environment", 

641 name: str, 

642 globals: t.Optional[t.MutableMapping[str, t.Any]] = None, 

643 ) -> "Template": 

644 key = self.get_template_key(name) 

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

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

647 

648 if mod is None: 

649 try: 

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

651 except ImportError as e: 

652 raise TemplateNotFound(name) from e 

653 

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

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

656 sys.modules.pop(module, None) 

657 

658 if globals is None: 

659 globals = {} 

660 

661 return environment.template_class.from_module_dict( 

662 environment, mod.__dict__, globals 

663 )