Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/template/base.py: 25%

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

309 statements  

1# -*- coding: utf-8 -*- 

2# 

3# Copyright (C) 2006-2010 Edgewall Software 

4# All rights reserved. 

5# 

6# This software is licensed as described in the file COPYING, which 

7# you should have received as part of this distribution. The terms 

8# are also available at http://genshi.edgewall.org/wiki/License. 

9# 

10# This software consists of voluntary contributions made by many 

11# individuals. For the exact contribution history, see the revision 

12# history and logs, available at http://genshi.edgewall.org/log/. 

13 

14"""Basic templating functionality.""" 

15 

16from collections import deque 

17import os 

18 

19from genshi.compat import add_metaclass, numeric_types, string_types, text_type, StringIO, BytesIO 

20from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure 

21from genshi.input import ParseError 

22 

23__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError', 

24 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError'] 

25__docformat__ = 'restructuredtext en' 

26 

27 

28class TemplateError(Exception): 

29 """Base exception class for errors related to template processing.""" 

30 

31 def __init__(self, message, filename=None, lineno=-1, offset=-1): 

32 """Create the exception. 

33  

34 :param message: the error message 

35 :param filename: the filename of the template 

36 :param lineno: the number of line in the template at which the error 

37 occurred 

38 :param offset: the column number at which the error occurred 

39 """ 

40 if filename is None: 

41 filename = '<string>' 

42 self.msg = message #: the error message string 

43 if filename != '<string>' or lineno >= 0: 

44 message = '%s (%s, line %d)' % (self.msg, filename, lineno) 

45 Exception.__init__(self, message) 

46 self.filename = filename #: the name of the template file 

47 self.lineno = lineno #: the number of the line containing the error 

48 self.offset = offset #: the offset on the line 

49 

50 

51class TemplateSyntaxError(TemplateError): 

52 """Exception raised when an expression in a template causes a Python syntax 

53 error, or the template is not well-formed. 

54 """ 

55 

56 def __init__(self, message, filename=None, lineno=-1, offset=-1): 

57 """Create the exception 

58  

59 :param message: the error message 

60 :param filename: the filename of the template 

61 :param lineno: the number of line in the template at which the error 

62 occurred 

63 :param offset: the column number at which the error occurred 

64 """ 

65 if isinstance(message, SyntaxError) and message.lineno is not None: 

66 message = str(message).replace(' (line %d)' % message.lineno, '') 

67 TemplateError.__init__(self, message, filename, lineno) 

68 

69 

70class BadDirectiveError(TemplateSyntaxError): 

71 """Exception raised when an unknown directive is encountered when parsing 

72 a template. 

73  

74 An unknown directive is any attribute using the namespace for directives, 

75 with a local name that doesn't match any registered directive. 

76 """ 

77 

78 def __init__(self, name, filename=None, lineno=-1): 

79 """Create the exception 

80  

81 :param name: the name of the directive 

82 :param filename: the filename of the template 

83 :param lineno: the number of line in the template at which the error 

84 occurred 

85 """ 

86 TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name, 

87 filename, lineno) 

88 

89 

90class TemplateRuntimeError(TemplateError): 

91 """Exception raised when an the evaluation of a Python expression in a 

92 template causes an error. 

93 """ 

94 

95 

96class Context(object): 

97 """Container for template input data. 

98  

99 A context provides a stack of scopes (represented by dictionaries). 

100  

101 Template directives such as loops can push a new scope on the stack with 

102 data that should only be available inside the loop. When the loop 

103 terminates, that scope can get popped off the stack again. 

104  

105 >>> ctxt = Context(one='foo', other=1) 

106 >>> ctxt.get('one') 

107 'foo' 

108 >>> ctxt.get('other') 

109 1 

110 >>> ctxt.push(dict(one='frost')) 

111 >>> ctxt.get('one') 

112 'frost' 

113 >>> ctxt.get('other') 

114 1 

115 >>> ctxt.pop() 

116 {'one': 'frost'} 

117 >>> ctxt.get('one') 

118 'foo' 

119 """ 

120 

121 def __init__(self, **data): 

122 """Initialize the template context with the given keyword arguments as 

123 data. 

124 """ 

125 self.frames = deque([data]) 

126 self.pop = self.frames.popleft 

127 self.push = self.frames.appendleft 

128 self._match_templates = [] 

