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

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

318 statements  

1import enum 

2import json 

3import os 

4import re 

5import typing as t 

6from collections import abc 

7from collections import deque 

8from random import choice 

9from random import randrange 

10from threading import Lock 

11from types import CodeType 

12from urllib.parse import quote_from_bytes 

13 

14import markupsafe 

15 

16if t.TYPE_CHECKING: 

17 import typing_extensions as te 

18 

19F = t.TypeVar("F", bound=t.Callable[..., t.Any]) 

20 

21 

22class _MissingType: 

23 def __repr__(self) -> str: 

24 return "missing" 

25 

26 def __reduce__(self) -> str: 

27 return "missing" 

28 

29 

30missing: t.Any = _MissingType() 

31"""Special singleton representing missing values for the runtime.""" 

32 

33internal_code: t.MutableSet[CodeType] = set() 

34 

35concat = "".join 

36 

37 

38def pass_context(f: F) -> F: 

39 """Pass the :class:`~jinja2.runtime.Context` as the first argument 

40 to the decorated function when called while rendering a template. 

41 

42 Can be used on functions, filters, and tests. 

43 

44 If only ``Context.eval_context`` is needed, use 

45 :func:`pass_eval_context`. If only ``Context.environment`` is 

46 needed, use :func:`pass_environment`. 

47 

48 .. versionadded:: 3.0.0 

49 Replaces ``contextfunction`` and ``contextfilter``. 

50 """ 

51 f.jinja_pass_arg = _PassArg.context # type: ignore 

52 return f 

53 

54 

55def pass_eval_context(f: F) -> F: 

56 """Pass the :class:`~jinja2.nodes.EvalContext` as the first argument 

57 to the decorated function when called while rendering a template. 

58 See :ref:`eval-context`. 

59 

60 Can be used on functions, filters, and tests. 

61 

62 If only ``EvalContext.environment`` is needed, use 

63 :func:`pass_environment`. 

64 

65 .. versionadded:: 3.0.0 

66 Replaces ``evalcontextfunction`` and ``evalcontextfilter``. 

67 """ 

68 f.jinja_pass_arg = _PassArg.eval_context # type: ignore 

69 return f 

70 

71 

72def pass_environment(f: F) -> F: 

73 """Pass the :class:`~jinja2.Environment` as the first argument to 

74 the decorated function when called while rendering a template. 

75 

76 Can be used on functions, filters, and tests. 

77 

78 .. versionadded:: 3.0.0 

79 Replaces ``environmentfunction`` and ``environmentfilter``. 

80 """ 

81 f.jinja_pass_arg = _PassArg.environment # type: ignore 

82 return f 

83 

84 

85class _PassArg(enum.Enum): 

86 context = enum.auto() 

87 eval_context = enum.auto() 

88 environment = enum.auto() 

89 

90 @classmethod 

91 def from_obj(cls, obj: F) -> t.Optional["_PassArg"]: 

92 if hasattr(obj, "jinja_pass_arg"): 

93 return obj.jinja_pass_arg # type: ignore 

94 

95 return None 

96 

97 

98def internalcode(f: F) -> F: 

99 """Marks the function as internally used""" 

100 internal_code.add(f.__code__) 

101 return f 

102 

103 

104def is_undefined(obj: t.Any) -> bool: 

105 """Check if the object passed is undefined. This does nothing more than 

106 performing an instance check against :class:`Undefined` but looks nicer. 

107 This can be used for custom filters or tests that want to react to 

108 undefined variables. For example a custom default filter can look like 

109 this:: 

110 

111 def default(var, default=''): 

112 if is_undefined(var): 

113 return default 

114 return var 

115 """ 

116 from .runtime import Undefined 

117 

118 return isinstance(obj, Undefined) 

119 

120 

121def consume(iterable: t.Iterable[t.Any]) -> None: 

122 """Consumes an iterable without doing anything with it.""" 

123 for _ in iterable: 

124 pass 

125 

126 

127def clear_caches() -> None: 

128 """Jinja keeps internal caches for environments and lexers. These are 

129 used so that Jinja doesn't have to recreate environments and lexers all 

130 the time. Normally you don't have to care about that but if you are 

131 measuring memory consumption you may want to clean the caches. 

132 """ 

133 from .environment import get_spontaneous_environment 

134 from .lexer import _lexer_cache 

135 

136 get_spontaneous_environment.cache_clear() 

137 _lexer_cache.clear() 

138 

139 

