File: tutorial_macros.rst

package info (click to toggle)
xonsh 0.22.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,628 kB
  • sloc: python: 49,591; sh: 185; makefile: 133; xml: 17
file content (541 lines) | stat: -rw-r--r-- 17,398 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
.. _tutorial_macros:

************************************
Tutorial: Macros
************************************
Bust out your DSLRs, people. It is time to closely examine macros!

What are macro instructions?
============================
In generic terms, a programming macro is a special kind of syntax that
replaces a smaller amount of code with a larger expression, syntax tree,
code object, etc after the macro has been evaluated.
In practice, macros pause the normal parsing and evaluation of the code
that they contain. This is so that they can perform their expansion with
a complete inputs. Roughly, the algorithm executing a macro follows is:

1. Macro start, pause or skip normal parsing
2. Gather macro inputs as strings
3. Evaluate macro with inputs
4. Resume normal parsing and execution.

Is this meta-programming? You betcha!

When and where are macros used?
===============================
Macros are a practicality-beats-purity feature of many programming
languages. Because they allow you break out of the normal parsing
cycle, depending on the language, you can do some truly wild things with
them. However, macros are really there to reduce the amount of boiler plate
code that users and developers have to write.

In C and C++ (and Fortran), the C Preprocessor ``cpp`` is a macro evaluation
engine. For example, every time you see an ``#include`` or ``#ifdef``, this is
the ``cpp`` macro system in action.
In these languages, the macros are technically outside of the definition
of the language at hand. Furthermore, because ``cpp`` must function with only
a single pass through the code, the sorts of macros that can be written with
``cpp`` are relatively simple.

Rust, on the other hand, has a first-class notion of macros that look and
feel a lot like normal functions. Macros in Rust are capable of pulling off
type information from their arguments and preventing their return values
from being consumed.

Other languages like Lisp, Forth, and Julia also provide their macro systems.
Even restructured text (rST) directives could be considered macros.

If these seem unfamiliar to the Python world, note that Jupyter and IPython
magics ``%`` and ``%%`` are macros!