129 self._choice_stack = [] 

130 

131 # Helper functions for use in expressions 

132 def defined(name): 

133 """Return whether a variable with the specified name exists in the 

134 expression scope.""" 

135 return name in self 

136 def value_of(name, default=None): 

137 """If a variable of the specified name is defined, return its value. 

138 Otherwise, return the provided default value, or ``None``.""" 

139 return self.get(name, default) 

140 data.setdefault('defined', defined) 

141 data.setdefault('value_of', value_of) 

142 

143 def __repr__(self): 

144 return repr(list(self.frames)) 

145 

146 def __contains__(self, key): 

147 """Return whether a variable exists in any of the scopes. 

148  

149 :param key: the name of the variable 

150 """ 

151 return self._find(key)[1] is not None 

152 has_key = __contains__ 

153 

154 def __delitem__(self, key): 

155 """Remove a variable from all scopes. 

156  

157 :param key: the name of the variable 

158 """ 

159 for frame in self.frames: 

160 if key in frame: 

161 del frame[key] 

162 

163 def __getitem__(self, key): 

164 """Get a variables's value, starting at the current scope and going 

165 upward. 

166  

167 :param key: the name of the variable 

168 :return: the variable value 

169 :raises KeyError: if the requested variable wasn't found in any scope 

170 """ 

171 value, frame = self._find(key) 

172 if frame is None: 

173 raise KeyError(key) 

174 return value 

175 

176 def __len__(self): 

177 """Return the number of distinctly named variables in the context. 

178  

179 :return: the number of variables in the context 

180 """ 

181 return len(self.items()) 

182 

183 def __setitem__(self, key, value): 

184 """Set a variable in the current scope. 

185  

186 :param key: the name of the variable 

187 :param value: the variable value 

188 """ 

189 self.frames[0][key] = value 

190 

191 def _find(self, key, default=None): 

192 """Retrieve a given variable's value and the frame it was found in. 

193 

194 Intended primarily for internal use by directives. 

195  

196 :param key: the name of the variable 

197 :param default: the default value to return when the variable is not 

198 found 

199 """ 

200 for frame in self.frames: 

201 if key in frame: 

202 return frame[key], frame 

203 return default, None 

204 

205 def get(self, key, default=None): 

206 """Get a variable's value, starting at the current scope and going 

207 upward. 

208  

209 :param key: the name of the variable 

210 :param default: the default value to return when the variable is not 

211 found 

212 """ 

213 for frame in self.frames: 

214 if key in frame: 

215 return frame[key] 

216 return default 

217 

218 def keys(self): 

219 """Return the name of all variables in the context. 

220  

221 :return: a list of variable names 

222 """ 

223 keys = [] 

224 for frame in self.frames: 

225 keys += [key for key in frame if key not in keys] 

226 return keys 

227 

228 def items(self): 

229 """Return a list of ``(name, value)`` tuples for all variables in the 

230 context. 

231  

232 :return: a list of variables 

233 """ 

234 return [(key, self.get(key)) for key in self.keys()] 

235 

236 def update(self, mapping): 

237 """Update the context from the mapping provided.""" 

238 self.frames[0].update(mapping) 

239 

240 def push(self, data): 

241 """Push a new scope on the stack. 

242  

243 :param data: the data dictionary to push on the context stack. 

244 """ 

245 

246 def pop(self): 

247 """Pop the top-most scope from the stack.""" 

248 

249 def copy(self): 

250 """Create a copy of this Context object.""" 

251 # required to make f_locals a dict-like object 

252 # See http://genshi.edgewall.org/ticket/249 for 

253 # example use case in Twisted tracebacks 

254 ctxt = Context() 

255 ctxt.frames.pop() # pop empty dummy context 

256 ctxt.frames.extend(self.frames) 

257 ctxt._match_templates.extend(self._match_templates) 

258 ctxt._choice_stack.extend(self._choice_stack) 

259 return ctxt 

260 

261 

262def _apply_directives(stream, directives, ctxt, vars): 

263 """Apply the given directives to the stream. 

264  

265 :param stream: the stream the directives should be applied to 

266 :param directives: the list of directives to apply 

267 :param ctxt: the `Context` 

268 :param vars: additional variables that should be available when Python 

269 code is executed 

270 :return: the stream with the given directives applied 

271 """ 

272 if directives: 