140def import_string(import_name: str, silent: bool = False) -> t.Any: 

141 """Imports an object based on a string. This is useful if you want to 

142 use import paths as endpoints or something similar. An import path can 

143 be specified either in dotted notation (``xml.sax.saxutils.escape``) 

144 or with a colon as object delimiter (``xml.sax.saxutils:escape``). 

145 

146 If the `silent` is True the return value will be `None` if the import 

147 fails. 

148 

149 :return: imported object 

150 """ 

151 try: 

152 if ":" in import_name: 

153 module, obj = import_name.split(":", 1) 

154 elif "." in import_name: 

155 module, _, obj = import_name.rpartition(".") 

156 else: 

157 return __import__(import_name) 

158 return getattr(__import__(module, None, None, [obj]), obj) 

159 except (ImportError, AttributeError): 

160 if not silent: 

161 raise 

162 

163 

164def open_if_exists(filename: str, mode: str = "rb") -> t.Optional[t.IO[t.Any]]: 

165 """Returns a file descriptor for the filename if that file exists, 

166 otherwise ``None``. 

167 """ 

168 if not os.path.isfile(filename): 

169 return None 

170 

171 return open(filename, mode) 

172 

173 

174def object_type_repr(obj: t.Any) -> str: 

175 """Returns the name of the object's type. For some recognized 

176 singletons the name of the object is returned instead. (For 

177 example for `None` and `Ellipsis`). 

178 """ 

179 if obj is None: 

180 return "None" 

181 elif obj is Ellipsis: 

182 return "Ellipsis" 

183 

184 cls = type(obj) 

185 

186 if cls.__module__ == "builtins": 

187 return f"{cls.__name__} object" 

188 

189 return f"{cls.__module__}.{cls.__name__} object" 

190 

191 

192def pformat(obj: t.Any) -> str: 

193 """Format an object using :func:`pprint.pformat`.""" 

194 from pprint import pformat 

195 

196 return pformat(obj) 

197 

198 

199_http_re = re.compile( 

200 r""" 

201 ^ 

202 ( 

203 (https?://|www\.) # scheme or www 

204 (([\w%-]+\.)+)? # subdomain 

205 ( 

206 [a-z]{2,63} # basic tld 

207 | 

208 xn--[\w%]{2,59} # idna tld 

209 ) 

210 | 

211 ([\w%-]{2,63}\.)+ # basic domain 

212 (com|net|int|edu|gov|org|info|mil) # basic tld 

213 | 

214 (https?://) # scheme 

215 ( 

216 (([\d]{1,3})(\.[\d]{1,3}){3}) # IPv4 

217 | 

218 (\[([\da-f]{0,4}:){2}([\da-f]{0,4}:?){1,6}]) # IPv6 

219 ) 

220 ) 

221 (?::[\d]{1,5})? # port 

222 (?:[/?#]\S*)? # path, query, and fragment 

223 $ 

224 """, 

225 re.IGNORECASE | re.VERBOSE, 

226) 

227_email_re = re.compile(r"^\S+@\w[\w.-]*\.\w+$") 

228 

229 

230def urlize( 

231 text: str, 

232 trim_url_limit: t.Optional[int] = None, 

233 rel: t.Optional[str] = None, 

234 target: t.Optional[str] = None, 

235 extra_schemes: t.Optional[t.Iterable[str]] = None, 

236) -> str: 

237 """Convert URLs in text into clickable links. 

238 

239 This may not recognize links in some situations. Usually, a more 

240 comprehensive formatter, such as a Markdown library, is a better 

241 choice. 

242 

243 Works on ``http://``, ``https://``, ``www.``, ``mailto:``, and email 

244 addresses. Links with trailing punctuation (periods, commas, closing 

245 parentheses) and leading punctuation (opening parentheses) are 

246 recognized excluding the punctuation. Email addresses that include 

247 header fields are not recognized (for example, 

248 ``mailto:address@example.com?cc=copy@example.com``). 

249 

250 :param text: Original text containing URLs to link. 

251 :param trim_url_limit: Shorten displayed URL values to this length. 

252 :param target: Add the ``target`` attribute to links. 

253 :param rel: Add the ``rel`` attribute to links. 

254 :param extra_schemes: Recognize URLs that start with these schemes 

255 in addition to the default behavior. 

256 

257 .. versionchanged:: 3.0 

258 The ``extra_schemes`` parameter was added. 

259 

260 .. versionchanged:: 3.0 

261 Generate ``https://`` links for URLs without a scheme. 

262 

263 .. versionchanged:: 3.0 

264 The parsing rules were updated. Recognize email addresses with 

265 or without the ``mailto:`` scheme. Validate IP addresses. Ignore 

266 parentheses and brackets in more cases. 

267 """ 

