Skip to content

Commit

Permalink
Apply suggestions from code review
Browse files Browse the repository at this point in the history
Co-authored-by: C.A.M. Gerlach <[email protected]>
Co-authored-by: Nick Coghlan <[email protected]>
  • Loading branch information
3 people committed Mar 14, 2023
1 parent 586afe7 commit f23e5d4
Showing 1 changed file with 68 additions and 60 deletions.
128 changes: 68 additions & 60 deletions pep-0501.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,29 @@ Title: General purpose string template literals
Version: $Revision$
Last-Modified: $Date$
Author: Nick Coghlan <[email protected]>, Nick Humrich <[email protected]>
Discussions-To: https://discuss.python.org/t/pep-501-reopen-general-purpose-string-template-literals/24625
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Requires: 498, 675, 701
Requires: 701
Created: 08-Aug-2015
Python-Version: 3.12
Post-History: 08-Aug-2015, 23-Aug-2015, 30-Aug-2015
Post-History: `08-Aug-2015 <https://mail.python.org/archives/list/[email protected]/thread/EAZ3P2M3CDDIQFR764NF6FXQHWXYMKJF/#ECGMIMPGUWWRQNRBW7CIP5SPP4LTZWTW>`__,
`05-Sep-2015 <https://mail.python.org/archives/list/[email protected]/thread/ILVRPS6DTFZ7IHL5HONDBB6INVXTFOZ2/>`__,
`09-Mar-2023 <https://discuss.python.org/t/pep-501-reopen-general-purpose-string-template-literals/24625>`__,

Abstract
========

:pep:`498` added new syntactic support for string interpolation that is
transparent to the compiler, allow name references from the interpolation
transparent to the compiler, allowing name references from the interpolation
operation full access to containing namespaces (as with any other expression),
rather than being limited to explicit name references. These are referred
to in the PEP as "f-strings" (a mnemonic for "formatted strings").

Since acceptance of the :pep:`498`, f-strings have become well-established and very popular.
Since acceptance of :pep:`498`, f-strings have become well-established and very popular.
F-strings are becoming even more useful with the addition of :pep:`701`.
While f-string are great, eager rending has its limitations. For example, the eagerness of f-strings
While f-strings are great, eager rendering has its limitations. For example, the eagerness of f-strings
has made code like the following likely::

os.system(f"echo {message_from_user}")
Expand Down Expand Up @@ -59,28 +62,27 @@ and :pep:`701` has been introduced. This PEP has been updated to reflect current
and improvements from 701. It is designed to be built on top of the :pep:`701` implementation.


Summary of differences from PEP 498
===================================
Summary of differences from f-strings
=====================================

The key additions this proposal makes relative to :pep:`498`:
The key differences between f-strings and t-strings are:

* the "t" (template literal) prefix indicates delayed rendering, but
* the ``t`` (template literal) prefix indicates delayed rendering, but
otherwise uses the same syntax and semantics as formatted strings
* template literals are available at runtime as a new kind of object
(``types.TemplateLiteral``)
* the default rendering used by formatted strings is invoked on a
template literal object by calling ``format(template)`` rather than
implicitly
* while f-string ``f"Message {here}"`` would be *semantically* equivalent to
``format(t"Message {here}")``, f-strings currently avoid the runtime overhead of using
``format(t"Message {here}")``, f-strings will continue to avoid the runtime overhead of using
the delayed rendering machinery that is needed for t-strings

NOTE: This proposal spells out a draft API for ``types.TemplateLiteral``

Proposal
========

This PEP proposes the introduction of a new string prefix that declares the
This PEP proposes a new string prefix that declares the
string to be a template literal rather than an ordinary string::

template = t"Substitute {names:>10} and {expressions()} at runtime"
Expand All @@ -94,22 +96,20 @@ This would be effectively interpreted as::
(" at runtime", None),
)
_field_values = (names, expressions())
_format_specifiers = (">10", "")
template = types.TemplateLiteral(_raw_template,
_parsed_template,
_field_values,
_format_specifiers)
_format_specifiers = (f">10", f"")
template = types.TemplateLiteral(
_raw_template, _parsed_template, _field_values, _format_specifiers)

