fst.fst

Main FST module. Contains the FST class as well as drop-in replacement parse() and unparse() functions for their respective ast module counterparts.

def parse( source: str | bytes | ast.AST, filename: str = '<unknown>', mode: str = 'exec', *, type_comments: bool = False, feature_version: tuple[int, int] | None = None, **kwargs) -> ast.AST:

Executes ast.parse() and then adds FST nodes to the parsed tree. Drop-in replacement for ast.parse(). For parameters, see ast.parse(). Returned AST tree has added .f attribute at each node which accesses the parallel FST tree.

Parameters:

  • source: The python source to parse.
  • filename: ast.parse() parameter.
  • mode: Parse mode, extended ast.parse() parameter, See fst.parsex.Mode.
  • type_comments: ast.parse() parameter.
  • feature_version: ast.parse() parameter.

Returns:

  • AST: Tree with an FST .f attribute added to each AST node.

Examples:

>>> import ast, fst
>>> a = fst.parse('if 1:\n  i = 2')
>>> type(a)
<class 'ast.Module'>
>>> a.f  # FST node
<Module ROOT 0,0..1,7>
>>> print(fst.dump(a, indent=2))
Module(
  body=[
    If(
      test=Constant(value=1),
      body=[
        Assign(
          targets=[
            Name(id='i', ctx=Store())],
          value=Constant(value=2))],
      orelse=[])],
  type_ignores=[])
>>> a.f.dump()
Module - ROOT 0,0..1,7
  .body[1]
   0] If - 0,0..1,7
     .test Constant 1 - 0,3..0,4
     .body[1]
      0] Assign - 1,2..1,7
        .targets[1]
         0] Name 'i' Store - 1,2..1,3
        .value Constant 2 - 1,6..1,7
def unparse(ast_obj: ast.AST) -> str:

Returns the formatted source that is kept for this tree. Drop-in replacement for ast.unparse() If there is no FST information in the AST tree then just executes ast.unparse().

Parameters:

  • ast_obj: The AST to unparse.

Returns:

  • str: The unparsed source code, formatted if it came from FST.

Examples:

>>> import ast, fst
>>> a = fst.parse('''
... if 1: i = 1  # same line
... else:
...   j=2 # comment
... '''.strip())
>>> print(ast.unparse(a))
if 1:
    i = 1
else:
    j = 2
>>> print(fst.unparse(a))
if 1: i = 1  # same line
else:
  j=2 # comment
>>> a = ast.parse('i = 1')
>>> ast.unparse(a)
'i = 1'
>>> fst.unparse(a)  # also unparses regular AST
'i = 1'
def dump( node: ast.AST, annotate_fields: bool = True, include_attributes: bool = False, *, indent: int | str | None = None, show_empty: bool = True) -> str:

This function is a convenience function and only exists to make python version 3.13 and above ast.dump() output compatible on a default call with previous python versions (important for doctests). All arguments correspond to their respective ast.dump() arguments and show_empty is eaten on python versions below 3.13.

class FST:

Class which maintains structure and formatted source code for an AST tree. An instance of this class is added to each AST node in a tree. It provides format-preserving operations as well as ability to navigate the tree in any direction.

FST( ast_or_src: ast.AST | str | list[str] | None, mode: Union[FST, list[str], Literal['all', 'strict', 'exec', 'eval', 'single', 'stmts', 'stmt', 'ExceptHandler', '_ExceptHandlers', 'match_case', '_match_cases', 'expr', 'expr_all', 'expr_arglike', 'expr_slice', 'Tuple_elt', 'Tuple', '_Assign_targets', '_decorator_list', 'boolop', 'operator', 'unaryop', 'cmpop', 'comprehension', '_comprehensions', '_comprehension_ifs', 'arguments', 'arguments_lambda', 'arg', 'keyword', 'alias', '_aliases', 'Import_name', '_Import_names', 'ImportFrom_name', '_ImportFrom_names', 'withitem', '_withitems', 'pattern', 'type_param', '_type_params', '_expr_arglikes'], type[ast.AST], NoneType] = None, pfield: Union[fst.common.astfield, Literal[False], NoneType] = False, /, **kwargs)

Create a new individual FST node or full tree. The main way to use this constructor is as a shortcut for FST.fromsrc() or FST.fromast(), the usage is:

FST(ast_or_src, mode=None)

This will create an FST from either an AST or source code in the form of a string or list of lines. The first parameter can be None instead of an AST or source to indicate a blank new module of one of the three types 'exec', 'eval' or 'single'. Otherwise if there is source or an AST then mode specifies how it will be parsed / reparsed and it can take any of the values from fst.parsex.Mode.

Parameters:

  • ast_or_src: Source code, an AST node or None.
  • mode: See fst.parsex.Mode. If this is None then if ast_or_src is an AST the mode defaults to the type of the AST. Otherwise if the ast_or_src is actual source code then mode used is 'all' to allow parsing anything. And if ast_or_src is None then mode must be provided and be one of 'exec', 'eval' or 'single'.

The other forms of this function are meant for internal use and their parameters are below:

Parameters:

  • ast_or_src: AST node for FST or source code in the form of a str or a list of lines. If an AST then will be processed differently depending on if creating child node, top level node or using this as a shortcut for a full fromsrc() or fromast().
  • mode: Is really mode_or_lines_or_parent. Parent node for this child node or lines for a root node creating a new tree. If pfield is False then this is a shortcut to create a full tree from an AST node or source provided in ast_or_src.
  • pfield: astfield indication position in parent of this node. If provided then creating a simple child node and it is created with the self.parent set to mode node and self.pfield set to this. If None then it means the creation of a full new FST tree and this is the root node with mode providing the source. If False then this is a shortcut for FST.fromast() or FST.fromsrc() or FST.new().
  • kwargs: Contextual parameters:
    • from_: If this is provided then it must be an FST node from which this node is being created. This allows to copy parse parameters and already determined default indentation.
    • parse_params: A dict with values for filename, type_comments and feature_version which will be used for any AST reparse done on this tree. Only valid when creating a root node.
    • indent: Indentation string to use as default indentation. If not provided and not gotten from from_ then indentation will be inferred from source. Only valid when creating a root node.
    • filename, type_comments and feature_version: If creating from an AST or source only then these are the parameteres passed to the respective .new(), .fromsrc() or .fromast() functions. Only valid when mode and pfield are None.
    • lcopy: Whether to copy lines of source on root node create or just use what is passed in, which in this case must be a list of bistr and this node takes ownership of the list.
a: ast.AST | None

The actual AST node. Will be set to None for FST nodes which were deleted or otherwise invalidated so can be checked for that to see if the FST is still alive (while walking and modifying for example).

parent: FST | None

Parent FST node, None in root node.

pfield: fst.common.astfield | None

The astfield location of this node in the parent, None in root node.

parse_params: Mapping[str, Any]

The parameters to use for any ast.parse() that needs to be done (filename, type_comments, feature_version), root node only.

indent: str

The default single level of block indentation string for this tree when not available from context, root node only.

is_root: bool

True for the root node, False otherwise.

root: FST

Root node of the tree this node belongs to.

lines: list[str] | None

Whole lines which contain this node, may also contain parts of enclosing nodes. If gotten at root then the entire source is returned, which may extend beyond the location of the top level node (mostly for statements which may have leading / trailing comments or empty lines).

Note: The lines list returned is always a copy so safe to modify.

src: str | None

Source code of this node clipped out of as a single string, without any dedentation. Will have indentation as it appears in the top level source if multiple lines. If gotten at root then the entire source is returned, regardless of whether the actual top level node location includes it or not.

has_own_loc: bool

True when the node has its own location which comes directly from AST lineno and other location fields. Otherwise False if no loc or loc is calculated.

whole_loc: fst.common.fstloc

Whole source location, from 0,0 to end of source. Works from any node (not just root).

loc: fst.common.fstloc | None

Zero based character indexed location of node (may not be entire location if node has decorators). Not all nodes have locations (like expr_context). Other nodes which normally don't have locations like arguments or most operators have this location calculated from their children or source. NOTE: Empty arguments do NOT have a location even though the AST exists.

bloc: fst.common.fstloc | None

Bounding location of node. For non-statements or non-block statements is same as loc. For block statements will include any leading decorators and a trailing line comment on the last child (or self if no children).

Examples:

>>> f = FST('''
... @decorator
... def func():
...     pass  # comment
... '''.strip(), 'exec')
>>> f.body[0].bloc
fstloc(0, 0, 2, 19)
>>> f.body[0].loc
fstloc(1, 0, 2, 8)
>>> f = FST('stmt  # comment', 'exec')
>>> f.body[0].bloc  # non-block statement doesn't include comment
fstloc(0, 0, 0, 4)
ln: int

Line number of the first line of this node (0 based).

col: int

CHARACTER index of the start of this node (0 based).

end_ln: int

Line number of the LAST LINE of this node (0 based).

end_col: int

CHARACTER index one past the end of this node (0 based).

bln: int

Line number of the first line of this node or the first decorator if present (0 based).

bcol: int

CHARACTER column of this node or the first decorator if present (0 based).

bend_ln: int

Line number of the last line of this node.

bend_col: int

CHARACTER column of the end of the last line of this node, past a trailing line comment on last child if self is a block statement.

lineno: int

AST-style Line number of the first line of this node (1 based), available for all nodes which have loc.

col_offset: int

AST-style BYTE index of the start of this node (0 based), available for all nodes which have loc.

end_lineno: int

AST-style Line number of the LAST LINE of this node (1 based), available for all nodes which have loc.

end_col_offset: int

AST-style BYTE index one past the end of this node (0 based), available for all nodes which have loc.

has_docstr: bool

Pure bool for whether this node has a docstring if it is a FunctionDef, AsyncFunctionDef, ClassDef or Module. For quick use as starting index.

docstr: str | None

The docstring of this node if it is a FunctionDef, AsyncFunctionDef, ClassDef or Module. None if not one of those nodes or has no docstring. Keep in mind an empty docstring may exist but will be a falsey value so make sure to check for None. If you want the actual docstring node then just check for presence and get .body[0].

@staticmethod
def new( mode: Literal['exec', 'eval', 'single'] = 'exec', *, filename: str = '<unknown>', type_comments: bool = False, feature_version: tuple[int, int] | None = None) -> FST:

Create a new empty FST tree with the top level node dictated by the mode parameter.

Parameters:

  • mode: ast.parse() parameter, can only be 'exec', 'eval' or 'single' here.
  • filename: ast.parse() parameter.
  • type_comments: ast.parse() parameter.
  • feature_version: ast.parse() parameter.

Returns:

  • FST: The new empty top level FST node.

Examples:

>>> FST.new()
<Module ROOT 0,0..0,0>
>>> FST.new(mode='single')
<Interactive ROOT 0,0..0,0>
>>> FST.new(mode='eval')
<Expression ROOT 0,0..0,4>
>>> _.dump()
Expression - ROOT 0,0..0,4
  .body Constant None - 0,0..0,4