268 if trim_url_limit is not None: 

269 

270 def trim_url(x: str) -> str: 

271 if len(x) > trim_url_limit: 

272 return f"{x[:trim_url_limit]}..." 

273 

274 return x 

275 

276 else: 

277 

278 def trim_url(x: str) -> str: 

279 return x 

280 

281 words = re.split(r"(\s+)", str(markupsafe.escape(text))) 

282 rel_attr = f' rel="{markupsafe.escape(rel)}"' if rel else "" 

283 target_attr = f' target="{markupsafe.escape(target)}"' if target else "" 

284 

285 for i, word in enumerate(words): 

286 head, middle, tail = "", word, "" 

287 match = re.match(r"^([(<]|&lt;)+", middle) 

288 

289 if match: 

290 head = match.group() 

291 middle = middle[match.end() :] 

292 

293 # Unlike lead, which is anchored to the start of the string, 

294 # need to check that the string ends with any of the characters 

295 # before trying to match all of them, to avoid backtracking. 

296 if middle.endswith((")", ">", ".", ",", "\n", "&gt;")): 

297 match = re.search(r"([)>.,\n]|&gt;)+$", middle) 

298 

299 if match: 

300 tail = match.group() 

301 middle = middle[: match.start()] 

302 

303 # Prefer balancing parentheses in URLs instead of ignoring a 

304 # trailing character. 

305 for start_char, end_char in ("(", ")"), ("<", ">"), ("&lt;", "&gt;"): 

306 start_count = middle.count(start_char) 

307 

308 if start_count <= middle.count(end_char): 

309 # Balanced, or lighter on the left 

310 continue 

311 

312 # Move as many as possible from the tail to balance 

313 for _ in range(min(start_count, tail.count(end_char))): 

314 end_index = tail.index(end_char) + len(end_char) 

315 # Move anything in the tail before the end char too 

316 middle += tail[:end_index] 

317 tail = tail[end_index:] 

318 

319 if _http_re.match(middle): 

320 if middle.startswith("https://") or middle.startswith("http://"): 

321 middle = ( 

322 f'<a href="{middle}"{rel_attr}{target_attr}>{trim_url(middle)}</a>' 

323 ) 

324 else: 

325 middle = ( 

326 f'<a href="https://{middle}"{rel_attr}{target_attr}>' 

327 f"{trim_url(middle)}</a>" 

328 ) 

329 

330 elif middle.startswith("mailto:") and _email_re.match(middle[7:]): 

331 middle = f'<a href="{middle}">{middle[7:]}</a>' 

332 

333 elif ( 

334 "@" in middle 

335 and not middle.startswith("www.") 

336 # ignore values like `@a@b` 

337 and not middle.startswith("@") 

338 and ":" not in middle 

339 and _email_re.match(middle) 

340 ): 

341 middle = f'<a href="mailto:{middle}">{middle}</a>' 

342 

343 elif extra_schemes is not None: 

344 for scheme in extra_schemes: 

345 if middle != scheme and middle.startswith(scheme): 

346 middle = f'<a href="{middle}"{rel_attr}{target_attr}>{middle}</a>' 

347 

348 words[i] = f"{head}{middle}{tail}" 

349 

350 return "".join(words) 

351 

352 

353def generate_lorem_ipsum( 

354 n: int = 5, html: bool = True, min: int = 20, max: int = 100 

355) -> str: 

356 """Generate some lorem ipsum for the template.""" 

357 from .constants import LOREM_IPSUM_WORDS 

358 

359 words = LOREM_IPSUM_WORDS.split() 

360 result = [] 

361 

362 for _ in range(n): 

363 next_capitalized = True 

364 last_comma = last_fullstop = 0 

365 word = None 

366 last = None 

367 p = [] 

368 

369 # each paragraph contains out of 20 to 100 words. 

370 for idx, _ in enumerate(range(randrange(min, max))): 

371 while True: 

372 word = choice(words) 

373 if word != last: 

374 last = word 

375 break 

376 if next_capitalized: 

377 word = word.capitalize() 

378 next_capitalized = False 

379 # add commas 

380 if idx - randrange(3, 8) > last_comma: 

381 last_comma = idx 

