exploration.commands

  • Authors: Peter Mawhorter
  • Consulted:
  • Date: 2023-12-27
  • Purpose: The Command type which implements a mini-DSL for graph editing. Command lists can be embedded as effects in a graph to give ultimate flexibility in defining graphs that modify themselves in complex ways.

Commands represent a simplified mini-programming-language for editing a graph and/or exploration. The language stores a single 'current value' which many effects set or operate on (and which can be referred to as '$_' where variable names are used) The previous 'current value' is also stored in '$__' for convenience. It also allows the definition of arbitrary variables, and calling graph/exploration methods works mainly via keyword arguments pulled automatically from the set of defined variables. This allows each command to have a fixed number of arguments. The following definitions specify the different types of command, each as a named tuple with a 'command' slot in the first position that names the command.

  1"""
  2- Authors: Peter Mawhorter
  3- Consulted:
  4- Date: 2023-12-27
  5- Purpose: The `Command` type which implements a mini-DSL for graph
  6    editing. Command lists can be embedded as effects in a graph to give
  7    ultimate flexibility in defining graphs that modify themselves in
  8    complex ways.
  9
 10Commands represent a simplified mini-programming-language for editing a
 11graph and/or exploration. The language stores a single 'current value'
 12which many effects set or operate on (and which can be referred to as
 13'$_' where variable names are used) The previous 'current value' is also
 14stored in '$__' for convenience. It also allows the definition of
 15arbitrary variables, and calling graph/exploration methods works mainly
 16via keyword arguments pulled automatically from the set of defined
 17variables. This allows each command to have a fixed number of arguments.
 18The following definitions specify the different types of command, each
 19as a named tuple with a 'command' slot in the first position that names
 20the command.
 21"""
 22
 23from typing import (
 24    Tuple, Literal, TypeAlias, Dict, Callable, Union, Any, Optional,
 25    List, Collection, get_args
 26)
 27
 28import collections
 29import math
 30import copy
 31import logging
 32import re
 33
 34#--------#
 35# Errors #
 36#--------#
 37
 38
 39class CommandError(Exception):
 40    """
 41    TODO: This?
 42    An error raised during command execution will be converted to one of
 43    the subtypes of this class. Stores the underlying error as `cause`,
 44    and also stores the `command` and `line` where the error occurred.
 45    """
 46    def __init__(
 47        self,
 48        command: 'Command',
 49        line: int,
 50        cause: Exception
 51    ) -> None:
 52        self.command = command
 53        self.line = line
 54        self.cause = cause
 55
 56    def __str__(self):
 57        return (
 58            f"\n  Command block, line {self.line}, running command:"
 59            f"\n    {self.command!r}"
 60            f"\n{type(self.cause).__name__}: {self.cause}"
 61        )
 62
 63
 64class CommandValueError(CommandError, ValueError):
 65    "A `ValueError` encountered during command execution."
 66    pass
 67
 68
 69class CommandTypeError(CommandError, TypeError):
 70    "A `TypeError` encountered during command execution."
 71    pass
 72
 73
 74class CommandIndexError(CommandError, IndexError):
 75    "A `IndexError` encountered during command execution."
 76    pass
 77
 78
 79class CommandKeyError(CommandError, KeyError):
 80    "A `KeyError` encountered during command execution."
 81    pass
 82
 83
 84class CommandOtherError(CommandError):
 85    """
 86    Any error other than a `ValueError`, `TypeError`, `IndexError`, or
 87    `KeyError` that's encountered during command execution. You can use
 88    the `.cause` field to figure out what the type of the underlying
 89    error was.
 90    """
 91    pass
 92
 93
 94#----------#
 95# Commands #
 96#----------#
 97
 98LiteralValue: Tuple[Literal['val'], str] = collections.namedtuple(
 99    'LiteralValue',
100    ['command', 'value']
101)
102"""
103A command that replaces the current value with a specific literal value.
104The values allowed are `None`, `True`, `False`, integers, floating-point
105numbers, and quoted strings (single or double quotes only). Note that
106lists, tuples, dictionaries, sets, and other complex data structures
107cannot be created this way.
108"""
109
110EstablishCollection: Tuple[
111    Literal['empty'],
112    Literal['list', 'tuple', 'set', 'dict']
113] = collections.namedtuple(
114    'EstablishCollection',
115    ['command', 'collection']
116)
117"""
118A command which replaces the current value with an empty collection. The
119collection type must be one of 'list', 'tuple', 'set', or 'dict'.
120"""
121
122AppendValue: Tuple[Literal['append'], str] = collections.namedtuple(
123    'AppendValue',
124    ['command', 'value']
125)
126"""
127A command which appends/adds a specific value (either a literal value
128that could be used with `LiteralValue` or a variable reference starting
129with '$') to the current value, which must be a list, tuple, or set.
130"""
131
132SetValue: Tuple[Literal['set'], str, str] = collections.namedtuple(
133    'SetValue',
134    ['command', 'location', 'value']
135)
136"""
137A command which sets the value for a specific key in a dictionary stored
138as the current value, or for at a specific index in a tuple or list.
139Both the key and the value may be either a literal that could be used
140with `LiteralValue` or a variable reference starting with '$'.
141
142When used with a set, if the value is truthy the location is added to
143the set, and otherwise the location is removed from the set.
144"""
145
146PopValue: Tuple[Literal['pop']] = collections.namedtuple(
147    'PopValue',
148    ['command']
149)
150"""
151A command which pops the last value in the tuple or list which is stored
152as the current value, setting the value it pops as the new current value.
153
154Does not work with sets or dictionaries.
155"""
156
157GetValue: Tuple[Literal['get'], str] = collections.namedtuple(
158    'GetValue',
159    ['command', 'location']
160)
161"""
162A command which reads a value at a particular index within the current
163value and sets that as the new current value. For lists or tuples, the
164index must be convertible to an integer (but can also be a variable
165reference storing such a value). For sets, if the listed value is in the
166set the result will be `True` and otherwise it will be `False`. For
167dictionaries, it looks up a value under that key.
168
169For all other kinds of values, it looks for an attribute with the same
170name as the string specified and returns the value of that attribute
171(it's an error to specify a non-string value in this case).
172"""
173
174RemoveValue: Tuple[Literal['remove'], str] = collections.namedtuple(
175    'RemoveValue',
176    ['command', 'location']
177)
178"""
179A command which removes an item from a tuple, list, set, or dictionary
180which is stored as the current value. The value should be an integer (or
181a variable holding one) if the current value is a tuple or list. Unlike
182python's `.remove` method, this removes a single item at a particular
183index/under a particular key, not all copies of a particular value.
184"""
185
186BinaryOperator: 'TypeAlias' = Literal[
187    '+', '-', '*', '/', '//', '**', '%', '^', '|', '&', 'and', 'or',
188    '<', '>', '<=', '>=', '==', 'is'
189]
190"""
191The supported binary operators for commands.
192"""
193
194UnaryOperator: 'TypeAlias' = Literal['-', '~', 'not']
195"""
196The supported binary operators for commands.
197"""
198
199ApplyOperator: Tuple[
200    Literal['op'],
201    BinaryOperator,
202    str,
203    str
204] = collections.namedtuple(
205    'ApplyOperator',
206    ['command', 'op', 'left', 'right']
207)
208"""
209A command which establishes a new current value based on the result of an
210operator. See `BinaryOperator` for the list of supported operators. The
211two operand may be literals as accepted by `LiteralValue` or variable
212references starting with '$'.
213"""
214
215ApplyUnary: Tuple[
216    Literal['unary'],
217    UnaryOperator,
218    str
219] = collections.namedtuple(
220    'ApplyUnary',
221    ['command', 'op', 'value']
222)
223"""
224The unary version of `ApplyOperator`. See `UnaryOperator` for the list
225of supported operators.
226"""
227
228VariableAssignment: Tuple[
229    Literal['assign'],
230    str,
231    str
232] = collections.namedtuple(
233    'VariableAssignment',
234    ['command', 'varname', 'value']
235)
236"""
237Assigns the specified value (may be a variable reference) into a named
238variable. The variable name should not start with '$', which is used when
239referencing variables. If it does, variable substitution will be
240performed to compute the name of the variable being created.
241"""
242
243VariableDeletion: Tuple[Literal['delete'], str] = collections.namedtuple(
244    'VariableDeletion',
245    ['command', 'varname']
246)
247"""
248Deletes the variable with the given name. Doesn't actually delete the
249stored object if it's reference elsewhere. Useful for unspecifying
250arguments for a 'call' command.
251"""
252
253LoadVariable: Tuple[Literal['load'], str] = collections.namedtuple(
254    'LoadVariable',
255    ['command', 'varname']
256)
257"""
258Loads the named variable as the current value, replacing the old current
259value. The variable name should normally be specified without the '$',
260with '$' variable substitution will take place and the resulting string
261will be used as the name of the variable to load.
262"""
263
264CallType: 'TypeAlias' = Literal[
265    'builtin',
266    'stored',
267    'graph',
268    'exploration'
269]
270"""
271Types of function calls available via the 'call' command.
272"""
273
274FunctionCall: Tuple[
275    Literal['call'],
276    CallType,
277    str
278] = collections.namedtuple(
279    'FunctionCall',
280    ['command', 'target', 'function']
281)
282"""
283A command which calls a function or method. IF the target is 'builtin',
284one of the `COMMAND_BUILTINS` will be called. If the target is 'graph' or
285'exploration' then a method of the current graph or exploration will be
286called. If the target is 'stored', then the function part will be
287treated as a variable reference and the function stored in that variable
288will be called.
289
290For builtins, the current value will be used as the only argument. There
291are two special cases: for `round`, if an 'ndigits' variable is defined
292its value will be used for the optional second argument, and for `range`,
293if the current value is `None`, then the values of the 'start', 'stop',
294and/or 'step' variables are used for its arguments, with a default start
295of 0 and a default step of 1 (there is no default stop; it's an error if
296you don't supply one). If the current value is not `None`, `range` just
297gets called with the current value as its only argument.
298
299For graph/exploration methods, the current value is ignored and each
300listed parameter is sourced from a defined variable of that name, with
301parameters for which there is no defined variable going unsupplied
302(which might be okay if they're optional). For varargs parameters, the
303value of the associated variable will be converted to a tuple and that
304will be supplied as if using '*'; for kwargs parameters the value of the
305associated variable must be a dictionary, and it will be applied as if
306using '**' (except that duplicate arguments will not cause an error;
307instead those coming fro the dictionary value will override any already
308supplied).
309"""
310
311COMMAND_BUILTINS: Dict[str, Callable] = {
312    'len': len,
313    'min': min,
314    'max': max,
315    'round': round,  # 'ndigits' may be specified via the environment
316    'ceil': math.ceil,
317    'floor': math.floor,
318    'int': int,
319    'float': float,
320    'str': str,
321    'list': list,
322    'tuple': tuple,
323    'dict': dict,
324    'set': set,
325    'copy': copy.copy,
326    'deepcopy': copy.deepcopy,
327    'range': range,  # parameter names are 'start', 'stop', and 'step'
328    'reversed': reversed,
329    'sorted': sorted,  # cannot use key= or reverse=
330    'print': print,  # prints just one value, ignores sep= and end=
331    'warning': logging.warning,  # just one argument
332}
333"""
334The mapping from names to built-in functions usable in commands. Each is
335available for use with the 'call' command when 'builtin' is used as the
336target. See `FunctionCall` for more details.
337"""
338
339SkipCommands: Tuple[
340    Literal['skip'],
341    str,
342    str
343] = collections.namedtuple(
344    'SkipCommands',
345    ['command', 'condition', 'amount']
346)
347"""
348A command which skips forward or backward within the command list it's
349included in, but only if a condition value is True. A skip amount of 0
350just continues execution as normal. Negative amounts jump to previous
351commands (so e.g., -2 will re-execute the two commands above the skip
352command), while positive amounts skip over subsequent commands (so e.g.,
3531 will skip over one command after this one, resuming execution with the
354second command after the skip).
355
356If the condition is False, execution continues with the subsequent
357command as normal.
358
359If the distance value is a string instead of an integer, the skip will
360redirect execution to the label that uses that name. Note that the
361distance value may be a variable reference, in which case the integer or
362string inside the reference will determine where to skip to.
363"""
364
365Label: Tuple[Literal['label'], str] = collections.namedtuple(
366    'Label',
367    ['command', 'name']
368)
369"""
370Has no effect, but establishes a label that can be skipped to using the
371'skip' command. Note that instead of just a fixed label, a variable name
372can be used and variable substitution will determine the label name in
373that case, BUT there are two restrictions: the value must be a string,
374and you cannot execute a forward-skip to a label which has not already
375been evaluated, since the value isn't known when the skip occurs. If you
376use a literal label name instead of a variable, you will be able to skip
377down to that label from above.
378
379When multiple labels with the same name occur, a skip command will go to
380the last label with that name before the skip, only considering labels
381after the skip if there are no labels with that name beforehand (and
382skipping to the first available label in that case).
383"""
384
385Command: 'TypeAlias' = Union[
386    LiteralValue,
387    EstablishCollection,
388    AppendValue,
389    SetValue,
390    PopValue,
391    GetValue,
392    RemoveValue,
393    ApplyOperator,
394    ApplyUnary,
395    VariableAssignment,
396    VariableDeletion,
397    LoadVariable,
398    FunctionCall,
399    SkipCommands,
400    Label
401]
402"""
403The union type for any kind of command. Note that these are all tuples,
404all of their members are strings in all cases, and their first member
405(named 'command') is always a string that uniquely identifies the type of
406the command. Use the `command` function to get some type-checking while
407constructing them.
408"""
409
410Scope: 'TypeAlias' = Dict[str, Any]
411"""
412A scope holds variables defined during the execution of a sequence of
413commands. Variable names (sans the '$' sign) are mapped to arbitrary
414Python values.
415"""
416
417CommandResult: 'TypeAlias' = Tuple[
418    Scope,
419    Union[int, str, None],
420    Optional[str]
421]
422"""
423The main result of a command is an updated scope (usually but not
424necessarily the same scope object that was used to execute the command).
425Additionally, there may be a skip integer that indicates how many
426commands should be skipped (if positive) or repeated (if negative) as a
427result of the command just executed. This value may also be a string to
428skip to a label. There may also be a label value which indicates that
429the command that was executed defines that label.
430"""
431
432
433def isSimpleValue(valStr: str) -> bool:
434    """
435    Returns `True` if the given string is a valid simple value for use
436    with a command. Simple values are strings that represent `None`,
437    `True`, `False`, integers, floating-point numbers, and quoted
438    strings (single or double quotes only). Numbers themselves are not
439    simple values.
440
441    Examples:
442
443    >>> isSimpleValue('None')
444    True
445    >>> isSimpleValue('True')
446    True
447    >>> isSimpleValue('False')
448    True
449    >>> isSimpleValue('none')
450    False
451    >>> isSimpleValue('12')
452    True
453    >>> isSimpleValue('5.6')
454    True
455    >>> isSimpleValue('3.2e-10')
456    True
457    >>> isSimpleValue('2 + 3j')  # ba-dump tsss
458    False
459    >>> isSimpleValue('hello')
460    False
461    >>> isSimpleValue('"hello"')
462    True
463    >>> isSimpleValue('"hel"lo"')
464    False
465    >>> isSimpleValue('"hel\\\\"lo"')  # note we're in a docstring here
466    True
467    >>> isSimpleValue("'hi'")
468    True
469    >>> isSimpleValue("'don\\\\'t'")
470    True
471    >>> isSimpleValue("")
472    False
473    >>> isSimpleValue(3)
474    False
475    >>> isSimpleValue(3.5)
476    False
477    """
478    if not isinstance(valStr, str):
479        return False
480    if valStr in ('None', 'True', 'False'):
481        return True
482    else:
483        try:
484            _ = int(valStr)
485            return True
486        except ValueError:
487            pass
488
489        try:
490            _ = float(valStr)
491            return True
492        except ValueError:
493            pass
494
495        if (
496            len(valStr) >= 2
497        and valStr.startswith("'") or valStr.startswith('"')
498        ):
499            quote = valStr[0]
500            ends = valStr.endswith(quote)
501            mismatched = re.search(r'[^\\]' + quote, valStr[1:-1])
502            return ends and mismatched is None
503        else:
504            return False
505
506
507def resolveValue(valStr: str, context: Scope) -> Any:
508    """
509    Given a value string which could be a literal value or a variable
510    reference, returns the value of that expression. Note that operators
511    are not handled: only variable substitution is done.
512    """
513    if isVariableReference(valStr):
514        varName = valStr[1:]
515        if varName not in context:
516            raise NameError(f"Variable '{varName}' is not defined.")
517        return context[varName]
518    elif not isSimpleValue(valStr):
519        raise ValueError(
520            f"{valStr!r} is not a valid value (perhaps you need to add"
521            f" quotes to get a string, or '$' to reference a variable?)"
522        )
523    else:
524        if valStr == "True":
525            return True
526        elif valStr == "False":
527            return False
528        elif valStr == "None":
529            return None
530        elif valStr.startswith('"') or valStr.startswith("'"):
531            return valStr[1:-1]
532        else:
533            try:
534                return int(valStr)
535            except ValueError:
536                pass
537
538            try:
539                return float(valStr)
540            except ValueError:
541                pass
542
543            raise RuntimeError(
544                f"Validated value {valStr!r} is not a string, a number,"
545                f" or a recognized keyword type."
546            )
547
548
549def isVariableReference(value: str) -> bool:
550    """
551    Returns `True` if the given value is a variable reference. Variable
552    references start with '$' and the rest of the reference must be a
553    valid python identifier (i.e., a sequence of alphabetic characters,
554    digits, and/or underscores which does not start with a digit).
555
556    There is one other possibility: references that start with '$@'
557    possibly followed by an identifier.
558
559    Examples:
560
561    >>> isVariableReference('$hi')
562    True
563    >>> isVariableReference('$good bye')
564    False
565    >>> isVariableReference('$_')
566    True
567    >>> isVariableReference('$123')
568    False
569    >>> isVariableReference('$1ab')
570    False
571    >>> isVariableReference('$ab1')
572    True
573    >>> isVariableReference('hi')
574    False
575    >>> isVariableReference('')
576    False
577    >>> isVariableReference('$@')
578    True
579    >>> isVariableReference('$@a')
580    True
581    >>> isVariableReference('$@1')
582    False
583    """
584    if len(value) < 2:
585        return False
586    elif value[0] != '$':
587        return False
588    elif len(value) == 2:
589        return value[1] == '@' or value[1].isidentifier()
590    else:
591        return (
592            value[1:].isidentifier()
593        ) or (
594            value[1] == '@'
595        and value[2:].isidentifier()
596        )
597
598
599def resolveVarName(name: str, scope: Scope) -> str:
600    """
601    Resolves a variable name as either a literal name, or if the name
602    starts with '$', via a variable reference in the given scope whose
603    value must be a string.
604    """
605    if name.startswith('$'):
606        result = scope[name[1:]]
607        if not isinstance(result, str):
608            raise TypeError(
609                f"Variable '{name[1:]}' cannot be referenced as a"
610                f" variable name because it does not hold a string (its"
611                f" value is: {result!r}"
612            )
613        return result
614    else:
615        return name
616
617
618def fixArgs(command: str, requires: int, args: List[str]) -> List[str]:
619    """
620    Checks that the proper number of arguments has been supplied, using
621    the command name as part of the message for a `ValueError` if not.
622    This will fill in '$_' and '$__' for the first two missing arguments
623    instead of generating an error, and returns the possibly modified
624    argument list.
625    """
626    if not (requires - 2 <= len(args) <= requires):
627        raise ValueError(
628            f"Command '{command}' requires {requires} argument(s) but"
629            f" you provided {len(args)}."
630        )
631    return (args + ['$_', '$__'])[:requires]
632
633
634def requiresValue(command: str, argDesc: str, arg: str) -> str:
635    """
636    Checks that the given argument is a simple value, and raises a
637    `ValueError` if it's not. Otherwise just returns. The given command
638    name and argument description are used in the error message. The
639    `argDesc` should be an adjectival phrase, like 'first'.
640
641    Returns the argument given to it.
642    """
643    if not isSimpleValue(arg):
644        raise ValueError(
645            f"The {argDesc} argument to '{command}' must be a simple"
646            f" value string (got {arg!r})."
647        )
648    return arg
649
650
651def requiresLiteralOrVariable(
652    command: str,
653    argDesc: str,
654    options: Collection[str],
655    arg: str
656) -> str:
657    """
658    Like `requiresValue` but only allows variable references or one of a
659    collection of specific strings as the argument.
660    """
661    if not isVariableReference(arg) and arg not in options:
662        raise ValueError(
663            (
664                f"The {argDesc} argument to '{command}' must be either"
665                f" a variable reference or one of the following strings"
666                f" (got {arg!r}):\n  "
667            ) + '\n  '.join(options)
668        )
669    return arg
670
671
672def requiresValueOrVariable(command: str, argDesc: str, arg: str) -> str:
673    """
674    Like `requiresValue` but allows variable references as well as
675    simple values.
676    """
677    if not (isSimpleValue(arg) or isVariableReference(arg)):
678        raise ValueError(
679            f"The {argDesc} argument to '{command}' must be a simple"
680            f" value or a variable reference (got {arg!r})."
681        )
682    return arg
683
684
685def requiresVariableName(command: str, argDesc: str, arg: str) -> str:
686    """
687    Like `requiresValue` but allows only variable names, with or without
688    the leading '$'.
689    """
690    if not (isVariableReference(arg) or isVariableReference('$' + arg)):
691        raise ValueError(
692            f"The {argDesc} argument to '{command}' must be a variable"
693            f" name without the '$' or a variable reference (got"
694            f" {arg!r})."
695        )
696    return arg
697
698
699COMMAND_SETUP: Dict[
700    str,
701    Tuple[
702        type[Command],
703        int,
704        List[
705            Union[
706                Literal[
707                    "requiresValue",
708                    "requiresVariableName",
709                    "requiresValueOrVariable"
710                ],
711                Tuple[
712                    Literal["requiresLiteralOrVariable"],
713                    Collection[str]
714                ]
715            ]
716        ]
717    ]
718] = {
719    'val': (LiteralValue, 1, ["requiresValue"]),
720    'empty': (
721        EstablishCollection,
722        1,
723        [("requiresLiteralOrVariable", {'list', 'tuple', 'set', 'dict'})]
724    ),
725    'append': (AppendValue, 1, ["requiresValueOrVariable"]),
726    'set': (
727        SetValue,
728        2,
729        ["requiresValueOrVariable", "requiresValueOrVariable"]
730    ),
731    'pop': (PopValue, 0, []),
732    'get': (GetValue, 1, ["requiresValueOrVariable"]),
733    'remove': (RemoveValue, 1, ["requiresValueOrVariable"]),
734    'op': (
735        ApplyOperator,
736        3,
737        [
738            ("requiresLiteralOrVariable", get_args(BinaryOperator)),
739            "requiresValueOrVariable",
740            "requiresValueOrVariable"
741        ]
742    ),
743    'unary': (
744        ApplyUnary,
745        2,
746        [
747            ("requiresLiteralOrVariable", get_args(UnaryOperator)),
748            "requiresValueOrVariable"
749        ]
750    ),
751    'assign': (
752        VariableAssignment,
753        2,
754        ["requiresVariableName", "requiresValueOrVariable"]
755    ),
756    'delete': (VariableDeletion, 1, ["requiresVariableName"]),
757    'load': (LoadVariable, 1, ["requiresVariableName"]),
758    'call': (
759        FunctionCall,
760        2,
761        [
762            ("requiresLiteralOrVariable", get_args(CallType)),
763            "requiresVariableName"
764        ]
765    ),
766    'skip': (
767        SkipCommands,
768        2,
769        [
770            "requiresValueOrVariable",
771            "requiresValueOrVariable"
772        ]
773    ),
774    'label': (Label, 1, ["requiresVariableName"]),
775}
776
777
778def command(commandType: str, *_args: str) -> Command:
779    """
780    A convenience function for constructing a command tuple which
781    type-checks the arguments a bit. Raises a `ValueError` if invalid
782    information is supplied; otherwise it returns a `Command` tuple.
783
784    Up to two missing arguments will be replaced automatically with '$_'
785    and '$__' respectively in most cases.
786
787    Examples:
788
789    >>> command('val', '5')
790    LiteralValue(command='val', value='5')
791    >>> command('val', '"5"')
792    LiteralValue(command='val', value='"5"')
793    >>> command('val')
794    Traceback (most recent call last):
795    ...
796    ValueError...
797    >>> command('empty')
798    EstablishCollection(command='empty', collection='$_')
799    >>> command('empty', 'list')
800    EstablishCollection(command='empty', collection='list')
801    >>> command('empty', '$ref')
802    EstablishCollection(command='empty', collection='$ref')
803    >>> command('empty', 'invalid')  # invalid argument
804    Traceback (most recent call last):
805    ...
806    ValueError...
807    >>> command('empty', 'list', 'dict')  # too many arguments
808    Traceback (most recent call last):
809    ...
810    ValueError...
811    >>> command('append', '5')
812    AppendValue(command='append', value='5')
813    """
814    args = list(_args)
815
816    spec = COMMAND_SETUP.get(commandType)
817    if spec is None:
818        raise ValueError(
819            f"Command type '{commandType}' cannot be constructed by"
820            f" assembleSimpleCommand (try the command function"
821            f" instead)."
822        )
823
824    commandVariant, nArgs, checkers = spec
825
826    args = fixArgs(commandType, nArgs, args)
827
828    checkedArgs = []
829    for i, (checker, arg) in enumerate(zip(checkers, args)):
830        argDesc = str(i + 1)
831        if argDesc.endswith('1') and nArgs != 11:
832            argDesc += 'st'
833        elif argDesc.endswith('2') and nArgs != 12:
834            argDesc += 'nd'
835        elif argDesc.endswith('3') and nArgs != 13:
836            argDesc += 'rd'
837        else:
838            argDesc += 'th'
839
840        if isinstance(checker, tuple):
841            checkName, allowed = checker
842            checkFn = globals()[checkName]
843            checkedArgs.append(
844                checkFn(commandType, argDesc, allowed, arg)
845            )
846        else:
847            checkFn = globals()[checker]
848            checkedArgs.append(checkFn(commandType, argDesc, arg))
849
850    return commandVariant(
851        commandType,
852        *checkedArgs
853    )
854
855
856def pushCurrentValue(scope: Scope, value: Any) -> None:
857    """
858    Pushes the given value as the 'current value' for the given scope,
859    storing it in the '_' variable. Stores the old '_' value into '__'
860    if there was one.
861    """
862    if '_' in scope:
863        scope['__'] = scope['_']
864    scope['_'] = value
class CommandError(builtins.Exception):
40class CommandError(Exception):
41    """
42    TODO: This?
43    An error raised during command execution will be converted to one of
44    the subtypes of this class. Stores the underlying error as `cause`,
45    and also stores the `command` and `line` where the error occurred.
46    """
47    def __init__(
48        self,
49        command: 'Command',
50        line: int,
51        cause: Exception
52    ) -> None:
53        self.command = command
54        self.line = line
55        self.cause = cause
56
57    def __str__(self):
58        return (
59            f"\n  Command block, line {self.line}, running command:"
60            f"\n    {self.command!r}"
61            f"\n{type(self.cause).__name__}: {self.cause}"
62        )