>>> _.src
'None'
@staticmethod
def fromsrc( src: str | list[str], mode: Union[Literal['all', 'strict', 'exec', 'eval', 'single', 'stmts', 'stmt', 'ExceptHandler', '_ExceptHandlers', 'match_case', '_match_cases', 'expr', 'expr_all', 'expr_arglike', 'expr_slice', 'Tuple_elt', 'Tuple', '_Assign_targets', '_decorator_list', 'boolop', 'operator', 'unaryop', 'cmpop', 'comprehension', '_comprehensions', '_comprehension_ifs', 'arguments', 'arguments_lambda', 'arg', 'keyword', 'alias', '_aliases', 'Import_name', '_Import_names', 'ImportFrom_name', '_ImportFrom_names', 'withitem', '_withitems', 'pattern', 'type_param', '_type_params', '_expr_arglikes'], type[ast.AST]] = 'exec', *, filename: str = '<unknown>', type_comments: bool = False, feature_version: tuple[int, int] | None = None) -> FST:

Parse and create a new FST tree from source, preserving the original source and locations.

Parameters:

  • src: The source to parse as a single str or list of individual line strings (without newlines).
  • mode: Parse mode, extended ast.parse() parameter, See fst.parsex.Mode.
  • filename: ast.parse() parameter.
  • type_comments: ast.parse() parameter.
  • feature_version: ast.parse() parameter.

Returns:

  • FST: The parsed tree with .f attributes added to each AST node for FST access.

Examples:

>>> FST.fromsrc('var').dump()
Module - ROOT 0,0..0,3
  .body[1]
   0] Expr - 0,0..0,3
     .value Name 'var' Load - 0,0..0,3
>>> FST.fromsrc('var', mode='stmt').dump()
Expr - ROOT 0,0..0,3
  .value Name 'var' Load - 0,0..0,3
>>> FST.fromsrc('var', mode='expr').dump()
Name 'var' Load - ROOT 0,0..0,3
>>> FST.fromsrc('except Exception: pass', 'ExceptHandler').dump()
ExceptHandler - ROOT 0,0..0,22
  .type Name 'Exception' Load - 0,7..0,16
  .body[1]
   0] Pass - 0,18..0,22
>>> FST.fromsrc('case f(a=1): pass', 'match_case').dump()
match_case - ROOT 0,0..0,17
  .pattern MatchClass - 0,5..0,11
    .cls Name 'f' Load - 0,5..0,6
    .kwd_attrs[1]
     0] 'a'
    .kwd_patterns[1]
     0] MatchValue - 0,9..0,10
       .value Constant 1 - 0,9..0,10
  .body[1]
   0] Pass - 0,13..0,17
>>> import ast
>>> FST.fromsrc('a:b', ast.Slice).dump()
Slice - ROOT 0,0..0,3
  .lower Name 'a' Load - 0,0..0,1
  .upper Name 'b' Load - 0,2..0,3
@staticmethod
def fromast( ast: ast.AST, mode: Union[Literal['all', 'strict', 'exec', 'eval', 'single', 'stmts', 'stmt', 'ExceptHandler', '_ExceptHandlers', 'match_case', '_match_cases', 'expr', 'expr_all', 'expr_arglike', 'expr_slice', 'Tuple_elt', 'Tuple', '_Assign_targets', '_decorator_list', 'boolop', 'operator', 'unaryop', 'cmpop', 'comprehension', '_comprehensions', '_comprehension_ifs', 'arguments', 'arguments_lambda', 'arg', 'keyword', 'alias', '_aliases', 'Import_name', '_Import_names', 'ImportFrom_name', '_ImportFrom_names', 'withitem', '_withitems', 'pattern', 'type_param', '_type_params', '_expr_arglikes'], type[ast.AST], Literal[False], NoneType] = None, *, filename: str = '<unknown>', type_comments: bool | None = False, feature_version: tuple[int, int] | None = None, ctx: bool = False) -> FST:

Unparse and reparse an AST for new FST (the reparse is necessary to make sure locations are correct).

Parameters:

  • ast: The root AST node.
  • mode: Parse mode, extended ast.parse() parameter, see fst.parsex.Mode. Two special values are added:
    • None: This will attempt to reparse to the same node type as was passed in. This is the default and all other values should be considered overrides for special cases.
    • False: This will skip the reparse and just ast.unparse() the AST to generate source for the FST. Use this only if you are absolutely certain that the AST unparsed source will correspond with the locations already present in the AST. This is almost never the case unless the AST was ast.parse()d from an explicitly ast.unparse()d AST.
  • filename: ast.parse() parameter.
  • type_comments: ast.parse() parameter.
  • feature_version: ast.parse() parameter.
  • ctx: Whether to make sure that the ctx field of the reparsed AST matches or not. False for convenience, True if you're feeling pedantic.

Returns:

  • FST: The augmented tree with .f attributes added to each AST node for FST access.

Examples:

>>> import ast
>>> from ast import Assign, Slice, Constant
>>> FST.fromast(Assign(targets=[Name(id='var')],
...                    value=Constant(value=123))).dump('stmt')
0: var = 123
Assign - ROOT 0,0..0,9
  .targets[1]
   0] Name 'var' Store - 0,0..0,3
  .value Constant 123 - 0,6..0,9
>>> FST.fromast(ast.parse('if 1:\n    j = 5')).dump('stmt')
Module - ROOT 0,0..1,9
  .body[1]
0: if 1:
   0] If - 0,0..1,9
     .test Constant 1 - 0,3..0,4
     .body[1]
1:     j = 5
      0] Assign - 1,4..1,9
        .targets[1]
         0] Name 'j' Store - 1,4..1,5
        .value Constant 5 - 1,8..1,9
>>> FST.fromast(Slice(lower=Constant(value=1), step=Name(id='step'))).dump('node')
0: 1::step
Slice - ROOT 0,0..0,7
0: 1
  .lower Constant 1 - 0,0..0,1
0:    step
  .step Name 'step' Load - 0,3..0,7
@staticmethod
def get_options() -> dict[str, typing.Any]:

Get a dictionary of ALL options. These are the same options that can be passed to operations and this function returns their global defaults which are used when those options are not passed to operations or if they are passed with a value of None.

When these options are missing or None in a call to an operation, then the default option as specified here is used.

Parameters:

Returns:

  • {option: value, ...}: Dictionary of all global default options.

Examples:

>>> from pprint import pp
>>> pp(FST.get_options())
{'raw': False,
 'trivia': True,
 'coerce': True,
 'elif_': True,
 'pep8space': True,
 'docstr': True,
 'pars': 'auto',
 'pars_walrus': True,
 'pars_arglike': True,
 'norm': False,
 'norm_self': None,
 'norm_get': None,
 'norm_put': None,
 'set_norm': 'both',
 'matchor_norm': 'value',
 'op_side': 'left'}
@staticmethod
def get_option(option: str, options: Mapping[str, Any] = {}) -> object:

Get a single option from options dict or global default if option not in dict or is None there. For a list of options used see options().

Parameters:

  • option: Name of option to get, see options().
  • options: Dictionary which may or may not contain the requested option.

Returns:

  • Any: The option value from the passed options dict, if passed and not None there, else the global default value for option.

Examples:

>>> FST.get_option('pars')
'auto'
>>> FST.get_option('pars', {'pars': True})
True
>>> FST.get_option('pars', {'pars': None})
'auto'
@staticmethod
def set_options(**options) -> dict[str, typing.Any]:

Set global defaults for options parameters.

Parameters:

  • options: Names / values of parameters to set. These can also be passed to various methods to override the defaults set here for those individual operations, see options().

Returns:

  • options: dict of previous values of changed parameters, reset with set_options(**options).

Examples:

>>> FST.get_option('pars')
'auto'
>>> FST.set_options(pars=False)
{'pars': 'auto'}
>>> FST.get_option('pars')
False
>>> FST.set_options(**{'pars': 'auto'})
{'pars': False}
>>> FST.get_option('pars')
'auto'
>>> FST.set_options(pars=True, raw=True, docstr=False)
{'pars': 'auto', 'raw': False, 'docstr': True}
@staticmethod
@contextmanager
def options(**options) -> Iterator[dict[<property object at 0x714cea7d89f0>, Any]]:

Context manager to temporarily set global options defaults for a group of operations.

WARNING! Only the options specified in the call to this function will be returned to their original values when the context manager exits.

