fst.docs.d07_views
Slice views and virtual fields
To be able to execute the examples, import this.
>>> from fst import *
How to get
An fstview is a lightweight object meant to facilitate access to lists of child objects like a block statement body
or Tuple / List / Set elts field. Any field which is a list of other objects (or even strings in the case of
Global or Nonlocal) will make use of an fstview when accessed through the field name on the parent FST object.
>>> view = FST('[1, 2, 3]').elts
>>> type(view)
<class 'fst.view.fstview'>
>>> print(str(view)[:88])
<<List ROOT 0,0..0,9>.elts [<Constant 0,1..0,2>, <Constant 0,4..0,5>, <Constant 0,7..0,8
>>> view.base, view.field, view.start, view.stop
(<List ROOT 0,0..0,9>, 'elts', 0, 3)
An fstview is not normally intended to be accessed by assigning it to a name and using it through that. An fstview
basically exists to facilitate direct access to slices of children for immediate operations.
>>> f = FST('[1, 2]')
>>> _ = f.elts.append('3') # _ just shuts up output
>>> print(f.src)
[1, 2, 3]
>>> del f.elts[1]
>>> print(f.src)
[1, 3]
>>> print(f.elts.insert('4', 1).base.src)
[1, 4, 3]
>>> del f.elts[:2]
>>> print(f.src)
[3]
Indexing
You can copy or cut slices from an FST using a view.
>>> f = FST('''
... i = 1
... j = 2
... k = 3
... l = 4
... '''.strip())
>>> print(f.body[1:3].copy().src)
j = 2
k = 3
>>> print(f.body[:2].cut().src)
i = 1
j = 2
>>> print(f.src)
k = 3
l = 4
You can assign slices to a view.
>>> f = FST('[a, b]')
>>> f.elts[1:1] = 'x, y'
>>> print(f.src)
[a, x, y, b]
>>> f.elts[2:] = f.elts.copy()
>>> print(f.src)
[a, x, a, x, y, b]
>>> f.elts[:] = Tuple(elts=[])
>>> print(f.src)
[]
Non-slice indexing also works as expected, including getting and putting a single element and not a slice, even if a slice is the source.
>>> f = FST('[x, y, z]')
>>> print(f.elts[1].copy().src)
y
>>> f.elts[2] = '[a, b]'
>>> print(f.src)
[x, y, [a, b]]
If you want to assign the element as a slice, you must use slice indexing.
>>> f.elts[3:3] = 'c, d'
>>> print(f.src)
[x, y, [a, b], c, d]
Or assign directly to the field name, replacing the entire slice, though this is not a view operation but rather a
property setter of the FST class itself.
>>> f.elts = 't, u, v'
>>> print(f.src)
[t, u, v]
It is safe to modify the underlying object outside of the view as the view validates its indices every time they are used and truncates them to the actual size of the target field.
>>> f = FST('[a, b, c, d, e]')
>>> view = f.elts[1:3]
>>> view
<<List ROOT 0,0..0,15>.elts[1:3] [<Name 0,4..0,5>, <Name 0,7..0,8>]>
>>> _ = f.put_slice(None, 2, 5)
>>> view
<<List ROOT 0,0..0,6>.elts[1:2] [<Name 0,4..0,5>]>
>>> _ = f.put_slice(None, 0, 2)
>>> view
<<List ROOT 0,0..0,2>.elts[:0] []>
Other operations
With the exception of insert(), these operations don't take indices but rather are meant to be executed on a
particular indexed view which selects their range in the list.
>>> print(FST('[a, b, c]').elts.replace('[x, y]').base.src)
[[x, y]]
>>> print(FST('[a, b, c]').elts.replace('x, y').base.src)
[(x, y)]
>>> print(FST('[a, b, c]').elts.replace('[x, y]', one=False).base.src)
[[x, y]]
>>> print(FST('[a, b, c]').elts.replace('x, y', one=False).base.src)
[x, y]
>>> print(FST('[a, b, c]').elts.remove().base.src)
[]
>>> print(FST('[a, b, c]').elts.insert('[x, y]', 1).base.src)
[a, [x, y], b, c]
>>> print(FST('[a, b, c]').elts[1:1].insert('[x, y]').base.src)
[a, [x, y], b, c]
>>> print(FST('[a, b, c]').elts.insert('[x, y]', 1, one=False).base.src)
[a, [x, y], b, c]
>>> print(FST('[a, b, c]').elts.insert('x, y', 1, one=False).base.src)
[a, x, y, b, c]
>>> print(FST('[a, b, c]').elts.insert('[x, y]', 'end', one=False).base.src)
[a, b, c, [x, y]]
>>> print(FST('[a, b, c]').elts.insert('x, y', 'end', one=False).base.src)
[a, b, c, x, y]
>>> print(FST('[a, b, c]').elts.append('[x, y]').base.src)
[a, b, c, [x, y]]
>>> print(FST('[a, b, c]').elts.append('x, y').base.src)
[a, b, c, (x, y)]
>>> print(FST('[a, b, c]').elts.extend('[x, y]').base.src)
[a, b, c, [x, y]]
>>> print(FST('[a, b, c]').elts.extend('x, y').base.src)
[a, b, c, x, y]
>>> print(FST('[a, b, c]').elts.prepend('[x, y]').base.src)
[[x, y], a, b, c]
>>> print(FST('[a, b, c]').elts.prepend('x, y').base.src)
[(x, y), a, b, c]
>>> print(FST('[a, b, c]').elts.prextend('[x, y]').base.src)
[[x, y], a, b, c]
>>> print(FST('[a, b, c]').elts.prextend('x, y').base.src)
[x, y, a, b, c]
They work on subviews as well.
>>> print(FST('[a, b, c]').elts[1:2].replace('[x, y]').base.src)
[a, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].replace('x, y').base.src)
[a, (x, y), c]
>>> print(FST('[a, b, c]').elts[1:2].replace('[x, y]', one=False).base.src)
[a, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].replace('x, y', one=False).base.src)
[a, x, y, c]
>>> print(FST('[a, b, c]').elts[1:3].remove().base.src)
[a]
>>> print(FST('[a, b, c]').elts[1:2].insert('[x, y]', 1).base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2][1:1].insert('[x, y]').base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2][1:1].insert('x, y').base.src)
[a, b, (x, y), c]
>>> print(FST('[a, b, c]').elts[1:2].insert('[x, y]', 1, one=False).base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].insert('x, y', 1, one=False).base.src)
[a, b, x, y, c]
>>> print(FST('[a, b, c]').elts[1:2].insert('[x, y]', 'end', one=False).base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].insert('x, y', 'end', one=False).base.src)
[a, b, x, y, c]
>>> print(FST('[a, b, c]').elts[1:2].append('[x, y]').base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].extend('[x, y]').base.src)
[a, b, [x, y], c]
>>> print(FST('[a, b, c]').elts[1:2].extend('x, y').base.src)
[a, b, x, y, c]
>>> print(FST('[a, b, c]').elts[1:2].prepend('[x, y]').base.src)
[a, [x, y], b, c]
>>> print(FST('[a, b, c]').elts[1:2].prextend('[x, y]').base.src)
[a, [x, y], b, c]
>>> print(FST('[a, b, c]').elts[1:2].prextend('x, y').base.src)
[a, x, y, b, c]
_all virtual field
Standard AST fields are used to access and modify values where there is a one-to-one correspondence and the indexing
aligns. Virtual fields allow access to the tree structure that wouldn't be possible if just trying to access
Dict.keys in the case of _all, or provides the convenience of working with statement bodies without having to check
for docstrings in the case of _body.
Virtual fields can be used in put and get functions for the field value, they can be gotten as special slice views
and they can be accessed directly as attributes to operate on just like any other normal AST field (but only on the
FST node, if you try to access these on the AST node you will get an error).
The _all field allows slice and index access to the paired key:value elements of a Dict or key:pattern elements
of a MatchMapping (along with the rest if present). This way you can get and put them in a common-sense manner.
>>> f = FST('{1: a, 2: b, 3: c, 4: d}')
>>> print(f.get_slice(1, 3, '_all').src)
{2: b, 3: c}
>>> f.put_slice('{-1: x}', 1, 3, '_all')
<Dict ROOT 0,0..0,19>
>>> print(f.src)
{1: a, -1: x, 4: d}
You can delete individual elements in this way.
>>> f.put(None, 1, '_all')
<Dict ROOT 0,0..0,12>
>>> print(f.src)
{1: a, 4: d}
But not put individual elements, since there is not an AST which represents a key:value pair. If you want to replace
a single element in this way you must use slice operations.
>>> f.put('{-2: y}', 1, '_all')
Traceback (most recent call last):
...
fst.NodeError: cannot put as 'one' item to a Dict slice
>>> f.put_slice('{-2: y}', 1, 2, '_all')
<Dict ROOT 0,0..0,13>
>>> print(f.src)
{1: a, -2: y}
When using _all on a MatchMapping, the rest element is included in the virtual field for both get and put
operations.
>>> f = FST('{1: a, 2: b, **c}', 'MatchMapping')
>>> print(f.get_slice(1, 3, '_all').src)
{2: b, **c}
_all is the default field for Dict, MatchMapping, Compare and arguments so you don't need to include it as a
parameter.
>>> f.put_slice('{-1: x, -2: y, **z}', 1, 3)
<MatchMapping ROOT 0,0..0,25>
>>> print(f.src)
{1: a, -1: x, -2: y, **z}
You can only modify the rest element within the rules of the MatchMapping syntax, only one of those is permitted and
only at the end of the pattern:
>>> f.put_slice('{**BAD}', 1, 2)
Traceback (most recent call last):
...
ValueError: put slice with 'rest' element to MatchMapping must be at end
For a Compare, there is no problem with having to pair elements for slicing, but the rather indexing is changed to
include the left element as the first element of the virtual field. This applies both to single element operations as
well as slices.
>>> f = FST('a < b == c > d')
>>> print(f.get(0).src)
a
>>> print(f.get_slice(1, 3).src)
b == c
When carrying out slice operations on a Compare though there are extra considerations to take into account for the
operators. This is explained in more detail in the section on slices fst.docs.d06_slices.
_all is also the default field for an arguments node and allows access to all the different types of arguments as
if they existed in a single list.
>>> f = FST('def f(a, /, b=2, *c, d=3, **e): pass')
>>> print(f.args._all[1:-1].copy().src)
b=2, *c, d=3
Since the _all field is the default field for the arguments node type you can omit it from the indexing access.
>>> print(f.args[-2:].copy().src)
*, d=3, **e
This allows you to deal with function arguments in an intuitive way without having to deal too much with the details of the different argument types and markers which are needed to delimit them.
>>> f.args[:1] = 'x=1, y=2'
>>> print(f.src)
def f(x=1, y=2, b=2, *c, d=3, **e): pass
>>> del f.args[3]
>>> print(f.src)
def f(x=1, y=2, b=2, *, d=3, **e): pass
_body virtual field
This is a pure convenience field. It doesn't solve any problem but rather allows you to work with blocks of statements
without having to take into account the possible presence of a docstring. _body provides a view on the real body
field as if the docstring expression were not present, so the first statement after the docstring becomes index 0 and
the length is reduced by 1 in this case.
>>> f = FST('''
... def func():
... \'\'\'docstring\'\'\'
... a = b
... call()
... '''.strip())
>>> len(f.body), len(f._body)
(3, 2)
>>> print(f.get_slice('body').src)
'''docstring'''
a = b
call()
>>> print(f.get_slice('_body').src)
a = b
call()
>>> print(f.get(0, '_body').src)
a = b
>>> f.put_slice(None, '_body')
<FunctionDef ROOT 0,0..1,19>
>>> print(f.src)
def func():
'''docstring'''
If a docstring is not present, or if the AST node is a block statement that cannot have a docstring then the _body
field acts identically to the normal body field.
>>> print(FST('''
... if 1:
... \'\'\'not-docstring\'\'\'
... a = b
... call()
... '''.strip()).get_slice('_body').src)
'''not-docstring'''
a = b
call()
Also note that _body is not the default field unlike _all is for its respective node types, you will always have to
specify it explicitly.
_args virtual field
This is a field on the Call node which combines the args and keywords fields into one and accesses them in
syntax order (this is important as the two separate lists may have elements which are intermixed).
>>> f = FST('call(a, b=c, *d)')
>>> print([g.src for g in [*f.args, *f.keywords]])
['a', '*d', 'b=c']
>>> print([g.src for g in f._args])
['a', 'b=c', '*d']
You can operate on this field just like any other normal or virtual field.
>>> print(f.put('*new_d', 2, '_args').src)
call(a, b=c, *new_d)
>>> print(f.get_slice(0, 2).src) # its the default field so you can omit it
a, b=c
>>> print(f._args[1:].copy().src)
b=c, *new_d
But you can't break syntax ordering rules.
>>> f._args[0] = '**nope'
Traceback (most recent call last):
...
fst.NodeError: keyword arglike unpacking cannot precede iterable arglike unpacking
>>> f.put_slice('a, b', 2, 'end')
Traceback (most recent call last):
...
fst.NodeError: positional arglike cannot follow keyword arglike
_bases virtual field
This is a field on a ClassDef which follows basically the same syntax rules as Call._args, even if it doesn't always
make sense.
>>> f = FST('class cls(a, b=c, *d): pass')
>>> print([g.src for g in [*f.bases, *f.keywords]])
['a', '*d', 'b=c']
>>> print([g.src for g in f._bases])
['a', 'b=c', '*d']
Just like with _args, you can operate on this field again like any other normal or virtual field.
>>> print(f.put('*new_d', 2, '_bases').src)
class cls(a, b=c, *new_d): pass
_bases is NOT the default field for a ClassDef, so if you want to use it you must always specify it. Otherwise
the operation is on body which is the default field for a ClassDef.
>>> print(f.get_slice(0, 2, '_bases').src)
a, b=c
>>> print(f._bases[1:].copy().src)
b=c, *new_d
And you still can't break syntax ordering rules.
>>> f._bases[0] = '**nope'
Traceback (most recent call last):
...
fst.NodeError: keyword arglike unpacking cannot precede iterable arglike unpacking
>>> f.put_slice('a, b', 2, 'end', '_bases')
Traceback (most recent call last):
...
fst.NodeError: positional arglike cannot follow keyword arglike
Virtual field attribute access
Just like normal fields, virtual fields can be accessed by their name on an FST class and give the same expected
results.
>>> f = FST('{1: a, 2: b, 3: c}')
>>> print(f._all[1:].copy().src)
{2: b, 3: c}
>>> f._all[:2] = '{-1: x}'
>>> print(f.src)
{-1: x, 3: c}
>>> del f._all[0]
>>> print(f.src)
{3: c}
>>> print(FST('left < comp0 < comp1')._all[-3].src)
left
>>> f = FST('''
... \'\'\'docstring\'\'\'
... a = b
... call()
... '''.strip()) # Module
>>> print(f._body.copy().src)
a = b
call()
>>> del f._body
>>> print(f.src)
'''docstring'''