TODO: This? An error raised during command execution will be converted to one of the subtypes of this class. Stores the underlying error as cause, and also stores the command and line where the error occurred.

47    def __init__(
48        self,
49        command: 'Command',
50        line: int,
51        cause: Exception
52    ) -> None:
53        self.command = command
54        self.line = line
55        self.cause = cause
command
line
cause
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class CommandValueError(CommandError, builtins.ValueError):
65class CommandValueError(CommandError, ValueError):
66    "A `ValueError` encountered during command execution."
67    pass

A ValueError encountered during command execution.

Inherited Members
CommandError
CommandError
command
line
cause
builtins.BaseException
with_traceback
add_note
args
class CommandTypeError(CommandError, builtins.TypeError):
70class CommandTypeError(CommandError, TypeError):
71    "A `TypeError` encountered during command execution."
72    pass

A TypeError encountered during command execution.

Inherited Members
CommandError
CommandError
command
line
cause
builtins.BaseException
with_traceback
add_note
args
class CommandIndexError(CommandError, builtins.IndexError):
75class CommandIndexError(CommandError, IndexError):
76    "A `IndexError` encountered during command execution."
77    pass

A IndexError encountered during command execution.

Inherited Members
CommandError
CommandError
command
line
cause
builtins.BaseException
with_traceback
add_note
args
class CommandKeyError(CommandError, builtins.KeyError):
80class CommandKeyError(CommandError, KeyError):
81    "A `KeyError` encountered during command execution."
82    pass

