Skip to content
Merged
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
1 change: 1 addition & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ New Features
* ``hyc`` now supports ``-q``/``--quiet`` to suppress progress messages.
* Added support for t-strings from Python 3.14.
* Added new pragma `bracketed-templates` to allow parsing `#[t[...]t]` and `#[t-<ident>[...]t-<ident>]` as template strings (analogous to bracketed f-strings).
* Added ``hy-repr``\s for ``Template`` and ``Interpolation``.

Bug Fixes
------------------------------
Expand Down
15 changes: 10 additions & 5 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,12 @@ Fundamentals
Each key is a literal keyword giving the name of a pragma. Each value is an
arbitrary form, which is evaluated as ordinary Hy code but at compile-time.

The effect of each pragma is locally scoped to its containing function,
class, or comprehension form (other than ``for``), if there is one.
Generally, the effect of each pragma is locally scoped to its containing
function, class, or comprehension form (other than ``for``), if there is one.
The exception is pragmata that affect the reader (marked "(reader)" below),
in which case the current reader is affected, so as with a reader macro, you
generally need to put the pragma *before* the top-level form in which you
want to use it.

These pragmata are currently implemented:

Expand All @@ -189,9 +193,10 @@ Fundamentals

.. _bracketed-templates:

- ``:bracketed-templates``: If set, then :ref:`bracket strings
<bracket-strings>` using the delimiter "t" or any delimiter starting with
"t-" are parsed as :ref:`template strings <syntax-tstrings>`.
- ``:bracketed-templates`` (reader; requires Hy 1.3): If true (default:
false), :ref:`bracket strings <bracket-strings>` using the delimiter "t" or
any delimiter starting with "t-" are parsed as :ref:`template strings
<syntax-tstrings>`.

Quoting
~~~~~~~~~~~~
Expand Down
47 changes: 20 additions & 27 deletions docs/syntax.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,8 @@ producing :class:`Bytes <hy.models.Bytes>` instead.
Unlike Python, Hy only recognizes string prefixes (``r``, ``b``, ``f``, ``t``) in
lowercase, and doesn't allow the no-op prefix ``u``.

:ref:`F-strings <syntax-fstrings>` are a string-like compound construct
documented further below.
:ref:`F-strings and t-strings <syntax-fstrings>` are string-like compound
constructs documented further below.

