Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions pep/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Examples for PEP 750. See README.md for details."""

import typing as t
from html.parser import HTMLParser

from templatelib import Interpolation, Template

#
# Known bugs/divergences between cpython/tstrings and the current PEP 750 spec
#
Expand Down Expand Up @@ -86,3 +89,44 @@ def parse_starttag(self, i: int) -> int:
response = super().parse_starttag(i)
print(f"parse_starttag: {i} -> {response}")
return response


class InterpolationProto(t.Protocol):
"""
This exists only to get my type checking tools, which do not currently
know about templatelib, to understand the structure of an Interpolation
when surfaced by pairs(), below.
"""

value: object
expr: str
conv: t.Literal["a", "r", "s"] | None
format_spec: str


def pairs(template: Template) -> t.Iterator[tuple[InterpolationProto | None, str]]:
"""
Yield pairs of interpolations and strings from a template.

This allows us to experiment with the structure of a template;
see the discussion here:

https://discuss.python.org/t/pep750-template-strings-new-updates/71594/65
"""
yield None, template.args[0]
for i, s in zip(template.args[1::2], template.args[2::2]):
yield i, s


def pairs_s_i(template: Template) -> t.Iterator[tuple[str, Interpolation | None]]:
"""
Yield pairs of strings and interpolations from a template.

This allows us to experiment with the structure of a template;
it is the *opposite* of Guido's pairs() proposal as discussed here:

https://discuss.python.org/t/pep750-template-strings-new-updates/71594/65
"""
for s, i in zip(template.args[::2], template.args[1::2]):
yield s, i
yield template.args[-1], None
25 changes: 12 additions & 13 deletions pep/afstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import inspect

from templatelib import Interpolation, Template
from templatelib import Template

from . import pairs
from .fstring import convert


Expand All @@ -21,16 +22,14 @@ async def async_f(template: Template) -> str:
formatting it.
"""
parts = []
for arg in template.args:
match arg:
case str() as s:
parts.append(s)
case Interpolation(value, _, conv, format_spec):
if inspect.iscoroutinefunction(value):
value = await value()
elif callable(value):
value = value()
value = convert(value, conv)
value = format(value, format_spec)
parts.append(value)
for i, s in pairs(template):
if i is not None:
if inspect.iscoroutinefunction(i.value):
value = await i.value()
elif callable(i.value):
value = i.value()
value = convert(value, i.conv)
value = format(value, i.format_spec)
parts.append(value)
parts.append(s)
return "".join(parts)
19 changes: 11 additions & 8 deletions pep/fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@

from templatelib import Interpolation, Template

from . import pairs


def convert(value: object, conv: Literal["a", "r", "s"] | None) -> object:
"""Convert the value to a string using the specified conversion."""
# Python has no convert() built-in function, so we have to implement it.
# For our purposes, we allow `conv` to be `None`; in practice, I imagine
# if Python had a real convert() method, that wouldn't be part of the
# type signature, and we'd return str.
if conv == "a":
return ascii(value)
if conv == "r":
Expand All @@ -29,12 +34,10 @@ def convert(value: object, conv: Literal["a", "r", "s"] | None) -> object:
def f(template: Template) -> str:
"""Implement f-string behavior using the PEP 750 t-string behavior."""
parts = []
for arg in template.args:
match arg:
case str() as s:
parts.append(s)
case Interpolation(value, _, conv, format_spec):
value = convert(value, conv)
value = format(value, format_spec)
parts.append(value)
for i, s in pairs(template):
if i is not None:
value = convert(i.value, i.conv)
value = format(value, i.format_spec)
parts.append(value)
parts.append(s)
return "".join(parts)
14 changes: 7 additions & 7 deletions pep/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from templatelib import Template

from . import pairs
from .fstring import convert


Expand All @@ -18,16 +19,15 @@ def format_some(selector: str, template: Template, ignored: str = "***") -> str:
unnecessary.
"""
parts = []
for t_arg in template.args:
if isinstance(t_arg, str):
parts.append(t_arg)
else:
if t_arg.format_spec == selector:
value = t_arg.value
for i, s in pairs(template):
if i is not None:
if i.format_spec == selector:
value = i.value
if callable(value):
value = value()
value = convert(value, t_arg.conv)
value = convert(value, i.conv)
else:
value = ignored
parts.append(value)
parts.append(s)
return "".join(parts)
13 changes: 3 additions & 10 deletions pep/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from templatelib import Interpolation, Template

from . import pairs
from .fstring import f


Expand All @@ -35,11 +36,7 @@ def message(self) -> str:

@property
def values(self) -> Mapping[str, object]:
return {
arg.expr: arg.value
for arg in self.template.args
if isinstance(arg, Interpolation)
}
return {i.expr: i.value for i, _ in pairs(self.template) if i is not None}

@property
def data(self) -> Mapping[str, object]:
Expand Down Expand Up @@ -103,11 +100,7 @@ class ValuesFormatter(TemplateFormatterBase):
"""A formatter that formats structured output from a Template's values."""