382 last_fullstop += 2 

383 word += "," 

384 # add end of sentences 

385 if idx - randrange(10, 20) > last_fullstop: 

386 last_comma = last_fullstop = idx 

387 word += "." 

388 next_capitalized = True 

389 p.append(word) 

390 

391 # ensure that the paragraph ends with a dot. 

392 p_str = " ".join(p) 

393 

394 if p_str.endswith(","): 

395 p_str = p_str[:-1] + "." 

396 elif not p_str.endswith("."): 

397 p_str += "." 

398 

399 result.append(p_str) 

400 

401 if not html: 

402 return "\n\n".join(result) 

403 return markupsafe.Markup( 

404 "\n".join(f"<p>{markupsafe.escape(x)}</p>" for x in result) 

405 ) 

406 

407 

408def url_quote(obj: t.Any, charset: str = "utf-8", for_qs: bool = False) -> str: 

409 """Quote a string for use in a URL using the given charset. 

410 

411 :param obj: String or bytes to quote. Other types are converted to 

412 string then encoded to bytes using the given charset. 

413 :param charset: Encode text to bytes using this charset. 

414 :param for_qs: Quote "/" and use "+" for spaces. 

415 """ 

416 if not isinstance(obj, bytes): 

417 if not isinstance(obj, str): 

418 obj = str(obj) 

419 

420 obj = obj.encode(charset) 

421 

422 safe = b"" if for_qs else b"/" 

423 rv = quote_from_bytes(obj, safe) 

424 

425 if for_qs: 

426 rv = rv.replace("%20", "+") 

427 

428 return rv 

429 

430 

431@abc.MutableMapping.register 

432class LRUCache: 

433 """A simple LRU Cache implementation.""" 

434 

435 # this is fast for small capacities (something below 1000) but doesn't 

436 # scale. But as long as it's only used as storage for templates this 

437 # won't do any harm. 

438 

439 def __init__(self, capacity: int) -> None: 

440 self.capacity = capacity 

441 self._mapping: t.Dict[t.Any, t.Any] = {} 

442 self._queue: te.Deque[t.Any] = deque() 

443 self._postinit() 

444 

445 def _postinit(self) -> None: 

446 # alias all queue methods for faster lookup 

447 self._popleft = self._queue.popleft 

448 self._pop = self._queue.pop 

449 self._remove = self._queue.remove 

450 self._wlock = Lock() 

451 self._append = self._queue.append 

452 

453 def __getstate__(self) -> t.Mapping[str, t.Any]: 

454 return { 

455 "capacity": self.capacity, 

456 "_mapping": self._mapping, 

457 "_queue": self._queue, 

458 } 

459 

460 def __setstate__(self, d: t.Mapping[str, t.Any]) -> None: 

461 self.__dict__.update(d) 

462 self._postinit() 

463 

464 def __getnewargs__(self) -> t.Tuple[t.Any, ...]: 

465 return (self.capacity,) 

466 

467 def copy(self) -> "te.Self": 

468 """Return a shallow copy of the instance.""" 

469 rv = self.__class__(self.capacity) 

470 rv._mapping.update(self._mapping) 

471 rv._queue.extend(self._queue) 

472 return rv 

473 

474 def get(self, key: t.Any, default: t.Any = None) -> t.Any: 

475 """Return an item from the cache dict or `default`""" 

476 try: 

477 return self[key] 

478 except KeyError: 

479 return default 

480 

481 def setdefault(self, key: t.Any, default: t.Any = None) -> t.Any: 

482 """Set `default` if the key is not in the cache otherwise 

483 leave unchanged. Return the value of this key. 

484 """ 

485 try: 

486 return self[key] 

487 except KeyError: 

488 self[key] = default 

489 return default 

490 

491 def clear(self) -> None: 

492 """Clear the cache.""" 

493 with self._wlock: 

494 self._mapping.clear() 

495 self._queue.clear() 

496 

497 def __contains__(self, key: t.Any) -> bool: 

498 """Check if a key exists in this cache.""" 

499 return key in self._mapping 

500 

501 def __len__(self) -> int: 

502 """Return the current size of the cache.""" 

503 return len(self._mapping) 

504 

505 def __repr__(self) -> str: 

506 return f"<{type(self).__name__} {self._mapping!r}>" 

507 

508 def __getitem__(self, key: t.Any) -> t.Any: 

509 """Get an item from the cache. Moves the item up so that it has the 

510 highest priority then. 

511 

512 Raise a `KeyError` if it does not exist. 

513 """ 

