Module scrolls.interpreter.struct

Data structures used to implement the interpreter state.

Expand source code
"""
Data structures used to implement the interpreter state.
"""

import dataclasses
import logging
import typing as t

from .. import ast

__all__ = (
    "ArgSourceMap",
    "CallContext",
    "VarScope",
    "ScopedVarStore",
)


logger = logging.getLogger(__name__)
T = t.TypeVar("T")


class ArgSourceMap(dict[int, T], t.Generic[T]):
    """A utility class that maps argument numbers to some source.

    The main purpose of this container is to map call arguments to the `scrolls.ast.syntax.ASTNode` they came from.
    This class is typically used to accurately point to a node in the case of a call error.

    Usage
    ```py
    # Note: SourceClass is just an example here.
    sources: typing.Sequence[SourceClass] = get_some_sources()
    source_map: ArgSourceMap[SourceClass] = ArgSourceMap()

    args = []
    for source in sources:
        args_from_source = source.get_args()
        source_map.add_args(args_from_source, source)

    # Now, you can use an arg number to look up which SourceClass it came from.
    arg_2_src = source_map[2]
    ```
    """

    def __init__(self, *args: t.Any, **kwargs: t.Any):
        super().__init__(*args, **kwargs)

        self.count = 0

    def add_args(self, args: t.Sequence, source: T) -> None:
        """
        Add an `(args, source)` pair to this mapping. See usage example above.
        """
        for i, _ in enumerate(args):
            self[i + self.count] = source

        self.count += len(args)


@dataclasses.dataclass
class CallContext:
    """
    The context of a call. Contains all information necessary to run a call. Under normal circumstances,
    you won't need to create instances of this yourself. Instead access instances through:

    - `scrolls.interpreter.state.InterpreterContext.call_stack`
    - `scrolls.interpreter.state.InterpreterContext.call_context`

    <br/>

    .. NOTE::
        Control structures such as `!for`, `!while`, etc., are also considered calls, but they do not create
        a new `scrolls.interpreter.struct.VarScope`. So, call contexts and variable scopes are considered separately.
    """

    call_name: str
    """The name of this call."""

    args: t.Sequence[str]
    """The arguments passed into this call."""

    arg_nodes: ArgSourceMap[ast.ASTNode]
    """A map of argument indices to the `scrolls.ast.syntax.ASTNode` they came from."""

    control_node: t.Optional[ast.ASTNode] = None
    """If this call is a control call, this will contain the call's `scrolls.ast.syntax.ASTNode` parameter."""

    return_value: t.Optional[t.Any] = None
    """The return value set by a runtime call."""

    runtime_call: bool = False
    """A runtime call is a call defined while the interpreter is running, such as through `!def`."""

    else_signal: bool = False
    """
    Read by the `!else` builtin. If True, the next `!else` called in the current
    context will execute, and set the signal back to False. From within a control
    call, this should be set through `scrolls.interpreter.state.InterpreterContext.parent_call_context`.
    """

    _id_format: t.ClassVar[str] = "{id:<{id_size}}"
    _str_format: t.ClassVar[str] = "{flags:<5} {name_and_args}"
    _trace_format: t.ClassVar[str] = f"{_id_format} {_str_format}"
    _id_title: t.ClassVar[str] = "ID"

    @classmethod
    def _min_id_size(cls, id_size: int) -> int:
        return max(id_size, len(cls._id_title))

    @classmethod
    def trace_banner(cls, id_size: int) -> str:
        """
        Gets a banner string for a backtrace. Should be followed by a list of
        `trace_str` outputs.

        Args:
            id_size: The size in characters of the ID field.
        """
        return cls._trace_format.format(
            id=cls._id_title,
            id_size=cls._min_id_size(id_size),
            flags="FLAGS",
            name_and_args="NAME+ARGS"
        )

    def trace_str(self, id: int, id_size: int) -> str:
        """
        Gets a string representation of this call context for the purposes
        of printing a stack trace.

        Args:
            id: The numeric ID of the trace item.
            id_size: The size in characters of the ID field.
        """
        id_str = self._id_format.format(id=id, id_size=self._min_id_size(id_size))
        return f"{id_str} {self}"

    def __str__(self) -> str:
        flags = [
            "!" if self.control_node is not None else "-",
            "e" if self.else_signal else "-",
            "r" if self.runtime_call else "-"
        ]

        name_and_args = [
            f"\"{self.call_name}\"",
            *[f"\"{arg}\"" for arg in self.args]
        ]

        return self._str_format.format(
            flags="".join(flags),
            name_and_args=" ".join(name_and_args)
        )