Options:

  • raw: When to do raw source operations. This may result in more nodes changed than just the targeted one(s).
    • False: Do not do raw source operations.
    • True: Only do raw source operations.
    • 'auto': Only do raw source operations if the normal operation fails in a way that raw might not.
  • trivia: What comments and empty lines to copy / delete when doing operations on elements which may have leading or trailing lines of stuff.
    • False: Don't copy / delete any trivia.
    • True: Same as ('block', 'line').
    • 'all': Same as ('all', 'all').
    • 'block': Same as ('block', 'block').
    • (leading, trailing): Tuple specifying individual behavior for leading and trailing trivia. The text options can also have a suffix of the form '+/-[#]', meaning plus or minus an optional integer which adds behavior for leading or trailing empty lines, explained below. The values for each element of the tuple can be:
      • False: Don't copy / delete any trivia.
      • True: For leading means 'block', for trailing means 'line'.
      • 'all[+/-[#]]': Get all leading or trailing comments regardless of if they are contiguous or not.
      • 'block[+/-[#]]': Get a single contiguous leading or trailing block of comments, an empty line ends the block.
      • 'line[+/-[#]]': Valid for trailing trivia only, means just the comment on the last line of the element.
      • int: A specific line number specifying the first or last line that can be returned as a comment or empty line. If not interrupted by other code, will always return up to this line.
  • coerce: Whether to allow coercion between compatible AST / FST types on put. For example allow put a non-slice expr as a slice to something that expects a slice of exprs or allowing use of arg where arguments is expected.
  • elif_: How to handle lone If statements as the only statements in an If statement orelse field.
    • False: Always put as a standalone If statement.
    • True: If putting a single If statement to an orelse field of a parent If statement then put it as an elif.
  • pep8space: Preceding and trailing empty lines for function and class definitions.
    • False: No empty lines.
    • True: Two empty lines at module scope and one empty line in other scopes.
    • 1: One empty line in all scopes.
  • docstr: Which docstrings are indentable / dedentable.
    • False: None.
    • True: All Expr string constants (as they serve no other coding purpose).
    • 'strict': Only string constants in expected docstring positions (functions, classes and top of module).
  • pars: How parentheses are handled, can be False, True or 'auto'. This is for individual element operations, slice operations ignore this as parentheses usually cannot be removed or may need to be added to keep the slices usable. Raw puts generally do not have parentheses added or removed automatically, except maybe removed according to this from the destination node if putting to a node instead of a pure location.
    • False: Parentheses are not MODIFIED, doesn't mean remove all parentheses. Not copied with nodes or removed on put from source or destination. Using incorrectly can result in invalid trees.
    • True: Parentheses are copied with nodes, added to copies if needed and not present, removed from destination on put if not needed there (but not source).
    • 'auto': Same as True except they are not returned with a copy and possibly removed from source on put if not needed (removed from destination first if needed and present on both).
  • pars_walrus: Whether to ADD parentheses to top level cut / copied NamedExpr nodes or not. If parentheses were already copied due to pars=True then setting this to False will not remove them.
    • True: Parenthesize cut / copied NamedExpr walrus expressions.
    • False: Do not parenthesize cut / copied NamedExpr walrus expressions.
    • None: Parenthesize according to the pars option.
  • pars_arglike: Whether to ADD parentheses to argument-like expressions (*not a, *b or c) when cut / copied either as single element or as part of a slice. If parentheses were already present then setting this to False will not remove them.
    • True: Parenthesize cut / copied argument-like expressions.
    • False: Do not parenthesize cut / copied argument-like expressions.
    • None: Parenthesize according to the pars option.
  • norm: Default normalize option for puts, gets and self target. Determines how ASTs which would otherwise be invalid because of an operation are handled. Mostly how zero or sometimes one-length elements which normally cannot be zero or one length are left / put / returned, e.g. zero-length Set. This option can be overridden individually for the three cases of norm_self (target), norm_get (return from a get()) and norm_put (what is being put if it is invalid or an alternate representation).
    • False: Allow the AST to go to an unsupported length or state and become invalid. A Set will result in empty curlies which reparse to a Dict. A MatchOr can go to 1 or zero length. Other AST types can also go to zero length. Useful for easier editing.
    • True: Do not allow the AST to go to an unsupported length or state. Can result in an exception being thrown no alternative exists or an alternate representation being used or accepted, e.g. A Set which results in zero length gets converted to {*()}.
    • str: In some cases an alternate representation can be specified instead of just True, e.g. 'call' for Set operations.
  • norm_self: Override for norm which only applies to a target self on which an operation is being carried out. If this is None then norm is used.
  • norm_get: Override for norm which only applies to the return value from a get() operation. If this is None then norm is used.
  • norm_put: Override for norm which only applies to the value to put for a put() operation. If this is None then norm is used.
  • set_norm: The alternate representation for an empty Set normalization by norm. This can also be set to False to disable normalization for all operations on a Set (unless one of the norm options is set to one of these string modes).
    • 'star': Starred sequence {*()} returned, this or other starred sequences {*[]} and {*{}} accepted to mean empty set on put operations.
    • 'call': set() call returned and recognized as empty.
    • 'both': Both 'star' and 'call' recognized on put as empty, 'start' used for return from get operation and normalization of self.
    • False: No Set normalization regardless of norm or norm_* options, just leave or return an invalid Set object.
  • matchor_norm: The possible alternate representation for MatchOr nodes with length 1. Length zero is always error unless False. This can also be set to False to disable normalization for all operations on a MatchOr (unless one of the norm options is set to one of these string modes).
    • value: Convert to single element pattern if length 1.
    • strict: Error on length 1 as well as length zero.
    • False: No MatchOr normalization regardless of norm or norm_* options, just leave or return an invalid MatchOr object. -op_side: When doing slice operations on a BoolOp or a Compare it may be necessary to specify which side operator is to be deleted or inserted before or after. This can take the values of 'left' or 'right' and specifies which side operator to delete for delete operations. For insert operations this specifies whether to insert before an operator 'left' or operand 'right', roughly translating to which side operator is treated as part of the operator+operand unit. This option is treated as a hint and may be overridden by source code passed to the slice operation or slice position constraints, it will never raise an error if set incompatible with what the operation needs. When inserting into a Compare a non-global op option may be necessary to specify extra missing operator to add if a dangling operator is not passed in the source to insert.
    • 'left': Delete preceding operator on left side of slice or insert before preceding operator.
    • 'right': Delete trailing operator on right side of slice or insert after preceding operator.

Note: pars behavior:

                                                          False      True    'auto'
Copy pars from source on copy / cut:                         no       yes        no
Add pars needed for parsability to copy:                     no       yes       yes
Remove unneeded pars from destination on put:                no       yes       yes
Remove unneeded pars from source on put:                     no        no       yes
Add pars needed for parse / precedence to source on put:     no       yes       yes

Note: trivia text suffix behavior:

  • '+[#]' On copy and delete an extra number of lines after any comments specified by the #. If no number is specified and just a '+' then all empty lines will be copied / deleted.
  • '-[#]' On delete but not copy, an extra number of lines after any comments specified by the #. If no number is specified and just a '-' then all empty lines will be deleted.

Examples:

>>> print(FST.get_option('pars'), FST.get_option('elif_'))
auto True
>>> with FST.options(pars=False, elif_=False):
...     print(FST.get_option('pars'), FST.get_option('elif_'))
False False
>>> print(FST.get_option('pars'), FST.get_option('elif_'))
auto True
def dump( self, src: Optional[Literal['stmt', 'all']] = None, full: bool = False, expand: bool = False, *, indent: int = 2, list_indent: int | bool = 1, loc: bool = True, color: bool | None = None, out: Union[Callable, TextIO] = <built-in function print>, eol: str | None = None) -> str | list[str] | None:

Dump a representation of the tree to stdout or other TextIO or return as a str or list of lines, or call a provided function once with each line of the output.

Parameters:

  • src: Either what level of source to show along with the nodes or a shorthand string which can specify almost all the formatting parameters as a string of characters.
    • 'stmt' or 'stmt+' means output statement source lines (including ExceptHandler and match_case) or top level source if level is below statement. The + means put all lines of source including non-coding ones so that whole source is shown.
    • 'node' or 'node+' means output source for each individual node. The + means the same as for 'stmt+'.
    • None does not output any source.
    • str: Can be a string for shortcut specification of source and flags by first letter, 's+feL' would be equivalent to .dump(src='stmt+', full=True, expand=True, loc=False).
      • 's' or 's+' means src='stmt' or src='stmt+'
      • 'n' or 'n+' means src='node' or src='node+'
      • 'f' means full=True
      • 'e' means expand=True
      • 'i' means list_indent=indent
      • 'I' means list_indent=False
      • 'L' means loc=False
      • 'c' means color=True
      • 'C' means color=False
  • full: If True then will list all fields in nodes including empty ones, otherwise will exclude most empty fields.
  • expand: If True then the output is a nice compact representation. If False then it is ugly and wasteful.
  • indent: Indentation per level as an integer (number of spaces) or a string.
  • list_indent: Extra indentation for elements of lists as an integer or string (added to indent, normally 0). If True then will be same as indent.
  • loc: Whether to put location of node in source or not.
  • color: True or False means whether to use ANSI color codes or not. If None then will only do so if out=print and sys.stdout.isatty() and not overridden by environment variables.
  • out: print means print to stdout, list returns a list of lines and str returns a whole string. TextIO will cann the write method for each line of output. Otherwise a Callable[[str], None] which is called for each line of output individually.
  • eol: What to put at the end of each text line, None means newline for TextIO out and nothing for other.

Returns:

  • str | list[str]: If those were requested with out=str or out=list else None and the output is sent one line at a time to linefunc, which by default is print.
  • None: Otherwise.

Examples:

>>> f = FST('''
... if 1:
...     call(a=b, **c)
... '''.strip())
>>> f.dump()
If - ROOT 0,0..1,18
  .test Constant 1 - 0,3..0,4
  .body[1]
   0] Expr - 1,4..1,18
     .value Call - 1,4..1,18
       .func Name 'call' Load - 1,4..1,8
       .keywords[2]
        0] keyword - 1,9..1,12
          .arg 'a'
          .value Name 'b' Load - 1,11..1,12
        1] keyword - 1,14..1,17
          .value Name 'c' Load - 1,16..1,17
>>> f.dump(src='node', indent=3, list_indent=True)
0: if 1:
If - ROOT 0,0..1,18
0:    1
   .test Constant 1 - 0,3..0,4
   .body[1]
1:     call(a=b, **c)
      0] Expr - 1,4..1,18
         .value Call - 1,4..1,18
1:     call
            .func Name 'call' Load - 1,4..1,8
            .keywords[2]
1:          a=b
               0] keyword - 1,9..1,12
                  .arg 'a'
1:            b
                  .value Name 'b' Load - 1,11..1,12
1:               **c
               1] keyword - 1,14..1,17
1:                 c
                  .value Name 'c' Load - 1,16..1,17
>>> f.dump(out=str)[:64]
'If - ROOT 0,0..1,18\n  .test Constant 1 - 0,3..0,4\n  .body[1]\n   '
>>> from pprint import pp
>>> pp(f.dump('stmt', loc=False, out=list))
['0: if 1:',
 'If - ROOT',
 '  .test Constant 1',
 '  .body[1]',
 '1:     call(a=b, **c)',
 '   0] Expr',
 '     .value Call',
 "       .func Name 'call' Load",
 '       .keywords[2]',
 '        0] keyword',
 "          .arg 'a'",
 "          .value Name 'b' Load",
 '        1] keyword',
 "          .value Name 'c' Load"]
def verify( self, mode: Union[Literal['all', 'strict', 'exec', 'eval', 'single', 'stmts', 'stmt', 'ExceptHandler', '_ExceptHandlers', 'match_case', '_match_cases', 'expr', 'expr_all', 'expr_arglike', 'expr_slice', 'Tuple_elt', 'Tuple', '_Assign_targets', '_decorator_list', 'boolop', 'operator', 'unaryop', 'cmpop', 'comprehension', '_comprehensions', '_comprehension_ifs', 'arguments', 'arguments_lambda', 'arg', 'keyword', 'alias', '_aliases', 'Import_name', '_Import_names', 'ImportFrom_name', '_ImportFrom_names', 'withitem', '_withitems', 'pattern', 'type_param', '_type_params', '_expr_arglikes'], type[ast.AST], NoneType] = None, reparse: bool = True, *, locs: bool = True, ctx: bool = True, raise_: bool = True) -> FST | None:

Sanity check. Walk the tree and make sure all ASTs have corresponding FST nodes with valid parent / child links, then (optionally) reparse source and make sure parsed tree matches currently stored tree (locations and everything). The reparse can only be carried out on root nodes but the link validation can be done on any level.

SPECIAL SLICEs like _decorator_list will verify ok with reparse but invalid nodes like an empty Set or block statements with empty bodies will not.

Parameters:

  • mode: Parse mode to use, otherwise if None then use the top level AST node type for the mode. Depending on how this is set will determine whether the verification is checking if is parsable by python ('exec' or 'strict' for example), or if the node itself is just in a valid state (where None is good). See fst.parsex.Mode.
  • reparse: Whether to reparse the source and compare ASTs (including location). Otherwise the check is limited to a structure check that all children have FST nodes which are all liked correctly to their parents. reparse=True only allowed on root node.
  • locs: Whether to compare locations after reparse or not.
  • ctx: Whether to compare ctx nodes after reparse or not.
  • raise_: Whether to raise an exception on verify failed or return None.

Returns:

  • None on failure to verify (if not raise_), otherwise self.

Examples:

>>> FST('var = 123').verify()
<Assign ROOT 0,0..0,9>
>>> FST('a:b:c').verify()
<Slice ROOT 0,0..0,5>
>>> bool(FST('a:b:c').verify('exec', raise_=False))
False
>>> FST('if a if b').verify()
<_comprehension_ifs ROOT 0,0..0,9>
>>> bool(FST('if a if b').verify('strict', raise_=False))
False
def mark(self) -> FST:

Return an object marking the current state of this FST tree. Used to reconcile() later for non-FST operation changes made (changing AST nodes directly). Currently is just a copy of the original tree but may change in the future.