The ``__format__`` method on ``types.TemplateLiteral`` would then
implement the following ``str.format`` inspired semantics::
implement the following :meth:`str.format` inspired semantics::

>>> import datetime
>>> name = 'Jane'
>>> age = 50
>>> anniversary = datetime.date(1991, 10, 12)
>>> format(t'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.')
'My name is Jane, my age next year is 51, my anniversary is Saturday, October 12, 1991.'
>>> format(t'She said her name is {repr(name)}.')
>>> format(t'She said her name is {name!r}.')
"She said her name is 'Jane'."

The implementation of template literals would be based on :pep:`701`, and use the same syntax.
Expand All @@ -122,7 +122,7 @@ strings that are not present directly in the source code of the application.
Rationale
=========

:pep:`498` made interpolating values into strings with full access to Python's
F-strings (:pep:`498`) made interpolating values into strings with full access to Python's
lexical namespace semantics simpler, but it does so at the cost of creating a
situation where interpolating values into sensitive targets like SQL queries,
shell commands and HTML templates will enjoy a much cleaner syntax when handled
Expand All @@ -136,13 +136,13 @@ While very different in the technical details, the
``types.TemplateLiteral`` interface proposed in this PEP is
conceptually quite similar to the ``FormattableString`` type underlying the
`native interpolation <https://msdn.microsoft.com/en-us/library/dn961160.aspx>`__ support introduced in C# 6.0,
as well as `template literals in Javascript <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals>`__ introduced in es6.
as well as `template literals in Javascript <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals>`__ introduced in ES6.


Specification
=============

This PEP proposes the introduction of ``t`` as a new string prefix that
This PEP proposes a new ``t`` string prefix that
results in the creation of an instance of a new type,
``types.TemplateLiteral``.

Expand All @@ -151,7 +151,7 @@ permitted), and string literal concatenation operates as normal, with the
entire combined literal forming the template literal.

The template string is parsed into literals, expressions and format specifiers
as described for f-strings in :pep:`498`. Conversion specifiers are handled
as described for f-strings in :pep:`498` and :pep:`701`. Conversion specifiers are handled
by the compiler, and appear as part of the field text in interpolation
templates.

Expand Down Expand Up @@ -207,41 +207,41 @@ following semantics::
self.format_specifiers)
)
else:
raise TypeError(f"unsupported operand type(s) for +: 'TemplateLiteral' and '{type(other)}'")
raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'")

def __radd__(self, other):
if isinstance(other, str):
if self.parsed_template:
new_parsed_template = ((other + self.parsed_template[0][0], self.parsed_template[0][1]),)
+ self.parsed_template[1:]
new_parsed_template = (
((other + self.parsed_template[0][0], self.parsed_template[0][1]),) + self.parsed_template[1:])
else:
new_parsed_template = ((other, None),)

return TemplateLiteral(
other + self.raw_template,
new_parsed_template,
self.field_values,
self.format_specifiers)
other + self.raw_template,
new_parsed_template,
self.field_values,
self.format_specifiers)
)
else:
raise TypeError(f"unsupported operand type(s) for +: '{type(other)}' and 'TemplateLiteral'")
raise TypeError(f"unsupported operand type(s) for +: '{type(other)}' and '{type(self)}'")

def __mul__(self, other):
if isinstance(other, int):
if other < 1:
return TemplateLiteral("", (), (), ())
final = self
for _ in range(1, int):
for _ in range(1, other):
final = final + self
return final
else:
raise TypeError(f"unsupported operand type(s) for *: 'TemplateLiteral' and '{type(other)}'")
raise TypeError(f"unsupported operand type(s) for *: '{type(self)}' and '{type(other)}'")