273 stream = directives[0](iter(stream), directives[1:], ctxt, **vars) 

274 return stream 

275 

276 

277def _eval_expr(expr, ctxt, vars=None): 

278 """Evaluate the given `Expression` object. 

279  

280 :param expr: the expression to evaluate 

281 :param ctxt: the `Context` 

282 :param vars: additional variables that should be available to the 

283 expression 

284 :return: the result of the evaluation 

285 """ 

286 if vars: 

287 ctxt.push(vars) 

288 retval = expr.evaluate(ctxt) 

289 if vars: 

290 ctxt.pop() 

291 return retval 

292 

293 

294def _exec_suite(suite, ctxt, vars=None): 

295 """Execute the given `Suite` object. 

296  

297 :param suite: the code suite to execute 

298 :param ctxt: the `Context` 

299 :param vars: additional variables that should be available to the 

300 code 

301 """ 

302 if vars: 

303 ctxt.push(vars) 

304 ctxt.push({}) 

305 suite.execute(ctxt) 

306 if vars: 

307 top = ctxt.pop() 

308 ctxt.pop() 

309 ctxt.frames[0].update(top) 

310 

311 

312class DirectiveFactoryMeta(type): 

313 """Meta class for directive factories.""" 

314 

315 def __new__(cls, name, bases, d): 

316 if 'directives' in d: 

317 d['_dir_by_name'] = dict(d['directives']) 

318 d['_dir_order'] = [directive[1] for directive in d['directives']] 

319 

320 return type.__new__(cls, name, bases, d) 

321 

322 

323@add_metaclass(DirectiveFactoryMeta) 

324class DirectiveFactory(object): 

325 """Base for classes that provide a set of template directives. 

326  

327 :since: version 0.6 

328 """ 

329 

330 directives = [] 

331 """A list of ``(name, cls)`` tuples that define the set of directives 

332 provided by this factory. 

333 """ 

334 

335 def get_directive(self, name): 

336 """Return the directive class for the given name. 

337  

338 :param name: the directive name as used in the template 

339 :return: the directive class 

340 :see: `Directive` 

341 """ 

342 return self._dir_by_name.get(name) 

343 

344 def get_directive_index(self, dir_cls): 

345 """Return a key for the given directive class that should be used to 

346 sort it among other directives on the same `SUB` event. 

347  

348 The default implementation simply returns the index of the directive in 

349 the `directives` list. 

350  

351 :param dir_cls: the directive class 

352 :return: the sort key 

353 """ 

354 if dir_cls in self._dir_order: 

355 return self._dir_order.index(dir_cls) 

356 return len(self._dir_order) 

357 

358 

359class Template(DirectiveFactory): 

360 """Abstract template base class. 

361  

362 This class implements most of the template processing model, but does not 

363 specify the syntax of templates. 

364 """ 

365 

366 EXEC = StreamEventKind('EXEC') 

367 """Stream event kind representing a Python code suite to execute.""" 

368 

369 EXPR = StreamEventKind('EXPR') 

370 """Stream event kind representing a Python expression.""" 

371 

372 INCLUDE = StreamEventKind('INCLUDE') 

373 """Stream event kind representing the inclusion of another template.""" 

374 

375 SUB = StreamEventKind('SUB') 

376 """Stream event kind representing a nested stream to which one or more 

377 directives should be applied. 

378 """ 

379 

380 serializer = None 

381 _number_conv = text_type # function used to convert numbers to event data 

382 

383 def __init__(self, source, filepath=None, filename=None, loader=None, 

384 encoding=None, lookup='strict', allow_exec=True): 

385 """Initialize a template from either a string, a file-like object, or 

386 an already parsed markup stream. 

387  

388 :param source: a string, file-like object, or markup stream to read the 

389 template from 

390 :param filepath: the absolute path to the template file 

391 :param filename: the path to the template file relative to the search 

392 path 

393 :param loader: the `TemplateLoader` to use for loading included 

394 templates 

395 :param encoding: the encoding of the `source` 

396 :param lookup: the variable lookup mechanism; either "strict" (the 

397 default), "lenient", or a custom lookup class 

398 :param allow_exec: whether Python code blocks in templates should be 

399 allowed 

400  

401 :note: Changed in 0.5: Added the `allow_exec` argument 

402 """ 

403 self.filepath = filepath or filename 

404 self.filename = filename 