class VarScope:
    """
    A variable scope. See `ScopedVarStore`.
    """

    def __init__(self) -> None:
        self.vars: t.MutableMapping[str, str] = {}
        """The local variables defined in this scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.set_var` instead.
        """

        self.nonlocals: t.MutableMapping[str, bool] = {}
        """Nonlocal variables defined in this scope.

        If a variable is declared nonlocal, attempts to read/write it will go to the enclosing scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.declare_nonlocal` instead.
        """

        self.globals: t.MutableMapping[str, bool] = {}
        """Global variables defined in this scope.

        If a variable is declared global, attempts to read/write it will go to the global (top level) variable scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.declare_global` instead.
        """


class ScopedVarStore:
    """
    A variable store divided into a stack of key-value pairs.

    This class is used to implement the concept of local vs global variables in scrolls. Runtime calls (see
    `scrolls.interpreter.struct.CallContext`) use scoped variable stores to allow the definition of local variables in call defs without
    stepping on existing variables.

    .. IMPORTANT::
        Calls implemented in Python do not enter a new variable scope by default. You typically won't need to enter
        a new scope unless you run Scrolls code during a call, i.e. for control calls, and runtime-defined calls.

        Most control calls, such as `while`, `for`, `if`, etc. do not need to define a new variable scope. The option
        is available if desired. See the source code of `scrolls.interpreter.callhandler.RuntimeCallHandler.handle_call` for an example of defining
        a new scope.
    """

    def __init__(self) -> None:
        self.scopes: t.MutableSequence[VarScope] = []
        """The `VarScope` stack. Later indices are deeper scopes. `scopes[0]` is the global scope, which is always available."""

        self.new_scope()  # There should always be one scope.

    def new_scope(self) -> None:
        """
        Push a new scope onto the stack.
        """
        self.scopes.append(VarScope())

    def destroy_scope(self) -> None:
        """
        Destroy the current scope and return to the last one. This will delete all local variables defined in the current
        scope.
        """
        if len(self.scopes) == 1:
            # there should always be at least one scope
            raise ValueError("There must be at least one scope.")

        self.scopes.pop()

    def declare_nonlocal(self, name: str) -> None:
        """
        Declare a variable as nonlocal. This means that all attempts to read/write the variable will automatically
        go to the enclosing scope.
        """
        self.current_scope.nonlocals[name] = True

    def declare_global(self, name: str) -> None:
        """
        Declare a variable as global. This means all attempts to read/write the variable will automatically go to
        the global scope.
        """
        self.current_scope.globals[name] = True

    def search_scope(self, name: str, scopes: t.Sequence[VarScope], read_search: bool = False) -> VarScope:
        """
        Using a variable name, search up the scope stack for something to read/write. This search will honor nonlocal
        and global declarations made for all scopes.

        Args:
            name: The name of the variable to search for.

            scopes: The scopes to search. Typically, this will be `ScopedVarStore.scopes`.

            read_search: Must be `True` if no writes will be performed on the scope you're searching for.
            Adds additional logic that reads the global store as a fallback if a defined value could not be found
            after searching up the stack.

        Raises:
            KeyError: If an appropriate scope could not be found.
        """
        scopes = list(scopes)
        scope = scopes[-1]

        while scopes:
            scope = scopes.pop()

            if name in scope.globals:
                # If global, immediately go to the highest scope
                return scopes[0] if scopes else scope

            if name in scope.nonlocals:
                # If nonlocal, go to the enclosing scope.
                continue

            # Just break as soon as we step off global/nonlocal references.
            break

        if read_search:
            # Do a little bit of extra logic for a read search. If we can't find a value in the
            # current scope, try globals as a fallback.
            if name in scope.vars:
                return scope
            elif scopes and name in scopes[0].vars:
                return scopes[0]
            else:
                raise KeyError(name)

        return scope

    def get_scope_for_read(self, name: str) -> VarScope:
        """Shortcut for `ScopedVarStore.search_scope(..., read_search=True)`

        See Also: `ScopedVarStore.search_scope`
        """
        return self.search_scope(name, self.scopes, read_search=True)

    def get_scope_for_write(self, name: str) -> VarScope:
        """Shortcut for `ScopedVarStore.search_scope(..., read_search=False)`

        See Also: `ScopedVarStore.search_scope`
        """
        return self.search_scope(name, self.scopes, read_search=False)

    @property
    def current_scope(self) -> VarScope:
        """The current scope."""
        return self.scopes[-1]

    def get_var(self, name: str) -> str:
        """Get a variable from this store, following all nonlocal and global declarations."""
        return self.get_scope_for_read(name).vars[name]

    def set_var(self, name: str, value: str) -> None:
        """Set a variable in this store, following all nonlocal and global declarations."""
        try:
            scope = self.get_scope_for_write(name)
            scope.vars[name] = value
        except KeyError:
            self.current_scope.vars[name] = value

    def del_var(self, name: str) -> None:
        """Delete a variable from this store, following all nonlocal and global declarations."""
        try:
            scope = self.get_scope_for_write(name)
            del scope.vars[name]
        except KeyError:
            del self.current_scope.vars[name]

