diff --git a/NEWS.rst b/NEWS.rst index a66c31c28..14b9bf457 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -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-[...]t-]` as template strings (analogous to bracketed f-strings). +* Added ``hy-repr``\s for ``Template`` and ``Interpolation``. Bug Fixes ------------------------------ diff --git a/docs/api.rst b/docs/api.rst index 159fa32ed..7a8391e15 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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: @@ -189,9 +193,10 @@ Fundamentals .. _bracketed-templates: - - ``:bracketed-templates``: If set, then :ref:`bracket strings - ` using the delimiter "t" or any delimiter starting with - "t-" are parsed as :ref:`template strings `. + - ``:bracketed-templates`` (reader; requires Hy 1.3): If true (default: + false), :ref:`bracket strings ` using the delimiter "t" or + any delimiter starting with "t-" are parsed as :ref:`template strings + `. Quoting ~~~~~~~~~~~~ diff --git a/docs/syntax.rst b/docs/syntax.rst index 6503f90bc..ccaf2938e 100644 --- a/docs/syntax.rst +++ b/docs/syntax.rst @@ -350,8 +350,8 @@ producing :class:`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 ` are a string-like compound construct -documented further below. +:ref:`F-strings and t-strings ` are string-like compound +constructs documented further below. .. autoclass:: hy.models.String .. autoclass:: hy.models.Bytes @@ -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 -`.) For example:: +`. You can enable an analogous feature for :ref:`t-strings +` with the :ref:`pragma ` +``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. @@ -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 @@ -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 +` containing the sequence of string and expression +components, without any interpolation. T-strings are modeled as :class:`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 @@ -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 ` (aka "t-strings" or "template -string literals") are similar to format strings, and parse to :class:`FString ` -with the parameter ``:is_tstring`` set to ``True``. -Template strings at runtime evaluate to instances of :py:class:`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 ` pragma is set, then -:ref:`bracket strings ` using the delimiter "t" or any -delimiter starting with "t-" are also parsed as template strings. - .. _more-sugar: Additional sugar diff --git a/hy/core/hy_repr.hy b/hy/core/hy_repr.hy index e469b9c79..84d96b6bd 100644 --- a/hy/core/hy_repr.hy +++ b/hy/core/hy_repr.hy @@ -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 @@ -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 {}>" diff --git a/hy/models.py b/hy/models.py index 4b66d258e..ecb6d585f 100644 --- a/hy/models.py +++ b/hy/models.py @@ -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") @@ -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): diff --git a/hy/reader/hy_reader.py b/hy/reader/hy_reader.py index e4e638c2f..dab7ee77b 100644 --- a/hy/reader/hy_reader.py +++ b/hy/reader/hy_reader.py @@ -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 diff --git a/tests/native_tests/hy_repr.hy b/tests/native_tests/hy_repr.hy index e50a836a4..6d28734aa 100644 --- a/tests/native_tests/hy_repr.hy +++ b/tests/native_tests/hy_repr.hy @@ -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 [] diff --git a/tests/native_tests/strings.hy b/tests/native_tests/strings.hy index d0f580e08..cde8c85cf 100644 --- a/tests/native_tests/strings.hy +++ b/tests/native_tests/strings.hy @@ -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)")))) diff --git a/tests/native_tests/tstrings.hy b/tests/native_tests/tstrings.hy index 4f38b515a..d6d2d4997 100644 --- a/tests/native_tests/tstrings.hy +++ b/tests/native_tests/tstrings.hy @@ -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))))) - ))) diff --git a/tests/resources/hy_repr_str_tests.txt b/tests/resources/hy_repr_str_tests.txt index dda6bd4e1..9b15f0e6e 100644 --- a/tests/resources/hy_repr_str_tests.txt +++ b/tests/resources/hy_repr_str_tests.txt @@ -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) diff --git a/tests/test_reader.py b/tests/test_reader.py index 5f1b8f22a..bd4494ca0 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -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, @@ -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():