A KeyError encountered during command execution.

Inherited Members
CommandError
CommandError
command
line
cause
builtins.BaseException
with_traceback
add_note
args
class CommandOtherError(CommandError):
85class CommandOtherError(CommandError):
86    """
87    Any error other than a `ValueError`, `TypeError`, `IndexError`, or
88    `KeyError` that's encountered during command execution. You can use
89    the `.cause` field to figure out what the type of the underlying
90    error was.
91    """
92    pass

Any error other than a ValueError, TypeError, IndexError, or KeyError that's encountered during command execution. You can use the .cause field to figure out what the type of the underlying error was.

Inherited Members
CommandError
CommandError
command
line
cause
builtins.BaseException
with_traceback
add_note
args
class LiteralValue(builtins.tuple):

A command that replaces the current value with a specific literal value. The values allowed are None, True, False, integers, floating-point numbers, and quoted strings (single or double quotes only). Note that lists, tuples, dictionaries, sets, and other complex data structures cannot be created this way.

LiteralValue(command, value)

Create new instance of LiteralValue(command, value)

command

Alias for field number 0

value

Alias for field number 1

Inherited Members
builtins.tuple
index
count
class EstablishCollection(builtins.tuple):

A command which replaces the current value with an empty collection. The collection type must be one of 'list', 'tuple', 'set', or 'dict'.