Classes

class ArgSourceMap (*args: Any, **kwargs: Any)

A utility class that maps argument numbers to some source.

The main purpose of this container is to map call arguments to the ASTNode they came from. This class is typically used to accurately point to a node in the case of a call error.

Usage

# Note: SourceClass is just an example here.
sources: typing.Sequence[SourceClass] = get_some_sources()
source_map: ArgSourceMap[SourceClass] = ArgSourceMap()

args = []
for source in sources:
    args_from_source = source.get_args()
    source_map.add_args(args_from_source, source)

# Now, you can use an arg number to look up which SourceClass it came from.
arg_2_src = source_map[2]
Expand source code
class ArgSourceMap(dict[int, T], t.Generic[T]):
    """A utility class that maps argument numbers to some source.

    The main purpose of this container is to map call arguments to the `scrolls.ast.syntax.ASTNode` they came from.
    This class is typically used to accurately point to a node in the case of a call error.

    Usage
    ```py
    # Note: SourceClass is just an example here.
    sources: typing.Sequence[SourceClass] = get_some_sources()
    source_map: ArgSourceMap[SourceClass] = ArgSourceMap()

    args = []
    for source in sources:
        args_from_source = source.get_args()
        source_map.add_args(args_from_source, source)

    # Now, you can use an arg number to look up which SourceClass it came from.
    arg_2_src = source_map[2]
    ```
    """

    def __init__(self, *args: t.Any, **kwargs: t.Any):
        super().__init__(*args, **kwargs)

        self.count = 0

    def add_args(self, args: t.Sequence, source: T) -> None:
        """
        Add an `(args, source)` pair to this mapping. See usage example above.
        """
        for i, _ in enumerate(args):
            self[i + self.count] = source

        self.count += len(args)

Ancestors

  • builtins.dict
  • typing.Generic

Methods

def add_args(self, args: Sequence, source: ~T) ‑> None

Add an (args, source) pair to this mapping. See usage example above.

Expand source code
def add_args(self, args: t.Sequence, source: T) -> None:
    """
    Add an `(args, source)` pair to this mapping. See usage example above.
    """
    for i, _ in enumerate(args):
        self[i + self.count] = source

    self.count += len(args)
class CallContext (call_name: str, args: Sequence[str], arg_nodes: ArgSourceMap, control_node: Optional[ASTNode] = None, return_value: Optional[Any] = None, runtime_call: bool = False, else_signal: bool = False)

The context of a call. Contains all information necessary to run a call. Under normal circumstances, you won't need to create instances of this yourself. Instead access instances through:


Note

Control structures such as !for, !while, etc., are also considered calls, but they do not create a new VarScope. So, call contexts and variable scopes are considered separately.