Returns:

  • FST: A marked copy of self with any necessary information added for a later reconcile().
def reconcile(self, mark: FST, **options) -> FST:

Reconcile self with a previously marked version and return a new valid FST tree. This is meant for allowing non-FST modifications to an FST tree and later converting it to a valid FST tree to preserve as much formatting as possible and maybe continue operating in FST land. Only AST nodes from the original tree carry formatting information, so the more of those are replaced the more formatting is lost.

Note: When replacing the AST nodes, make sure you are replacing the nodes in the parent AST fields, not the .a attribute in FST nodes, that won't do anything.

WARNING! Just like an ast.unparse(), the fact that this function completes successfully does NOT mean the output is syntactically correct if you put weird nodes where they don't belong, maybe accidentally. In order to make sure the result is valid (syntactically) you should run verify() on the output. This still won't guarantee you have actual valid code, def f(x, x): pass parses ok but will cause an error if you try to compile it.

Parameters:

  • mark: A previously marked snapshot of self. This object is not consumed on use, success or failure.
  • options: See options().

Returns:

  • FST: A new valid reconciled FST if possible.

Examples:

>>> f = FST('''
... @decorator  # something
... def function(a: int, b=2)->int:  # blah
...     return a+b  # return this
...
... def other_function(a, b):
...     return a - b  # return that
... '''.strip())
>>> m = f.mark()
>>> f.a.body[0].returns = Name('float')  # pure AST
>>> f.a.body[0].args.args[0].annotation = Name('float')
>>> f.a.body[0].decorator_list[0] = FST('call_decorator(1, 2, 3)').a
>>> f.a.body[1].name = 'last_function'  # can change non-AST
>>> f.a.body[1].body[0] = f.a.body[0].body[0]  # AST from same FST tree
>>> other = FST('def first_function(a, b): return a * b  # yay!')
>>> f.a.body.insert(0, other.a)  # AST from other FST tree
>>> f = f.reconcile(m, pep8space=1)
>>> print('\n'.join(l or '.' for l in f.lines))  # print this way for doctest
def first_function(a, b): return a * b  # yay!
.
@call_decorator(1, 2, 3)  # something
def function(a: float, b=2)->float:  # blah
    return a+b  # return this
.
def last_function(a, b):
    return a+b  # return this
>>> m = f.mark()
>>> body = f.a.body[1].body
>>> f.a.body[1] = FST('def f(): pass').a
>>> f.a.body[1].body = body
>>> f = f.reconcile(m, pep8space=1)
>>> print('\n'.join(l or '.' for l in f.lines))
def first_function(a, b): return a * b  # yay!
.
def f():
    return a+b  # return this
.
def last_function(a, b):
    return a+b  # return this
def copy_ast(self) -> ast.AST:

Copy the AST node tree of this FST node, not including any FST stuff. Use when you just want a copy of the AST tree from this point down.

Needless to say since this just returns an AST all formatting is lost, except that the AST nodes will have the same lineno, col_offset, end_lineno and end_col_offset values as they had in the FST tree.

Returns:

  • AST: Copied AST tree from this point down.

Examples:

>>> a = FST('[0, 1, 2, 3]').copy_ast()
>>> print(type(a))
<class 'ast.List'>
>>> print(dump(a))
List(elts=[Constant(value=0), Constant(value=1), Constant(value=2), Constant(value=3)], ctx=Load())
def copy(self, **options) -> FST:

Copy this node to a new top-level tree, dedenting and fixing as necessary. If copying root node then an identical copy is made and no fixes / modifications are applied.

Parameters:

Returns:

  • FST: Copied node.

Examples:

>>> FST('[0, 1, 2, 3]').elts[1].copy().src
'1'
def cut(self, **options) -> FST:

Cut out this node to a new top-level tree (if possible), dedenting and fixing as necessary. Cannot cut root node.

Parameters:

Returns:

  • FST: Cut node.

Examples:

>>> (f := FST('[0, 1, 2, 3]')).elts[1].cut().src
'1'
>>> f.src
'[0, 2, 3]'
def replace( self, code: Union[FST, ast.AST, list[str], str, NoneType], one: bool = True, **options) -> FST | None:

Replace or delete (if code=None, if possible) this node. Returns the new node for self, not the old replaced node, or None if was deleted or raw replaced and the old node disappeared. Cannot delete root node. CAN replace root node, in which case the accessing FST node remains the same but the top-level AST and source change.

Parameters:

  • code: FST, AST or source str or list[str] to put at this location. None to delete this node.
  • one: Default True means replace with a single element. If False and field allows it then can replace single element with a slice.
  • options: See options().
    • to: Special option which only applies replacing in raw mode (either through True or 'auto'). Instead of replacing just this node, will replace the entire span from this node to the node specified in to with the code passed.

Returns:

  • FST or None: Returns the new node if successfully replaced or None if deleted or raw replace and corresponding new node could not be found.

Examples:

>>> FST('[0, 1, 2, 3]').elts[1].replace('4').root.src
'[0, 4, 2, 3]'
>>> f = FST('def f(a, /, b, *c, **d) -> int: pass')
>>> f.args.posonlyargs[0].replace(')', to=f.returns, raw=True)  # raw reparse
>>> f.src
'def f(): pass'
def remove(self, **options) -> None:

Delete this node if possible, equivalent to replace(None, ...). Cannot delete root node.

Parameters:

Examples:

>>> (f := FST('[0, 1, 2, 3]')).elts[1].remove()
>>> f.src
'[0, 2, 3]'
def get( self, idx: Union[int, Literal['end'], NoneType] = None, stop: Union[int, NoneType, Literal[False]] = False, field: str | None = None, *, cut: bool = False, **options) -> FST | None | str | ellipsis | int | float | complex | bytes | bool:

Copy or cut an individual child node or a slice of child nodes from self if possible. This function can do everything that get_slice() can.

Parameters:

  • idx: The index of the child node to get if the field being gotten from contains multiple elements or the start of the slice to get if getting a slice (by specifying stop). If the field being gotten from is an individual element then this should be None. If stop is specified and getting a slice then a None here means copy from the start of the list.
  • stop: The end index (exclusive) of the child node to get if getting a slice from a field that contains multiple elements. This should be one past the last element to get (like python list indexing). If this is False then it indicates that a single element is being requested and not a slice. If this is None then it indicates a slice operation to the end of the list (like python a[start:]).
  • field: The name of the field to get the element(s) from, which can be an individual element like a value or a list like body. If this is None then the default field for the node type is used. Most node types have a common-sense default field, e.g. body for all block statements, value for things like Return and Yield. Dict, MatchMapping and Compare nodes have special-case handling for a None field.
  • cut: Whether to cut out the child node (if possible) or not (just copy).
  • options: See options().

Note: The field value can be passed positionally in either the idx or stop parameter. If passed in idx then the field is assumed individual and if passed in stop then it is a list and an individual element is being gotten from idx and not a slice.

Returns:

  • FST: When getting an actual node (most situations).
  • str: When getting am identifier, like from Name.id.
  • constant: When getting a constant (fst.astutil.constant), like from MatchSingleton.value.

Examples:

>>> FST('[0, 1, 2, 3]').get(1).src
'1'
>>> (f := FST('[0, 1, 2, 3]')).get(1, 3).src
'[1, 2]'
>>> f.src
'[0, 1, 2, 3]'
>>> (f := FST('[0, 1, 2, 3]')).get(1, 3, cut=True).src
'[1, 2]'
>>> f.src
'[0, 3]'
>>> FST('[0, 1, 2, 3]').get(None, 3).src
'[0, 1, 2]'
>>> FST('[0, 1, 2, 3]').get(-3, None).src
'[1, 2, 3]'
>>> FST('if 1: i = 1\nelse: j = 2').get(0).src
'i = 1'
>>> FST('if 1: i = 1\nelse: j = 2').get('orelse').src
'j = 2'
>>> FST('if 1: i = 1\nelse: j = 2; k = 3').get(1, 'orelse').src
'k = 3'
>>> FST('if 1: i = 1\nelse: j = 2; k = 3; l = 4; m = 5').get(1, 3, 'orelse').src
'k = 3; l = 4'
>>> FST('return 1').get().src  # default field is 'value'
'1'
>>> FST('[0, 1, 2, 3]').get().src  # 'elts' slice copy is made
'[0, 1, 2, 3]'
def put( self, code: Union[FST, ast.AST, list[str], str, ellipsis, int, float, complex, bytes, bool, NoneType], idx: Union[int, Literal['end'], NoneType] = None, stop: Union[int, NoneType, Literal[False]] = False, field: str | None = None, *, one: bool = True, **options) -> FST | None:

Put an individual node or a slice of nodes to self if possible. This function can do everything that put_slice() can. The node is passed as an existing top-level FST, AST, string or list of string lines. If passed as an FST then it should be considered "consumed" after this function returns and is no logner valid, even on failure. AST is copied.

WARNING! The original self node may be invalidated during the operation if using raw mode (either specifically or as a fallback), so make sure to swap it out for the return value of this function if you will keep using the variable you called this method on. It will be changed accordingly in the tree but any other outside references to the node may become invalid.

Parameters:

  • code: The node to put as an FST (must be root node), AST, a string or list of line strings. If putting to an identifier field then this should be a string and it will be taken literally (no parsing). If putting to a constant likew MatchSingleton.value or Constant.value then this should be an appropriate primitive constant value.
  • idx: The index of the field node to put to if the field being put to contains multiple elements or the start of the slice to put if putting a slice (by specifying stop). If the field being put to is an individual element then this should be None. If stop is specified and putting a slice then a None here means put starting from the beginning of the list.
  • stop: The end index (exclusive) of the field node to put to if putting a slice to a field that contains multiple elements. This should be one past the last element to put (like python list indexing). If this is False then it indicates that a single element is being put and not a slice. If this is None then it indicates a slice operation to the end of the list (like python a[start:]).
  • field: The name of the field to put the element(s) to, which can be an individual element like a value or a list like body. If this is None then the default field for the node type is used. Most node types have a common-sense default field, e.g. body for all block statements, value for things like Return and Yield. Dict, MatchMapping and Compare nodes have special-case handling for a None field.
  • one: Only has meaning if putting a slice, and in this case True specifies that the source should be put as a single element to the range specified even if it is a valid slice. False indicates a true slice operation replacing the range with the slice passed, which must in this case be a compatible slice type.
  • options: See options().
    • to: Special option which only applies when putting a single element in raw mode (either through True or 'auto'). Instead of replacing just the target node, will replace the entire span from the target node to the node specified in to with the code passed.

Note: The field value can be passed positionally in either the idx or stop parameter. If passed in idx then the field is assumed individual and if passed in stop then it is a list and an individual element is being gotten from idx and not a slice.

Returns:

  • self or None if a raw put was done and corresponding new node could not be found.

Examples:

>>> FST('[0, 1, 2, 3]').put('4', 1).src
'[0, 4, 2, 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', 1, 3).src
'[0, (4, 5), 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', 1, 3, one=False).src
'[0, 4, 5, 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', None, 3).src
'[(4, 5), 3]'
>>> (f := FST('[0, 1, 2, 3]')).put('4, 5', -3, None, one=False).src
'[0, 4, 5]'
>>> print(FST('if 1: i = 1\nelse: j = 2').put('z = -1', 0).src)
if 1:
    z = -1