EstablishCollection(command, collection)

Create new instance of EstablishCollection(command, collection)

command

Alias for field number 0

collection

Alias for field number 1

Inherited Members
builtins.tuple
index
count
class AppendValue(builtins.tuple):

A command which appends/adds a specific value (either a literal value that could be used with LiteralValue or a variable reference starting with '$') to the current value, which must be a list, tuple, or set.

AppendValue(command, value)

Create new instance of AppendValue(command, value)

command

Alias for field number 0

value

Alias for field number 1

Inherited Members
builtins.tuple
index
count
class SetValue(builtins.tuple):

A command which sets the value for a specific key in a dictionary stored as the current value, or for at a specific index in a tuple or list. Both the key and the value may be either a literal that could be used with LiteralValue or a variable reference starting with '$'.

When used with a set, if the value is truthy the location is added to the set, and otherwise the location is removed from the set.

SetValue(command, location, value)

Create new instance of SetValue(command, location, value)

command

Alias for field number 0

location

Alias for field number 1

value

Alias for field number 2

Inherited Members
builtins.tuple
index
count
class PopValue(builtins.tuple):

A command which pops the last value in the tuple or list which is stored as the current value, setting the value it pops as the new current value.

Does not work with sets or dictionaries.

PopValue(command)

Create new instance of PopValue(command,)

