From 2789f32134675f1fdff7ede2f3b377e7b56d5efa Mon Sep 17 00:00:00 2001 From: Martin Xu <15661672+martinxu9@users.noreply.github.com> Date: Fri, 10 May 2024 12:12:42 -0700 Subject: [PATCH] [REF-2774] Add ReflexError and subclasses, send in telemetry (#3271) --- reflex/app.py | 27 ++++++--- reflex/base.py | 7 ++- reflex/components/component.py | 3 +- reflex/config.py | 8 ++- reflex/event.py | 27 ++++++--- reflex/state.py | 34 +++++++---- reflex/utils/exceptions.py | 72 +++++++++++++++++++--- reflex/utils/prerequisites.py | 41 +++++++------ reflex/vars.py | 107 +++++++++++++++++---------------- 9 files changed, 214 insertions(+), 112 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index e7cb8f027..5c85a9ef8 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -403,9 +403,12 @@ class App(Base): The generated component. Raises: + VarOperationTypeError: When an invalid component var related function is passed. TypeError: When an invalid component function is passed. exceptions.MatchTypeError: If the return types of match cases in rx.match are different. """ + from reflex.utils.exceptions import VarOperationTypeError + try: return component if isinstance(component, Component) else component() except exceptions.MatchTypeError: @@ -413,7 +416,7 @@ class App(Base): except TypeError as e: message = str(e) if "BaseVar" in message or "ComputedVar" in message: - raise TypeError( + raise VarOperationTypeError( "You may be trying to use an invalid Python function on a state var. " "When referencing a var inside your render code, only limited var operations are supported. " "See the var operation docs here: https://reflex.dev/docs/vars/var-operations/" @@ -555,11 +558,13 @@ class App(Base): Based on conflicts that NextJS would throw if not intercepted. Raises: - ValueError: exception showing which conflict exist with the route to be added + RouteValueError: exception showing which conflict exist with the route to be added Args: new_route: the route being newly added. """ + from reflex.utils.exceptions import RouteValueError + if "[" not in new_route: return @@ -576,7 +581,7 @@ class App(Base): ): if rw in segments and r != nr: # If the slugs in the segments of both routes are not the same, then the route is invalid - raise ValueError( + raise RouteValueError( f"You cannot use different slug names for the same dynamic path in {route} and {new_route} ('{r}' != '{nr}')" ) elif rw not in segments and r != nr: @@ -755,8 +760,10 @@ class App(Base): export: Whether to compile the app for export. Raises: - RuntimeError: When any page uses state, but no rx.State subclass is defined. + ReflexRuntimeError: When any page uses state, but no rx.State subclass is defined. """ + from reflex.utils.exceptions import ReflexRuntimeError + # Render a default 404 page if the user didn't supply one if constants.Page404.SLUG not in self.pages: self.add_custom_404_page() @@ -837,7 +844,7 @@ class App(Base): # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State. if code_uses_state_contexts(stateful_components_code) and self.state is None: - raise RuntimeError( + raise ReflexRuntimeError( "To access rx.State in frontend components, at least one " "subclass of rx.State must be defined in the app." ) @@ -1137,10 +1144,12 @@ def upload(app: App): emitted by the upload handler. Raises: - ValueError: if there are no args with supported annotation. - TypeError: if a background task is used as the handler. + UploadValueError: if there are no args with supported annotation. + UploadTypeError: if a background task is used as the handler. HTTPException: when the request does not include token / handler headers. """ + from reflex.utils.exceptions import UploadTypeError, UploadValueError + token = request.headers.get("reflex-client-token") handler = request.headers.get("reflex-event-handler") @@ -1166,7 +1175,7 @@ def upload(app: App): # check if there exists any handler args with annotation, List[UploadFile] if isinstance(func, EventHandler): if func.is_background: - raise TypeError( + raise UploadTypeError( f"@rx.background is not supported for upload handler `{handler}`.", ) func = func.fn @@ -1181,7 +1190,7 @@ def upload(app: App): break if not handler_upload_param: - raise ValueError( + raise UploadValueError( f"`{handler}` handler should have a parameter annotated as " "List[rx.UploadFile]" ) diff --git a/reflex/base.py b/reflex/base.py index 55bb7abfe..d15a7bedc 100644 --- a/reflex/base.py +++ b/reflex/base.py @@ -1,4 +1,5 @@ """Define the base Reflex class.""" + from __future__ import annotations import os @@ -26,15 +27,17 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None field_name: name of attribute Raises: - NameError: If state var field shadows another in its parent state + VarNameError: If state var field shadows another in its parent state """ + from reflex.utils.exceptions import VarNameError + reload = os.getenv(constants.RELOAD_CONFIG) == "True" for base in bases: try: if not reload and getattr(base, field_name, None): pass except TypeError as te: - raise NameError( + raise VarNameError( f'State var "{field_name}" in {base} has been shadowed by a substate var; ' f'use a different field name instead".' ) from te diff --git a/reflex/components/component.py b/reflex/components/component.py index 65b2cfa77..7cc26348c 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -731,6 +731,7 @@ class Component(BaseComponent, ABC): # Import here to avoid circular imports. from reflex.components.base.bare import Bare from reflex.components.base.fragment import Fragment + from reflex.utils.exceptions import ComponentTypeError # Translate deprecated props to new names. new_prop_names = [ @@ -757,7 +758,7 @@ class Component(BaseComponent, ABC): validate_children(child) # Make sure the child is a valid type. if not types._isinstance(child, ComponentChild): - raise TypeError( + raise ComponentTypeError( "Children of Reflex components must be other components, " "state vars, or primitive Python types. " f"Got child {child} of type {type(child)}.", diff --git a/reflex/config.py b/reflex/config.py index 6436a851c..40c2ef8bf 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -251,8 +251,10 @@ class Config(Base): The updated config values. Raises: - ValueError: If an environment variable is set to an invalid type. + EnvVarValueError: If an environment variable is set to an invalid type. """ + from reflex.utils.exceptions import EnvVarValueError + updated_values = {} # Iterate over the fields. for key, field in self.__fields__.items(): @@ -273,11 +275,11 @@ class Config(Base): env_var = env_var.lower() in ["true", "1", "yes"] else: env_var = field.type_(env_var) - except ValueError: + except ValueError as ve: console.error( f"Could not convert {key.upper()}={env_var} to type {field.type_}" ) - raise + raise EnvVarValueError from ve # Set the value. updated_values[key] = env_var diff --git a/reflex/event.py b/reflex/event.py index ece101582..e1ae88076 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -180,8 +180,10 @@ class EventHandler(EventActionsMixin): The event spec, containing both the function and args. Raises: - TypeError: If the arguments are invalid. + EventHandlerTypeError: If the arguments are invalid. """ + from reflex.utils.exceptions import EventHandlerTypeError + # Get the function args. fn_args = inspect.getfullargspec(self.fn).args[1:] fn_args = (Var.create_safe(arg) for arg in fn_args) @@ -197,7 +199,7 @@ class EventHandler(EventActionsMixin): try: values.append(Var.create(arg, _var_is_string=isinstance(arg, str))) except TypeError as e: - raise TypeError( + raise EventHandlerTypeError( f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." ) from e payload = tuple(zip(fn_args, values)) @@ -256,8 +258,10 @@ class EventSpec(EventActionsMixin): The event spec with the new arguments. Raises: - TypeError: If the arguments are invalid. + EventHandlerTypeError: If the arguments are invalid. """ + from reflex.utils.exceptions import EventHandlerTypeError + # Get the remaining unfilled function args. fn_args = inspect.getfullargspec(self.handler.fn).args[1 + len(self.args) :] fn_args = (Var.create_safe(arg) for arg in fn_args) @@ -268,7 +272,7 @@ class EventSpec(EventActionsMixin): try: values.append(Var.create(arg, _var_is_string=isinstance(arg, str))) except TypeError as e: - raise TypeError( + raise EventHandlerTypeError( f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." ) from e new_payload = tuple(zip(fn_args, values)) @@ -312,10 +316,12 @@ class CallableEventSpec(EventSpec): The EventSpec returned from calling the function. Raises: - TypeError: If the CallableEventSpec has no associated function. + EventHandlerTypeError: If the CallableEventSpec has no associated function. """ + from reflex.utils.exceptions import EventHandlerTypeError + if self.fn is None: - raise TypeError("CallableEventSpec has no associated function.") + raise EventHandlerTypeError("CallableEventSpec has no associated function.") return self.fn(*args, **kwargs) @@ -834,10 +840,11 @@ def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]: The event specs from calling the function. Raises: - ValueError: If the lambda has an invalid signature. + EventHandlerValueError: If the lambda has an invalid signature. """ # Import here to avoid circular imports. from reflex.event import EventHandler, EventSpec + from reflex.utils.exceptions import EventHandlerValueError # Get the args of the lambda. args = inspect.getfullargspec(fn).args @@ -851,7 +858,7 @@ def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]: elif len(args) == 1: out = fn(arg) else: - raise ValueError(f"Lambda {fn} must have 0 or 1 arguments.") + raise EventHandlerValueError(f"Lambda {fn} must have 0 or 1 arguments.") # Convert the output to a list. if not isinstance(out, List): @@ -869,7 +876,9 @@ def call_event_fn(fn: Callable, arg: Union[Var, ArgsSpec]) -> list[EventSpec]: # Make sure the event spec is valid. if not isinstance(e, EventSpec): - raise ValueError(f"Lambda {fn} returned an invalid event spec: {e}.") + raise EventHandlerValueError( + f"Lambda {fn} returned an invalid event spec: {e}." + ) # Add the event spec to the chain. events.append(e) diff --git a/reflex/state.py b/reflex/state.py index 4663fc4de..bc77f8140 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -279,11 +279,13 @@ class EventHandlerSetVar(EventHandler): Raises: AttributeError: If the given Var name does not exist on the state. - ValueError: If the given Var name is not a str + EventHandlerValueError: If the given Var name is not a str """ + from reflex.utils.exceptions import EventHandlerValueError + if args: if not isinstance(args[0], str): - raise ValueError( + raise EventHandlerValueError( f"Var name must be passed as a string, got {args[0]!r}" ) # Check that the requested Var setter exists on the State at compile time. @@ -380,10 +382,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): **kwargs: The kwargs to pass to the Pydantic init method. Raises: - RuntimeError: If the state is instantiated directly by end user. + ReflexRuntimeError: If the state is instantiated directly by end user. """ + from reflex.utils.exceptions import ReflexRuntimeError + if not _reflex_internal_init and not is_testing_env(): - raise RuntimeError( + raise ReflexRuntimeError( "State classes should not be instantiated directly in a Reflex app. " "See https://reflex.dev/docs/state/ for further information." ) @@ -438,8 +442,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): **kwargs: The kwargs to pass to the pydantic init_subclass method. Raises: - ValueError: If a substate class shadows another. + StateValueError: If a substate class shadows another. """ + from reflex.utils.exceptions import StateValueError + super().__init_subclass__(**kwargs) # Event handlers should not shadow builtin state methods. cls._check_overridden_methods() @@ -471,7 +477,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): else: # During normal operation, subclasses cannot have the same name, even if they are # defined in different modules. - raise ValueError( + raise StateValueError( f"The substate class '{cls.__name__}' has been defined multiple times. " "Shadowing substate classes is not allowed." ) @@ -829,10 +835,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): prop: The variable to initialize Raises: - TypeError: if the variable has an incorrect type + VarTypeError: if the variable has an incorrect type """ + from reflex.utils.exceptions import VarTypeError + if not types.is_valid_var_type(prop._var_type): - raise TypeError( + raise VarTypeError( "State vars must be primitive Python types, " "Plotly figures, Pandas dataframes, " "or subclasses of rx.Base. " @@ -1688,10 +1696,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if initial: computed_vars = { # Include initial computed vars. - prop_name: cv._initial_value - if isinstance(cv, ComputedVar) - and not isinstance(cv._initial_value, types.Unset) - else self.get_value(getattr(self, prop_name)) + prop_name: ( + cv._initial_value + if isinstance(cv, ComputedVar) + and not isinstance(cv._initial_value, types.Unset) + else self.get_value(getattr(self, prop_name)) + ) for prop_name, cv in self.computed_vars.items() } elif include_computed: diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index e4a9a6e6c..aabaaef14 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -1,21 +1,77 @@ """Custom Exceptions.""" -class InvalidStylePropError(TypeError): +class ReflexError(Exception): + """Base exception for all Reflex exceptions.""" + + +class ReflexRuntimeError(ReflexError, RuntimeError): + """Custom RuntimeError for Reflex.""" + + +class UploadTypeError(ReflexError, TypeError): + """Custom TypeError for upload related errors.""" + + +class EnvVarValueError(ReflexError, ValueError): + """Custom ValueError raised when unable to convert env var to expected type.""" + + +class ComponentTypeError(ReflexError, TypeError): + """Custom TypeError for component related errors.""" + + +class EventHandlerTypeError(ReflexError, TypeError): + """Custom TypeError for event handler related errors.""" + + +class EventHandlerValueError(ReflexError, ValueError): + """Custom ValueError for event handler related errors.""" + + +class StateValueError(ReflexError, ValueError): + """Custom ValueError for state related errors.""" + + +class VarNameError(ReflexError, NameError): + """Custom NameError for when a state var has been shadowed by a substate var.""" + + +class VarTypeError(ReflexError, TypeError): + """Custom TypeError for var related errors.""" + + +class VarValueError(ReflexError, ValueError): + """Custom ValueError for var related errors.""" + + +class VarAttributeError(ReflexError, AttributeError): + """Custom AttributeError for var related errors.""" + + +class UploadValueError(ReflexError, ValueError): + """Custom ValueError for upload related errors.""" + + +class RouteValueError(ReflexError, ValueError): + """Custom ValueError for route related errors.""" + + +class VarOperationTypeError(ReflexError, TypeError): + """Custom TypeError for when unsupported operations are performed on vars.""" + + +class InvalidStylePropError(ReflexError, TypeError): """Custom Type Error when style props have invalid values.""" - pass - -class ImmutableStateError(AttributeError): +class ImmutableStateError(ReflexError): """Raised when a background task attempts to modify state outside of context.""" -class LockExpiredError(Exception): +class LockExpiredError(ReflexError): """Raised when the state lock expires while an event is being processed.""" -class MatchTypeError(TypeError): +class MatchTypeError(ReflexError, TypeError): """Raised when the return types of match cases are different.""" - - pass diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 3d13e2bc4..870b0a7be 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -210,28 +210,35 @@ def get_app(reload: bool = False) -> ModuleType: Raises: RuntimeError: If the app name is not set in the config. + exceptions.ReflexError: Reflex specific errors. """ - os.environ[constants.RELOAD_CONFIG] = str(reload) - config = get_config() - if not config.app_name: - raise RuntimeError( - "Cannot get the app module because `app_name` is not set in rxconfig! " - "If this error occurs in a reflex test case, ensure that `get_app` is mocked." - ) - module = config.module - sys.path.insert(0, os.getcwd()) - app = __import__(module, fromlist=(constants.CompileVars.APP,)) + from reflex.utils import exceptions, telemetry - if reload: - from reflex.state import reload_state_module + try: + os.environ[constants.RELOAD_CONFIG] = str(reload) + config = get_config() + if not config.app_name: + raise RuntimeError( + "Cannot get the app module because `app_name` is not set in rxconfig! " + "If this error occurs in a reflex test case, ensure that `get_app` is mocked." + ) + module = config.module + sys.path.insert(0, os.getcwd()) + app = __import__(module, fromlist=(constants.CompileVars.APP,)) - # Reset rx.State subclasses to avoid conflict when reloading. - reload_state_module(module=module) + if reload: + from reflex.state import reload_state_module - # Reload the app module. - importlib.reload(app) + # Reset rx.State subclasses to avoid conflict when reloading. + reload_state_module(module=module) - return app + # Reload the app module. + importlib.reload(app) + + return app + except exceptions.ReflexError as ex: + telemetry.send("error", context="frontend", detail=str(ex)) + raise def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: diff --git a/reflex/vars.py b/reflex/vars.py index 4a8e6b30f..6ac78706a 100644 --- a/reflex/vars.py +++ b/reflex/vars.py @@ -35,6 +35,7 @@ from typing import ( from reflex import constants from reflex.base import Base from reflex.utils import console, format, imports, serializers, types +from reflex.utils.exceptions import VarAttributeError, VarTypeError, VarValueError # This module used to export ImportVar itself, so we still import it for export here from reflex.utils.imports import ImportDict, ImportVar @@ -353,7 +354,7 @@ class Var: The var. Raises: - TypeError: If the value is JSON-unserializable. + VarTypeError: If the value is JSON-unserializable. """ # Check for none values. if value is None: @@ -372,7 +373,7 @@ class Var: type_ = type(value) name = value if type_ in types.JSONType else serializers.serialize(value) if name is None: - raise TypeError( + raise VarTypeError( f"No JSON serializer found for var {value} of type {type_}." ) name = name if isinstance(name, str) else format.json_dumps(name) @@ -542,9 +543,9 @@ class Var: """Raise exception if using Var in a boolean context. Raises: - TypeError: when attempting to bool-ify the Var. + VarTypeError: when attempting to bool-ify the Var. """ - raise TypeError( + raise VarTypeError( f"Cannot convert Var {self._var_full_name!r} to bool for use with `if`, `and`, `or`, and `not`. " "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." ) @@ -553,9 +554,9 @@ class Var: """Raise exception if using Var in an iterable context. Raises: - TypeError: when attempting to iterate over the Var. + VarTypeError: when attempting to iterate over the Var. """ - raise TypeError( + raise VarTypeError( f"Cannot iterate over Var {self._var_full_name!r}. Instead use `rx.foreach`." ) @@ -584,7 +585,7 @@ class Var: The indexed var. Raises: - TypeError: If the var is not indexable. + VarTypeError: If the var is not indexable. """ # Indexing is only supported for strings, lists, tuples, dicts, and dataframes. if not ( @@ -592,11 +593,11 @@ class Var: or types.is_dataframe(self._var_type) ): if self._var_type == Any: - raise TypeError( + raise VarTypeError( "Could not index into var of type Any. (If you are trying to index into a state var, " "add the correct type annotation to the var.)" ) - raise TypeError( + raise VarTypeError( f"Var {self._var_name} of type {self._var_type} does not support indexing." ) @@ -615,7 +616,7 @@ class Var: or isinstance(i, Var) and not i._var_type == int ): - raise TypeError("Index must be an integer or an integer var.") + raise VarTypeError("Index must be an integer or an integer var.") # Handle slices first. if isinstance(i, slice): @@ -658,7 +659,7 @@ class Var: i._var_type, types.get_args(Union[int, str, float]) ) ): - raise TypeError( + raise VarTypeError( "Index must be one of the following types: int, str, int or str Var" ) # Get the type of the indexed var. @@ -687,7 +688,7 @@ class Var: The var attribute. Raises: - AttributeError: If the attribute cannot be found, or if __getattr__ fallback should be used. + VarAttributeError: If the attribute cannot be found, or if __getattr__ fallback should be used. """ try: var_attribute = super().__getattribute__(name) @@ -698,10 +699,12 @@ class Var: super().__getattribute__("_var_type"), name ) if type_ is not None: - raise AttributeError(f"{name} is being accessed through the Var.") + raise VarAttributeError( + f"{name} is being accessed through the Var." + ) # Return the attribute as-is. return var_attribute - except AttributeError: + except VarAttributeError: raise # fall back to __getattr__ anyway def __getattr__(self, name: str) -> Var: @@ -714,13 +717,13 @@ class Var: The var attribute. Raises: - AttributeError: If the var is wrongly annotated or can't find attribute. - TypeError: If an annotation to the var isn't provided. + VarAttributeError: If the var is wrongly annotated or can't find attribute. + VarTypeError: If an annotation to the var isn't provided. """ # Check if the attribute is one of the class fields. if not name.startswith("_"): if self._var_type == Any: - raise TypeError( + raise VarTypeError( f"You must provide an annotation for the state var `{self._var_full_name}`. Annotation cannot be `{self._var_type}`" ) from None is_optional = types.is_optional(self._var_type) @@ -734,16 +737,16 @@ class Var: ) if name in REPLACED_NAMES: - raise AttributeError( + raise VarAttributeError( f"Field {name!r} was renamed to {REPLACED_NAMES[name]!r}" ) - raise AttributeError( + raise VarAttributeError( f"The State var `{self._var_full_name}` has no attribute '{name}' or may have been annotated " f"wrongly." ) - raise AttributeError( + raise VarAttributeError( f"The State var has no attribute '{name}' or may have been annotated wrongly.", ) @@ -770,8 +773,8 @@ class Var: The operation result. Raises: - TypeError: If the operation between two operands is invalid. - ValueError: If flip is set to true and value of operand is not provided + VarTypeError: If the operation between two operands is invalid. + VarValueError: If flip is set to true and value of operand is not provided """ if isinstance(other, str): other = Var.create(json.dumps(other)) @@ -781,7 +784,7 @@ class Var: type_ = type_ or self._var_type if other is None and flip: - raise ValueError( + raise VarValueError( "flip_operands cannot be set to True if the value of 'other' operand is not provided" ) @@ -804,7 +807,7 @@ class Var: types.get_base_class(right_operand._var_type), # type: ignore op, ): - raise TypeError( + raise VarTypeError( f"Unsupported Operand type(s) for {op}: `{left_operand._var_full_name}` of type {left_operand._var_type.__name__} and `{right_operand._var_full_name}` of type {right_operand._var_type.__name__}" # type: ignore ) @@ -925,10 +928,10 @@ class Var: A var with the absolute value. Raises: - TypeError: If the var is not a list. + VarTypeError: If the var is not a list. """ if not types._issubclass(self._var_type, List): - raise TypeError(f"Cannot get length of non-list var {self}.") + raise VarTypeError(f"Cannot get length of non-list var {self}.") return self._replace( _var_name=f"{self._var_name}.length", _var_type=int, @@ -1328,9 +1331,9 @@ class Var: """Override the 'in' operator to alert the user that it is not supported. Raises: - TypeError: the operation is not supported + VarTypeError: the operation is not supported """ - raise TypeError( + raise VarTypeError( "'in' operator not supported for Var types, use Var.contains() instead." ) @@ -1341,13 +1344,13 @@ class Var: other: The object to check. Raises: - TypeError: If the var is not a valid type: dict, list, tuple or str. + VarTypeError: If the var is not a valid type: dict, list, tuple or str. Returns: A var representing the contain check. """ if not (types._issubclass(self._var_type, Union[dict, list, tuple, str, set])): - raise TypeError( + raise VarTypeError( f"Var {self._var_full_name} of type {self._var_type} does not support contains check." ) method = ( @@ -1371,7 +1374,7 @@ class Var: if types._issubclass(self._var_type, str) and not types._issubclass( other._var_type, str ): - raise TypeError( + raise VarTypeError( f"'in ' requires string as left operand, not {other._var_type}" ) return self._replace( @@ -1385,13 +1388,13 @@ class Var: """Reverse a list var. Raises: - TypeError: If the var is not a list. + VarTypeError: If the var is not a list. Returns: A var with the reversed list. """ if not types._issubclass(self._var_type, list): - raise TypeError(f"Cannot reverse non-list var {self._var_full_name}.") + raise VarTypeError(f"Cannot reverse non-list var {self._var_full_name}.") return self._replace( _var_name=f"[...{self._var_full_name}].reverse()", @@ -1406,10 +1409,10 @@ class Var: A var with the lowercase string. Raises: - TypeError: If the var is not a string. + VarTypeError: If the var is not a string. """ if not types._issubclass(self._var_type, str): - raise TypeError( + raise VarTypeError( f"Cannot convert non-string var {self._var_full_name} to lowercase." ) @@ -1426,10 +1429,10 @@ class Var: A var with the uppercase string. Raises: - TypeError: If the var is not a string. + VarTypeError: If the var is not a string. """ if not types._issubclass(self._var_type, str): - raise TypeError( + raise VarTypeError( f"Cannot convert non-string var {self._var_full_name} to uppercase." ) @@ -1449,10 +1452,10 @@ class Var: A var with the stripped string. Raises: - TypeError: If the var is not a string. + VarTypeError: If the var is not a string. """ if not types._issubclass(self._var_type, str): - raise TypeError(f"Cannot strip non-string var {self._var_full_name}.") + raise VarTypeError(f"Cannot strip non-string var {self._var_full_name}.") other = Var.create_safe(json.dumps(other)) if isinstance(other, str) else other @@ -1472,10 +1475,10 @@ class Var: A var with the list. Raises: - TypeError: If the var is not a string. + VarTypeError: If the var is not a string. """ if not types._issubclass(self._var_type, str): - raise TypeError(f"Cannot split non-string var {self._var_full_name}.") + raise VarTypeError(f"Cannot split non-string var {self._var_full_name}.") other = Var.create_safe(json.dumps(other)) if isinstance(other, str) else other @@ -1496,10 +1499,10 @@ class Var: A var with the string. Raises: - TypeError: If the var is not a list. + VarTypeError: If the var is not a list. """ if not types._issubclass(self._var_type, list): - raise TypeError(f"Cannot join non-list var {self._var_full_name}.") + raise VarTypeError(f"Cannot join non-list var {self._var_full_name}.") if other is None: other = Var.create_safe('""') @@ -1525,11 +1528,11 @@ class Var: A var representing foreach operation. Raises: - TypeError: If the var is not a list. + VarTypeError: If the var is not a list. """ inner_types = get_args(self._var_type) if not inner_types: - raise TypeError( + raise VarTypeError( f"Cannot foreach over non-sequence var {self._var_full_name} of type {self._var_type}." ) arg = BaseVar( @@ -1566,25 +1569,27 @@ class Var: A var representing range operation. Raises: - TypeError: If the var is not an int. + VarTypeError: If the var is not an int. """ if not isinstance(v1, Var): v1 = Var.create_safe(v1) if v1._var_type != int: - raise TypeError(f"Cannot get range on non-int var {v1._var_full_name}.") + raise VarTypeError(f"Cannot get range on non-int var {v1._var_full_name}.") if not isinstance(v2, Var): v2 = Var.create(v2) if v2 is None: v2 = Var.create_safe("undefined") elif v2._var_type != int: - raise TypeError(f"Cannot get range on non-int var {v2._var_full_name}.") + raise VarTypeError(f"Cannot get range on non-int var {v2._var_full_name}.") if not isinstance(step, Var): step = Var.create(step) if step is None: step = Var.create_safe(1) elif step._var_type != int: - raise TypeError(f"Cannot get range on non-int var {step._var_full_name}.") + raise VarTypeError( + f"Cannot get range on non-int var {step._var_full_name}." + ) return BaseVar( _var_name=f"Array.from(range({v1._var_full_name}, {v2._var_full_name}, {step._var_name}))", @@ -1919,7 +1924,7 @@ class ComputedVar(Var, property): A set of variable names accessed by the given obj. Raises: - ValueError: if the function references the get_state, parent_state, or substates attributes + VarValueError: if the function references the get_state, parent_state, or substates attributes (cannot track deps in a related state, only implicitly via parent state). """ d = set() @@ -1966,7 +1971,7 @@ class ComputedVar(Var, property): except Exception: ref_obj = None if instruction.argval in invalid_names: - raise ValueError( + raise VarValueError( f"Cached var {self._var_full_name} cannot access arbitrary state via `{instruction.argval}`." ) if callable(ref_obj):