else: j = 2
>>> print(FST('if 1: i = 1\nelse: j = 2').put('z = -1', 0, 'orelse').src)
if 1: i = 1
else:
    z = -1
>>> print(FST('if 1: i = 1\nelse: j = 2')
...       .put('z = -1\ny = -2\nx = -3', 'orelse', one=False).src)
if 1: i = 1
else:
    z = -1
    y = -2
    x = -3
>>> print((f := FST('if 1: i = 1\nelse: j = 2'))
...       .put('z = -1', 0, raw=True, to=f.orelse[0]).root.src)
if 1: z = -1
def get_slice( self, start: Union[int, Literal['end'], NoneType] = None, stop: int | None = None, field: str | None = None, *, cut: bool = False, **options) -> FST:

Copy or cut a slice of child nodes from self if possible.

Parameters:

  • start: The start of the slice to get, or None for the beginning of the entire range.
  • stop: The end index (exclusive) of the slice to get. This should be one past the last element to get (like python list indexing). If this is None then it indicates a slice operation to the end of the list (like python a[start:]).
  • field: The name of the field to get the elements from, which can be an individual element like a value or a list like body. If this is None then the default field for the node type is used. Most node types have a common-sense default field, e.g. body for all block statements, elts for things like List and Tuple. MatchMapping and Compare nodes have special-case handling for a None field.
  • cut: Whether to cut out the slice or not (just copy).
  • options: See options().

Note: The field value can be passed positionally in either the start or stop parameter. If passed in start then the slice is assumed to be the entire range, and if passed in stop then the slice goes from start to the end of the range.

Returns:

  • FST: Slice node of nodes gotten.

Examples:

>>> FST('[0, 1, 2, 3]').get_slice(1).src
'[1, 2, 3]'
>>> FST('[0, 1, 2, 3]').get_slice(None, -1).src
'[0, 1, 2]'
>>> (f := FST('[0, 1, 2, 3]')).get_slice(1, 3, cut=True).src
'[1, 2]'
>>> f.src
'[0, 3]'
>>> f = FST('if 1: i = 1\nelse: j = 2; k = 3; l = 4; m = 5')
>>> print(f.src)
if 1: i = 1
else: j = 2; k = 3; l = 4; m = 5
>>> print(f.get_slice(1, 3, 'orelse', cut=True).src)
k = 3; l = 4
>>> print(f.src)
if 1: i = 1
else: j = 2; m = 5
def put_slice( self, code: Union[FST, ast.AST, list[str], str, NoneType], start: Union[int, Literal['end'], NoneType] = None, stop: int | None = None, field: str | None = None, *, one: bool = False, **options) -> FST | None:

Put a slice of nodes to self if possible. The node is passed as an existing top-level FST, AST, string or list of string lines. If passed as an FST then it should be considered "consumed" after this function returns and is no logner valid, even on failure. AST is copied.

WARNING! The original self node may be invalidated during the operation if using raw mode (either specifically or as a fallback), so make sure to swap it out for the return value of this function if you will keep using the variable you called this method on. It will be changed accordingly in the tree but any other outside references to the node may become invalid.

Parameters:

  • code: The slice to put as an FST (must be root node), AST, a string or list of line strings.
  • start: The start of the slice to put, or None for the beginning of the entire range.
  • stop: The end index (exclusive) of the slice. This should be one past the last element to put (like python list indexing). If this is None then it indicates a slice operation to the end of the list (like python a[start:]).
  • field: The name of the field to put the elements to. If this is None then the default field for the node type is used. Most node types have a common-sense default field, e.g. body for all block statements, elts for things like List and Tuple. MatchMapping and Compare nodes have special-case handling for a None field.
  • one: True specifies that the source should be put as a single element to the range specified even if it is a valid slice. False indicates a true slice operation replacing the range with the slice passed, which must in this case be a compatible slice type.
  • options: See options().

Note: The field value can be passed positionally in either the start or stop parameter. If passed in start then the slice is assumed to be the entire range, and if passed in stop then the slice goes from start to the end of the range.

Returns:

  • self or None if a raw put was done and corresponding new node could not be found.

Examples:

>>> FST('[0, 1, 2, 3]').put('4', 1).src
'[0, 4, 2, 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', 1, 3).src
'[0, (4, 5), 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', 1, 3, one=False).src
'[0, 4, 5, 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', None, 3).src
'[(4, 5), 3]'
>>> FST('[0, 1, 2, 3]').put('4, 5', -3, None, one=False).src
'[0, 4, 5]'
>>> print(FST('if 1: i = 1\nelse: j = 2').put('z = -1', 0).src)
if 1:
    z = -1
else: j = 2
>>> print(FST('if 1: i = 1\nelse: j = 2').put('z = -1', 0, 'orelse').src)
if 1: i = 1
else:
    z = -1
>>> print(FST('if 1: i = 1\nelse: j = 2')
...       .put('z = -1\ny = -2\nx = -3', 'orelse', one=False).src)
if 1: i = 1
else:
    z = -1
    y = -2
    x = -3
>>> print((f := FST('if 1: i = 1\nelse: j = 2'))
...       .put('z = -1', 0, raw=True, to=f.orelse[0]).root.src)
if 1: z = -1
def get_src( self, ln: int, col: int, end_ln: int, end_col: int, as_lines: bool = False) -> str | list[str]:

Get source at location, without dedenting or any other modification, returned as a string or individual lines. The first and last lines are cropped to start col and end_col.

Can call on any node in tree to access source for the whole tree.

The coordinates passed in are clipped to the whole valid source area.

Parameters:

  • ln: Start line of span to get (0 based).
  • col: Start column (character) on start line.
  • end_ln: End line of span to get (0 based, inclusive).
  • end_col: End column (character, exclusive) on end line.
  • as_lines: If False then source is returned as a single string with embedded newlines. If True then source is returned as a list of line strings (without newlines).

Returns:

  • str | list[str]: A single string or a list of lines if as_lines=True. If lines then there are no trailing newlines in the individual line strings.

Examples:

>>> FST('if 1:\n  i = 2').get_src(0, 3, 1, 5)
'1:\n  i ='
>>> FST('if 1:\n  i = 2').get_src(0, 3, 1, 5, as_lines=True)
['1:', '  i =']
>>> (f := FST('if 1:\n  i = 2')).get_src(*f.body[0].bloc)
'i = 2'
def put_src( self, code: Union[FST, ast.AST, list[str], str, NoneType], ln: int, col: int, end_ln: int, end_col: int, action: Optional[Literal['reparse', 'offset']] = 'reparse') -> tuple[int, int]:

