diff --git a/docs/lissp_whirlwind_tour.rst b/docs/lissp_whirlwind_tour.rst index 7a263dbe5..85a2a878b 100644 --- a/docs/lissp_whirlwind_tour.rst +++ b/docs/lissp_whirlwind_tour.rst @@ -1886,203 +1886,3 @@ Lissp Whirlwind Tour ;; Statement injections work at the top level only. #> .#"from operator import *" ;All your operator are belong to us. >>> from operator import * - - - ;;;; 15.4 Extra (!), the Final Builtin Reader Macro - - ;;; Reader macros have one primary argument, but additional arguments - ;;; can be passed in with the extra macro !. A tag consumes the next - ;;; parsed object, and if it's an Extra, consumes one again. Thus, - ;;; extras must be written between the tag and primary, but because - ;;; they're often optional refinements, which are easier to define as - ;;; trailing optional parameters in Python functions, they get passed in - ;;; after the primary. - - #> (setattr _macro_ 'L\# en#list) ; (help _macro_.en\#) - #.. - >>> setattr( - ... _macro_, - ... 'LQzHASH_', - ... (lambda *_Qz6RFWTTVXz_xs: - ... list( - ... _Qz6RFWTTVXz_xs))) - - - #> L#primary - >>> ['primary'] - ['primary'] - - #> L#!1 primary - >>> ['primary', 1] - ['primary', 1] - - - ;; Alias can work on reader macros too! - #> (hissp.._macro_.alias M: hissp.._macro_) - >>> # hissp.._macro_.alias - ... # hissp.macros.._macro_.defmacro - ... # hissp.macros.._macro_.let - ... (lambda _QzAW22OE5Kz_fn=(lambda _QzARAQTXTEz_prime,_QzARAQTXTEz_reader=None,*_QzARAQTXTEz_args:( - ... 'Aliases ``hissp.._macro_`` as ``MQzCOLON_#``.', - ... # hissp.macros.._macro_.ifQz_else - ... (lambda b,c,a:c()if b else a())( - ... _QzARAQTXTEz_reader, - ... (lambda : - ... __import__('builtins').getattr( - ... __import__('hissp')._macro_, - ... ('{}{}').format( - ... _QzARAQTXTEz_reader, - ... # hissp.macros.._macro_.ifQz_else - ... (lambda b,c,a:c()if b else a())( - ... 'hissp.._macro_'.endswith( - ... '._macro_'), - ... (lambda :'QzHASH_'), - ... (lambda :('')))))( - ... _QzARAQTXTEz_prime, - ... *_QzARAQTXTEz_args)), - ... (lambda : - ... ('{}.{}').format( - ... 'hissp.._macro_', - ... _QzARAQTXTEz_prime))))[-1]):( - ... __import__('builtins').setattr( - ... _QzAW22OE5Kz_fn, - ... '__doc__', - ... 'Aliases ``hissp.._macro_`` as ``MQzCOLON_#``.'), - ... __import__('builtins').setattr( - ... _QzAW22OE5Kz_fn, - ... '__qualname__', - ... ('.').join( - ... ('_macro_', - ... 'MQzCOLON_QzHASH_',))), - ... __import__('builtins').setattr( - ... __import__('operator').getitem( - ... __import__('builtins').globals(), - ... '_macro_'), - ... 'MQzCOLON_QzHASH_', - ... _QzAW22OE5Kz_fn))[-1])() - - #> M:#!b"Read-time b# via alias." ;Extra arg for alias with (!) - >>> b'Read-time b# via alias.' - b'Read-time b# via alias.' - - - #> L# !1 !2 primary ;Note the order! - >>> ['primary', 1, 2] - ['primary', 1, 2] - - #> .#(en#list "primary" 1 2) ;Inject. Note the order. - >>> ['primary', 1, 2] - ['primary', 1, 2] - - - #> !1 ;! is for a single Extra. - >>> __import__('pickle').loads( # Extra([1]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(I1\n' - ... b'ttR.' - ... ) - Extra([1]) - - #> hissp.reader..Extra#(: :? 0 :* (1 2 3)) ; but Extra can have multiple elements. - >>> __import__('pickle').loads( # Extra([':', ':?', 0, ':*', (1, 2, 3)]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(V:\n' - ... b'V:?\n' - ... b'I0\n' - ... b'V:*\n' - ... b'(I1\n' - ... b'I2\n' - ... b'I3\n' - ... b'tttR.' - ... ) - Extra([':', ':?', 0, ':*', (1, 2, 3)]) - - #> !!!1 2 3 ;Extras can have extras. - >>> __import__('pickle').loads( # Extra([1, 2, 3]) - ... b'ccopyreg\n' - ... b'_reconstructor\n' - ... b'(chissp.reader\n' - ... b'Extra\n' - ... b'cbuiltins\n' - ... b'tuple\n' - ... b'(I1\n' - ... b'I2\n' - ... b'I3\n' - ... b'ttR.' - ... ) - Extra([1, 2, 3]) - - - #> L#!: !:* !(0 1 2) !:? !3 primary ;Unpacking works like calls. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#!0 !: !:* !(1 2 3)primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#hissp.reader..Extra#(0 : :* (1 2 3))primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - - #> (setattr _macro_ 'E\# hissp.reader..Extra) - >>> setattr( - ... _macro_, - ... 'EQzHASH_', - ... __import__('hissp.reader',fromlist='?').Extra) - - - #> L# !0 E#(1 2) !3 primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - #> L#E#(0 : :* (1 2 3))primary ;Same effect. - >>> ['primary', 0, 1, 2, 3] - ['primary', 0, 1, 2, 3] - - - ;; Kwargs also work like calls. - #> builtins..dict#() - >>> {} - {} - - #> builtins..dict#!: !spam !1 !foo !2 !:** !.#(dict : eggs 3 bar 4)() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - #> builtins..dict#E#(: spam 1 foo 2 :** .#(dict : eggs 3 bar 4))() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - #> builtins..dict#!: !!spam 1 !!foo 2 !!:** .#(dict : eggs 3 bar 4)() - >>> {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - {'spam': 1, 'foo': 2, 'eggs': 3, 'bar': 4} - - - ;; Yeah, you can nest these if you have to. - #> L# !x - #.. !L# !1 L# !A - #.. inner - #.. !y - #..outer - >>> ['outer', 'x', [['inner', 'A'], 1], 'y'] - ['outer', 'x', [['inner', 'A'], 1], 'y'] - - - ;; The compiler will evaluate tuples no matter how the reader produces them. - #> builtins..tuple#L# !"Hello" !"World!" print - >>> print( - ... ('Hello'), - ... ('World!')) - Hello World! diff --git a/docs/primer.rst b/docs/primer.rst index d33a1a8d7..6beeec813 100644 --- a/docs/primer.rst +++ b/docs/primer.rst @@ -757,7 +757,7 @@ The ``:*`` can likewise act as a separator starting the keyword-only arguments, and can likewise be paired with ``:?``. The normal parameters in between these can be passed in either as positional arguments -or as keyword arguments. +or as keyword arguments (kwargs). The ``:*`` can instead pair with a parameter name, which collects the remainder of the positional arguments into a tuple. @@ -1367,14 +1367,13 @@ Unfortunately, there are some objects even pickle can't handle. Hissp had to give up with an error this time. -Qualified Reader Macros -+++++++++++++++++++++++ +Reader Tags ++++++++++++ Besides a few built-ins, -reader macros in Lissp consist of a symbol ending with a ``#``, +reader macros in Lissp consist of a special symbol ending with ``#``\ s, called a *tag*, -followed by another form, -called its *primary*. +followed by additional argument forms. A function named by a `qualified identifier`_ is invoked on the form, and the reader embeds the resulting object into the output Hissp: @@ -1444,12 +1443,98 @@ then there is no run-time overhead for the alternative notation, because it's compiled to ``(81)``, just like there's no run-time overhead for using a hex literal instead of decimal in Python. -Sometimes tags can be unqualified. -Three tags are built into the reader: +Multiary Tags ++++++++++++++ + +Reader tags may take multiple arguments. +You indicate how many with the number of trailing ``#``\ s. + +.. code-block:: REPL + + #> fractions..Fraction# .#"2/3" ; Two thirds. + >>> __import__('pickle').loads( # Fraction(2, 3) + ... b'cfractions\n' + ... b'Fraction\n' + ... b'(V2/3\n' + ... b'tR.' + ... ) + Fraction(2, 3) + + #> fractions..Fraction## 2 3 ; Notice the extra #. + >>> __import__('pickle').loads( # Fraction(2, 3) + ... b'cfractions\n' + ... b'Fraction\n' + ... b'(V2/3\n' + ... b'tR.' + ... ) + Fraction(2, 3) + +Reader tags may also take keyword arguments indicated by keyword prefixes, +which can be helpful quick refinements for functions with optional arguments, +without the need to create a new reader macro for each specialization. + +.. code-block:: REPL + + #> builtins..int#.#"21" ; Normal base ten + >>> (21) + 21 + + #> base=builtins..int##6 .#"21" ; base six via optional base= kwarg + >>> (13) + 13 + +The special prefixes ``*=`` and ``**=`` unpack the agument at that position, +either as positional arguments or keyword arguments, respectively. +Prefixes pull from the reader stream in the order written. +Each prefix requires another ``#``. +Any leftover ``#``\ s each pull a positional argument after that. +An empty prefix (``=``) indicates a single positional argument. +These can be used to put positional arguments between or before kwargs. + +Pack Objects +++++++++++++ + +Try to avoid using more than about 3 or 4 ``#``\ s in a tag, +because that gets hard to read. +You typically won't need more than that. +For too many homogeneous arguments, +(i.e., with the same semantic type) +consider using ``*=`` applied to a tuple instead. +For too many heterogeneous arguments, consider ``**=``. +For complicated expressions, +consider using inject (``.#``) on tuple expressions instead of using tags. + +A tag can be empty if it has at least one prefix, +even the empty prefix (``=``). +An empty tag creates a `Pack` object, +which contains any args and kwargs given. +When a reader tag pulls one, it automatically unpacks it. + +`Pack`\ s are used to order and group tag arguments in a hierarchical way, +for improved legibility. +They're another way to avoid using too many ``#``\ s in a row. +They allow you to write the keywords immediately before their values, +instead of up front. + +`Pack`\ s are only meant for reader tags. +They should be consumed immediately at read time, +and are only allowed to survive past that for debugging purposes. + +Unqualified Tags +++++++++++++++++ + +Sometimes tags have no qualifier. +Three such tags are built into the reader: inject ``.#``, discard ``_#``, and gensym ``$#``. + The reader will also check the current module's ``_macro_`` namespace (if it has one) for attributes ending in ``#`` (i.e. ``QzHASH_``) when it encounters an unqualified tag. +The ``#`` is only in an attribute name to distinguish them from normal compile-time macros, +not to indicate arity. +Prefixes should not be included in the attribute name either. +It is possible to use a tag name containing ``=`` or extra ``#``\ s, +but they must be escaped with a ``\``. Discard +++++++ @@ -1470,7 +1555,6 @@ Templates +++++++++ Besides ``'``, which we've already seen, -and ``!``, which we'll cover later, Lissp has three other built-in reader macros that don't require a ``#``: * ````` template quote @@ -1593,7 +1677,7 @@ Gensyms +++++++ The built-in tag ``$#`` creates a *generated symbol* -(gensym) based on the given primary symbol. +(gensym) based on the given symbol. Within a template, the same gensym name always makes the same gensym: .. code-block:: REPL @@ -1640,56 +1724,6 @@ which can sometimes happen when they are very short. By default, the hash is a prefix, but you can mark some other location for it using a $. -Extra -+++++ - -The final built-in reader macro ``!`` -is used to pass extra arguments to other reader macros. -None of Lissp's built-in reader macros use it -(although some of the `bundled macros ` do), -but extras can be helpful quick refinements for functions with optional arguments, -without the need to create a new reader macro for each specialization. - -.. code-block:: REPL - - #> builtins..int#.#"21" ; normal base ten - >>> (21) - 21 - - #> builtins..int#!6 .#"21" ; base six via optional base arg - >>> (13) - 13 - -A reader macro can have more than one extra. - -Note that since extras are often optional arguments, -they're passed in *after* the reader macro's primary argument, -even though they're written first. - -.. code-block:: REPL - - #> builtins..range# !0 !-1 20 - >>> __import__('pickle').loads( # range(20, 0, -1) - ... b'cbuiltins\n' - ... b'range\n' - ... b'(I20\n' - ... b'I0\n' - ... b'I-1\n' - ... b'tR.' - ... ) - range(20, 0, -1) - -Pass in keyword arguments by pairing with a name after ``:``, -like calls. ``:*`` and ``:**`` unpacking also work here. - -.. code-block:: REPL - - #> builtins..int# !: !base !6 .#"21" - >>> (13) - 13 - -See the section on Extras in the `lissp_whirlwind_tour` for more examples. - Macros ====== diff --git a/docs/style_guide.rst b/docs/style_guide.rst index 8db011ae1..30fb16fbe 100644 --- a/docs/style_guide.rst +++ b/docs/style_guide.rst @@ -378,7 +378,7 @@ or adding it to a collection may be attached to it. However, in many of these cases, the groups could be written as a single top-level form insead, given the appropriate functions or macros. -E.g. `dict.update` (on `globals`), `let`, `@#!`, `attach`, `doto`. +E.g. `dict.update` (on `globals`), `let`, `@##`, `attach`, `doto`. Try to avoid blank lines within forms. You may need them for separating groups whose elements span lines @@ -777,8 +777,8 @@ but they are a valid target for reader macros, in which case they may be treated as literal values. Avoid using inline or margin comments as commentary between a tag and its target, as this can cause errors when they are instead treated as arguments. -(Usually, tags are attached to their primary, so this doesn't come up, -but e.g. the bundled decorator macro `@#!` typically is not.) +(Usually, tags are attached to one argument, so this doesn't come up, +but e.g. the bundled decorator macro `@##` typically is not.) You may use a discarded string instead ``_#"NB foo"``. A good syntax highlighter specialized for Lissp may be able to indicate when a comment token is not discarded, diff --git a/src/hissp/macros.lissp b/src/hissp/macros.lissp index 41d0bc71e..f1884bf0b 100644 --- a/src/hissp/macros.lissp +++ b/src/hissp/macros.lissp @@ -514,7 +514,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... b=(2))) ;; {'a': 1, 'b': 2} ;; - ;; See also: `attach`, `type`, `@#!`, :keyword:`class`, + ;; See also: `attach`, `type`, `@#`, :keyword:`class`, ;; `types.new_class` ;; (let (ibases (iter bases)) @@ -823,34 +823,38 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; >>> # hissp.._macro_.alias ;; ... # hissp.macros.._macro_.defmacro ;; ... # hissp.macros.._macro_.let - ;; ... (lambda _QzAW22OE5Kz_fn=(lambda _QzARAQTXTEz_prime,_QzARAQTXTEz_reader=None,*_QzARAQTXTEz_args:( + ;; ... (lambda _Qz2D5FNHXZz_fn=(lambda _QzE4JATHEUz_first,*_QzE4JATHEUz_rest,**_QzE4JATHEUz_kwargs:( ;; ... 'Aliases ``hissp.._macro_`` as ``HQzCOLON_#``.', ;; ... # hissp.macros.._macro_.ifQz_else ;; ... (lambda b,c,a:c()if b else a())( - ;; ... _QzARAQTXTEz_reader, + ;; ... _QzE4JATHEUz_rest, ;; ... (lambda : - ;; ... __import__('builtins').getattr( - ;; ... __import__('hissp')._macro_, - ;; ... ('{}{}').format( - ;; ... _QzARAQTXTEz_reader, - ;; ... # hissp.macros.._macro_.ifQz_else - ;; ... (lambda b,c,a:c()if b else a())( - ;; ... 'hissp.._macro_'.endswith( - ;; ... '._macro_'), - ;; ... (lambda :'QzHASH_'), - ;; ... (lambda :('')))))( - ;; ... _QzARAQTXTEz_prime, - ;; ... *_QzARAQTXTEz_args)), + ;; ... # hissp.macros.._macro_.let + ;; ... (lambda _QzE4JATHEUz_rest=__import__('builtins').list( + ;; ... _QzE4JATHEUz_rest): + ;; ... __import__('builtins').getattr( + ;; ... __import__('hissp')._macro_, + ;; ... ('{}{}').format( + ;; ... _QzE4JATHEUz_first, + ;; ... # hissp.macros.._macro_.ifQz_else + ;; ... (lambda b,c,a:c()if b else a())( + ;; ... 'hissp.._macro_'.endswith( + ;; ... '._macro_'), + ;; ... (lambda :'QzHASH_'), + ;; ... (lambda :('')))))( + ;; ... _QzE4JATHEUz_rest.pop(), + ;; ... *_QzE4JATHEUz_rest, + ;; ... **_QzE4JATHEUz_kwargs))()), ;; ... (lambda : ;; ... ('{}.{}').format( ;; ... 'hissp.._macro_', - ;; ... _QzARAQTXTEz_prime))))[-1]):( + ;; ... _QzE4JATHEUz_first))))[-1]):( ;; ... __import__('builtins').setattr( - ;; ... _QzAW22OE5Kz_fn, + ;; ... _Qz2D5FNHXZz_fn, ;; ... '__doc__', ;; ... 'Aliases ``hissp.._macro_`` as ``HQzCOLON_#``.'), ;; ... __import__('builtins').setattr( - ;; ... _QzAW22OE5Kz_fn, + ;; ... _Qz2D5FNHXZz_fn, ;; ... '__qualname__', ;; ... ('.').join( ;; ... ('_macro_', @@ -860,7 +864,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; ... __import__('builtins').globals(), ;; ... '_macro_'), ;; ... 'HQzCOLON_QzHASH_', - ;; ... _QzAW22OE5Kz_fn))[-1])() + ;; ... _Qz2D5FNHXZz_fn))[-1])() ;; ;; #> 'H:#alias ;; >>> 'hissp.._macro_.alias' @@ -879,7 +883,7 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; >>> b'Fully-qualified b# macro at read time.' ;; b'Fully-qualified b# macro at read time.' ;; - ;; #> H:#!b"Read-time b# via alias." ;Extra arg for alias with (!) + ;; #> H:##b"Read-time b# via alias." ;; >>> b'Read-time b# via alias.' ;; b'Read-time b# via alias.' ;; @@ -889,16 +893,17 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; See also: `prelude`, `attach`. ;; `(defmacro ,(.format "{}{}" abbreviation '#) - ($#prime : $#reader None :* $#args) + ($#first : :* $#rest :** $#kwargs) ',(.format "Aliases ``{}`` as ``{}#``." qualifier abbreviation) - (if-else $#reader - ((getattr ,qualifier (.format "{}{}" - $#reader - (if-else (.endswith ',qualifier ','._macro_) - ','# - ""))) - $#prime : :* $#args) - (.format "{}.{}" ',qualifier $#prime)))) + (if-else $#rest + (let ($#rest (list $#rest)) + ((getattr ,qualifier (.format "{}{}" + $#first + (if-else (.endswith ',qualifier ','._macro_) + ','# + ""))) + (.pop $#rest) : :* $#rest :** $#kwargs)) + (.format "{}.{}" ',qualifier $#first)))) (alias i itertools.) (alias op operator.) @@ -946,8 +951,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. ;; `(op#itemgetter ,e)) -(defmacro @\# (definition decoration) - "``@#!`` 'decorator' applies ``decoration`` to a global and reassigns. +(defmacro @\# (decoration definition) + "``@#`` 'decorator' applies ``decoration`` to a global and reassigns. ``definition`` form must assign a global identified by its first arg. Expands to a `define`, meaning decorators can stack. @@ -958,8 +963,8 @@ Hidden doctest adds bundled macros for REPL-consistent behavior. .. code-block:: REPL - #> @#!str.swapcase - #..@#!str.title + #> @##str.swapcase + #..@##str.title #..(define spam 'spam) ; Unlike Python def, not always a function. >>> # hissp.macros.._macro_.define ... __import__('builtins').globals().update( @@ -2823,7 +2828,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> (op#add 5 spy#!sys..stdout(op#mul 7 3)) + #> (op#add 5 file=spy##sys..stdout(op#mul 7 3)) >>> __import__('operator').add( ... (5), ... # hissp.._macro_._spy @@ -2856,7 +2861,7 @@ Creates a lambda of arity {X} containing a `^*#` .. code-block:: REPL - #> time#!sys..stdout(time..sleep .05) + #> file=time##sys..stdout(time..sleep .05) >>> # hissp.macros.._macro_.let ... (lambda _QzPMWTVFTZz_time=__import__('time').time_ns: ... # hissp.macros.._macro_.letQz_from diff --git a/src/hissp/reader.py b/src/hissp/reader.py index 2e35f7fe7..b2f629237 100644 --- a/src/hissp/reader.py +++ b/src/hissp/reader.py @@ -14,6 +14,7 @@ import builtins import hashlib import re +import warnings from base64 import b32encode from collections import namedtuple from contextlib import contextmanager, nullcontext, suppress @@ -72,7 +73,7 @@ |(?P\)) |(?P ,@ - |['`,!] + |['`,] |[.][#] # Any atom that ends in ``#``, but not ``.#`` or ``\#``. |(?:[^\\ \n"();#]|\\.)*(?:[^.\\ \n"();#]|\\.)[#]+ @@ -187,6 +188,10 @@ def __repr__(self): class Pack: + """Contains read-time arguments for reader macros. + + Normally made with empty tags, but can be constructed directly. + """ def __init__(self, *args, **kwargs): self.args = list(args) self.kwargs = kwargs @@ -346,8 +351,6 @@ def parse_macro(self, tag: str, form, extras): * - ``'`` - `quote` - * - ``!`` - - `Extra` * - :literal:`\`` (backtick) - template quote (starts a `template`) * - ``_#`` @@ -461,6 +464,9 @@ def _custom_macro(self, form, tag: str, extras): label = re.sub(r"(^\.)", lambda m: force_qz_encode(m[1]), label) fn: Fn[[str], Fn] = self._fully_qualified if ".." in label else self._local if arity == 1 and not keywords and type(form) is not Pack: + # warnings.warn( + # str(SyntaxError("Extra", self.position(self._pos))), DeprecationWarning + # ) with self.compiler.macro_context(): args, kwargs = parse_extras(extras) return fn(label)(form, *args, **kwargs) diff --git a/tests/test_reader.py b/tests/test_reader.py index 3562058e4..dc7ffb6fe 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -156,28 +156,10 @@ def test_bad_macro(self): with self.assertRaises(SyntaxError): next(self.parser.reads("foo#bar")) - def test_bad_extra(self): - for m in {"'", "`", "_#", ".#"}: - with self.subTest(macro=m), self.assertRaises(SyntaxError): - next(self.parser.reads(f"{m}!x()")) - - def test_bad_template_extra(self): - for m in {",", ",@", "$#"}: - with self.subTest(macro=m), self.assertRaises(SyntaxError): - next(self.parser.reads(f"`({m}!x y)")) - def test_reader_missing(self): with self.assertRaises(SyntaxError): next(self.parser.reads("(x#)")) - def test_reader_no_primary(self): - with self.assertRaises(SyntaxError): - next(self.parser.reads("(x# !2)")) - - def test_reader_soft_no_primary(self): - with self.assertRaises(SyntaxError): - next(self.parser.reads("(x# !2")) - def test_reader_initial_dot(self): msg = r"Unknown reader macro 'QzFULLxSTOP_foo'." with self.assertRaisesRegex(SyntaxError, msg):