Skip to content

Commit ae17d24

Browse files
committed
Try things out with pairs().
1 parent 59ec1b0 commit ae17d24

File tree

7 files changed

+103
-84
lines changed

7 files changed

+103
-84
lines changed

pep/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Examples for PEP 750. See README.md for details."""
22

3+
import typing as t
34
from html.parser import HTMLParser
45

6+
from templatelib import Interpolation, Template
7+
58
#
69
# Known bugs/divergences between cpython/tstrings and the current PEP 750 spec
710
#
@@ -86,3 +89,30 @@ def parse_starttag(self, i: int) -> int:
8689
response = super().parse_starttag(i)
8790
print(f"parse_starttag: {i} -> {response}")
8891
return response
92+
93+
94+
class InterpolationProto(t.Protocol):
95+
"""
96+
This exists only to get my type checking tools, which do not currently
97+
know about templatelib, to understand the structure of an Interpolation
98+
when surfaced by pairs(), below.
99+
"""
100+
101+
value: object
102+
expr: str
103+
conv: t.Literal["a", "r", "s"] | None
104+
format_spec: str
105+
106+
107+
def pairs(template: Template) -> t.Iterator[tuple[InterpolationProto | None, str]]:
108+
"""
109+
Yield pairs of interpolations and strings from a template.
110+
111+
This allows us to experiment with the structure of a template;
112+
see the discussion here:
113+
114+
https://discuss.python.org/t/pep750-template-strings-new-updates/71594/65
115+
"""
116+
yield None, template.args[0]
117+
for i, s in zip(template.args[1::2], template.args[2::2]):
118+
yield i, s

pep/afstring.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import inspect
88

9-
from templatelib import Interpolation, Template
9+
from templatelib import Template
1010

11+
from . import pairs
1112
from .fstring import convert
1213

1314

@@ -21,16 +22,14 @@ async def async_f(template: Template) -> str:
2122
formatting it.
2223
"""
2324
parts = []
24-
for arg in template.args:
25-
match arg:
26-
case str() as s:
27-
parts.append(s)
28-
case Interpolation(value, _, conv, format_spec):
29-
if inspect.iscoroutinefunction(value):
30-
value = await value()
31-
elif callable(value):
32-
value = value()
33-
value = convert(value, conv)
34-
value = format(value, format_spec)
35-
parts.append(value)
25+
for i, s in pairs(template):
26+
if i is not None:
27+
if inspect.iscoroutinefunction(i.value):
28+
value = await i.value()
29+
elif callable(i.value):
30+
value = i.value()
31+
value = convert(value, i.conv)
32+
value = format(value, i.format_spec)
33+
parts.append(value)
34+
parts.append(s)
3635
return "".join(parts)

pep/fstring.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313

1414
from templatelib import Interpolation, Template
1515

16+
from . import pairs
17+
1618

1719
def convert(value: object, conv: Literal["a", "r", "s"] | None) -> object:
1820
"""Convert the value to a string using the specified conversion."""
1921
# Python has no convert() built-in function, so we have to implement it.
22+
# For our purposes, we allow `conv` to be `None`; in practice, I imagine
23+
# if Python had a real convert() method, that wouldn't be part of the
24+
# type signature, and we'd return str.
2025
if conv == "a":
2126
return ascii(value)
2227
if conv == "r":
@@ -29,12 +34,10 @@ def convert(value: object, conv: Literal["a", "r", "s"] | None) -> object:
2934
def f(template: Template) -> str:
3035
"""Implement f-string behavior using the PEP 750 t-string behavior."""
3136
parts = []
32-
for arg in template.args:
33-
match arg:
34-
case str() as s:
35-
parts.append(s)
36-
case Interpolation(value, _, conv, format_spec):
37-
value = convert(value, conv)
38-
value = format(value, format_spec)
39-
parts.append(value)
37+
for i, s in pairs(template):
38+
if i is not None:
39+
value = convert(i.value, i.conv)
40+
value = format(value, i.format_spec)
41+
parts.append(value)
42+
parts.append(s)
4043
return "".join(parts)

pep/lazy.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from templatelib import Template
44

5+
from . import pairs
56
from .fstring import convert
67

78