Put source and maybe adjust AST tree for source modified. The adjustment may be a reparse of the area changed, an offset of nodes (assuming put source was just trivia and wouldn't affect the tree) or nothing at all. The action options are:

  • 'reparse': Put source and reparse. There are no rules on what is put, it is simply put and parse is attempted.

    The reparse that is triggered is of at least a statement level node or a statement block header, and can be multiple statements if the location spans those or even statements outside of the location if the reparse affects things like elif. FST nodes in the region of the put or even outside of it can become invalid. The only FST node guaranteed not to change is the root node (identity, the AST it holds can change).

    When putting source raw by location like this there are no automatic modifications made to the source or destination. No parenthesization, prefixes or suffixes or indentation, the source is just put and parsed so you are responsible for the correct indentation and precedence.

    After put and successful reparse a new node can be found using find_loc() functions from the original start ln and col and newly returned end_ln and end_col. It is possible that None is returned from these functions if no good candidate is found (since this can be used to delete or merge nodes).

    self doesn't matter in this case, can call on any node in tree, even one which is not touched by the source change and the appropriate nodes will be found and reparsed.

  • 'offset': Put source and offset nodes around it according to what node the put_src() was called on. In this case self matters and should be the last node down within which the location is contained. Any child nodes of this node are offset differently from this self node and its parents.

    The self node and its parents will not have their start locations offset if the put is an insert at the start (as the location is considered to be INSIDE these nodes), whereas child nodes will be moved.

    The self node and its parents will have their end locations offset if the put ends at this location whereas child nodes will not.

  • None: Nothing is reparsed or offset, the source is just put. There are few cases where this will result in a valid tree but can include removal of comments and trailing whitespace on a line or changing lines between empty and comment.

If the code is passed as an AST then it is unparsed to a string and that string is put into the location. If code is an FST then the exact source of the FST is put. If passed as a string or lines then that is put directly.

The coordinates passed in are clipped to the whole valid source area.

Parameters:

  • code: The code to put as an FST (must be root node), AST, a string or list of line strings.
  • ln: Start line of span to put (0 based).
  • col: Start column (character) on start line.
  • end_ln: End line of span to put (0 based, inclusive).
  • end_col: End column (character, exclusive) on end line.
  • action: What action to take on the AST tree, the options are 'reparse', 'offset' or None.

Returns:

  • (end_ln, end_col): New end location of source put (all source after this was not modified).

Examples:

>>> f = FST('i = 1')
>>> f.put_src('2', 0, 4, 0, 5)
(0, 5)
>>> f.src
'i = 2'
>>> f = FST('i = 1')
>>> f.put_src('+= 3', 0, 2, 0, 5)
(0, 6)
>>> f.src
'i += 3'
>>> f = FST('{a: b, c: d, e: f}')
>>> f.put_src('**', 0, 7, 0, 10)
(0, 9)
>>> f.src
'{a: b, **d, e: f}'
>>> f = FST('''
... if a:
...   i = 2
... elif b:
...   j = 3
... '''.strip())
>>> print(f.src)
if a:
  i = 2
elif b:
  j = 3
>>> f.put_src('''
... else:
...   if b:
...     k = 4
... '''.strip(), *f.orelse[0].loc[:2], *f.loc[2:])
(4, 9)
>>> print(f.src)
if a:
  i = 2
else:
  if b:
    k = 4
>>> (f := FST('a, b')).put_src(' ', 0, 4, 0, 4, 'offset')
(0, 5)
>>> f.src, f.loc, f.elts[1].loc
('a, b ', fstloc(0, 0, 0, 5), fstloc(0, 3, 0, 4))
def pars(self, *, shared: bool | None = True) -> fst.common.fstloc | None:

Return the location of enclosing GROUPING parentheses if present. Will balance parentheses if self is an element of a tuple and not return the parentheses of the tuple. Likwise will not normally return the parentheses of an enclosing arguments parent or class bases list (unless shared=None, but that is mostly for internal use).

Only normally works on (and makes sense for) expr or pattern nodes, otherwise returns self.bloc and count of 0. Also handles special case of a single generator expression argument to a function sharing parameters with the call arguments, in which case a count of -1 and the location of the GeneratorExp without its enclosing parentheses may be returned, if this is enabled with shared=False.

This function is cached so feel free to call as often as is needed.

Note: For a Starred this will always return the location of the whole Starred since that cannot be parenthesized itself but rather its child. If you want he parenteses of the child then do starred.value.pars().

Parameters:

  • shared: If True then will include parentheses of a single call argument generator expression if they are shared with the call arguments enclosing parentheses with a count of 0. If False then does not return these and returns a count of -1, and thus the location is not a full valid GeneratorExp location. If None then returns ANY directly enclosing parentheses, whether they belong to this node or not.

Returns:

  • fstloc | None: Location of enclosing parentheses if present else self.bloc (which can be None). Negative parentheses count (from shared parens solo call arg generator expression) can also be checked in the case of shared=False via fst.pars() > fst.bloc. If only loc is returned, it will be an fstloc which will still have the count of parentheses in an attribute .n.

Examples:

>>> FST('i').pars()
fstlocn(0, 0, 0, 1, n=0)
>>> FST('(i)').pars()
fstlocn(0, 0, 0, 3, n=1)
>>> FST('((i))').pars()
fstlocn(0, 0, 0, 5, n=2)
>>> FST('(1, 2)').pars()  # tuple pars are not considered grouping pars
fstlocn(0, 0, 0, 6, n=0)
>>> FST('((1, 2))').pars()
fstlocn(0, 0, 0, 8, n=1)
>>> FST('call(a)').args[0].pars()  # any node, not just root
fstlocn(0, 5, 0, 6, n=0)
>>> FST('call((a))').args[0].pars()
fstlocn(0, 5, 0, 8, n=1)
>>> FST('call(i for i in j)').args[0].pars()
fstlocn(0, 4, 0, 18, n=0)
>>> FST('call(i for i in j)').args[0].pars(shared=False)  # exclude shared pars
fstlocn(0, 5, 0, 17, n=-1)
>>> FST('call((i for i in j))').args[0].pars(shared=False)
fstlocn(0, 5, 0, 19, n=0)
def par(self, force: bool = False, *, whole: bool = True) -> FST:

Parenthesize node if it MAY need it. Will not parenthesize atoms which are always enclosed like List, or nodes which are not _is_parenthesizable(), unless force=True. Will add parentheses to unparenthesized Tuple and brackets to unbracketed MatchSequence adjusting the node location. If dealing with a Starred then the parentheses are applied to the child.

WARNING! This function doesn't do any higher level syntactic validation. So if you parenthesize something that shouldn't be parenthesized, and you wind up poking an eye out, that's on you.

Parameters:

  • force: If True then will add another layer of parentheses regardless if any already present.
  • whole: If at root then parenthesize whole source instead of just node, if False then only node.

Returns:

  • self

Examples:

>>> FST('a + b').par().src
'(a + b)'
>>> FST('(a + b)').par().src  # already parenthesized, so nothing done
'(a + b)'
>>> FST('(a + b)').par(force=True).src  # force it
'((a + b))'
>>> FST('1, 2').par().src  # parenthesize tuple
'(1, 2)'
>>> FST('i').par().src  # an atom doesn't need parentheses
'i'
>>> FST('i').par(force=True).src  # so must be forced
'(i)'
>>> # parethesize MatchSequence puts brackets like ast.unparse()
>>> FST('1, 2', 'pattern').par().src
'[1, 2]'
>>> FST('*a or b').par().src  # par() a Starred parenthesizes its child
'*(a or b)'
>>> FST('call(i = 1 + 2)').keywords[0].value.par().root.src  # not just root node
'call(i = (1 + 2))'
def unpar(self, node: bool = False, *, shared: bool | None = True) -> FST:

Remove all parentheses if present. Normally removes just grouping parentheses but can also remove Tuple parentheses and MatchSequence parentheses or brackets intrinsic to the node if node=True. If dealing with a Starred then the parentheses are checked in and removed from the child. If shared=None then will also remove parentheses which do not belong to this node but enclose it directly, this is mostly for internal use.

WARNING! This function doesn't do any higher level syntactic validation. So if you unparenthesize something that shouldn't be unparenthesized, and you wind up poking an eye out, that's on you.

Parameters:

  • node: If True then will remove intrinsic parentheses from a parenthesized Tuple and parentheses / brackets from parenthesized / bracketed MatchSequence, otherwise only removes grouping parentheses if present.
  • shared: Whether to allow merge of parentheses of single call argument generator expression with Call parentheses or not. If None then will attempt to unparenthesize any enclosing parentheses, whether they belong to this node or not (meant for internal use).

Returns:

  • self

Examples:

>>> FST('a + b').unpar().src  # nothing done if no pars
'a + b'
>>> FST('(a + b)').unpar().src
'a + b'
>>> FST('((a + b))').unpar().src  # removes all
'a + b'
>>> FST('(1, 2)').unpar().src  # but not from tuple
'(1, 2)'
>>> FST('(1, 2)').unpar(node=True).src  # unless explicitly specified
'1, 2'
>>> FST('(((1, 2)))').unpar().src
'(1, 2)'
>>> FST('(((1, 2)))').unpar(node=True).src
'1, 2'
>>> FST('[1, 2]', 'pattern').unpar().src
'[1, 2]'
>>> FST('[1, 2]', 'pattern').unpar(node=True).src
'1, 2'
>>> FST('*(a or b)').unpar().src  # unpar() a Starred unparenthesizes its child
'*a or b'
>>> # not just root node
>>> FST('call(i = (1 + 2))').keywords[0].value.unpar().root.src
'call(i = 1 + 2)'
>>> # by default allows sharing
>>> FST('call(((i for i in j)))').args[0].unpar().root.src
'call(i for i in j)'
>>> # unless told not to
>>> FST('call(((i for i in j)))').args[0].unpar(shared=False).root.src
'call((i for i in j))'
def next( self, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get next sibling of self in syntactic order, only within parent.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if last valid sibling in parent, otherwise next node.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.elts[0].next().src
'[3, 4]'
>>> print(f.elts[1].next())
None
def prev( self, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get previous sibling of self in syntactic order, only within parent.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if first valid sibling in parent, otherwise previous node.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.elts[1].prev().src
'[1, 2]'
>>> print(f.elts[0].prev())
None
def first_child( self, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get first valid child in syntactic order.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if no valid children, otherwise first valid child.

Examples:

>>> f = FST('def f(a: list[str], /, reject: int, *c, d=100, **e): pass')
>>> f.first_child().src
'a: list[str], /, reject: int, *c, d=100, **e'
>>> f.args.first_child().src
'a: list[str]'
>>> f.args.first_child().first_child().src
'list[str]'
def last_child( self, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get last valid child in syntactic order.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if no valid children, otherwise last valid child.

Examples:

>>> f = FST('def f(a: list[str], /, reject: int, *c, d=100, **e): pass')
>>> f.last_child().src
'pass'
>>> f.args.last_child().src
'e'
def last_header_child( self, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get last valid child in syntactic order in a block header (before the :), e.g. the something in if something: pass.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if no valid children or if self is not a block statement, otherwise last valid child in the block header.

Examples:

>>> print(FST('if something:\n    i = 2\n    i = 3')
...       .last_header_child().src)
something
>>> print(FST('try: pass\nexcept Exception as exc: pass').handlers[0]
...       .last_header_child().src)
Exception
>>> print(FST('with a, b: pass').last_header_child().src)
b
>>> print(FST('try: pass\nfinally: pass').last_header_child())
None
>>> print(FST('i = 1').last_header_child())
None
def next_child( self, from_child: FST | None, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get the next child in syntactic order, meant for simple iteration.

This is a slower way to iterate vs. walk(), but will work correctly if ANYTHING in the tree is modified during the walk as long as the replaced node and its parent is used for the following call.

Parameters:

  • from_child: Child node we are coming from which may or may not have location.
  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if last valid child in self, otherwise next child node.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.next_child(f.elts[0]).src
'[3, 4]'
>>> print(f.next_child(f.elts[1]))
None
>>> f = FST('[this, is_, reparsed, each, step, and_, still, walks, ok]')
>>> n = None
>>> while n := f.next_child(n):
...     if isinstance(n.a, Name):
...         n = n.replace(n.id[::-1])
>>> f.src
'[siht, _si, desraper, hcae, pets, _dna, llits, sklaw, ko]'
def prev_child( self, from_child: FST | None, with_loc: Union[bool, Literal['all', 'own']] = True) -> FST | None:

Get the previous child in syntactic order, meant for simple iteration.

This is a slower way to iterate vs. walk(), but will work correctly if ANYTHING in the tree is modified during the walk as long as the replaced node and its parent is used for the following call.

Parameters:

  • from_child: Child node we are coming from which may or may not have location.
  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.

Returns:

  • None if first valid child in self, otherwise previous child node.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.prev_child(f.elts[1]).src
'[1, 2]'
>>> print(f.prev_child(f.elts[0]))
None
>>> f = FST('[this, is_, reparsed, each, step, and_, still, walks, ok]')
>>> n = None
>>> while n := f.prev_child(n):
...     if isinstance(n.a, Name):
...         n = n.replace(n.id[::-1])
>>> f.src
'[siht, _si, desraper, hcae, pets, _dna, llits, sklaw, ko]'
def step_fwd( self, with_loc: Union[bool, Literal['all', 'own', 'allown']] = True, *, recurse_self: bool = True) -> FST | None:

Step forward in the tree in syntactic order, as if walk()ing forward, NOT the inverse of step_back(). Will walk up parents and down children to get the next node, returning None only when we are at the end of the whole thing.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.
    • 'allown' Same as 'own' but recurse into nodes with non-own locations (even though those nodes are not returned). This is only really meant for internal use to safely call from .loc location calculation.
  • recurse_self: Whether to allow recursion into self to return children or move directly to next nodes of self on start.

Returns:

  • None if last valid node in tree, otherwise next node in order.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.elts[0].src
'[1, 2]'
>>> f.elts[0].step_fwd().src
'1'
>>> f.elts[0].step_fwd(recurse_self=False).src
'[3, 4]'
>>> f.elts[0].elts[1].src
'2'
>>> f.elts[0].elts[1].step_fwd().src
'[3, 4]'
>>> f = FST('[this, [is_, [reparsed, each], step, and_, still], walks, ok]')
>>> n = f.elts[0]
>>> while True:
...     if isinstance(n.a, Name):
...         n = n.replace(n.id[::-1])
...     if not (n := n.step_fwd()):
...         break
>>> f.src
'[siht, [_si, [desraper, hcae], pets, _dna, llits], sklaw, ko]'
def step_back( self, with_loc: Union[bool, Literal['all', 'own', 'allown']] = True, *, recurse_self: bool = True) -> FST | None:

Step backward in the tree in syntactic order, as if walk()ing backward, NOT the inverse of step_fwd(). Will walk up parents and down children to get the next node, returning None only when we are at the beginning of the whole thing.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.
    • 'allown' Same as 'own' but recurse into nodes with non-own locations (even though those nodes are not returned). This is only really meant for internal use to safely call from .loc location calculation.
  • recurse_self: Whether to allow recursion into self to return children or move directly to previous nodes of self on start.

Returns:

  • None if first valid node in tree, otherwise previous node in order.

Examples:

>>> f = FST('[[1, 2], [3, 4]]')
>>> f.elts[1].src
'[3, 4]'
>>> f.elts[1].step_back().src
'4'
>>> f.elts[1].step_back(recurse_self=False).src
'[1, 2]'
>>> f.elts[1].elts[0].src
'3'
>>> f.elts[1].elts[0].step_back().src
'[1, 2]'
>>> f = FST('[this, [is_, [reparsed, each], step, and_, still], walks, ok]')
>>> n = f.elts[-1]
>>> while True:
...     if isinstance(n.a, Name):
...         n = n.replace(n.id[::-1])
...     if not (n := n.step_back()):
...         break
>>> f.src
'[siht, [_si, [desraper, hcae], pets, _dna, llits], sklaw, ko]'
def walk( self, with_loc: Union[bool, Literal['all', 'own']] = False, *, self_: bool = True, recurse: bool = True, scope: bool = False, back: bool = False) -> Generator[FST, bool, NoneType]:

Walk self and descendants in syntactic order. When walking, you can send(False) to the generator to skip recursion into the current child. send(True) to allow recursion into child if called with recurse=False or scope=True would otherwise disallow it. Can send multiple times, last value sent takes effect.

The walk is defined forwards or backwards in that it returns a parent, then recurses into the children and walks those in the given direction, recursing into each child's children before continuing with siblings. Walking backwards will not generate the same sequence as list(walk())[::-1] due to this behavior.

It is safe to modify the nodes (or previous nodes) as they are being walked as long as those modifications don't touch the parent or following nodes. This means normal .replace() is fine as long as raw=False.

The walk is relatively efficient but if all you need to do is just walk ALL the AST children without any bells or whistles then ast.walk() will be a bit faster.

Parameters:

  • with_loc: Return nodes depending on their location information.
    • False: All nodes with or without location.
    • True: Only nodes which have intrinsic AST locations and also larger computed location nodes like comprehension, withitem, match_case and arguments (the last one only if there are actually arguments present).
    • 'all': Same as True but also operators with calculated locations (excluding and and or since they do not always have a well defined location).
    • 'own': Only nodes with their own intrinsic AST locations, same as True but excludes those larger nodes with calculated locations.
  • self_: If True then self will be returned first with the possibility to skip children with send(False), otherwise will start directly with children.
  • recurse: Whether to recurse into children by default, send(True) for a given node will always override this.
  • scope: If True then will walk only within the scope of self. Meaning if called on a FunctionDef then will only walk children which are within the function scope. Will yield children which have their own scopes, and the parts of them which are visible in this scope (like default argument values), but will not recurse into them unless send(True) is done for that child.
  • back: If True then walk every node in reverse syntactic order. This is not the same as a full forwards walk reversed due to recursion (parents are still returned before children, only in reverse sibling order).

Examples:

>>> import ast
>>> f = FST('def f(a: list[str], /, reject: int, *c, d=100, **e): pass')
>>> for g in (gen := f.walk(with_loc=True)):
...     if isinstance(g.a, ast.arg) and g.a.arg == 'reject':
...         _ = gen.send(False)
...     else:
...         print(f'{g!r:<30}{g.src[:50]!r}')
<FunctionDef ROOT 0,0..0,57>  'def f(a: list[str], /, reject: int, *c, d=100, **e'
<arguments 0,6..0,50>         'a: list[str], /, reject: int, *c, d=100, **e'
<arg 0,6..0,18>               'a: list[str]'
<Subscript 0,9..0,18>         'list[str]'
<Name 0,9..0,13>              'list'
<Name 0,14..0,17>             'str'
<arg 0,37..0,38>              'c'
<arg 0,40..0,41>              'd'
<Constant 0,42..0,45>         '100'
<arg 0,49..0,50>              'e'
<Pass 0,53..0,57>             'pass'
>>> f = FST('''
... def f():
...     def g(arg=1) -> int:
...         pass
...     val = [i for i in iterator]
... '''.strip())
>>> for g in f.walk(True, scope=True):
...     print(f'{g!r:<30}{g.src[:47]!r}')
<FunctionDef ROOT 0,0..3,31>  'def f():\n    def g(arg=1) -> int:\n        pass\n'
<FunctionDef 1,4..2,12>       'def g(arg=1) -> int:\n        pass'
<Constant 1,14..1,15>         '1'
<Assign 3,4..3,31>            'val = [i for i in iterator]'
<Name 3,4..3,7>               'val'
<ListComp 3,10..3,31>         '[i for i in iterator]'
<Name 3,22..3,30>             'iterator'
>>> for g in f.walk(True, back=True):
...     print(f'{g!r:<30}{g.src[:47]!r}')
<FunctionDef ROOT 0,0..3,31>  'def f():\n    def g(arg=1) -> int:\n        pass\n'
<Assign 3,4..3,31>            'val = [i for i in iterator]'
<ListComp 3,10..3,31>         '[i for i in iterator]'
<comprehension 3,13..3,30>    'for i in iterator'
<Name 3,22..3,30>             'iterator'
<Name 3,17..3,18>             'i'
<Name 3,11..3,12>             'i'
<Name 3,4..3,7>               'val'
<FunctionDef 1,4..2,12>       'def g(arg=1) -> int:\n        pass'
<Pass 2,8..2,12>              'pass'
<Name 1,20..1,23>             'int'
<arguments 1,10..1,15>        'arg=1'
<Constant 1,14..1,15>         '1'
<arg 1,10..1,13>              'arg'
def parents(self, self_: bool = False) -> Generator[FST, NoneType, NoneType]:

Generator which yields parents all the way up to root. If self_ is True then will yield self first.

Parameters:

  • self_: Whether to yield self first.

Examples:

>>> list(FST('i = (f(), g())', 'exec').body[0].value.elts[0].parents())
[<Tuple 0,4..0,14>, <Assign 0,0..0,14>, <Module ROOT 0,0..0,14>]
>>> list(FST('i = (f(), g())', 'exec').body[0].value.elts[0].parents(self_=True))
[<Call 0,5..0,8>, <Tuple 0,4..0,14>, <Assign 0,0..0,14>, <Module ROOT 0,0..0,14>]
def parent_stmt(self, self_: bool = False, mod: bool = True) -> FST | None:

The first parent which is a stmt or optionally mod node (if any). If self_ is True then will check self first (possibly returning self), otherwise only checks parents.

Parameters:

  • self_: Whether to include self in the search, if so and self matches criteria then it is returned.
  • mod: Whether to return mod nodes if found.

Examples:

>>> FST('if 1: i = 1', 'exec').body[0].body[0].value.parent_stmt()
<Assign 0,6..0,11>
>>> FST('if 1: i = 1', 'exec').body[0].body[0].parent_stmt()
<If 0,0..0,11>
>>> FST('if 1: i = 1', 'exec').body[0].parent_stmt()
<Module ROOT 0,0..0,11>
>>> print(FST('if 1: i = 1', 'exec').body[0].parent_stmt(mod=False))
None
>>> FST('if 1: i = 1', 'exec').body[0].parent_stmt(self_=True)
<If 0,0..0,11>
def parent_stmtish(self, self_: bool = False, mod: bool = True) -> FST | None:

The first parent which is a stmt, ExceptHandler, match_case or optionally mod node (if any). If self_ is True then will check self first, otherwise only checks parents.

Examples:

>>> (FST('try: pass\nexcept: pass', 'exec')
...  .body[0].handlers[0].body[0].parent_stmtish())
<ExceptHandler 1,0..1,12>
>>> FST('try: pass\nexcept: pass', 'exec').body[0].handlers[0].parent_stmtish()
<Try 0,0..1,12>
>>> FST('try: pass\nexcept: pass', 'exec').body[0].parent_stmtish()
<Module ROOT 0,0..1,12>
>>> FST('match a:\n  case 1: pass').cases[0].body[0].parent_stmtish()
<match_case 1,2..1,14>
>>> FST('match a:\n  case 1: pass').cases[0].pattern.parent_stmtish()
<match_case 1,2..1,14>
def parent_block(self, self_: bool = False, mod: bool = True) -> FST | None:

The first parent which opens a block that self lives in (if any). Types include FunctionDef, AsyncFunctionDef, ClassDef, For, AsyncFor, While, If, With, AsyncWith, Match, Try, TryStar, ExceptHandler, match_case or optionally mod node (if any). If self_ is True then will check self first, otherwise only checks parents.

Examples:

>>> FST('if 1: i = 1', 'exec').body[0].body[0].value.parent_block()
<If 0,0..0,11>
>>> FST('if 1: i = 1', 'exec').body[0].parent_block()
<Module ROOT 0,0..0,11>
def parent_scope(self, self_: bool = False, mod: bool = True) -> FST | None:

The first parent which opens a scope that self lives in (if any). Types include FunctionDef, AsyncFunctionDef, ClassDef, Lambda, ListComp, SetComp, DictComp, GeneratorExp or optionally mod node (if any). If self_ is True then will check self first, otherwise only checks parents.

Examples:

>>> FST('if 1: i = 1', 'exec').body[0].body[0].value.parent_scope()
<Module ROOT 0,0..0,11>
>>> (FST('def f():\n  if 1: i = 1', 'exec')
...  .body[0].body[0].body[0].value.parent_scope())
<FunctionDef 0,0..1,13>
>>> FST('lambda: None', 'exec').body[0].value.body.parent_scope()
<Lambda 0,0..0,12>
>>> FST('[i for i in j]', 'exec').body[0].value.elt.parent_scope()
<ListComp 0,0..0,14>
def parent_named_scope(self, self_: bool = False, mod: bool = True) -> FST | None:

The first parent which opens a named scope that self lives in (if any). Types include FunctionDef, AsyncFunctionDef, ClassDef or optionally mod node (if any). If self_ is True then will check self first, otherwise only checks parents.

Examples:

>>> FST('if 1: i = 1', 'exec').body[0].body[0].value.parent_named_scope()
<Module ROOT 0,0..0,11>
>>> (FST('def f():\n  if 1: i = 1', 'exec')
...  .body[0].body[0].body[0].value.parent_named_scope())
<FunctionDef 0,0..1,13>
>>> (FST('def f(): lambda: None', 'exec')
...  .body[0].body[0].value.body.parent_named_scope())
<FunctionDef 0,0..0,21>
>>> (FST('class cls: [i for i in j]', 'exec')
...  .body[0].body[0].value.elt.parent_named_scope())
<ClassDef 0,0..0,25>
def parent_non_expr(self, self_: bool = False, strict: bool = False) -> FST | None:

The first parent which is not an expr. If self_ is True then will check self first (possibly returning self), otherwise only checks parents.

Parameters:

  • self_: Whether to include self in the search, if so and self matches criteria then it is returned.
  • strict: False means consider comprehension, arguments, arg and keyword nodes as expr for the sake of the walk up since these nodes can have other expr parents (meaning they will be skipped). True means only expr nodes, which means you could get an arg or comprehension node for example which still has expr parents. Also expr_context, boolop, operator, unaruop and cmpop are included if strict=False but this only makes sense if self_=True and you are calling this function on one of those.

Examples:

>>> FST('if 1: i = 1 + a[b]').body[0].value.right.value.parent_non_expr()
<Assign 0,6..0,18>
>>> (FST('match a:\n case {a.b.c: 1}: pass')
...  .cases[0].pattern.keys[0].value.value.parent_non_expr())
<MatchMapping 1,6..1,16>
>>> FST('var = call(a, b=1)').value.keywords[0].value.parent_non_expr()
<Assign ROOT 0,0..0,18>
>>> (FST('var = call(a, b=1)')
...  .value.keywords[0].value.parent_non_expr(strict=True))
<keyword 0,14..0,17>
def parent_pattern(self, self_: bool = False) -> FST | None:

The first parent which is a pattern. If self_ is True then will check self first (possibly returning self), otherwise only checks parents.

Parameters:

  • self_: Whether to include self in the search, if so and self matches criteria then it is returned.

Examples:

>>> FST('case 1+1j: pass').pattern.value.left.parent_pattern().src
'1+1j'
>>> (FST('case 1 | {a.b: c}: pass')
...  .pattern.patterns[1].patterns[0].parent_pattern(self_=True))
<MatchAs 0,15..0,16>
>>> (FST('case 1 | {a.b: c}: pass')
...  .pattern.patterns[1].patterns[0].parent_pattern())
<MatchMapping 0,9..0,17>
>>> (FST('case 1 | {a.b: c}: pass')
...  .pattern.patterns[1].patterns[0].parent_pattern().parent_pattern())
<MatchOr 0,5..0,17>
def child_path( self, child: FST, as_str: bool = False) -> list[fst.common.astfield] | str:

Get path to child node from self which can later be used on a copy of this tree to get to the same relative child node.

Parameters:

  • child: Child node to get path to, can be self in which case an empty path is returned.
  • as_str: If True will return the path as a python-ish string suitable for attribute access, else a list of astfields which can be used more directly.

Returns:

  • list[astfield] | str: Path to child if exists, otherwise raises.

Examples:

>>> (f := FST('[i for i in j]', 'exec')).child_path(f.body[0].value.elt)
[astfield('body', 0), astfield('value'), astfield('elt')]
>>> ((f := FST('[i for i in j]', 'exec'))
...  .child_path(f.body[0].value.elt, as_str=True))
'body[0].value.elt'
>>> (f := FST('i')).child_path(f)
[]
>>> (f := FST('i')).child_path(f, as_str=True)
''
def child_from_path( self, path: list[fst.common.astfield] | str, last_valid: bool = False) -> Union[FST, Literal[False]]:

Get child node specified by path if it exists. If succeeds then it doesn't mean that the child node is guaranteed to be the same or even same type as was originally used to get the path, just that the path is valid. For example after deleting an element from a list the item at the former element's location will be the previous next element.

Parameters:

  • path: Path to child as a list of astfields or string.
  • last_valid: If True then return the last valid node along the path, will not fail, can return self.

Returns:

  • FST: Child node if path is valid, otherwise False if path invalid. False and not None because None can be in a field that can hold an AST but False can not.

Examples:

>>> f = FST('[i for i in j]', 'exec')
>>> f.child_from_path(f.child_path(f.body[0].value.elt)).src
'i'
>>> f.child_from_path(f.child_path(f.body[0].value.elt, True)).src
'i'
>>> FST('[0, 1, 2, 3]', 'exec').child_from_path('body[0].value.elts[4]')
False
>>> (FST('[0, 1, 2, 3]', 'exec')
...  .child_from_path('body[0].value.elts[4]', last_valid=True).src)
'[0, 1, 2, 3]'
>>> (f := FST('i')).child_from_path([]) is f
True
>>> (f := FST('i')).child_from_path('') is f
True
def repath(self) -> FST:

Recalculate self from path from root. Useful if self has been replaced by another node by some operation. When nodes are deleted the corresponding FST.a and AST.f attributes are set to None. The root, parent and pfield attributes are left so that things like this can work. Useful when a node has been deleted but you want to know where it was and what may be there now.

Returns:

  • FST: Possibly self or the node which took our place at our relative position from root.

Examples:

>>> f = FST('[0, 1, 2, 3]')
>>> g = f.elts[1]
>>> print(type(g.a), g.root)
<class 'ast.Constant'> <List ROOT 0,0..0,12>
>>> f.put('x', 1, raw=True)  # raw forces reparse at List
<List ROOT 0,0..0,12>
>>> print(g.a, g.root)
None <List ROOT 0,0..0,12>
>>> g = g.repath()
>>> print(type(g.a), g.root)
<class 'ast.Name'> <List ROOT 0,0..0,12>
def find_loc_in( self, ln: int, col: int, end_ln: int, end_col: int, allow_exact: Union[bool, Literal['top']] = True) -> FST | None:

Find the lowest level node which entirely contains location (starting search at self). The search will only find nodes at self or below, no parents.

Parameters:

  • ln: Start line of location to search for (0 based).
  • col: Start column (character) on start line.
  • end_ln: End line of location to search for (0 based, inclusive).
  • end_col: End column (character, inclusive with FST.end_col, exclusive with FST.col) on end line.
  • allow_exact: Whether to allow return of exact location match with node or not. True means allow return of node which matches location exactly. False means location must be inside the node but cannot be touching BOTH ends of the node. This basically determines whether you can get the exact node of the location or its parent. A value of 'top' specifies an exact match is allowed and return the highest level node with the match, otherwise the lowest level exact match is returned. This only applies to nodes like Expr which will have the same location as the contained expr or a Module which only contains a single statement without any other junk like comments or empty lines surrounding it.

Returns:

  • FST | None: Node which entirely contains location, either exactly or not, or None if no such node.

Examples:

>>> FST('i = val', 'exec').find_loc_in(0, 6, 0, 7)
<Name 0,4..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 4, 0, 7)
<Name 0,4..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 4, 0, 7, allow_exact=False)
<Assign 0,0..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 5, 0, 7, allow_exact=False)
<Name 0,4..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 4, 0, 6, allow_exact=False)
<Name 0,4..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 3, 0, 7)
<Assign 0,0..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 3, 0, 7, allow_exact=False)
<Assign 0,0..0,7>
>>> print(FST('i = val', 'exec').find_loc_in(0, 0, 0, 7, allow_exact=False))
None
>>> FST('i = val\n', 'exec').find_loc_in(0, 0, 0, 7, allow_exact=False)
<Module ROOT 0,0..1,0>
>>> FST('i = val', 'exec').find_loc_in(0, 0, 0, 7)
<Assign 0,0..0,7>
>>> FST('i = val', 'exec').find_loc_in(0, 0, 0, 7, allow_exact='top')
<Module ROOT 0,0..0,7>
def find_in_loc(self, ln: int, col: int, end_ln: int, end_col: int) -> FST | None:

Find the first highest level node which is contained entirely in location (inclusive, starting search at self). To reiterate, the search will only find nodes at self or below, no parents.

Parameters:

  • ln: Start line of location to search (0 based).
  • col: Start column (character) on start line.
  • end_ln: End line of location to search (0 based, inclusive).
  • end_col: End column (character, inclusive with FST.end_col, exclusive with FST.col) on end line.

Returns:

  • FST | None: First node in syntactic order which is entirely contained in the location or None if no such node.

Examples:

>>> FST('i = val', 'exec').find_in_loc(0, 0, 0, 7)
<Module ROOT 0,0..0,7>
>>> FST('i = val', 'exec').find_in_loc(0, 1, 0, 7)
<Name 0,4..0,7>
>>> FST('i = val', 'exec').find_in_loc(0, 4, 0, 7)
<Name 0,4..0,7>
>>> print(FST('i = val', 'exec').find_in_loc(0, 5, 0, 7))
None
def find_loc( self, ln: int, col: int, end_ln: int, end_col: int, exact_top: bool = False) -> FST | None:

Find node which best fits location. If an exact location match is found then that is returned with the top parameter specifying which of multiple nodes at same location is returned if that is the case. Otherwise find_in_loc() is preferred if there is a match and if not then find_loc_in(). The search is done efficiently so that the same nodes are not walked multiple times.

Parameters:

  • ln: Start line of location to search for (0 based).
  • col: Start column (character) on start line.
  • end_ln: End line of location to search for (0 based, inclusive).
  • end_col: End column (character, inclusive with FST.end_col, exclusive with FST.col) on end line.
  • exact_top: If an exact location match is found and multiple nodes share this location (Expr and expr) then this decides whether the highest level node is returned or the lowest (True means highest).

Returns:

  • FST | None: Node found if any.

Examples:

>>> FST('var', 'exec').find_loc(0, 0, 0, 3)
<Name 0,0..0,3>
>>> FST('var', 'exec').find_loc(0, 0, 0, 3, exact_top=True)
<Module ROOT 0,0..0,3>
>>> FST('var', 'exec').find_loc(0, 1, 0, 2)
<Name 0,0..0,3>
>>> FST('var', 'exec').find_loc(0, 1, 0, 2, exact_top=True)
<Name 0,0..0,3>
>>> FST('var', 'exec').find_loc(0, 0, 1, 4)
<Module ROOT 0,0..0,3>
>>> repr(FST('var', 'exec').find_loc(0, 2, 1, 4))
'None'
is_stmt_or_mod: bool

Is a stmt or mod.

is_stmtish: bool

Is a stmt, ExceptHandler or match_case.

is_stmtish_or_mod: bool

Is a stmt, ExceptHandler, match_case or mod.

is_block: bool

Is a node which opens a block. Types include FunctionDef, AsyncFunctionDef, ClassDef, For, AsyncFor, While, If, With, AsyncWith, Match, Try, TryStar, ExceptHandler or match_case.

is_block_or_mod: bool

Is a node which opens a block. Types include FunctionDef, AsyncFunctionDef, ClassDef, For, AsyncFor, While, If, With, AsyncWith, Match, Try, TryStar, ExceptHandler, match_case or mod.

is_scope: bool

Is a node which opens a scope. Types include FunctionDef, AsyncFunctionDef, ClassDef, Lambda, ListComp, SetComp, DictComp or GeneratorExp.

is_scope_or_mod: bool

Is a node which opens a scope. Types include FunctionDef, AsyncFunctionDef, ClassDef, Lambda, ListComp, SetComp, DictComp, GeneratorExp or mod.

is_named_scope: bool

Is a node which opens a named scope. Types include FunctionDef, AsyncFunctionDef or ClassDef.

is_named_scope_or_mod: bool

Is a node which opens a named scope. Types include FunctionDef, AsyncFunctionDef, ClassDef or mod.

is_anon_scope: bool

Is a node which opens an anonymous scope. Types include Lambda, ListComp, SetComp, DictComp or GeneratorExp.

is_funcdef: bool

Is a sync or async function definition node, FunctionDef or AsyncFunctionDef.

is_def: bool

Is a sync or async function or class definition node, FunctionDef, AsyncFunctionDef or ClassDef.

is_def_or_mod: bool

Is a sync or async function or class definition node, FunctionDef, AsyncFunctionDef, ClassDef or mod.

is_for: bool

Is a sync or async for node, For or AsyncFor.

is_with: bool

Is a sync or async with node, With or AsyncWith.

def is_elif(self) -> bool | None:

Whether self is an elif or not, or not an If at all.

Returns:

  • True if is elif If, False if is normal If, None if is not If at all.

Examples:

>>> FST('if 1: pass\nelif 2: pass').orelse[0].is_elif()
True
>>> FST('if 1: pass\nelse:\n  if 2: pass').orelse[0].is_elif()
False
>>> print(FST('if 1: pass\nelse:\n  i = 2').orelse[0].is_elif())
None