ext.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. """Extension API for adding custom tags and behavior."""
  2. import pprint
  3. import re
  4. import typing as t
  5. from markupsafe import Markup
  6. from . import defaults
  7. from . import nodes
  8. from .environment import Environment
  9. from .exceptions import TemplateAssertionError
  10. from .exceptions import TemplateSyntaxError
  11. from .runtime import concat # type: ignore
  12. from .runtime import Context
  13. from .runtime import Undefined
  14. from .utils import import_string
  15. from .utils import pass_context
  16. if t.TYPE_CHECKING:
  17. import typing_extensions as te
  18. from .lexer import Token
  19. from .lexer import TokenStream
  20. from .parser import Parser
  21. class _TranslationsBasic(te.Protocol):
  22. def gettext(self, message: str) -> str: ...
  23. def ngettext(self, singular: str, plural: str, n: int) -> str:
  24. pass
  25. class _TranslationsContext(_TranslationsBasic):
  26. def pgettext(self, context: str, message: str) -> str: ...
  27. def npgettext(
  28. self, context: str, singular: str, plural: str, n: int
  29. ) -> str: ...
  30. _SupportedTranslations = t.Union[_TranslationsBasic, _TranslationsContext]
  31. # I18N functions available in Jinja templates. If the I18N library
  32. # provides ugettext, it will be assigned to gettext.
  33. GETTEXT_FUNCTIONS: t.Tuple[str, ...] = (
  34. "_",
  35. "gettext",
  36. "ngettext",
  37. "pgettext",
  38. "npgettext",
  39. )
  40. _ws_re = re.compile(r"\s*\n\s*")
  41. class Extension:
  42. """Extensions can be used to add extra functionality to the Jinja template
  43. system at the parser level. Custom extensions are bound to an environment
  44. but may not store environment specific data on `self`. The reason for
  45. this is that an extension can be bound to another environment (for
  46. overlays) by creating a copy and reassigning the `environment` attribute.
  47. As extensions are created by the environment they cannot accept any
  48. arguments for configuration. One may want to work around that by using
  49. a factory function, but that is not possible as extensions are identified
  50. by their import name. The correct way to configure the extension is
  51. storing the configuration values on the environment. Because this way the
  52. environment ends up acting as central configuration storage the
  53. attributes may clash which is why extensions have to ensure that the names
  54. they choose for configuration are not too generic. ``prefix`` for example
  55. is a terrible name, ``fragment_cache_prefix`` on the other hand is a good
  56. name as includes the name of the extension (fragment cache).
  57. """
  58. identifier: t.ClassVar[str]
  59. def __init_subclass__(cls) -> None:
  60. cls.identifier = f"{cls.__module__}.{cls.__name__}"
  61. #: if this extension parses this is the list of tags it's listening to.
  62. tags: t.Set[str] = set()
  63. #: the priority of that extension. This is especially useful for
  64. #: extensions that preprocess values. A lower value means higher
  65. #: priority.
  66. #:
  67. #: .. versionadded:: 2.4
  68. priority = 100
  69. def __init__(self, environment: Environment) -> None:
  70. self.environment = environment
  71. def bind(self, environment: Environment) -> "Extension":
  72. """Create a copy of this extension bound to another environment."""
  73. rv = object.__new__(self.__class__)
  74. rv.__dict__.update(self.__dict__)
  75. rv.environment = environment
  76. return rv
  77. def preprocess(
  78. self, source: str, name: t.Optional[str], filename: t.Optional[str] = None
  79. ) -> str:
  80. """This method is called before the actual lexing and can be used to
  81. preprocess the source. The `filename` is optional. The return value
  82. must be the preprocessed source.
  83. """
  84. return source
  85. def filter_stream(
  86. self, stream: "TokenStream"
  87. ) -> t.Union["TokenStream", t.Iterable["Token"]]:
  88. """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used
  89. to filter tokens returned. This method has to return an iterable of
  90. :class:`~jinja2.lexer.Token`\\s, but it doesn't have to return a
  91. :class:`~jinja2.lexer.TokenStream`.
  92. """
  93. return stream
  94. def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
  95. """If any of the :attr:`tags` matched this method is called with the
  96. parser as first argument. The token the parser stream is pointing at
  97. is the name token that matched. This method has to return one or a
  98. list of multiple nodes.
  99. """
  100. raise NotImplementedError()
  101. def attr(
  102. self, name: str, lineno: t.Optional[int] = None
  103. ) -> nodes.ExtensionAttribute:
  104. """Return an attribute node for the current extension. This is useful
  105. to pass constants on extensions to generated template code.
  106. ::
  107. self.attr('_my_attribute', lineno=lineno)
  108. """
  109. return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno)
  110. def call_method(
  111. self,
  112. name: str,
  113. args: t.Optional[t.List[nodes.Expr]] = None,
  114. kwargs: t.Optional[t.List[nodes.Keyword]] = None,
  115. dyn_args: t.Optional[nodes.Expr] = None,
  116. dyn_kwargs: t.Optional[nodes.Expr] = None,
  117. lineno: t.Optional[int] = None,
  118. ) -> nodes.Call:
  119. """Call a method of the extension. This is a shortcut for
  120. :meth:`attr` + :class:`jinja2.nodes.Call`.
  121. """
  122. if args is None:
  123. args = []
  124. if kwargs is None:
  125. kwargs = []
  126. return nodes.Call(
  127. self.attr(name, lineno=lineno),
  128. args,
  129. kwargs,
  130. dyn_args,
  131. dyn_kwargs,
  132. lineno=lineno,
  133. )
  134. @pass_context
  135. def _gettext_alias(
  136. __context: Context, *args: t.Any, **kwargs: t.Any
  137. ) -> t.Union[t.Any, Undefined]:
  138. return __context.call(__context.resolve("gettext"), *args, **kwargs)
  139. def _make_new_gettext(func: t.Callable[[str], str]) -> t.Callable[..., str]:
  140. @pass_context
  141. def gettext(__context: Context, __string: str, **variables: t.Any) -> str:
  142. rv = __context.call(func, __string)
  143. if __context.eval_ctx.autoescape:
  144. rv = Markup(rv)
  145. # Always treat as a format string, even if there are no
  146. # variables. This makes translation strings more consistent
  147. # and predictable. This requires escaping
  148. return rv % variables # type: ignore
  149. return gettext
  150. def _make_new_ngettext(func: t.Callable[[str, str, int], str]) -> t.Callable[..., str]:
  151. @pass_context
  152. def ngettext(
  153. __context: Context,
  154. __singular: str,
  155. __plural: str,
  156. __num: int,
  157. **variables: t.Any,
  158. ) -> str:
  159. variables.setdefault("num", __num)
  160. rv = __context.call(func, __singular, __plural, __num)
  161. if __context.eval_ctx.autoescape:
  162. rv = Markup(rv)
  163. # Always treat as a format string, see gettext comment above.
  164. return rv % variables # type: ignore
  165. return ngettext
  166. def _make_new_pgettext(func: t.Callable[[str, str], str]) -> t.Callable[..., str]:
  167. @pass_context
  168. def pgettext(
  169. __context: Context, __string_ctx: str, __string: str, **variables: t.Any
  170. ) -> str:
  171. variables.setdefault("context", __string_ctx)
  172. rv = __context.call(func, __string_ctx, __string)
  173. if __context.eval_ctx.autoescape:
  174. rv = Markup(rv)
  175. # Always treat as a format string, see gettext comment above.
  176. return rv % variables # type: ignore
  177. return pgettext
  178. def _make_new_npgettext(
  179. func: t.Callable[[str, str, str, int], str],
  180. ) -> t.Callable[..., str]:
  181. @pass_context
  182. def npgettext(
  183. __context: Context,
  184. __string_ctx: str,
  185. __singular: str,
  186. __plural: str,
  187. __num: int,
  188. **variables: t.Any,
  189. ) -> str:
  190. variables.setdefault("context", __string_ctx)
  191. variables.setdefault("num", __num)
  192. rv = __context.call(func, __string_ctx, __singular, __plural, __num)
  193. if __context.eval_ctx.autoescape:
  194. rv = Markup(rv)
  195. # Always treat as a format string, see gettext comment above.
  196. return rv % variables # type: ignore
  197. return npgettext
  198. class InternationalizationExtension(Extension):
  199. """This extension adds gettext support to Jinja."""
  200. tags = {"trans"}
  201. # TODO: the i18n extension is currently reevaluating values in a few
  202. # situations. Take this example:
  203. # {% trans count=something() %}{{ count }} foo{% pluralize
  204. # %}{{ count }} fooss{% endtrans %}
  205. # something is called twice here. One time for the gettext value and
  206. # the other time for the n-parameter of the ngettext function.
  207. def __init__(self, environment: Environment) -> None:
  208. super().__init__(environment)
  209. environment.globals["_"] = _gettext_alias
  210. environment.extend(
  211. install_gettext_translations=self._install,
  212. install_null_translations=self._install_null,
  213. install_gettext_callables=self._install_callables,
  214. uninstall_gettext_translations=self._uninstall,
  215. extract_translations=self._extract,
  216. newstyle_gettext=False,
  217. )
  218. def _install(
  219. self, translations: "_SupportedTranslations", newstyle: t.Optional[bool] = None
  220. ) -> None:
  221. # ugettext and ungettext are preferred in case the I18N library
  222. # is providing compatibility with older Python versions.
  223. gettext = getattr(translations, "ugettext", None)
  224. if gettext is None:
  225. gettext = translations.gettext
  226. ngettext = getattr(translations, "ungettext", None)
  227. if ngettext is None:
  228. ngettext = translations.ngettext
  229. pgettext = getattr(translations, "pgettext", None)
  230. npgettext = getattr(translations, "npgettext", None)
  231. self._install_callables(
  232. gettext, ngettext, newstyle=newstyle, pgettext=pgettext, npgettext=npgettext
  233. )
  234. def _install_null(self, newstyle: t.Optional[bool] = None) -> None:
  235. import gettext
  236. translations = gettext.NullTranslations()
  237. if hasattr(translations, "pgettext"):
  238. # Python < 3.8
  239. pgettext = translations.pgettext
  240. else:
  241. def pgettext(c: str, s: str) -> str: # type: ignore[misc]
  242. return s
  243. if hasattr(translations, "npgettext"):
  244. npgettext = translations.npgettext
  245. else:
  246. def npgettext(c: str, s: str, p: str, n: int) -> str: # type: ignore[misc]
  247. return s if n == 1 else p
  248. self._install_callables(
  249. gettext=translations.gettext,
  250. ngettext=translations.ngettext,
  251. newstyle=newstyle,
  252. pgettext=pgettext,
  253. npgettext=npgettext,
  254. )
  255. def _install_callables(
  256. self,
  257. gettext: t.Callable[[str], str],
  258. ngettext: t.Callable[[str, str, int], str],
  259. newstyle: t.Optional[bool] = None,
  260. pgettext: t.Optional[t.Callable[[str, str], str]] = None,
  261. npgettext: t.Optional[t.Callable[[str, str, str, int], str]] = None,
  262. ) -> None:
  263. if newstyle is not None:
  264. self.environment.newstyle_gettext = newstyle # type: ignore
  265. if self.environment.newstyle_gettext: # type: ignore
  266. gettext = _make_new_gettext(gettext)
  267. ngettext = _make_new_ngettext(ngettext)
  268. if pgettext is not None:
  269. pgettext = _make_new_pgettext(pgettext)
  270. if npgettext is not None:
  271. npgettext = _make_new_npgettext(npgettext)
  272. self.environment.globals.update(
  273. gettext=gettext, ngettext=ngettext, pgettext=pgettext, npgettext=npgettext
  274. )
  275. def _uninstall(self, translations: "_SupportedTranslations") -> None:
  276. for key in ("gettext", "ngettext", "pgettext", "npgettext"):
  277. self.environment.globals.pop(key, None)
  278. def _extract(
  279. self,
  280. source: t.Union[str, nodes.Template],
  281. gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
  282. ) -> t.Iterator[
  283. t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
  284. ]:
  285. if isinstance(source, str):
  286. source = self.environment.parse(source)
  287. return extract_from_ast(source, gettext_functions)
  288. def parse(self, parser: "Parser") -> t.Union[nodes.Node, t.List[nodes.Node]]:
  289. """Parse a translatable tag."""
  290. lineno = next(parser.stream).lineno
  291. context = None
  292. context_token = parser.stream.next_if("string")
  293. if context_token is not None:
  294. context = context_token.value
  295. # find all the variables referenced. Additionally a variable can be
  296. # defined in the body of the trans block too, but this is checked at
  297. # a later state.
  298. plural_expr: t.Optional[nodes.Expr] = None
  299. plural_expr_assignment: t.Optional[nodes.Assign] = None
  300. num_called_num = False
  301. variables: t.Dict[str, nodes.Expr] = {}
  302. trimmed = None
  303. while parser.stream.current.type != "block_end":
  304. if variables:
  305. parser.stream.expect("comma")
  306. # skip colon for python compatibility
  307. if parser.stream.skip_if("colon"):
  308. break
  309. token = parser.stream.expect("name")
  310. if token.value in variables:
  311. parser.fail(
  312. f"translatable variable {token.value!r} defined twice.",
  313. token.lineno,
  314. exc=TemplateAssertionError,
  315. )
  316. # expressions
  317. if parser.stream.current.type == "assign":
  318. next(parser.stream)
  319. variables[token.value] = var = parser.parse_expression()
  320. elif trimmed is None and token.value in ("trimmed", "notrimmed"):
  321. trimmed = token.value == "trimmed"
  322. continue
  323. else:
  324. variables[token.value] = var = nodes.Name(token.value, "load")
  325. if plural_expr is None:
  326. if isinstance(var, nodes.Call):
  327. plural_expr = nodes.Name("_trans", "load")
  328. variables[token.value] = plural_expr
  329. plural_expr_assignment = nodes.Assign(
  330. nodes.Name("_trans", "store"), var
  331. )
  332. else:
  333. plural_expr = var
  334. num_called_num = token.value == "num"
  335. parser.stream.expect("block_end")
  336. plural = None
  337. have_plural = False
  338. referenced = set()
  339. # now parse until endtrans or pluralize
  340. singular_names, singular = self._parse_block(parser, True)
  341. if singular_names:
  342. referenced.update(singular_names)
  343. if plural_expr is None:
  344. plural_expr = nodes.Name(singular_names[0], "load")
  345. num_called_num = singular_names[0] == "num"
  346. # if we have a pluralize block, we parse that too
  347. if parser.stream.current.test("name:pluralize"):
  348. have_plural = True
  349. next(parser.stream)
  350. if parser.stream.current.type != "block_end":
  351. token = parser.stream.expect("name")
  352. if token.value not in variables:
  353. parser.fail(
  354. f"unknown variable {token.value!r} for pluralization",
  355. token.lineno,
  356. exc=TemplateAssertionError,
  357. )
  358. plural_expr = variables[token.value]
  359. num_called_num = token.value == "num"
  360. parser.stream.expect("block_end")
  361. plural_names, plural = self._parse_block(parser, False)
  362. next(parser.stream)
  363. referenced.update(plural_names)
  364. else:
  365. next(parser.stream)
  366. # register free names as simple name expressions
  367. for name in referenced:
  368. if name not in variables:
  369. variables[name] = nodes.Name(name, "load")
  370. if not have_plural:
  371. plural_expr = None
  372. elif plural_expr is None:
  373. parser.fail("pluralize without variables", lineno)
  374. if trimmed is None:
  375. trimmed = self.environment.policies["ext.i18n.trimmed"]
  376. if trimmed:
  377. singular = self._trim_whitespace(singular)
  378. if plural:
  379. plural = self._trim_whitespace(plural)
  380. node = self._make_node(
  381. singular,
  382. plural,
  383. context,
  384. variables,
  385. plural_expr,
  386. bool(referenced),
  387. num_called_num and have_plural,
  388. )
  389. node.set_lineno(lineno)
  390. if plural_expr_assignment is not None:
  391. return [plural_expr_assignment, node]
  392. else:
  393. return node
  394. def _trim_whitespace(self, string: str, _ws_re: t.Pattern[str] = _ws_re) -> str:
  395. return _ws_re.sub(" ", string.strip())
  396. def _parse_block(
  397. self, parser: "Parser", allow_pluralize: bool
  398. ) -> t.Tuple[t.List[str], str]:
  399. """Parse until the next block tag with a given name."""
  400. referenced = []
  401. buf = []
  402. while True:
  403. if parser.stream.current.type == "data":
  404. buf.append(parser.stream.current.value.replace("%", "%%"))
  405. next(parser.stream)
  406. elif parser.stream.current.type == "variable_begin":
  407. next(parser.stream)
  408. name = parser.stream.expect("name").value
  409. referenced.append(name)
  410. buf.append(f"%({name})s")
  411. parser.stream.expect("variable_end")
  412. elif parser.stream.current.type == "block_begin":
  413. next(parser.stream)
  414. block_name = (
  415. parser.stream.current.value
  416. if parser.stream.current.type == "name"
  417. else None
  418. )
  419. if block_name == "endtrans":
  420. break
  421. elif block_name == "pluralize":
  422. if allow_pluralize:
  423. break
  424. parser.fail(
  425. "a translatable section can have only one pluralize section"
  426. )
  427. elif block_name == "trans":
  428. parser.fail(
  429. "trans blocks can't be nested; did you mean `endtrans`?"
  430. )
  431. parser.fail(
  432. f"control structures in translatable sections are not allowed; "
  433. f"saw `{block_name}`"
  434. )
  435. elif parser.stream.eos:
  436. parser.fail("unclosed translation block")
  437. else:
  438. raise RuntimeError("internal parser error")
  439. return referenced, concat(buf)
  440. def _make_node(
  441. self,
  442. singular: str,
  443. plural: t.Optional[str],
  444. context: t.Optional[str],
  445. variables: t.Dict[str, nodes.Expr],
  446. plural_expr: t.Optional[nodes.Expr],
  447. vars_referenced: bool,
  448. num_called_num: bool,
  449. ) -> nodes.Output:
  450. """Generates a useful node from the data provided."""
  451. newstyle = self.environment.newstyle_gettext # type: ignore
  452. node: nodes.Expr
  453. # no variables referenced? no need to escape for old style
  454. # gettext invocations only if there are vars.
  455. if not vars_referenced and not newstyle:
  456. singular = singular.replace("%%", "%")
  457. if plural:
  458. plural = plural.replace("%%", "%")
  459. func_name = "gettext"
  460. func_args: t.List[nodes.Expr] = [nodes.Const(singular)]
  461. if context is not None:
  462. func_args.insert(0, nodes.Const(context))
  463. func_name = f"p{func_name}"
  464. if plural_expr is not None:
  465. func_name = f"n{func_name}"
  466. func_args.extend((nodes.Const(plural), plural_expr))
  467. node = nodes.Call(nodes.Name(func_name, "load"), func_args, [], None, None)
  468. # in case newstyle gettext is used, the method is powerful
  469. # enough to handle the variable expansion and autoescape
  470. # handling itself
  471. if newstyle:
  472. for key, value in variables.items():
  473. # the function adds that later anyways in case num was
  474. # called num, so just skip it.
  475. if num_called_num and key == "num":
  476. continue
  477. node.kwargs.append(nodes.Keyword(key, value))
  478. # otherwise do that here
  479. else:
  480. # mark the return value as safe if we are in an
  481. # environment with autoescaping turned on
  482. node = nodes.MarkSafeIfAutoescape(node)
  483. if variables:
  484. node = nodes.Mod(
  485. node,
  486. nodes.Dict(
  487. [
  488. nodes.Pair(nodes.Const(key), value)
  489. for key, value in variables.items()
  490. ]
  491. ),
  492. )
  493. return nodes.Output([node])
  494. class ExprStmtExtension(Extension):
  495. """Adds a `do` tag to Jinja that works like the print statement just
  496. that it doesn't print the return value.
  497. """
  498. tags = {"do"}
  499. def parse(self, parser: "Parser") -> nodes.ExprStmt:
  500. node = nodes.ExprStmt(lineno=next(parser.stream).lineno)
  501. node.node = parser.parse_tuple()
  502. return node
  503. class LoopControlExtension(Extension):
  504. """Adds break and continue to the template engine."""
  505. tags = {"break", "continue"}
  506. def parse(self, parser: "Parser") -> t.Union[nodes.Break, nodes.Continue]:
  507. token = next(parser.stream)
  508. if token.value == "break":
  509. return nodes.Break(lineno=token.lineno)
  510. return nodes.Continue(lineno=token.lineno)
  511. class DebugExtension(Extension):
  512. """A ``{% debug %}`` tag that dumps the available variables,
  513. filters, and tests.
  514. .. code-block:: html+jinja
  515. <pre>{% debug %}</pre>
  516. .. code-block:: text
  517. {'context': {'cycler': <class 'jinja2.utils.Cycler'>,
  518. ...,
  519. 'namespace': <class 'jinja2.utils.Namespace'>},
  520. 'filters': ['abs', 'attr', 'batch', 'capitalize', 'center', 'count', 'd',
  521. ..., 'urlencode', 'urlize', 'wordcount', 'wordwrap', 'xmlattr'],
  522. 'tests': ['!=', '<', '<=', '==', '>', '>=', 'callable', 'defined',
  523. ..., 'odd', 'sameas', 'sequence', 'string', 'undefined', 'upper']}
  524. .. versionadded:: 2.11.0
  525. """
  526. tags = {"debug"}
  527. def parse(self, parser: "Parser") -> nodes.Output:
  528. lineno = parser.stream.expect("name:debug").lineno
  529. context = nodes.ContextReference()
  530. result = self.call_method("_render", [context], lineno=lineno)
  531. return nodes.Output([result], lineno=lineno)
  532. def _render(self, context: Context) -> str:
  533. result = {
  534. "context": context.get_all(),
  535. "filters": sorted(self.environment.filters.keys()),
  536. "tests": sorted(self.environment.tests.keys()),
  537. }
  538. # Set the depth since the intent is to show the top few names.
  539. return pprint.pformat(result, depth=3, compact=True)
  540. def extract_from_ast(
  541. ast: nodes.Template,
  542. gettext_functions: t.Sequence[str] = GETTEXT_FUNCTIONS,
  543. babel_style: bool = True,
  544. ) -> t.Iterator[
  545. t.Tuple[int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]]
  546. ]:
  547. """Extract localizable strings from the given template node. Per
  548. default this function returns matches in babel style that means non string
  549. parameters as well as keyword arguments are returned as `None`. This
  550. allows Babel to figure out what you really meant if you are using
  551. gettext functions that allow keyword arguments for placeholder expansion.
  552. If you don't want that behavior set the `babel_style` parameter to `False`
  553. which causes only strings to be returned and parameters are always stored
  554. in tuples. As a consequence invalid gettext calls (calls without a single
  555. string parameter or string parameters after non-string parameters) are
  556. skipped.
  557. This example explains the behavior:
  558. >>> from jinja2 import Environment
  559. >>> env = Environment()
  560. >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}')
  561. >>> list(extract_from_ast(node))
  562. [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))]
  563. >>> list(extract_from_ast(node, babel_style=False))
  564. [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))]
  565. For every string found this function yields a ``(lineno, function,
  566. message)`` tuple, where:
  567. * ``lineno`` is the number of the line on which the string was found,
  568. * ``function`` is the name of the ``gettext`` function used (if the
  569. string was extracted from embedded Python code), and
  570. * ``message`` is the string, or a tuple of strings for functions
  571. with multiple string arguments.
  572. This extraction function operates on the AST and is because of that unable
  573. to extract any comments. For comment support you have to use the babel
  574. extraction interface or extract comments yourself.
  575. """
  576. out: t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]]
  577. for node in ast.find_all(nodes.Call):
  578. if (
  579. not isinstance(node.node, nodes.Name)
  580. or node.node.name not in gettext_functions
  581. ):
  582. continue
  583. strings: t.List[t.Optional[str]] = []
  584. for arg in node.args:
  585. if isinstance(arg, nodes.Const) and isinstance(arg.value, str):
  586. strings.append(arg.value)
  587. else:
  588. strings.append(None)
  589. for _ in node.kwargs:
  590. strings.append(None)
  591. if node.dyn_args is not None:
  592. strings.append(None)
  593. if node.dyn_kwargs is not None:
  594. strings.append(None)
  595. if not babel_style:
  596. out = tuple(x for x in strings if x is not None)
  597. if not out:
  598. continue
  599. else:
  600. if len(strings) == 1:
  601. out = strings[0]
  602. else:
  603. out = tuple(strings)
  604. yield node.lineno, node.node.name, out
  605. class _CommentFinder:
  606. """Helper class to find comments in a token stream. Can only
  607. find comments for gettext calls forwards. Once the comment
  608. from line 4 is found, a comment for line 1 will not return a
  609. usable value.
  610. """
  611. def __init__(
  612. self, tokens: t.Sequence[t.Tuple[int, str, str]], comment_tags: t.Sequence[str]
  613. ) -> None:
  614. self.tokens = tokens
  615. self.comment_tags = comment_tags
  616. self.offset = 0
  617. self.last_lineno = 0
  618. def find_backwards(self, offset: int) -> t.List[str]:
  619. try:
  620. for _, token_type, token_value in reversed(
  621. self.tokens[self.offset : offset]
  622. ):
  623. if token_type in ("comment", "linecomment"):
  624. try:
  625. prefix, comment = token_value.split(None, 1)
  626. except ValueError:
  627. continue
  628. if prefix in self.comment_tags:
  629. return [comment.rstrip()]
  630. return []
  631. finally:
  632. self.offset = offset
  633. def find_comments(self, lineno: int) -> t.List[str]:
  634. if not self.comment_tags or self.last_lineno > lineno:
  635. return []
  636. for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset :]):
  637. if token_lineno > lineno:
  638. return self.find_backwards(self.offset + idx)
  639. return self.find_backwards(len(self.tokens))
  640. def babel_extract(
  641. fileobj: t.BinaryIO,
  642. keywords: t.Sequence[str],
  643. comment_tags: t.Sequence[str],
  644. options: t.Dict[str, t.Any],
  645. ) -> t.Iterator[
  646. t.Tuple[
  647. int, str, t.Union[t.Optional[str], t.Tuple[t.Optional[str], ...]], t.List[str]
  648. ]
  649. ]:
  650. """Babel extraction method for Jinja templates.
  651. .. versionchanged:: 2.3
  652. Basic support for translation comments was added. If `comment_tags`
  653. is now set to a list of keywords for extraction, the extractor will
  654. try to find the best preceding comment that begins with one of the
  655. keywords. For best results, make sure to not have more than one
  656. gettext call in one line of code and the matching comment in the
  657. same line or the line before.
  658. .. versionchanged:: 2.5.1
  659. The `newstyle_gettext` flag can be set to `True` to enable newstyle
  660. gettext calls.
  661. .. versionchanged:: 2.7
  662. A `silent` option can now be provided. If set to `False` template
  663. syntax errors are propagated instead of being ignored.
  664. :param fileobj: the file-like object the messages should be extracted from
  665. :param keywords: a list of keywords (i.e. function names) that should be
  666. recognized as translation functions
  667. :param comment_tags: a list of translator tags to search for and include
  668. in the results.
  669. :param options: a dictionary of additional options (optional)
  670. :return: an iterator over ``(lineno, funcname, message, comments)`` tuples.
  671. (comments will be empty currently)
  672. """
  673. extensions: t.Dict[t.Type[Extension], None] = {}
  674. for extension_name in options.get("extensions", "").split(","):
  675. extension_name = extension_name.strip()
  676. if not extension_name:
  677. continue
  678. extensions[import_string(extension_name)] = None
  679. if InternationalizationExtension not in extensions:
  680. extensions[InternationalizationExtension] = None
  681. def getbool(options: t.Mapping[str, str], key: str, default: bool = False) -> bool:
  682. return options.get(key, str(default)).lower() in {"1", "on", "yes", "true"}
  683. silent = getbool(options, "silent", True)
  684. environment = Environment(
  685. options.get("block_start_string", defaults.BLOCK_START_STRING),
  686. options.get("block_end_string", defaults.BLOCK_END_STRING),
  687. options.get("variable_start_string", defaults.VARIABLE_START_STRING),
  688. options.get("variable_end_string", defaults.VARIABLE_END_STRING),
  689. options.get("comment_start_string", defaults.COMMENT_START_STRING),
  690. options.get("comment_end_string", defaults.COMMENT_END_STRING),
  691. options.get("line_statement_prefix") or defaults.LINE_STATEMENT_PREFIX,
  692. options.get("line_comment_prefix") or defaults.LINE_COMMENT_PREFIX,
  693. getbool(options, "trim_blocks", defaults.TRIM_BLOCKS),
  694. getbool(options, "lstrip_blocks", defaults.LSTRIP_BLOCKS),
  695. defaults.NEWLINE_SEQUENCE,
  696. getbool(options, "keep_trailing_newline", defaults.KEEP_TRAILING_NEWLINE),
  697. tuple(extensions),
  698. cache_size=0,
  699. auto_reload=False,
  700. )
  701. if getbool(options, "trimmed"):
  702. environment.policies["ext.i18n.trimmed"] = True
  703. if getbool(options, "newstyle_gettext"):
  704. environment.newstyle_gettext = True # type: ignore
  705. source = fileobj.read().decode(options.get("encoding", "utf-8"))
  706. try:
  707. node = environment.parse(source)
  708. tokens = list(environment.lex(environment.preprocess(source)))
  709. except TemplateSyntaxError:
  710. if not silent:
  711. raise
  712. # skip templates with syntax errors
  713. return
  714. finder = _CommentFinder(tokens, comment_tags)
  715. for lineno, func, message in extract_from_ast(node, keywords):
  716. yield lineno, func, message, finder.find_comments(lineno)
  717. #: nicer import names
  718. i18n = InternationalizationExtension
  719. do = ExprStmtExtension
  720. loopcontrols = LoopControlExtension
  721. debug = DebugExtension