Expand source code
@dataclasses.dataclass
class CallContext:
    """
    The context of a call. Contains all information necessary to run a call. Under normal circumstances,
    you won't need to create instances of this yourself. Instead access instances through:

    - `scrolls.interpreter.state.InterpreterContext.call_stack`
    - `scrolls.interpreter.state.InterpreterContext.call_context`

    <br/>

    .. NOTE::
        Control structures such as `!for`, `!while`, etc., are also considered calls, but they do not create
        a new `scrolls.interpreter.struct.VarScope`. So, call contexts and variable scopes are considered separately.
    """

    call_name: str
    """The name of this call."""

    args: t.Sequence[str]
    """The arguments passed into this call."""

    arg_nodes: ArgSourceMap[ast.ASTNode]
    """A map of argument indices to the `scrolls.ast.syntax.ASTNode` they came from."""

    control_node: t.Optional[ast.ASTNode] = None
    """If this call is a control call, this will contain the call's `scrolls.ast.syntax.ASTNode` parameter."""

    return_value: t.Optional[t.Any] = None
    """The return value set by a runtime call."""

    runtime_call: bool = False
    """A runtime call is a call defined while the interpreter is running, such as through `!def`."""

    else_signal: bool = False
    """
    Read by the `!else` builtin. If True, the next `!else` called in the current
    context will execute, and set the signal back to False. From within a control
    call, this should be set through `scrolls.interpreter.state.InterpreterContext.parent_call_context`.
    """

    _id_format: t.ClassVar[str] = "{id:<{id_size}}"
    _str_format: t.ClassVar[str] = "{flags:<5} {name_and_args}"
    _trace_format: t.ClassVar[str] = f"{_id_format} {_str_format}"
    _id_title: t.ClassVar[str] = "ID"

    @classmethod
    def _min_id_size(cls, id_size: int) -> int:
        return max(id_size, len(cls._id_title))

    @classmethod
    def trace_banner(cls, id_size: int) -> str:
        """
        Gets a banner string for a backtrace. Should be followed by a list of
        `trace_str` outputs.

        Args:
            id_size: The size in characters of the ID field.
        """
        return cls._trace_format.format(
            id=cls._id_title,
            id_size=cls._min_id_size(id_size),
            flags="FLAGS",
            name_and_args="NAME+ARGS"
        )

    def trace_str(self, id: int, id_size: int) -> str:
        """
        Gets a string representation of this call context for the purposes
        of printing a stack trace.

        Args:
            id: The numeric ID of the trace item.
            id_size: The size in characters of the ID field.
        """
        id_str = self._id_format.format(id=id, id_size=self._min_id_size(id_size))
        return f"{id_str} {self}"

    def __str__(self) -> str:
        flags = [
            "!" if self.control_node is not None else "-",
            "e" if self.else_signal else "-",
            "r" if self.runtime_call else "-"
        ]

        name_and_args = [
            f"\"{self.call_name}\"",
            *[f"\"{arg}\"" for arg in self.args]
        ]

        return self._str_format.format(
            flags="".join(flags),
            name_and_args=" ".join(name_and_args)
        )

Class variables

var arg_nodesArgSourceMap

A map of argument indices to the ASTNode they came from.

var args : Sequence[str]

The arguments passed into this call.

var call_name : str

The name of this call.

var control_node : Optional[ASTNode]

If this call is a control call, this will contain the call's ASTNode parameter.

var else_signal : bool

Read by the !else builtin. If True, the next !else called in the current context will execute, and set the signal back to False. From within a control call, this should be set through InterpreterContext.parent_call_context.

var return_value : Optional[Any]

The return value set by a runtime call.

var runtime_call : bool

A runtime call is a call defined while the interpreter is running, such as through !def.

Static methods

def trace_banner(id_size: int) ‑> str

Gets a banner string for a backtrace. Should be followed by a list of trace_str outputs.

Args

id_size
The size in characters of the ID field.
Expand source code
@classmethod
def trace_banner(cls, id_size: int) -> str:
    """
    Gets a banner string for a backtrace. Should be followed by a list of
    `trace_str` outputs.

    Args:
        id_size: The size in characters of the ID field.
    """
    return cls._trace_format.format(
        id=cls._id_title,
        id_size=cls._min_id_size(id_size),
        flags="FLAGS",
        name_and_args="NAME+ARGS"
    )