Function Macros
===============
Xonsh supports Rust-like macros that are based on normal Python callables.
Macros do not require a special definition in xonsh. However, like in Rust,
they must be called with an exclamation point ``!`` between the callable
and the opening parentheses ``(``. Macro arguments are split on the top-level
commas ``,``, like normal Python functions.  For example, say we have the
functions ``f`` and ``g``. We could perform a macro call on these functions
with the following:

.. code-block:: xonsh

    # No macro args
    f!()

    # Single arg
    f!(x)
    g!([y, 43, 44])

    # Two args
    f!(x, x + 42)
    g!([y, 43, 44], f!(z))

Not so bad, right?  So what actually happens to the arguments when used
in a macro call?  Well, that depends on the definition of the function. In
particular, each argument in the macro call is matched up with the corresponding
parameter annotation in the callable's signature.  For example, say we have
an ``identity()`` function that annotates its sole argument as a string:

.. code-block:: xonsh

    def identity(x : str):
        return x

If we call this normally, we'll just get whatever object we put in back out,
even if that object is not a string:

.. code-block:: xonshcon

    @ identity('me')
    'me'

    @ identity(42)
    42

    @ identity(identity)
    <function __main__.identity>

However, if we perform macro calls instead we are now guaranteed to get
the string of the source code that is in the macro call:

.. code-block:: xonshcon

    @ identity!('me')
    "'me'"

    @ identity!(42)
    '42'

    @ identity!(identity)
    'identity'

Also note that each macro argument is stripped prior to passing it to the
macro itself. This is done for consistency.

.. code-block:: xonshcon

    @ identity!(42)
    '42'

    @ identity!(  42 )
    '42'

Importantly, because we are capturing and not evaluating the source code,
a macro call can contain input that is beyond the usual syntax. In fact, that
is sort of the whole point. Here are some cases to start your gears turning:

.. code-block:: xonshcon

    @ identity!(import os)
    'import os'

    @ identity!(if True:
    @     pass)
    'if True:\n    pass'

    @ identity!(std::vector<std::string> x = {"yoo", "hoo"})
    'std::vector<std::string> x = {"yoo", "hoo"}'

You do you, ``identity()``.

Calling Function Macros
=======================
There are a couple of points to consider when calling macros. The first is
that passing in arguments by name will not behave as expected. This is because
the ``<name>=`` is captured by the macro itself. Using the ``identity()``
function from above:

.. code-block:: xonshcon

    @ identity!(x=42)
    'x=42'

Performing a macro call uses only argument order to pass in values.

Additionally, macro calls split arguments only on the top-level commas.
The top-level commas are not included in any argument.
This behaves analogously to normal Python function calls. For instance,
say we have the following ``g()`` function that accepts two arguments:

.. code-block:: xonsh

    def g(x : str, y : str):
        print('x = ' + repr(x))
        print('y = ' + repr(y))

Then you can see the splitting and stripping behavior on each macro
argument:

.. code-block:: xonshcon

    @ g!(42, 65)
    x = '42'
    y = '65'

    @ g!(42, 65,)
    x = '42'
    y = '65'

    @ g!( 42, 65, )
    x = '42'
    y = '65'

    @ g!(['x', 'y'], {1: 1, 2: 3})
    x = "['x', 'y']"
    y = '{1: 1, 2: 3}'

Sometimes you may only want to pass in the first few arguments as macro
arguments and you want the rest to be treated as normal Python arguments.
By convention, xonsh's macro caller will look for a lone ``*`` argument
in order to split the macro arguments and the regular arguments. So for
example:

.. code-block:: xonshcon

    @ g!(42, *, 65)
    x = '42'
    y = 65

    @ g!(42, *, y=65)
    x = '42'
    y = 65

In the above, note that ``x`` is still captured as a macro argument. However,
everything after the ``*``, namely ``y``, is evaluated is if it were passed
in to a normal function call.  This can be useful for large interfaces where
only a handful of args are expected as macro arguments.

Hopefully, now you see the big picture.

Writing Function Macros
=======================
Though any function (or callable) can be used as a macro, this functionality
is probably most useful if the function was *designed* as a macro. There
are two main aspects of macro design to consider: argument annotations and
call site execution context.


Macro Annotations
-----------------------------------
There are six kinds of annotations that macros are able to interpret:

.. list-table:: Kinds of Annotation
   :header-rows: 1

   * - Category
     - Object
     - Flags
     - Modes
     - Returns
   * - String
     - ``str``
     - ``'s'``, ``'str'``, or ``'string'``
     -
     - Source code of argument as string, *default*.
   * - AST
     - ``ast.AST``
     - ``'a'`` or ``'ast'``
     - ``'eval'`` (default), ``'exec'``, or ``'single'``
     - Abstract syntax tree of argument.
   * - Code
     - ``types.CodeType`` or ``compile``
     - ``'c'``, ``'code'``, or ``'compile'``
     - ``'eval'`` (default), ``'exec'``, or ``'single'``
     - Compiled code object of argument.
   * - Eval
     - ``eval`` or ``None``
     - ``'v'`` or ``'eval'``
     -
     - Evaluation of the argument.
   * - Exec
     - ``exec``
     - ``'x'`` or ``'exec'``
     - ``'exec'`` (default) or ``'single'``
     - Execs the argument and returns None.
   * - Type
     - ``type``
     - ``'t'`` or ``'type'``
     -
     - The type of the argument after it has been evaluated.

These annotations allow you to hook into whichever stage of the compilation
that you desire. It is important to note that the string form of the arguments
is split and stripped (as described above) prior to conversion to the
annotation type.

Each argument may be annotated with its own individual type. Annotations
may be provided as either objects or as the string flags seen in the above
table. String flags are case-insensitive.
If an argument does not have an annotation, ``str`` is selected.
This makes the macro function call behave like the subprocess macros and
context manager macros below. For example,

.. code-block:: xonsh

    def func(a, b : 'AST', c : compile):
        pass

In a macro call of ``func!()``,

* ``a`` will be evaluated with ``str`` since no annotation was provided,
* ``b`` will be parsed into a syntax tree node, and
* ``c`` will be compiled into code object since the builtin ``compile()``
  function was used as the annotation.

Additionally, certain kinds of annotations have different modes that
affect the parsing, compilation, and execution of its argument.  While a
sensible default is provided, you may also supply your own. This is
done by annotating with a (kind, mode) tuple.  The first element can
be any valid object or flag. The second element must be a corresponding
mode as a string.  For instance,

.. code-block:: xonsh

    def gunc(d : (exec, 'single'), e : ('c', 'exec')):
        pass

Thus in a macro call of ``gunc!()``,

* ``d`` will be exec'd in single-mode (rather than exec-mode), and
* ``e`` will be compiled in exec-mode (rather than eval-mode).

For more information on the differences between the exec, eval, and single
modes please see the Python documentation.


Macro Function Execution Context
--------------------------------
Equally important as having the macro arguments is knowing the execution
context of the macro call itself. Rather than mucking around with frames,
macros provide both the globals and locals of the call site.  These are
accessible as the ``macro_globals`` and ``macro_locals`` attributes of
the macro function itself while the macro is being executed.

For example, consider a macro which replaces all literal ``1`` digits
with the literal ``2``, evaluates the modification, and returns the results.
To eval, the macro will need to pull off its globals and locals:

.. code-block:: xonsh

    def one_to_two(x : str):
        s = x.replace('1', '2')
        glbs = one_to_two.macro_globals
        locs = one_to_two.macro_locals
        return eval(s, glbs, locs)

Running this with a few of different inputs, we see:

.. code-block:: xonshcon

    @ one_to_two!(1 + 1)
    4

    @ one_to_two!(11)
    22

    @ x = 1
    @ one_to_two!(x + 1)
    3

Of course, many other more sophisticated options are available depending on the
use case.


Subprocess Macros
=================
Like with function macros above, subprocess macros allow you to pause the parser
for until you are ready to exit subprocess mode. Unlike function macros, there
is only a single macro argument and its macro type is always a string.  This
is because it (usually) doesn't make sense to pass non-string arguments to a
command. And when it does, there is the ``@()`` syntax!

In the simplest case, subprocess macros look like the equivalent of their
function macro counterparts:

.. code-block:: xonshcon

    @ echo! I'm Mr. Meeseeks.
    I'm Mr. Meeseeks.

Again, note that everything to the right of the ``!`` is passed down to the
``echo`` command as the final, single argument. This is space preserving,
like wrapping with quotes:

.. code-block:: xonshcon

    # normally, xonsh will split on whitespace,
    # so each argument is passed in separately
    @ echo x  y       z
    x y z

    # usually space can be preserved with quotes
    @ echo "x  y       z"
    x  y       z

    # however, subprocess macros will pause and then strip
    # all input after the exclamation point
    @ echo! x  y       z
    x  y       z

However, the macro will pause everything, including path and environment variable
expansion, that might be present even with quotes.  For example:

.. code-block:: xonshcon

    # without macros, environment variable are expanded
    @ echo $USER
    lou

    # inside of a macro, all additional munging is turned off.
    @ echo! $USER
    $USER

Everything to the right of the exclamation point, except the leading and trailing
whitespace, is passed into the command directly as written. This allows certain
commands to function in cases where quoting or piping might be more burdensome.
The ``timeit`` command is a great example where simple syntax will often fail,
but will be easily executable as a macro:

.. code-block:: xonshcon

    # fails normally
    @ timeit "hello mom " + "and dad"
    xonsh: subprocess mode: command not found: hello

    # macro success!
    @ timeit! "hello mom " + "and dad"
    100000000 loops, best of 3: 8.24 ns per loop

All expressions to the left of the exclamation point are passed in normally and
are not treated as the special macro argument. This allows the mixing of
simple and complex command line arguments. For example, sometimes you might
really want to write some code in another language:

.. code-block:: xonshcon

    # don't worry, it is temporary!
    @ bash -c ! export var=42; echo $var
    42

    # that's better!
    @ python -c ! import os; print(os.path.abspath("/"))
    /

Compared to function macros, subprocess macros are relatively simple.
However, they can still be very expressive!

Context Manager Macros
======================
Now that we have seen what life can be like with macro expressions, it is time
to introduce the macro statement: ``with!``.  With-bang provides macros
on top of existing Python context managers. This provides both anonymous
and onymous blocks in xonsh.

The syntax for context manager macros is the same as the usual with-statement
in Python, but with an additional exclamation point between the ``with`` word
and the first context manager expression. As a simple example,

.. code-block:: xonsh

    with! x:
        y = 10
        print(y)

In the above, everything to the left of the colon (``x``) will be evaluated
normally. However, the body will not be executed and ``y`` will not be defined
or printed. In this case, the body will be attached to x as a string, along with
globals and locals, prior to the body even being entered. The body is then
replaced with a ``pass`` statement. You can think of the above as being
transformed into the following:

.. code-block:: xonsh

    x.macro_block = 'y = 10\nprint(y)\n'
    x.macro_globals = globals()
    x.macro_locals = locals()
    with! x:
        pass

There are a few important things about this to notice:

1. The ``macro_block`` string is dedented,
2. The ``macro_*`` attributes are set *before* the context manager is entered so
   the ``__enter__()`` method may use them, and
3. The ``macro_*`` attributes are not cleaned up automatically so that the
   context manager may use them even after the object is exited. The
   ``__exit__()`` method may clean up these attributes, if desired.

By default, macro blocks are returned as a string. However, like with function
macro arguments, the kind of ``macro_block`` is determined by a special
annotation.  This annotation is given via the ``__xonsh_block__`` attribute
on the context manager itself.  This allows the block to be interpreted as
an AST, byte compiled, etc.

The convenient part about this syntax is that the macro block is only
exited once it sees a dedent back to the level of the ``with!``. All other
code is indiscriminately skipped! This allows you to write blocks of code in
languages other than xonsh without pause.

For example, consider a simple
XML macro context manager. This will return the parsed XML tree from a
macro block. The context manager itself can be written as:


.. code-block:: python

    import xml.etree.ElementTree as ET

    class XmlBlock:

        # make sure the macro_block comes back as a string
        __xonsh_block__ = str

        def __enter__(self):
            # parse and return the block on entry
            root = ET.fromstring(self.macro_block)
            return root

        def __exit__(self, *exc):
            # no reason to keep these attributes around.
            del self.macro_block, self.macro_globals, self.macro_locals


The above class may then be used in a with-bang as follows:

.. code-block:: xonsh

    with! XmlBlock() as tree:
        <note>
          <to>You</to>
          <from>Xonsh</from>
          <heading>Don't You Want Me, Baby</heading>
          <body>
            You know I don't believe you when you say that you don't need me.
          </body>
        </note>

And if you run this, you'll see that the ``tree`` object really is a parsed
XML object.

.. code-block:: xonshcon

    @ print(tree.tag)
    note


So in roughly eight lines of xonsh code, you can seamlessly interface
with another, vastly different language.

The possibilities for this are not limited to just markup languages or other
party tricks. You could be a remote execution interface via SSH, RPC,
dask / distributed, etc. The real benefit of context manager macros is
that they allow you to select when, where, and what code is executed as a
part of the xonsh language itself.

The power is there; use it without reservation!

Take Away
=========
Hopefully, at this point, you see that a few well placed macros can be extremely
convenient and valuable to any project.