command

Alias for field number 0

Inherited Members
builtins.tuple
index
count
class GetValue(builtins.tuple):

A command which reads a value at a particular index within the current value and sets that as the new current value. For lists or tuples, the index must be convertible to an integer (but can also be a variable reference storing such a value). For sets, if the listed value is in the set the result will be True and otherwise it will be False. For dictionaries, it looks up a value under that key.

For all other kinds of values, it looks for an attribute with the same name as the string specified and returns the value of that attribute (it's an error to specify a non-string value in this case).

GetValue(command, location)

Create new instance of GetValue(command, location)

command

Alias for field number 0

location

Alias for field number 1

Inherited Members
builtins.tuple
index
count
class RemoveValue(builtins.tuple):

A command which removes an item from a tuple, list, set, or dictionary which is stored as the current value. The value should be an integer (or a variable holding one) if the current value is a tuple or list. Unlike python's .remove method, this removes a single item at a particular index/under a particular key, not all copies of a particular value.

RemoveValue(command, location)

Create new instance of RemoveValue(command, location)

command

Alias for field number 0

location

Alias for field number 1

Inherited Members
builtins.tuple
index
count
BinaryOperator: TypeAlias = Literal['+', '-', '*', '/', '//', '**', '%', '^', '|', '&', 'and', 'or', '<', '>', '<=', '>=', '==', 'is']

The supported binary operators for commands.

UnaryOperator: TypeAlias = Literal['-', '~', 'not']

The supported binary operators for commands.

class ApplyOperator(builtins.tuple):

A command which establishes a new current value based on the result of an operator. See BinaryOperator for the list of supported operators. The two operand may be literals as accepted by LiteralValue or variable references starting with '$'.

ApplyOperator(command, op, left, right)

Create new instance of ApplyOperator(command, op, left, right)

command

Alias for field number 0

op

Alias for field number 1

left

Alias for field number 2

right

Alias for field number 3

Inherited Members
builtins.tuple
index
count
class ApplyUnary(builtins.tuple):

The unary version of ApplyOperator. See UnaryOperator for the list of supported operators.

ApplyUnary(command, op, value)

Create new instance of ApplyUnary(command, op, value)

command

Alias for field number 0

op

Alias for field number 1

value

Alias for field number 2

Inherited Members
builtins.tuple
index
count
class VariableAssignment(builtins.tuple):

Assigns the specified value (may be a variable reference) into a named variable. The variable name should not start with '$', which is used when referencing variables. If it does, variable substitution will be performed to compute the name of the variable being created.

VariableAssignment(command, varname, value)

Create new instance of VariableAssignment(command, varname, value)

command

Alias for field number 0

varname

Alias for field number 1

value

Alias for field number 2

Inherited Members
builtins.tuple
index
count
class VariableDeletion(builtins.tuple):

Deletes the variable with the given name. Doesn't actually delete the stored object if it's reference elsewhere. Useful for unspecifying arguments for a 'call' command.

VariableDeletion(command, varname)

Create new instance of VariableDeletion(command, varname)

command

Alias for field number 0

varname

Alias for field number 1

Inherited Members
builtins.tuple
index
count
class LoadVariable(builtins.tuple):

Loads the named variable as the current value, replacing the old current value. The variable name should normally be specified without the '$', with '$' variable substitution will take place and the resulting string will be used as the name of the variable to load.

LoadVariable(command, varname)

Create new instance of LoadVariable(command, varname)

command

Alias for field number 0

varname

Alias for field number 1

Inherited Members
builtins.tuple
index
count
CallType: TypeAlias = Literal['builtin', 'stored', 'graph', 'exploration']

Types of function calls available via the 'call' command.

class FunctionCall(builtins.tuple):

A command which calls a function or method. IF the target is 'builtin', one of the COMMAND_BUILTINS will be called. If the target is 'graph' or 'exploration' then a method of the current graph or exploration will be called. If the target is 'stored', then the function part will be treated as a variable reference and the function stored in that variable will be called.

For builtins, the current value will be used as the only argument. There are two special cases: for round, if an 'ndigits' variable is defined its value will be used for the optional second argument, and for range, if the current value is None, then the values of the 'start', 'stop', and/or 'step' variables are used for its arguments, with a default start of 0 and a default step of 1 (there is no default stop; it's an error if you don't supply one). If the current value is not None, range just gets called with the current value as its only argument.

For graph/exploration methods, the current value is ignored and each listed parameter is sourced from a defined variable of that name, with parameters for which there is no defined variable going unsupplied (which might be okay if they're optional). For varargs parameters, the value of the associated variable will be converted to a tuple and that will be supplied as if using ''; for kwargs parameters the value of the associated variable must be a dictionary, and it will be applied as if using '*' (except that duplicate arguments will not cause an error; instead those coming fro the dictionary value will override any already supplied).

FunctionCall(command, target, function)

Create new instance of FunctionCall(command, target, function)

command

Alias for field number 0

target

Alias for field number 1

function

Alias for field number 2

Inherited Members
builtins.tuple
index
count
COMMAND_BUILTINS: Dict[str, Callable] = {'len': <built-in function len>, 'min': <built-in function min>, 'max': <built-in function max>, 'round': <built-in function round>, 'ceil': <built-in function ceil>, 'floor': <built-in function floor>, 'int': <class 'int'>, 'float': <class 'float'>, 'str': <class 'str'>, 'list': <class 'list'>, 'tuple': <class 'tuple'>, 'dict': <class 'dict'>, 'set': <class 'set'>, 'copy': <function copy>, 'deepcopy': <function deepcopy>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'sorted': <built-in function sorted>, 'print': <built-in function print>, 'warning': <function warning>}

The mapping from names to built-in functions usable in commands. Each is available for use with the 'call' command when 'builtin' is used as the target. See FunctionCall for more details.

class SkipCommands(builtins.tuple):

A command which skips forward or backward within the command list it's included in, but only if a condition value is True. A skip amount of 0 just continues execution as normal. Negative amounts jump to previous commands (so e.g., -2 will re-execute the two commands above the skip command), while positive amounts skip over subsequent commands (so e.g., 1 will skip over one command after this one, resuming execution with the second command after the skip).

If the condition is False, execution continues with the subsequent command as normal.

If the distance value is a string instead of an integer, the skip will redirect execution to the label that uses that name. Note that the distance value may be a variable reference, in which case the integer or string inside the reference will determine where to skip to.

SkipCommands(command, condition, amount)

Create new instance of SkipCommands(command, condition, amount)

command

Alias for field number 0

condition

Alias for field number 1

amount

Alias for field number 2

Inherited Members
builtins.tuple
index
count
class Label(builtins.tuple):

Has no effect, but establishes a label that can be skipped to using the 'skip' command. Note that instead of just a fixed label, a variable name can be used and variable substitution will determine the label name in that case, BUT there are two restrictions: the value must be a string, and you cannot execute a forward-skip to a label which has not already been evaluated, since the value isn't known when the skip occurs. If you use a literal label name instead of a variable, you will be able to skip down to that label from above.

When multiple labels with the same name occur, a skip command will go to the last label with that name before the skip, only considering labels after the skip if there are no labels with that name beforehand (and skipping to the first available label in that case).

Label(command, name)

Create new instance of Label(command, name)

command

Alias for field number 0

name

Alias for field number 1

Inherited Members
builtins.tuple
index
count

The union type for any kind of command. Note that these are all tuples, all of their members are strings in all cases, and their first member (named 'command') is always a string that uniquely identifies the type of the command. Use the command function to get some type-checking while constructing them.

Scope: TypeAlias = Dict[str, Any]

A scope holds variables defined during the execution of a sequence of commands. Variable names (sans the '$' sign) are mapped to arbitrary Python values.

CommandResult: TypeAlias = Tuple[Dict[str, Any], Union[int, str, NoneType], Optional[str]]

The main result of a command is an updated scope (usually but not necessarily the same scope object that was used to execute the command). Additionally, there may be a skip integer that indicates how many commands should be skipped (if positive) or repeated (if negative) as a result of the command just executed. This value may also be a string to skip to a label. There may also be a label value which indicates that the command that was executed defines that label.

def isSimpleValue(valStr: str) -> bool:
434def isSimpleValue(valStr: str) -> bool:
435    """
436    Returns `True` if the given string is a valid simple value for use
437    with a command. Simple values are strings that represent `None`,
438    `True`, `False`, integers, floating-point numbers, and quoted
439    strings (single or double quotes only). Numbers themselves are not
440    simple values.
441
442    Examples:
443
444    >>> isSimpleValue('None')
445    True
446    >>> isSimpleValue('True')
447    True
448    >>> isSimpleValue('False')
449    True
450    >>> isSimpleValue('none')
451    False
452    >>> isSimpleValue('12')
453    True
454    >>> isSimpleValue('5.6')
455    True
456    >>> isSimpleValue('3.2e-10')
457    True
458    >>> isSimpleValue('2 + 3j')  # ba-dump tsss
459    False
460    >>> isSimpleValue('hello')
461    False
462    >>> isSimpleValue('"hello"')
463    True
464    >>> isSimpleValue('"hel"lo"')
465    False
466    >>> isSimpleValue('"hel\\\\"lo"')  # note we're in a docstring here
467    True
468    >>> isSimpleValue("'hi'")
469    True
470    >>> isSimpleValue("'don\\\\'t'")
471    True
472    >>> isSimpleValue("")
473    False
474    >>> isSimpleValue(3)
475    False
476    >>> isSimpleValue(3.5)
477    False
478    """
479    if not isinstance(valStr, str):
480        return False
481    if valStr in ('None', 'True', 'False'):
482        return True
483    else:
484        try:
485            _ = int(valStr)
486            return True
487        except ValueError:
488            pass
489
490        try:
491            _ = float(valStr)
492            return True
493        except ValueError:
494            pass
495
496        if (
497            len(valStr) >= 2
498        and valStr.startswith("'") or valStr.startswith('"')
499        ):
500            quote = valStr[0]
501            ends = valStr.endswith(quote)
502            mismatched = re.search(r'[^\\]' + quote, valStr[1:-1])
503            return ends and mismatched is None
504        else:
505            return False

Returns True if the given string is a valid simple value for use with a command. Simple values are strings that represent None, True, False, integers, floating-point numbers, and quoted strings (single or double quotes only). Numbers themselves are not simple values.

Examples:

>>> isSimpleValue('None')
True
>>> isSimpleValue('True')
True
>>> isSimpleValue('False')
True
>>> isSimpleValue('none')
False
>>> isSimpleValue('12')
True
>>> isSimpleValue('5.6')
True
>>> isSimpleValue('3.2e-10')
True
>>> isSimpleValue('2 + 3j')  # ba-dump tsss
False
>>> isSimpleValue('hello')
False
>>> isSimpleValue('"hello"')
True
>>> isSimpleValue('"hel"lo"')
False
>>> isSimpleValue('"hel\\"lo"')  # note we're in a docstring here
True
>>> isSimpleValue("'hi'")
True
>>> isSimpleValue("'don\\'t'")
True
>>> isSimpleValue("")
False
>>> isSimpleValue(3)
False
>>> isSimpleValue(3.5)
False
def resolveValue(valStr: str, context: Dict[str, Any]) -> Any:
508def resolveValue(valStr: str, context: Scope) -> Any:
509    """
510    Given a value string which could be a literal value or a variable
511    reference, returns the value of that expression. Note that operators
512    are not handled: only variable substitution is done.
513    """
514    if isVariableReference(valStr):
515        varName = valStr[1:]
516        if varName not in context:
517            raise NameError(f"Variable '{varName}' is not defined.")
518        return context[varName]
519    elif not isSimpleValue(valStr):
520        raise ValueError(
521            f"{valStr!r} is not a valid value (perhaps you need to add"
522            f" quotes to get a string, or '$' to reference a variable?)"
523        )
524    else:
525        if valStr == "True":
526            return True
527        elif valStr == "False":
528            return False
529        elif valStr == "None":
530            return None
531        elif valStr.startswith('"') or valStr.startswith("'"):
532            return valStr[1:-1]
533        else:
534            try:
535                return int(valStr)
536            except ValueError:
537                pass
538
539            try:
540                return float(valStr)
541            except ValueError:
542                pass
543
544            raise RuntimeError(
545                f"Validated value {valStr!r} is not a string, a number,"
546                f" or a recognized keyword type."
547            )

Given a value string which could be a literal value or a variable reference, returns the value of that expression. Note that operators are not handled: only variable substitution is done.

def isVariableReference(value: str) -> bool:
550def isVariableReference(value: str) -> bool:
551    """
552    Returns `True` if the given value is a variable reference. Variable
553    references start with '$' and the rest of the reference must be a
554    valid python identifier (i.e., a sequence of alphabetic characters,
555    digits, and/or underscores which does not start with a digit).
556
557    There is one other possibility: references that start with '$@'
558    possibly followed by an identifier.
559
560    Examples:
561
562    >>> isVariableReference('$hi')
563    True
564    >>> isVariableReference('$good bye')
565    False
566    >>> isVariableReference('$_')
567    True
568    >>> isVariableReference('$123')
569    False
570    >>> isVariableReference('$1ab')
571    False
572    >>> isVariableReference('$ab1')
573    True
574    >>> isVariableReference('hi')
575    False
576    >>> isVariableReference('')
577    False
578    >>> isVariableReference('$@')
579    True
580    >>> isVariableReference('$@a')
581    True
582    >>> isVariableReference('$@1')
583    False
584    """
585    if len(value) < 2:
586        return False
587    elif value[0] != '$':
588        return False
589    elif len(value) == 2:
590        return value[1] == '@' or value[1].isidentifier()
591    else:
592        return (
593            value[1:].isidentifier()
594        ) or (
595            value[1] == '@'
596        and value[2:].isidentifier()
597        )

Returns True if the given value is a variable reference. Variable references start with '$' and the rest of the reference must be a valid python identifier (i.e., a sequence of alphabetic characters, digits, and/or underscores which does not start with a digit).

There is one other possibility: references that start with '$@' possibly followed by an identifier.

Examples:

>>> isVariableReference('$hi')
True
>>> isVariableReference('$good bye')
False
>>> isVariableReference('$_')
True
>>> isVariableReference('$123')
False
>>> isVariableReference('$1ab')
False
>>> isVariableReference('$ab1')
True
>>> isVariableReference('hi')
False
>>> isVariableReference('')
False
>>> isVariableReference('$@')
True
>>> isVariableReference('$@a')
True
>>> isVariableReference('$@1')
False
def resolveVarName(name: str, scope: Dict[str, Any]) -> str:
600def resolveVarName(name: str, scope: Scope) -> str:
601    """
602    Resolves a variable name as either a literal name, or if the name
603    starts with '$', via a variable reference in the given scope whose
604    value must be a string.
605    """
606    if name.startswith('$'):
607        result = scope[name[1:]]
608        if not isinstance(result, str):
609            raise TypeError(
610                f"Variable '{name[1:]}' cannot be referenced as a"
611                f" variable name because it does not hold a string (its"
612                f" value is: {result!r}"
613            )
614        return result
615    else:
616        return name

Resolves a variable name as either a literal name, or if the name starts with '$', via a variable reference in the given scope whose value must be a string.

def fixArgs(command: str, requires: int, args: List[str]) -> List[str]:
619def fixArgs(command: str, requires: int, args: List[str]) -> List[str]:
620    """
621    Checks that the proper number of arguments has been supplied, using
622    the command name as part of the message for a `ValueError` if not.
623    This will fill in '$_' and '$__' for the first two missing arguments
624    instead of generating an error, and returns the possibly modified
625    argument list.
626    """
627    if not (requires - 2 <= len(args) <= requires):
628        raise ValueError(
629            f"Command '{command}' requires {requires} argument(s) but"
630            f" you provided {len(args)}."
631        )
632    return (args + ['$_', '$__'])[:requires]

Checks that the proper number of arguments has been supplied, using the command name as part of the message for a ValueError if not. This will fill in '$_' and '$__' for the first two missing arguments instead of generating an error, and returns the possibly modified argument list.

def requiresValue(command: str, argDesc: str, arg: str) -> str:
635def requiresValue(command: str, argDesc: str, arg: str) -> str:
636    """
637    Checks that the given argument is a simple value, and raises a
638    `ValueError` if it's not. Otherwise just returns. The given command
639    name and argument description are used in the error message. The
640    `argDesc` should be an adjectival phrase, like 'first'.
641
642    Returns the argument given to it.
643    """
644    if not isSimpleValue(arg):
645        raise ValueError(
646            f"The {argDesc} argument to '{command}' must be a simple"
647            f" value string (got {arg!r})."
648        )
649    return arg

Checks that the given argument is a simple value, and raises a ValueError if it's not. Otherwise just returns. The given command name and argument description are used in the error message. The argDesc should be an adjectival phrase, like 'first'.

Returns the argument given to it.

def requiresLiteralOrVariable(command: str, argDesc: str, options: Collection[str], arg: str) -> str:
652def requiresLiteralOrVariable(
653    command: str,
654    argDesc: str,
655    options: Collection[str],
656    arg: str
657) -> str:
658    """
659    Like `requiresValue` but only allows variable references or one of a
660    collection of specific strings as the argument.
661    """
662    if not isVariableReference(arg) and arg not in options:
663        raise ValueError(
664            (
665                f"The {argDesc} argument to '{command}' must be either"
666                f" a variable reference or one of the following strings"
667                f" (got {arg!r}):\n  "
668            ) + '\n  '.join(options)
669        )
670    return arg

Like requiresValue but only allows variable references or one of a collection of specific strings as the argument.

def requiresValueOrVariable(command: str, argDesc: str, arg: str) -> str:
673def requiresValueOrVariable(command: str, argDesc: str, arg: str) -> str:
674    """
675    Like `requiresValue` but allows variable references as well as
676    simple values.
677    """
678    if not (isSimpleValue(arg) or isVariableReference(arg)):
679        raise ValueError(
680            f"The {argDesc} argument to '{command}' must be a simple"
681            f" value or a variable reference (got {arg!r})."
682        )
683    return arg

Like requiresValue but allows variable references as well as simple values.

def requiresVariableName(command: str, argDesc: str, arg: str) -> str:
686def requiresVariableName(command: str, argDesc: str, arg: str) -> str:
687    """
688    Like `requiresValue` but allows only variable names, with or without
689    the leading '$'.
690    """
691    if not (isVariableReference(arg) or isVariableReference('$' + arg)):
692        raise ValueError(
693            f"The {argDesc} argument to '{command}' must be a variable"
694            f" name without the '$' or a variable reference (got"
695            f" {arg!r})."
696        )
697    return arg

Like requiresValue but allows only variable names, with or without the leading '$'.

COMMAND_SETUP: Dict[str, Tuple[type[Union[LiteralValue, EstablishCollection, AppendValue, SetValue, PopValue, GetValue, RemoveValue, ApplyOperator, ApplyUnary, VariableAssignment, VariableDeletion, LoadVariable, FunctionCall, SkipCommands, Label]], int, List[Union[Literal['requiresValue', 'requiresVariableName', 'requiresValueOrVariable'], Tuple[Literal['requiresLiteralOrVariable'], Collection[str]]]]]] = {'val': (<class 'LiteralValue'>, 1, ['requiresValue']), 'empty': (<class 'EstablishCollection'>, 1, [('requiresLiteralOrVariable', {'set', 'dict', 'list', 'tuple'})]), 'append': (<class 'AppendValue'>, 1, ['requiresValueOrVariable']), 'set': (<class 'SetValue'>, 2, ['requiresValueOrVariable', 'requiresValueOrVariable']), 'pop': (<class 'PopValue'>, 0, []), 'get': (<class 'GetValue'>, 1, ['requiresValueOrVariable']), 'remove': (<class 'RemoveValue'>, 1, ['requiresValueOrVariable']), 'op': (<class 'ApplyOperator'>, 3, [('requiresLiteralOrVariable', ('+', '-', '*', '/', '//', '**', '%', '^', '|', '&', 'and', 'or', '<', '>', '<=', '>=', '==', 'is')), 'requiresValueOrVariable', 'requiresValueOrVariable']), 'unary': (<class 'ApplyUnary'>, 2, [('requiresLiteralOrVariable', ('-', '~', 'not')), 'requiresValueOrVariable']), 'assign': (<class 'VariableAssignment'>, 2, ['requiresVariableName', 'requiresValueOrVariable']), 'delete': (<class 'VariableDeletion'>, 1, ['requiresVariableName']), 'load': (<class 'LoadVariable'>, 1, ['requiresVariableName']), 'call': (<class 'FunctionCall'>, 2, [('requiresLiteralOrVariable', ('builtin', 'stored', 'graph', 'exploration')), 'requiresVariableName']), 'skip': (<class 'SkipCommands'>, 2, ['requiresValueOrVariable', 'requiresValueOrVariable']), 'label': (<class 'Label'>, 1, ['requiresVariableName'])}
779def command(commandType: str, *_args: str) -> Command:
780    """
781    A convenience function for constructing a command tuple which
782    type-checks the arguments a bit. Raises a `ValueError` if invalid
783    information is supplied; otherwise it returns a `Command` tuple.
784
785    Up to two missing arguments will be replaced automatically with '$_'
786    and '$__' respectively in most cases.
787
788    Examples:
789
790    >>> command('val', '5')
791    LiteralValue(command='val', value='5')
792    >>> command('val', '"5"')
793    LiteralValue(command='val', value='"5"')
794    >>> command('val')
795    Traceback (most recent call last):
796    ...
797    ValueError...
798    >>> command('empty')
799    EstablishCollection(command='empty', collection='$_')
800    >>> command('empty', 'list')
801    EstablishCollection(command='empty', collection='list')
802    >>> command('empty', '$ref')
803    EstablishCollection(command='empty', collection='$ref')
804    >>> command('empty', 'invalid')  # invalid argument
805    Traceback (most recent call last):
806    ...
807    ValueError...
808    >>> command('empty', 'list', 'dict')  # too many arguments
809    Traceback (most recent call last):
810    ...
811    ValueError...
812    >>> command('append', '5')
813    AppendValue(command='append', value='5')
814    """
815    args = list(_args)
816
817    spec = COMMAND_SETUP.get(commandType)
818    if spec is None:
819        raise ValueError(
820            f"Command type '{commandType}' cannot be constructed by"
821            f" assembleSimpleCommand (try the command function"
822            f" instead)."
823        )
824
825    commandVariant, nArgs, checkers = spec
826
827    args = fixArgs(commandType, nArgs, args)
828
829    checkedArgs = []
830    for i, (checker, arg) in enumerate(zip(checkers, args)):
831        argDesc = str(i + 1)
832        if argDesc.endswith('1') and nArgs != 11:
833            argDesc += 'st'
834        elif argDesc.endswith('2') and nArgs != 12:
835            argDesc += 'nd'
836        elif argDesc.endswith('3') and nArgs != 13:
837            argDesc += 'rd'
838        else:
839            argDesc += 'th'
840
841        if isinstance(checker, tuple):
842            checkName, allowed = checker
843            checkFn = globals()[checkName]
844            checkedArgs.append(
845                checkFn(commandType, argDesc, allowed, arg)
846            )
847        else:
848            checkFn = globals()[checker]
849            checkedArgs.append(checkFn(commandType, argDesc, arg))
850
851    return commandVariant(
852        commandType,
853        *checkedArgs
854    )

A convenience function for constructing a command tuple which type-checks the arguments a bit. Raises a ValueError if invalid information is supplied; otherwise it returns a Command tuple.

Up to two missing arguments will be replaced automatically with '$_' and '$__' respectively in most cases.

Examples:

>>> command('val', '5')
LiteralValue(command='val', value='5')
>>> command('val', '"5"')
LiteralValue(command='val', value='"5"')
>>> command('val')
Traceback (most recent call last):
...
ValueError...
>>> command('empty')
EstablishCollection(command='empty', collection='$_')
>>> command('empty', 'list')
EstablishCollection(command='empty', collection='list')
>>> command('empty', '$ref')
EstablishCollection(command='empty', collection='$ref')
>>> command('empty', 'invalid')  # invalid argument
Traceback (most recent call last):
...
ValueError...
>>> command('empty', 'list', 'dict')  # too many arguments
Traceback (most recent call last):
...
ValueError...
>>> command('append', '5')
AppendValue(command='append', value='5')
def pushCurrentValue(scope: Dict[str, Any], value: Any) -> None:
857def pushCurrentValue(scope: Scope, value: Any) -> None:
858    """
859    Pushes the given value as the 'current value' for the given scope,
860    storing it in the '_' variable. Stores the old '_' value into '__'
861    if there was one.
862    """
863    if '_' in scope:
864        scope['__'] = scope['_']
865    scope['_'] = value

Pushes the given value as the 'current value' for the given scope, storing it in the '_' variable. Stores the old '_' value into '__' if there was one.