Methods

def trace_str(self, id: int, id_size: int) ‑> str

Gets a string representation of this call context for the purposes of printing a stack trace.

Args

id
The numeric ID of the trace item.
id_size
The size in characters of the ID field.
Expand source code
def trace_str(self, id: int, id_size: int) -> str:
    """
    Gets a string representation of this call context for the purposes
    of printing a stack trace.

    Args:
        id: The numeric ID of the trace item.
        id_size: The size in characters of the ID field.
    """
    id_str = self._id_format.format(id=id, id_size=self._min_id_size(id_size))
    return f"{id_str} {self}"
class ScopedVarStore

A variable store divided into a stack of key-value pairs.

This class is used to implement the concept of local vs global variables in scrolls. Runtime calls (see CallContext) use scoped variable stores to allow the definition of local variables in call defs without stepping on existing variables.

Important

Calls implemented in Python do not enter a new variable scope by default. You typically won't need to enter a new scope unless you run Scrolls code during a call, i.e. for control calls, and runtime-defined calls.

Most control calls, such as while, for, if, etc. do not need to define a new variable scope. The option is available if desired. See the source code of RuntimeCallHandler.handle_call() for an example of defining a new scope.

Expand source code
class ScopedVarStore:
    """
    A variable store divided into a stack of key-value pairs.

    This class is used to implement the concept of local vs global variables in scrolls. Runtime calls (see
    `scrolls.interpreter.struct.CallContext`) use scoped variable stores to allow the definition of local variables in call defs without
    stepping on existing variables.

    .. IMPORTANT::
        Calls implemented in Python do not enter a new variable scope by default. You typically won't need to enter
        a new scope unless you run Scrolls code during a call, i.e. for control calls, and runtime-defined calls.

        Most control calls, such as `while`, `for`, `if`, etc. do not need to define a new variable scope. The option
        is available if desired. See the source code of `scrolls.interpreter.callhandler.RuntimeCallHandler.handle_call` for an example of defining
        a new scope.
    """

    def __init__(self) -> None:
        self.scopes: t.MutableSequence[VarScope] = []
        """The `VarScope` stack. Later indices are deeper scopes. `scopes[0]` is the global scope, which is always available."""

        self.new_scope()  # There should always be one scope.

    def new_scope(self) -> None:
        """
        Push a new scope onto the stack.
        """
        self.scopes.append(VarScope())

    def destroy_scope(self) -> None:
        """
        Destroy the current scope and return to the last one. This will delete all local variables defined in the current
        scope.
        """
        if len(self.scopes) == 1:
            # there should always be at least one scope
            raise ValueError("There must be at least one scope.")

        self.scopes.pop()

    def declare_nonlocal(self, name: str) -> None:
        """
        Declare a variable as nonlocal. This means that all attempts to read/write the variable will automatically
        go to the enclosing scope.
        """
        self.current_scope.nonlocals[name] = True

    def declare_global(self, name: str) -> None:
        """
        Declare a variable as global. This means all attempts to read/write the variable will automatically go to
        the global scope.
        """
        self.current_scope.globals[name] = True

    def search_scope(self, name: str, scopes: t.Sequence[VarScope], read_search: bool = False) -> VarScope:
        """
        Using a variable name, search up the scope stack for something to read/write. This search will honor nonlocal
        and global declarations made for all scopes.

        Args:
            name: The name of the variable to search for.

            scopes: The scopes to search. Typically, this will be `ScopedVarStore.scopes`.

            read_search: Must be `True` if no writes will be performed on the scope you're searching for.
            Adds additional logic that reads the global store as a fallback if a defined value could not be found
            after searching up the stack.

        Raises:
            KeyError: If an appropriate scope could not be found.
        """
        scopes = list(scopes)
        scope = scopes[-1]

        while scopes:
            scope = scopes.pop()

            if name in scope.globals:
                # If global, immediately go to the highest scope
                return scopes[0] if scopes else scope

            if name in scope.nonlocals:
                # If nonlocal, go to the enclosing scope.
                continue

            # Just break as soon as we step off global/nonlocal references.
            break

        if read_search:
            # Do a little bit of extra logic for a read search. If we can't find a value in the
            # current scope, try globals as a fallback.
            if name in scope.vars:
                return scope
            elif scopes and name in scopes[0].vars:
                return scopes[0]
            else:
                raise KeyError(name)

        return scope

    def get_scope_for_read(self, name: str) -> VarScope:
        """Shortcut for `ScopedVarStore.search_scope(..., read_search=True)`

        See Also: `ScopedVarStore.search_scope`
        """
        return self.search_scope(name, self.scopes, read_search=True)

    def get_scope_for_write(self, name: str) -> VarScope:
        """Shortcut for `ScopedVarStore.search_scope(..., read_search=False)`

        See Also: `ScopedVarStore.search_scope`
        """
        return self.search_scope(name, self.scopes, read_search=False)

    @property
    def current_scope(self) -> VarScope:
        """The current scope."""
        return self.scopes[-1]

    def get_var(self, name: str) -> str:
        """Get a variable from this store, following all nonlocal and global declarations."""
        return self.get_scope_for_read(name).vars[name]

    def set_var(self, name: str, value: str) -> None:
        """Set a variable in this store, following all nonlocal and global declarations."""
        try:
            scope = self.get_scope_for_write(name)
            scope.vars[name] = value
        except KeyError:
            self.current_scope.vars[name] = value

    def del_var(self, name: str) -> None:
        """Delete a variable from this store, following all nonlocal and global declarations."""
        try:
            scope = self.get_scope_for_write(name)
            del scope.vars[name]
        except KeyError:
            del self.current_scope.vars[name]