def __rmul__(self, other):
if isinstance(other, int):
return self * other
else:
raise TypeError(f"unsupported operand type(s) for *: '{type(other)}' and 'TemplateLiteral'")
raise TypeError(f"unsupported operand type(s) for *: '{type(other)}' and '{type(self)}'")

def __repr__(self):
return (f"<{type(self).__qualname__} {repr(self._raw_template)} "
Expand All @@ -256,7 +256,7 @@ following semantics::
# See definition of the template rendering semantics below

The result of a template literal expression is an instance of this
type, rather than an already rendered string - rendering only takes
type, rather than an already rendered string rendering only takes
place when the instance's ``render`` method is called (either directly, or
indirectly via ``__format__``).

Expand Down Expand Up @@ -430,10 +430,10 @@ immediately (though one for shell escaping is proposed), but rather proposes to
third party libraries, and potentially incorporated into the standard library
at a later date.

It is proposed that a renderer is included in the `shlex` module, aimed to offer a POSIX shell style experience for
It is proposed that a renderer is included in the :mod:`shlex` module, aimed to offer a POSIX shell style experience for
accessing external programs, without the significant risks posed by running
``os.system`` or enabling the system shell when using the ``subprocess`` module
APIs, which will provide an interface for running external programs similar to that
APIs, which will provide an interface for running external programs inspired by that
offered by the
`Julia programming language <http://julia.readthedocs.org/en/latest/manual/running-external-programs/>`__,
only with the backtick based ``\`cat $filename\``` syntax replaced by
Expand All @@ -459,6 +459,8 @@ Unmatched braces::

>>> t'x={x'
File "<stdin>", line 1
t'x={x'
^
SyntaxError: missing '}' in template literal expression

Invalid expressions::
Expand All @@ -480,9 +482,12 @@ details, which will be reported as runtime exceptions.

Renderer for shell escaping added to shlex
==========================================
As a reference implementation, a renderer for safe POSIX shell escaping can be added to the ``shlex``
module. This renderer would be called ``sh`` and would be equivalent of calling ``shlex.quote`` on
each field value in the template literal.:

As a reference implementation, a renderer for safe POSIX shell escaping can be added to the :mod:`shlex`
module. This renderer would be called ``sh`` and would be equivalent to calling ``shlex.quote`` on
each field value in the template literal.

Thus::

os.system(shlex.sh(t'cat {myfile}'))

Expand All @@ -500,24 +505,24 @@ Changes to subprocess module
============================

With the additional renderer in the shlex module, and the addition of template literals,
the subprocess module can be changed to handle accepting template literal
the :mod:`subprocess` module can be changed to handle accepting template literals
as an additional input type to ``Popen``, as it already accepts a sequence, or a string,
with different behavior for each.
With the addition of template literals, ``subprocess.Popen`` (and in return, all its higher level functions such as ``run``)
could accept a string in a more safe way.
With the addition of template literals, :func:`subprocess.Popen` (and in return, all its higher level functions such as :func:`~subprocess.run`)
could accept strings in a safe way.
For example::

subprocess.run(t'cat {myfile}', shell=True)

would automatically use the ``shlex.sh`` renderer provided in this PEP. Therefore, using shlex
inside a t-string like so::
inside a ``subprocess.run`` call like so::

subprocess.run(shlex.sh(t'cat {myfile}'))
subprocess.run(shlex.sh(t'cat {myfile}'), shell=True)

would be irrelevant, as ``run`` would automatically render any template literals through ``shlex.sh``
would be redundant, as ``run`` would automatically render any template literals through ``shlex.sh``


Alternatively, when ``subprocess.Popen`` is ran without ``shell=True``, it could still provide
Alternatively, when ``subprocess.Popen`` is run without ``shell=True``, it could still provide
subprocess with a more ergonomic syntax. For example::

subprocess.run(t'cat {myfile} --flag {value}')
Expand All @@ -526,7 +531,7 @@ would be equivalent to::

subprocess.run(['cat', shlex.quote(myfile), '--flag', shlex.quote(value)])

It would do this by first using the ``shlex.sh`` renderer, as above, then using ``shlex.split`` afterwords.
It would do this by first using the ``shlex.sh`` renderer, as above, then using ``shlex.split`` on the result.

The implementation inside ``subprocess.Popen._execute_child`` would look like::

Expand Down Expand Up @@ -567,7 +572,7 @@ keyword arguments would also remain available::
As part of any such integration, a recommended approach would need to be
defined for "lazy evaluation" of interpolated fields, as the ``logging``
module's existing delayed interpolation support provides access to
`various attributes <https://docs.python.org/3/library/logging.html#logrecord-attributes>`__ of the event ``LogRecord`` instance.
:ref:`various attributes <logrecord-attributes>` of the event ``LogRecord`` instance.