@@ -18,16 +19,15 @@ def format_some(selector: str, template: Template, ignored: str = "***") -> str:
1819
unnecessary.
1920
"""
2021
parts = []
21-
for t_arg in template.args:
22-
if isinstance(t_arg, str):
23-
parts.append(t_arg)
24-
else:
25-
if t_arg.format_spec == selector:
26-
value = t_arg.value
22+
for i, s in pairs(template):
23+
if i is not None:
24+
if i.format_spec == selector:
25+
value = i.value
2726
if callable(value):
2827
value = value()
29-
value = convert(value, t_arg.conv)
28+
value = convert(value, i.conv)
3029
else:
3130
value = ignored
3231
parts.append(value)
32+
parts.append(s)
3333
return "".join(parts)

pep/logging.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from templatelib import Interpolation, Template
1414

15+
from . import pairs
1516
from .fstring import f
1617

1718

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

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

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

105102
def values(self, template: Template) -> Mapping[str, object]:
106-
return {
107-
arg.expr: arg.value
108-
for arg in template.args
109-
if isinstance(arg, Interpolation)
110-
}
103+
return {i.expr: i.value for i, _ in pairs(template) if i is not None}
111104

112105
def format(self, record: LogRecord) -> str:
113106
msg = record.msg

pep/reuse.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from templatelib import Interpolation, Template
1111

12+
from . import pairs
1213
from .fstring import convert
1314

1415

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

3232
def format(self, **kwargs) -> str:
3333
"""Render the t-string using the given values."""
3434
parts = []
35-
for t_arg in self.template.args:
36-
if isinstance(t_arg, str):
37-
parts.append(t_arg)
38-
else:
39-
assert isinstance(t_arg.value, str)
40-
value = kwargs[t_arg.value]
41-
value = convert(value, t_arg.conv)
42-
value = format(value, t_arg.format_spec)
35+
for i, s in pairs(self.template):
36+
if i is not None:
37+
assert isinstance(i.value, str)
38+
value = kwargs[i.value]
39+
value = convert(value, i.conv)
40+
value = format(value, i.format_spec)
4341
parts.append(value)
42+
parts.append(s)
4443
return "".join(parts)
4544

4645

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

6462
def bind(self, **kwargs) -> Template:
6563
"""Bind values to the template."""
6664
args = []
67-
for t_arg in self.template.args:
68-
if isinstance(t_arg, str):
69-
args.append(t_arg)
70-
else:
71-
assert isinstance(t_arg.value, str)
72-
value = kwargs[t_arg.value]
73-
expr = repr(t_arg.value)[1:-1] # remove quotes from original expression
74-
interpolation = Interpolation(
75-
value, expr, t_arg.conv, t_arg.format_spec
76-
)
65+
for i, s in pairs(self.template):
66+
if i is not None:
67+
assert isinstance(i.value, str)
68+
value = kwargs[i.value]
69+
expr = repr(i.value)[1:-1] # remove quotes from original expression
70+
interpolation = Interpolation(value, expr, i.conv, i.format_spec)
7771
args.append(interpolation)
72+
args.append(s)
7873
return Template(*args)

pep/web.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
from templatelib import Interpolation, Template
2626

27+
from . import pairs
28+
2729

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

296298
# TODO: consider moving all of this into an overridden parser.feed() method?
297-
for arg in template.args:
298-
match arg:
299-
case str() as s:
300-
# String content is easy: just continue to parse it as-is
301-
parser.feed(s)
302-
case Interpolation() as i:
303-
# Interpolations are more complex. They can be strings, dicts,
304-
# Elements, or Templates. It matters *where* in the HTML grammar
305-
# they appear, so we need to handle each case separately.
306-
if parser.in_start_tag:
307-
value = _process_start_tag_interpolation(i.value)
308-
else:
309-
value = i.value
310-
# Handle component interpolations
311-
if callable(value):
312-
components[_make_component_name(i.expr)] = value
313-
value = _make_component_name(i.expr)
314-
# TODO what if we're in an end tag?
315-
value = _process_content_interpolation(value)
316-
parser.feed(value)
299+
for i, s in pairs(template):
300+
if i is not None:
301+
# Interpolations are more complex. They can be strings, dicts,
302+
# Elements, or Templates. It matters *where* in the HTML grammar
303+
# they appear, so we need to handle each case separately.
304+
if parser.in_start_tag:
305+
value = _process_start_tag_interpolation(i.value)
306+
else:
307+
value = i.value
308+
# Handle component interpolations
309+
if callable(value):
310+
components[_make_component_name(i.expr)] = value
311+
value = _make_component_name(i.expr)
312+
# TODO what if we're in an end tag?
313+
value = _process_content_interpolation(value)
314+
parser.feed(value)
315+
parser.feed(s)
317316
parser.close()
318317
if not parser.root:
319318
raise HTMLParseError("No root element")

0 commit comments

Comments
 (0)