Instance variables

var current_scopeVarScope

The current scope.

Expand source code
@property
def current_scope(self) -> VarScope:
    """The current scope."""
    return self.scopes[-1]
var scopes

The VarScope stack. Later indices are deeper scopes. scopes[0] is the global scope, which is always available.

Methods

def declare_global(self, name: str) ‑> None

Declare a variable as global. This means all attempts to read/write the variable will automatically go to the global scope.

Expand source code
def declare_global(self, name: str) -> None:
    """
    Declare a variable as global. This means all attempts to read/write the variable will automatically go to
    the global scope.
    """
    self.current_scope.globals[name] = True
def declare_nonlocal(self, name: str) ‑> None

Declare a variable as nonlocal. This means that all attempts to read/write the variable will automatically go to the enclosing scope.

Expand source code
def declare_nonlocal(self, name: str) -> None:
    """
    Declare a variable as nonlocal. This means that all attempts to read/write the variable will automatically
    go to the enclosing scope.
    """
    self.current_scope.nonlocals[name] = True
def del_var(self, name: str) ‑> None

Delete a variable from this store, following all nonlocal and global declarations.

Expand source code
def del_var(self, name: str) -> None:
    """Delete a variable from this store, following all nonlocal and global declarations."""
    try:
        scope = self.get_scope_for_write(name)
        del scope.vars[name]
    except KeyError:
        del self.current_scope.vars[name]
def destroy_scope(self) ‑> None

Destroy the current scope and return to the last one. This will delete all local variables defined in the current scope.

Expand source code
def destroy_scope(self) -> None:
    """
    Destroy the current scope and return to the last one. This will delete all local variables defined in the current
    scope.
    """
    if len(self.scopes) == 1:
        # there should always be at least one scope
        raise ValueError("There must be at least one scope.")

    self.scopes.pop()
def get_scope_for_read(self, name: str) ‑> VarScope

Shortcut for ScopedVarStore.search_scope(..., read_search=True)

See Also: ScopedVarStore.search_scope()

Expand source code
def get_scope_for_read(self, name: str) -> VarScope:
    """Shortcut for `ScopedVarStore.search_scope(..., read_search=True)`

    See Also: `ScopedVarStore.search_scope`
    """
    return self.search_scope(name, self.scopes, read_search=True)
def get_scope_for_write(self, name: str) ‑> VarScope

Shortcut for ScopedVarStore.search_scope(..., read_search=False)

See Also: ScopedVarStore.search_scope()

Expand source code
def get_scope_for_write(self, name: str) -> VarScope:
    """Shortcut for `ScopedVarStore.search_scope(..., read_search=False)`

    See Also: `ScopedVarStore.search_scope`
    """
    return self.search_scope(name, self.scopes, read_search=False)