.. autoclass:: hy.models.String
.. autoclass:: hy.models.Bytes
Expand All @@ -367,7 +367,9 @@ like the here-documents of other languages. A bracket string begins with
``#[FOO[`` and ends with ``]FOO]``, where ``FOO`` is any string not containing
``[`` or ``]``, including the empty string. (If ``FOO`` is exactly ``f`` or
begins with ``f-``, the bracket string is interpreted as an :ref:`f-string
<syntax-fstrings>`.) For example::
<syntax-fstrings>`. You can enable an analogous feature for :ref:`t-strings
<syntax-tstrings>` with the :ref:`pragma <bracketed-templates>`
``bracketed-templates``.) For example::

(print #[["That's very kind of yuo [sic]" Tom wrote back.]])
; "That's very kind of yuo [sic]" Tom wrote back.
Expand Down Expand Up @@ -473,8 +475,8 @@ strings. Compare the following alternatives::

.. _syntax-fstrings:

Format strings
~~~~~~~~~~~~~~
Format strings and template strings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A format string (or "f-string", or "formatted string literal") is a string
literal with embedded code, possibly accompanied by formatting commands. The
Expand All @@ -484,6 +486,19 @@ rather than Python. ::

(print f"The sum is {(+ 1 1)}.") ; => The sum is 2.

.. _syntax-tstrings:

A template string (or "t-string", or "template string literal"; :pep:`750`;
requires Hy 1.3) is similar but evaluates to a :py:class:`Template
<string.templatelib.Template>` containing the sequence of string and expression
components, without any interpolation. T-strings are modeled as :class:`FString
<hy.models.FString>`\s with the instance variable ``:is_tstring`` set to
``True``. Hy's syntax and models for t-strings work on any Python version, but
actually compiling a t-string requires Python 3.14 or later. ::

(print (hy.repr t"The sum is {(+ 1 1)}."))
; => (Template "The sum is " (Interpolation 2 "(+ 1 1)") ".")

Since ``=``, ``!``, and ``:`` are identifier characters in Hy, Hy decides where
the code in a replacement field ends (and any debugging ``=``, conversion
specifier, or format specifier begins) by parsing exactly one form. You can use
Expand All @@ -504,28 +519,6 @@ language. Thus e.g. ``f"{"a"}"`` is legal, and equivalent to ``"a"``.
.. autoclass:: hy.models.FString
.. autoclass:: hy.models.FComponent

.. _syntax-tstrings:

Template strings
~~~~~~~~~~~~~~~~

:py:mod:`Template strings <string.templatelib>` (aka "t-strings" or "template
string literals") are similar to format strings, and parse to :class:`FString <hy.models.FString>`
with the parameter ``:is_tstring`` set to ``True``.
Template strings at runtime evaluate to instances of :py:class:`Template
<string.templatelib.Template>` containing the sequence of string and
expression components, without any interpolation.
Template strings use the prefix "t" in front of a string. Otherwise, the syntax
of t-strings are exactly the same as f-strings. ::

(setv tstr t"The sum is {(+ 1 1)}.")
tstr.strings ; => #("The sum is " ".")
tstr.values ; => #(2)

If the :ref:`bracketed-templates <bracketed-templates>` pragma is set, then
:ref:`bracket strings <bracket-strings>` using the delimiter "t" or any
delimiter starting with "t-" are also parsed as template strings.

.. _more-sugar:

Additional sugar
Expand Down
37 changes: 28 additions & 9 deletions hy/core/hy_repr.hy
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,16 @@
(hy-repr-register [hy.models.Complex complex] (fn [x]
(.replace (.replace (.strip (_base-repr x) "()") "inf" "Inf") "nan" "NaN")))

(hy-repr-register [range slice]
(fn [x]
(setv op (. (type x) __name__))
(defn r [attr] (hy.repr (getattr x attr)))
(if (= x.step (if (is (type x) range) 1 None))
(if (= x.start (if (is (type x) range) 0 None))
f"({op} {(r "stop")})"
f"({op} {(r "start")} {(r "stop")})")
f"({op} {(r "start")} {(r "stop")} {(r "step")})")))
(hy-repr-register [range slice] (fn [x]
(defn r [attr]
(hy-repr (getattr x attr)))
(.format "({})" (.join " " (+
[(. (type x) __name__)]
(if (= x.step (if (is (type x) range) 1 None))
(if (= x.start (if (is (type x) range) 0 None))
[(r "stop")]
[(r "start") (r "stop")])
[(r "start") (r "stop") (r "step")]))))))

(hy-repr-register
hy.models.FComponent
Expand Down Expand Up @@ -210,6 +211,24 @@
s))
"\""))))

(when hy.compat.PY3_14
; These look pretty different from the Python `repr`s, since the
; Python `repr`s don't actually evaluate.
(import string.templatelib [Template Interpolation])
(hy-repr-register Template (fn [x]
(.format "(Template{})" (.join "" (gfor
y (+
(lfor y (zip x.strings x.interpolations) #* y)
[(get x.strings -1)])
:if y
(+ " " (hy-repr y)))))))
(hy-repr-register Interpolation (fn [x]
(.format "(Interpolation {} {}{}{})"
(hy-repr x.value)
(hy-repr x.expression)
(if (is x.conversion None) "" (+ " " (hy-repr x.conversion)))
(if (= x.format-spec "") "" (+ " " (hy-repr x.format-spec)))))))

(setv _matchobject-type (type (re.match "" "")))
(hy-repr-register _matchobject-type (fn [x]
(.format "<{}.{} object; :span {} :match {}>"
Expand Down
17 changes: 7 additions & 10 deletions hy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,11 +503,12 @@ def _string_in_node(string, node):

class FString(Sequence):
"""
Represents a format string as an iterable collection of :class:`hy.models.String`
and :class:`hy.models.FComponent`. The design mimics :class:`ast.JoinedStr`.
Represents a format string or template string as an iterable collection of
:class:`hy.models.String` and :class:`hy.models.FComponent`. The design
mimics :class:`ast.JoinedStr`.

:ivar brackets: As in :class:`hy.models.String`.
:ivar is_tstring: Whether this represents a template string rather than a format string.
:ivar is_tstring: Whether this represents a template string, rather than a format string.
"""

_extra_kwargs = ("brackets", "is_tstring")
Expand Down Expand Up @@ -544,14 +545,10 @@ def _suffixize(self, x):
args = []
if self.brackets is not None:
args.append(f"brackets={self.brackets!r}")
if PY3_14 and self.is_tstring:
if self.is_tstring:
args.append(f"is_tstring={self.is_tstring!r}")
s = x[:-1] # Clip off the final close paren
if s[-1] != "(":
s += ", "
s += ", ".join(args)
s += ")"
return s
x = x[:-1] # Clip off the final close paren
return x + ("" if x[-1] == "(" else ", ") + ", ".join(args) + ")"


class List(Sequence):
Expand Down
5 changes: 2 additions & 3 deletions hy/reader/hy_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,11 @@ def quote_closing(c):
escaping = False
return 0

fstring_mode = (
return self.read_string_until(quote_closing, prefix, fstring_mode = (
"f" if "f" in prefix_chars
else "t" if "t" in prefix_chars
else ""
)
return self.read_string_until(quote_closing, prefix, fstring_mode)
))

###
# Special annotations
Expand Down
7 changes: 6 additions & 1 deletion tests/native_tests/hy_repr.hy
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@
collections [deque ChainMap OrderedDict]
fractions [Fraction]
re)
(when hy.compat.PY3_14
(import string.templatelib [Template Interpolation]))

(for [original-str (lfor
x (with [o (open "tests/resources/hy_repr_str_tests.txt")]
(list o))
:setv x (.rstrip x)
:if (and x (not (.startswith x ";")))
:if (or hy.compat.PY3_14 (not (.startswith x "(Template")))
:if (or hy.compat.PY3_15 (not (.startswith x "(frozendict")))
#* (if (in (get x 0) "':")
[x]
[x (+ "'" x)]))]

(setv rep (hy.repr (hy.eval (hy.read original-str))))
(setv rep (hy.repr (hy.eval (hy.read
original-str
:reader (hy.HyReader :bracketed-templates True)))))
(assert (= rep original-str))))

(defn test-hy-repr-roundtrip-from-value []
Expand Down
8 changes: 8 additions & 0 deletions tests/native_tests/strings.hy
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,11 @@ cee"} dee" "ey bee\ncee dee"))
"hy.models.FString([hy.models.String('foo')], brackets='f-x')"))
(assert (= (f '#[f-x[]f-x])
"hy.models.FString(brackets='f-x')"))))


(pragma :bracketed-templates True)
(defn test-tstring-repr []
(with [(hy.models.pretty False)]
(assert (=
(.replace (repr '#[t[hello]t]) " " "")
"hy.models.FString((hy.models.String('hello'),),brackets='t',is_tstring=True)"))))
14 changes: 0 additions & 14 deletions tests/native_tests/tstrings.hy
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,4 @@
(assert (. tstr [1] is-tstring))
(assert (. tstr [1] expression) "3"))

(defn test-tstring-repr []
(setv tstrings (.strip #[[
t"hello world"
t"hello{3}world"
t"hello{(+ 2 1)}world"
t"hello{3 !r}world"
t"hello{3 :#x}world"
#[t[this is {1} template]t]
#[t-algo[so {(+ 1 1)} is this {(- 5 4)}]t-algo]
]]))
(setv forms (hy.read-many tstrings :reader (hy.HyReader :bracketed-templates True)))
(for [[literal form] (zip (.split tstrings "\n") forms)]
(assert (= (+ "'" literal) (hy.repr form)))))

)))
17 changes: 17 additions & 0 deletions tests/resources/hy_repr_str_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ b"\"double \" quotes\""

'#[f-delim[the answer is {(+ 2 2) :{(+ 2 3)}}]f-delim]

;; * T-strings

't"hello world"
't"hello{3}world"
't"hello{(+ 2 1)}world"
't"hello{3 !r}world"
't"hello{3 :#x}world"
'#[t[this is {1} template]t]
'#[t-algo[so {(+ 1 1)} is this {(- 5 4)}]t-algo]

;; * `Template` and `Interpolation`

(Template)
(Template "a")
(Template "the answer is " (Interpolation 7 "(+ 3 4)"))
(Template "the answer is " (Interpolation 7 "(+ 3 4)" "r" "20") ", obviously")

;; * Ranges and slices

(range 5)
Expand Down
4 changes: 1 addition & 3 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pytest

from hy import PrematureEndOfInput
from hy.compat import PY3_14
from hy.errors import hy_exc_handler
from hy.models import (
Bytes,
Expand Down Expand Up @@ -504,8 +503,7 @@ def test_string_prefixes():
assert s(r'b"hello"') == Bytes(b"hello")
assert s(r'rb"hello"') == Bytes(b"hello")
assert s(r'fr"hello"') == FString([String("hello")])
if PY3_14:
assert s(r'tr"hello"') == FString([String("hello")], is_tstring=True)
assert s(r'tr"hello"') == FString([String("hello")], is_tstring=True)

for bad in list("zRBFu") + ["bf", "rr", "rbr"]:
with lexe():
Expand Down
Loading