def values(self, template: Template) -> Mapping[str, object]:
return {
arg.expr: arg.value
for arg in template.args
if isinstance(arg, Interpolation)
}
return {i.expr: i.value for i, _ in pairs(template) if i is not None}

def format(self, record: LogRecord) -> str:
msg = record.msg
Expand Down
47 changes: 21 additions & 26 deletions pep/reuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from templatelib import Interpolation, Template

from . import pairs
from .fstring import convert


Expand All @@ -23,24 +24,22 @@ class Formatter:
def __init__(self, template: Template):
"""Construct a formatter for the provided template."""
# Ensure that all interpolations are strings.
for arg in template.args:
if isinstance(arg, Interpolation):
if not isinstance(arg.value, str):
raise ValueError(f"Non-string interpolation: {arg.value}")
for i, _ in pairs(template):
if i is not None and not isinstance(i.value, str):
raise ValueError(f"Non-string interpolation: {i.value}")
self.template = template

def format(self, **kwargs) -> str:
"""Render the t-string using the given values."""
parts = []
for t_arg in self.template.args:
if isinstance(t_arg, str):
parts.append(t_arg)
else:
assert isinstance(t_arg.value, str)
value = kwargs[t_arg.value]
value = convert(value, t_arg.conv)
value = format(value, t_arg.format_spec)
for i, s in pairs(self.template):
if i is not None:
assert isinstance(i.value, str)
value = kwargs[i.value]
value = convert(value, i.conv)
value = format(value, i.format_spec)
parts.append(value)
parts.append(s)
return "".join(parts)


Expand All @@ -55,24 +54,20 @@ class Binder:
def __init__(self, template: Template):
"""Construct a binder for the provided template."""
# Ensure that all interpolations are strings.
for arg in template.args:
if isinstance(arg, Interpolation):
if not isinstance(arg.value, str):
raise ValueError(f"Non-string interpolation: {arg.value}")
for i, _ in pairs(template):
if i is not None and not isinstance(i.value, str):
raise ValueError(f"Non-string interpolation: {i.value}")
self.template = template

def bind(self, **kwargs) -> Template:
"""Bind values to the template."""
args = []
for t_arg in self.template.args:
if isinstance(t_arg, str):
args.append(t_arg)
else:
assert isinstance(t_arg.value, str)
value = kwargs[t_arg.value]
expr = repr(t_arg.value)[1:-1] # remove quotes from original expression
interpolation = Interpolation(
value, expr, t_arg.conv, t_arg.format_spec
)
for i, s in pairs(self.template):
if i is not None:
assert isinstance(i.value, str)
value = kwargs[i.value]
expr = repr(i.value)[1:-1] # remove quotes from original expression
interpolation = Interpolation(value, expr, i.conv, i.format_spec)
args.append(interpolation)
args.append(s)
return Template(*args)
39 changes: 19 additions & 20 deletions pep/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

from templatelib import Interpolation, Template

from . import pairs


class HTMLParseError(Exception):
"""An error occurred while parsing an HTML template."""
Expand Down Expand Up @@ -294,26 +296,23 @@ def html(template: Template) -> Element:
components: dict[str, Callable] = {}

# TODO: consider moving all of this into an overridden parser.feed() method?
for arg in template.args:
match arg:
case str() as s:
# String content is easy: just continue to parse it as-is
parser.feed(s)
case Interpolation() as i:
# Interpolations are more complex. They can be strings, dicts,
# Elements, or Templates. It matters *where* in the HTML grammar
# they appear, so we need to handle each case separately.
if parser.in_start_tag:
value = _process_start_tag_interpolation(i.value)
else:
value = i.value
# Handle component interpolations
if callable(value):
components[_make_component_name(i.expr)] = value
value = _make_component_name(i.expr)
# TODO what if we're in an end tag?
value = _process_content_interpolation(value)
parser.feed(value)
for i, s in pairs(template):
if i is not None:
# Interpolations are more complex. They can be strings, dicts,
# Elements, or Templates. It matters *where* in the HTML grammar
# they appear, so we need to handle each case separately.
if parser.in_start_tag:
value = _process_start_tag_interpolation(i.value)
else:
value = i.value
# Handle component interpolations
if callable(value):
components[_make_component_name(i.expr)] = value
value = _make_component_name(i.expr)
# TODO what if we're in an end tag?
value = _process_content_interpolation(value)
parser.feed(value)
parser.feed(s)
parser.close()
if not parser.root:
raise HTMLParseError("No root element")
Expand Down
Loading