def get_var(self, name: str) ‑> str

Get a variable from this store, following all nonlocal and global declarations.

Expand source code
def get_var(self, name: str) -> str:
    """Get a variable from this store, following all nonlocal and global declarations."""
    return self.get_scope_for_read(name).vars[name]
def new_scope(self) ‑> None

Push a new scope onto the stack.

Expand source code
def new_scope(self) -> None:
    """
    Push a new scope onto the stack.
    """
    self.scopes.append(VarScope())
def search_scope(self, name: str, scopes: Sequence[VarScope], read_search: bool = False) ‑> VarScope

Using a variable name, search up the scope stack for something to read/write. This search will honor nonlocal and global declarations made for all scopes.

Args

name
The name of the variable to search for.
scopes
The scopes to search. Typically, this will be ScopedVarStore.scopes.
read_search
Must be True if no writes will be performed on the scope you're searching for.

Adds additional logic that reads the global store as a fallback if a defined value could not be found after searching up the stack.

Raises

KeyError
If an appropriate scope could not be found.
Expand source code
def search_scope(self, name: str, scopes: t.Sequence[VarScope], read_search: bool = False) -> VarScope:
    """
    Using a variable name, search up the scope stack for something to read/write. This search will honor nonlocal
    and global declarations made for all scopes.

    Args:
        name: The name of the variable to search for.

        scopes: The scopes to search. Typically, this will be `ScopedVarStore.scopes`.

        read_search: Must be `True` if no writes will be performed on the scope you're searching for.
        Adds additional logic that reads the global store as a fallback if a defined value could not be found
        after searching up the stack.

    Raises:
        KeyError: If an appropriate scope could not be found.
    """
    scopes = list(scopes)
    scope = scopes[-1]

    while scopes:
        scope = scopes.pop()

        if name in scope.globals:
            # If global, immediately go to the highest scope
            return scopes[0] if scopes else scope

        if name in scope.nonlocals:
            # If nonlocal, go to the enclosing scope.
            continue

        # Just break as soon as we step off global/nonlocal references.
        break

    if read_search:
        # Do a little bit of extra logic for a read search. If we can't find a value in the
        # current scope, try globals as a fallback.
        if name in scope.vars:
            return scope
        elif scopes and name in scopes[0].vars:
            return scopes[0]
        else:
            raise KeyError(name)

    return scope
def set_var(self, name: str, value: str) ‑> None

Set a variable in this store, following all nonlocal and global declarations.

Expand source code
def set_var(self, name: str, value: str) -> None:
    """Set a variable in this store, following all nonlocal and global declarations."""
    try:
        scope = self.get_scope_for_write(name)
        scope.vars[name] = value
    except KeyError:
        self.current_scope.vars[name] = value
class VarScope

A variable scope. See ScopedVarStore.

Expand source code
class VarScope:
    """
    A variable scope. See `ScopedVarStore`.
    """

    def __init__(self) -> None:
        self.vars: t.MutableMapping[str, str] = {}
        """The local variables defined in this scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.set_var` instead.
        """

        self.nonlocals: t.MutableMapping[str, bool] = {}
        """Nonlocal variables defined in this scope.

        If a variable is declared nonlocal, attempts to read/write it will go to the enclosing scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.declare_nonlocal` instead.
        """

        self.globals: t.MutableMapping[str, bool] = {}
        """Global variables defined in this scope.

        If a variable is declared global, attempts to read/write it will go to the global (top level) variable scope.

        .. NOTE::
            Generally this should not be modified directly, use `ScopedVarStore.declare_global` instead.
        """

Instance variables

var globals

Global variables defined in this scope.

If a variable is declared global, attempts to read/write it will go to the global (top level) variable scope.

Note

Generally this should not be modified directly, use ScopedVarStore.declare_global() instead.

var nonlocals

Nonlocal variables defined in this scope.

If a variable is declared nonlocal, attempts to read/write it will go to the enclosing scope.

Note

Generally this should not be modified directly, use ScopedVarStore.declare_nonlocal() instead.

var vars

The local variables defined in this scope.

Note

Generally this should not be modified directly, use ScopedVarStore.set_var() instead.