405 self.loader = loader 

406 self.lookup = lookup 

407 self.allow_exec = allow_exec 

408 self._init_filters() 

409 self._init_loader() 

410 self._prepared = False 

411 

412 if not isinstance(source, Stream) and not hasattr(source, 'read'): 

413 if isinstance(source, text_type): 

414 source = StringIO(source) 

415 else: 

416 source = BytesIO(source) 

417 try: 

418 self._stream = self._parse(source, encoding) 

419 except ParseError as e: 

420 raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset) 

421 

422 def __getstate__(self): 

423 state = self.__dict__.copy() 

424 state['filters'] = [] 

425 return state 

426 

427 def __setstate__(self, state): 

428 self.__dict__ = state 

429 self._init_filters() 

430 

431 def __repr__(self): 

432 return '<%s "%s">' % (type(self).__name__, self.filename) 

433 

434 def _init_filters(self): 

435 self.filters = [self._flatten, self._include] 

436 

437 def _init_loader(self): 

438 if self.loader is None: 

439 from genshi.template.loader import TemplateLoader 

440 if self.filename: 

441 if self.filepath != self.filename: 

442 basedir = os.path.normpath(self.filepath)[:-len( 

443 os.path.normpath(self.filename)) 

444 ] 

445 else: 

446 basedir = os.path.dirname(self.filename) 

447 else: 

448 basedir = '.' 

449 self.loader = TemplateLoader([os.path.abspath(basedir)]) 

450 

451 @property 

452 def stream(self): 

453 if not self._prepared: 

454 self._prepare_self() 

455 return self._stream 

456 

457 def _parse(self, source, encoding): 

458 """Parse the template. 

459  

460 The parsing stage parses the template and constructs a list of 

461 directives that will be executed in the render stage. The input is 

462 split up into literal output (text that does not depend on the context 

463 data) and directives or expressions. 

464  

465 :param source: a file-like object containing the XML source of the 

466 template, or an XML event stream 

467 :param encoding: the encoding of the `source` 

468 """ 

469 raise NotImplementedError 

470 

471 def _prepare_self(self, inlined=None): 

472 if not self._prepared: 

473 self._stream = list(self._prepare(self._stream, inlined)) 

474 self._prepared = True 

475 

476 def _prepare(self, stream, inlined): 

477 """Call the `attach` method of every directive found in the template. 

478  

479 :param stream: the event stream of the template 

480 """ 

481 from genshi.template.loader import TemplateNotFound 

482 if inlined is None: 

483 inlined = set((self.filepath,)) 

484 

485 for kind, data, pos in stream: 

486 if kind is SUB: 

487 directives = [] 

488 substream = data[1] 

489 for _, cls, value, namespaces, pos in sorted( 

490 data[0], key=lambda x: x[0]): 

491 directive, substream = cls.attach(self, substream, value, 

492 namespaces, pos) 

493 if directive: 

494 directives.append(directive) 

495 substream = self._prepare(substream, inlined) 

496 if directives: 

497 yield kind, (directives, list(substream)), pos 

498 else: 

499 for event in substream: 

500 yield event 

501 else: 

502 if kind is INCLUDE: 

503 href, cls, fallback = data 

504 tmpl_inlined = False 

505 if (isinstance(href, string_types) and 

506 not getattr(self.loader, 'auto_reload', True)): 

507 # If the path to the included template is static, and 

508 # auto-reloading is disabled on the template loader, 

509 # the template is inlined into the stream provided it 

510 # is not already in the stack of templates being 

511 # processed. 

512 tmpl = None 

513 try: 

514 tmpl = self.loader.load(href, relative_to=pos[0], 

515 cls=cls or self.__class__) 

516 except TemplateNotFound: 

517 if fallback is None: 

518 raise 

519 if tmpl is not None: 

520 if tmpl.filepath not in inlined: 

521 inlined.add(tmpl.filepath) 

522 tmpl._prepare_self(inlined) 

523 for event in tmpl.stream: 

524 yield event 

525 inlined.discard(tmpl.filepath) 

526 tmpl_inlined = True 

527 else: 

528 for event in self._prepare(fallback, inlined): 

529 yield event 

530 tmpl_inlined = True 

531 if tmpl_inlined: 

532 continue 

533 if fallback: 

534 # Otherwise the include is performed at run time 