514 with self._wlock: 

515 rv = self._mapping[key] 

516 

517 if self._queue[-1] != key: 

518 try: 

519 self._remove(key) 

520 except ValueError: 

521 # if something removed the key from the container 

522 # when we read, ignore the ValueError that we would 

523 # get otherwise. 

524 pass 

525 

526 self._append(key) 

527 

528 return rv 

529 

530 def __setitem__(self, key: t.Any, value: t.Any) -> None: 

531 """Sets the value for an item. Moves the item up so that it 

532 has the highest priority then. 

533 """ 

534 with self._wlock: 

535 if key in self._mapping: 

536 self._remove(key) 

537 elif len(self._mapping) == self.capacity: 

538 del self._mapping[self._popleft()] 

539 

540 self._append(key) 

541 self._mapping[key] = value 

542 

543 def __delitem__(self, key: t.Any) -> None: 

544 """Remove an item from the cache dict. 

545 Raise a `KeyError` if it does not exist. 

546 """ 

547 with self._wlock: 

548 del self._mapping[key] 

549 

550 try: 

551 self._remove(key) 

552 except ValueError: 

553 pass 

554 

555 def items(self) -> t.Iterable[t.Tuple[t.Any, t.Any]]: 

556 """Return a list of items.""" 

557 result = [(key, self._mapping[key]) for key in list(self._queue)] 

558 result.reverse() 

559 return result 

560 

561 def values(self) -> t.Iterable[t.Any]: 

562 """Return a list of all values.""" 

563 return [x[1] for x in self.items()] 

564 

565 def keys(self) -> t.Iterable[t.Any]: 

566 """Return a list of all keys ordered by most recent usage.""" 

567 return list(self) 

568 

569 def __iter__(self) -> t.Iterator[t.Any]: 

570 return reversed(tuple(self._queue)) 

571 

572 def __reversed__(self) -> t.Iterator[t.Any]: 

573 """Iterate over the keys in the cache dict, oldest items 

574 coming first. 

575 """ 

576 return iter(tuple(self._queue)) 

577 

578 __copy__ = copy 

579 

580 

581def select_autoescape( 

582 enabled_extensions: t.Collection[str] = ("html", "htm", "xml"), 

583 disabled_extensions: t.Collection[str] = (), 

584 default_for_string: bool = True, 

585 default: bool = False, 

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

587 """Intelligently sets the initial value of autoescaping based on the 

588 filename of the template. This is the recommended way to configure 

589 autoescaping if you do not want to write a custom function yourself. 

590 

591 If you want to enable it for all templates created from strings or 

592 for all templates with `.html` and `.xml` extensions:: 

593 

594 from jinja2 import Environment, select_autoescape 

595 env = Environment(autoescape=select_autoescape( 

596 enabled_extensions=('html', 'xml'), 

597 default_for_string=True, 

598 )) 

599 

600 Example configuration to turn it on at all times except if the template 

601 ends with `.txt`:: 

602 

603 from jinja2 import Environment, select_autoescape 

604 env = Environment(autoescape=select_autoescape( 

605 disabled_extensions=('txt',), 

606 default_for_string=True, 

607 default=True, 

608 )) 

609 

610 The `enabled_extensions` is an iterable of all the extensions that 

611 autoescaping should be enabled for. Likewise `disabled_extensions` is 

612 a list of all templates it should be disabled for. If a template is 

613 loaded from a string then the default from `default_for_string` is used. 

614 If nothing matches then the initial value of autoescaping is set to the 

615 value of `default`. 

616 

617 For security reasons this function operates case insensitive. 

618 

619 .. versionadded:: 2.9 

620 """ 

621 enabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in enabled_extensions) 

622 disabled_patterns = tuple(f".{x.lstrip('.').lower()}" for x in disabled_extensions) 

623 

624 def autoescape(template_name: t.Optional[str]) -> bool: 

625 if template_name is None: 

626 return default_for_string 

627 template_name = template_name.lower() 

628 if template_name.endswith(enabled_patterns): 

629 return True 

630 if template_name.endswith(disabled_patterns): 

631 return False 

632 return default 

633 

634 return autoescape 

635 

636 

637def htmlsafe_json_dumps( 

638 obj: t.Any, dumps: t.Optional[t.Callable[..., str]] = None, **kwargs: t.Any 

639) -> markupsafe.Markup: 