For example, since template literal expressions are arbitrary Python expressions,
string literals could be used to indicate cases where evaluation itself is
Expand All @@ -589,30 +594,32 @@ that happen to be passed in as data values in a normal field.

Comparison to PEP 675
=====================
This PEP is similar in its goals as :pep:`675`.

This PEP has similar goals to :pep:`675`.
While both are attempting to provide a way to have safer code, they are doing so in different ways.
:pep:`675` provides a way to find potential security issues as a static analysis.
:pep:`675` provides a way to find potential security issues via static analysis.
It does so by providing a way for the type checker to flag sections of code that are using
dynamic strings incorrectly. This requires a user to actually run a static analysis type checker such as mypy.

If :pep:`675` tells you that you are violating a type check, it is up to the programmer to know how to handle the dynamic-ness of the string.
This PEP provides a safe alternative to f-strings at runtime.
This PEP provides a safer alternative to f-strings at runtime.
If a user recieves a type-error, changing an existing f-string into a t-string could be an easy way to solve the problem.

t-strings create safer code by correctly escaping the dynamic sections of strings, while maintaining the static portions.
t-strings enable safer code by correctly escaping the dynamic sections of strings, while maintaining the static portions.

This PEP also allows a way for a library/codebase to be safe, but it does so at runtime rather than
only during static analysis. For example, if a library wanted to ensure "only safe strings", it
could check that the type of object passed in at runtime is a template literal.::
could check that the type of object passed in at runtime is a template literal::

def my_safe_function(string_like_object):
if not isinstance(string_like_object, types.TemplateLiteral):
raise TypeError("Argument 'string_like_object' must be a t-string")

The two PEPs could also be used together by typing your function as accepting either a string literal or a template literal.
This way, your function can provide the same api for both static strings, and dynamic strings.
This way, your function can provide the same API for both static and dynamic strings::

def my_safe_function(string_like_object: LiteralString | TemplateLiteral):
...

Discussion
==========
Expand All @@ -623,7 +630,7 @@ also apply to this PEP.
Support for binary interpolation
--------------------------------

As :pep:`498` does not handle byte strings, neither will this one.
As f-strings don't handle byte strings, neither will t-strings.

Interoperability with str-only interfaces
-----------------------------------------
Expand Down Expand Up @@ -653,12 +660,13 @@ a creating a new kind of object for later consumption by interpolation
functions. Creating a rich descriptive object with a useful default renderer
made it much easier to support customisation of the semantics of interpolation.

Building atop PEP 701 rather than competing with PEP 498
--------------------------------------------------------
Building atop f-strings rather than replacing them
--------------------------------------------------

Earlier versions of this PEP attempted to serve as a complete substitute for
:pep:`498` . With the introduction of 701, this pep can now build a more flexible delayed
rendering capability on top of :pep:`498` and :pep:`701`'s eager rendering.
:pep:`498` (f-strings) . With the acceptance of that PEP and the more recent :pep:`701`,
this PEP can now build a more flexible delayed rendering capability
on top of the existing f-string eager rendering.

Assuming the presence of f-strings as a supporting capability simplified a
number of aspects of the proposal in this PEP (such as how to handle substitution
Expand Down

0 comments on commit f23e5d4

Please sign in to comment.