535 data = href, cls, list( 

536 self._prepare(fallback, inlined)) 

537 yield kind, data, pos 

538 else: 

539 yield kind, data, pos 

540 

541 def generate(self, *args, **kwargs): 

542 """Apply the template to the given context data. 

543  

544 Any keyword arguments are made available to the template as context 

545 data. 

546  

547 Only one positional argument is accepted: if it is provided, it must be 

548 an instance of the `Context` class, and keyword arguments are ignored. 

549 This calling style is used for internal processing. 

550  

551 :return: a markup event stream representing the result of applying 

552 the template to the context data. 

553 """ 

554 vars = {} 

555 if args: 

556 assert len(args) == 1 

557 ctxt = args[0] 

558 if ctxt is None: 

559 ctxt = Context(**kwargs) 

560 else: 

561 vars = kwargs 

562 assert isinstance(ctxt, Context) 

563 else: 

564 ctxt = Context(**kwargs) 

565 

566 stream = self.stream 

567 for filter_ in self.filters: 

568 stream = filter_(iter(stream), ctxt, **vars) 

569 return Stream(stream, self.serializer) 

570 

571 def _flatten(self, stream, ctxt, **vars): 

572 number_conv = self._number_conv 

573 stack = [] 

574 push = stack.append 

575 pop = stack.pop 

576 stream = iter(stream) 

577 

578 while 1: 

579 for kind, data, pos in stream: 

580 

581 if kind is START and data[1]: 

582 # Attributes may still contain expressions in start tags at 

583 # this point, so do some evaluation 

584 tag, attrs = data 

585 new_attrs = [] 

586 for name, value in attrs: 

587 if type(value) is list: # this is an interpolated string 

588 values = [event[1] 

589 for event in self._flatten(value, ctxt, **vars) 

590 if event[0] is TEXT and event[1] is not None 

591 ] 

592 if not values: 

593 continue 

594 value = ''.join(values) 

595 new_attrs.append((name, value)) 

596 yield kind, (tag, Attrs(new_attrs)), pos 

597 

598 elif kind is EXPR: 

599 result = _eval_expr(data, ctxt, vars) 

600 if result is not None: 

601 # First check for a string, otherwise the iterable test 

602 # below succeeds, and the string will be chopped up into 

603 # individual characters 

604 if isinstance(result, string_types): 

605 yield TEXT, result, pos 

606 elif isinstance(result, numeric_types): 

607 yield TEXT, number_conv(result), pos 

608 elif hasattr(result, '__iter__'): 

609 push(stream) 

610 stream = _ensure(result) 

611 break 

612 else: 

613 yield TEXT, text_type(result), pos 

614 

615 elif kind is SUB: 

616 # This event is a list of directives and a list of nested 

617 # events to which those directives should be applied 

618 push(stream) 

619 stream = _apply_directives(data[1], data[0], ctxt, vars) 

620 break 

621 

622 elif kind is EXEC: 

623 _exec_suite(data, ctxt, vars) 

624 

625 else: 

626 yield kind, data, pos 

627 

628 else: 

629 if not stack: 

630 break 

631 stream = pop() 

632 

633 def _include(self, stream, ctxt, **vars): 

634 """Internal stream filter that performs inclusion of external 

635 template files. 

636 """ 

637 from genshi.template.loader import TemplateNotFound 

638 

639 for event in stream: 

640 if event[0] is INCLUDE: 

641 href, cls, fallback = event[1] 

642 if not isinstance(href, string_types): 

643 parts = [] 

644 for subkind, subdata, subpos in self._flatten(href, ctxt, 

645 **vars): 

646 if subkind is TEXT: 

647 parts.append(subdata) 

648 href = ''.join([x for x in parts if x is not None]) 

649 try: 

650 tmpl = self.loader.load(href, relative_to=event[2][0], 

651 cls=cls or self.__class__) 

652 for event in tmpl.generate(ctxt, **vars): 

653 yield event 

654 except TemplateNotFound: 

655 if fallback is None: 

656 raise 

657 for filter_ in self.filters: 

658 fallback = filter_(iter(fallback), ctxt, **vars) 

659 for event in fallback: 

660 yield event 

661 else: 

662 yield event 

663 

664 

665EXEC = Template.EXEC 

666EXPR = Template.EXPR 

667INCLUDE = Template.INCLUDE 

668SUB = Template.SUB