640 """Serialize an object to a string of JSON with :func:`json.dumps`, 

641 then replace HTML-unsafe characters with Unicode escapes and mark 

642 the result safe with :class:`~markupsafe.Markup`. 

643 

644 This is available in templates as the ``|tojson`` filter. 

645 

646 The following characters are escaped: ``<``, ``>``, ``&``, ``'``. 

647 

648 The returned string is safe to render in HTML documents and 

649 ``<script>`` tags. The exception is in HTML attributes that are 

650 double quoted; either use single quotes or the ``|forceescape`` 

651 filter. 

652 

653 :param obj: The object to serialize to JSON. 

654 :param dumps: The ``dumps`` function to use. Defaults to 

655 ``env.policies["json.dumps_function"]``, which defaults to 

656 :func:`json.dumps`. 

657 :param kwargs: Extra arguments to pass to ``dumps``. Merged onto 

658 ``env.policies["json.dumps_kwargs"]``. 

659 

660 .. versionchanged:: 3.0 

661 The ``dumper`` parameter is renamed to ``dumps``. 

662 

663 .. versionadded:: 2.9 

664 """ 

665 if dumps is None: 

666 dumps = json.dumps 

667 

668 return markupsafe.Markup( 

669 dumps(obj, **kwargs) 

670 .replace("<", "\\u003c") 

671 .replace(">", "\\u003e") 

672 .replace("&", "\\u0026") 

673 .replace("'", "\\u0027") 

674 ) 

675 

676 

677class Cycler: 

678 """Cycle through values by yield them one at a time, then restarting 

679 once the end is reached. Available as ``cycler`` in templates. 

680 

681 Similar to ``loop.cycle``, but can be used outside loops or across 

682 multiple loops. For example, render a list of folders and files in a 

683 list, alternating giving them "odd" and "even" classes. 

684 

685 .. code-block:: html+jinja 

686 

687 {% set row_class = cycler("odd", "even") %} 

688 <ul class="browser"> 

689 {% for folder in folders %} 

690 <li class="folder {{ row_class.next() }}">{{ folder }} 

691 {% endfor %} 

692 {% for file in files %} 

693 <li class="file {{ row_class.next() }}">{{ file }} 

694 {% endfor %} 

695 </ul> 

696 

697 :param items: Each positional argument will be yielded in the order 

698 given for each cycle. 

699 

700 .. versionadded:: 2.1 

701 """ 

702 

703 def __init__(self, *items: t.Any) -> None: 

704 if not items: 

705 raise RuntimeError("at least one item has to be provided") 

706 self.items = items 

707 self.pos = 0 

708 

709 def reset(self) -> None: 

710 """Resets the current item to the first item.""" 

711 self.pos = 0 

712 

713 @property 

714 def current(self) -> t.Any: 

715 """Return the current item. Equivalent to the item that will be 

716 returned next time :meth:`next` is called. 

717 """ 

718 return self.items[self.pos] 

719 

720 def next(self) -> t.Any: 

721 """Return the current item, then advance :attr:`current` to the 

722 next item. 

723 """ 

724 rv = self.current 

725 self.pos = (self.pos + 1) % len(self.items) 

726 return rv 

727 

728 __next__ = next 

729 

730 

731class Joiner: 

732 """A joining helper for templates.""" 

733 

734 def __init__(self, sep: str = ", ") -> None: 

735 self.sep = sep 

736 self.used = False 

737 

738 def __call__(self) -> str: 

739 if not self.used: 

740 self.used = True 

741 return "" 

742 return self.sep 

743 

744 

745class Namespace: 

746 """A namespace object that can hold arbitrary attributes. It may be 

747 initialized from a dictionary or with keyword arguments.""" 

748 

749 def __init__(*args: t.Any, **kwargs: t.Any) -> None: # noqa: B902 

750 self, args = args[0], args[1:] 

751 self.__attrs = dict(*args, **kwargs) 

752 

753 def __getattribute__(self, name: str) -> t.Any: 

754 # __class__ is needed for the awaitable check in async mode 

755 if name in {"_Namespace__attrs", "__class__"}: 

756 return object.__getattribute__(self, name) 

757 try: 

758 return self.__attrs[name] 

759 except KeyError: 

760 raise AttributeError(name) from None 

761 

762 def __setitem__(self, name: str, value: t.Any) -> None: 

763 self.__attrs[name] = value 

764 

765 def __repr__(self) -> str: 

766 return f"<Namespace {self.__attrs!r}>"