Skip to content

Add error checking to component template tag #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 27, 2023
Merged
6 changes: 2 additions & 4 deletions src/reactpy_django/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,7 @@ async def execute_query() -> None:
set_data(None)
set_loading(False)
set_error(e)
_logger.exception(
f"Failed to execute query: {generate_obj_name(query) or query}"
)
_logger.exception(f"Failed to execute query: {generate_obj_name(query)}")
return

# Query was successful
Expand Down Expand Up @@ -252,7 +250,7 @@ async def execute_mutation(exec_args, exec_kwargs) -> None:
set_loading(False)
set_error(e)
_logger.exception(
f"Failed to execute mutation: {generate_obj_name(mutation) or mutation}"
f"Failed to execute mutation: {generate_obj_name(mutation)}"
)

# Mutation was successful
Expand Down
30 changes: 16 additions & 14 deletions src/reactpy_django/templatetags/reactpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)
from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError
from reactpy_django.types import ComponentParamData
from reactpy_django.utils import _register_component, check_component_params
from reactpy_django.utils import _register_component, check_component_args


REACTPY_WEB_MODULES_URL = reverse("reactpy:web_modules", args=["x"])[:-1][1:]
Expand Down Expand Up @@ -52,30 +52,32 @@ def component(dotted_path: str, *args, **kwargs):
kwargs.pop("key", "") # `key` is effectively useless for the root node

except Exception as e:
_logger.error(
f"Error while fetching '%s'. {(str(e.__cause__).capitalize())}."
if isinstance(e, ComponentDoesNotExistError)
else "An unknown error has occurred while registering component '%s'.",
dotted_path,
)
if isinstance(e, ComponentDoesNotExistError):
_logger.error(str(e))
else:
_logger.exception(
"An unknown error has occurred while registering component '%s'.",
dotted_path,
)
return failure_context(dotted_path, e)

# Store the component's args/kwargs in the database if needed
# This will be fetched by the websocket consumer later
try:
check_component_params(component, *args, **kwargs)
check_component_args(component, *args, **kwargs)
params = ComponentParamData(args, kwargs)
model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
model.full_clean()
model.save(using=REACTPY_DATABASE)

except Exception as e:
_logger.error(
f"Invalid component parameters for '%s'. {(str(e.__cause__).capitalize())}."
if isinstance(e, ComponentParamError)
else "An unknown error has occurred while saving component params for '%s'.",
dotted_path,
)
if isinstance(e, ComponentParamError):
_logger.error(str(e))
else:
_logger.exception(
"An unknown error has occurred while saving component params for '%s'.",
dotted_path,
)
return failure_context(dotted_path, e)

# Return the template rendering context
Expand Down
20 changes: 12 additions & 8 deletions src/reactpy_django/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _register_component(dotted_path: str) -> Callable:
REACTPY_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path)
except AttributeError as e:
raise ComponentDoesNotExistError(
f"Component '{dotted_path}' does not exist."
f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}."
) from e
_logger.debug("ReactPy has registered component %s", dotted_path)
return REACTPY_REGISTERED_COMPONENTS[dotted_path]
Expand Down Expand Up @@ -217,15 +217,18 @@ def register_components(self, components: set[str]) -> None:
)


def generate_obj_name(object: Any) -> str | None:
def generate_obj_name(object: Any) -> str:
"""Makes a best effort to create a name for an object.
Useful for JSON serialization of Python objects."""
if hasattr(object, "__module__"):
if hasattr(object, "__name__"):
return f"{object.__module__}.{object.__name__}"
if hasattr(object, "__class__"):
return f"{object.__module__}.{object.__class__.__name__}"
return None

with contextlib.suppress(Exception):
return str(object)
return ""


def django_query_postprocessor(
Expand Down Expand Up @@ -291,15 +294,15 @@ def django_query_postprocessor(
return data


def func_has_params(func: Callable) -> bool:
"""Checks if a function has any args or kwarg parameters."""
def func_has_args(func: Callable) -> bool:
"""Checks if a function has any args or kwarg."""
signature = inspect.signature(func)

# Check if the function has any args/kwargs
return str(signature) != "()" or True
return str(signature) != "()"


def check_component_params(func: Callable, *args, **kwargs):
def check_component_args(func: Callable, *args, **kwargs):
"""
Validate whether a set of args/kwargs would work on the given function.

Expand All @@ -310,8 +313,9 @@ def check_component_params(func: Callable, *args, **kwargs):
try:
signature.bind(*args, **kwargs)
except TypeError as e:
name = generate_obj_name(func)
raise ComponentParamError(
f"Invalid parameters passed to component. {str(e).capitalize()}."
f"Invalid args for '{name}'. {str(e).capitalize()}."
) from e


Expand Down
4 changes: 2 additions & 2 deletions src/reactpy_django/websocket/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from reactpy.core.serve import serve_layout

from reactpy_django.types import ComponentParamData, ComponentWebsocket
from reactpy_django.utils import db_cleanup, func_has_params
from reactpy_django.utils import db_cleanup, func_has_args


_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -111,7 +111,7 @@ async def _run_dispatch_loop(self):

# Fetch the component's args/kwargs from the database, if needed
try:
if func_has_params(component_constructor):
if func_has_args(component_constructor):
try:
# Always clean up expired entries first
await database_sync_to_async(db_cleanup, thread_sensitive=False)()
Expand Down