Skip to content

Commit

Permalink
Better error messages on StopIteration errors (#465)
Browse files Browse the repository at this point in the history
This avoids `TypeError: StopIteration interacts badly with generators and cannot be raised into a Future` and provides (hopefully) actionable error messages.
  • Loading branch information
farmio authored Sep 16, 2024
1 parent e212d4e commit a4dbf41
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 32 deletions.
2 changes: 2 additions & 0 deletions xknxproject/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .exceptions import (
InvalidPasswordException,
ProjectNotFoundException,
UnexpectedDataError,
UnexpectedFileContent,
XknxProjectException,
)
Expand All @@ -12,4 +13,5 @@
"InvalidPasswordException",
"ProjectNotFoundException",
"UnexpectedFileContent",
"UnexpectedDataError",
]
4 changes: 4 additions & 0 deletions xknxproject/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ class ProjectNotFoundException(XknxProjectException):

class UnexpectedFileContent(XknxProjectException):
"""Unexpected file content."""


class UnexpectedDataError(XknxProjectException):
"""Unexpected data in project file."""
16 changes: 11 additions & 5 deletions xknxproject/loader/project_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
from xml.etree import ElementTree

from xknxproject.exceptions import UnexpectedDataError
from xknxproject.models import (
ChannelNode,
ComObjectInstanceRef,
Expand Down Expand Up @@ -112,11 +113,16 @@ def load(
)

for group_address in function.group_addresses:
group_address.address = next(
ga.address
for ga in group_address_list
if ga.identifier == group_address.ref_id
)
try:
group_address.address = next(
ga.address
for ga in group_address_list
if ga.identifier == group_address.ref_id
)
except StopIteration:
raise UnexpectedDataError(
f"Group address {group_address.ref_id} referred in function not found"
) from None

return (
group_address_list,
Expand Down
99 changes: 72 additions & 27 deletions xknxproject/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re

from xknxproject import util
from xknxproject.exceptions import UnexpectedDataError
from xknxproject.models.knxproject import DPTType, ModuleInstanceInfos
from xknxproject.models.static import GroupAddressStyle, SpaceType
from xknxproject.zip import KNXProjContents
Expand Down Expand Up @@ -206,6 +207,15 @@ def merge_application_program_info(self, application: ApplicationProgram) -> Non
channel.resolve_channel_name(device_instance=self, application=application)
channel.resolve_channel_module_placeholders(device_instance=self)

def __str__(self) -> str:
"""Return string representation."""
return (
"DeviceInstance("
f'"{self.individual_address} {self.manufacturer_name} - {self.product_name}" '
f'Id="{self.identifier}" Puid="{self.project_uid}" '
f'ApplicationProgram="{self.application_program_ref}")'
)


@dataclass
class ChannelNode:
Expand Down Expand Up @@ -247,11 +257,10 @@ def resolve_channel_name(
]
except KeyError:
_LOGGER.debug(
"ParameterInstanceRef %s not found for Channel %s in device %s (%s)",
"ParameterInstanceRef %s not found for Channel %s of %s",
parameter_instance_ref,
self.ref_id,
device_instance.identifier,
device_instance.individual_address,
device_instance,
)
parameter = None

Expand All @@ -276,11 +285,17 @@ def resolve_channel_module_placeholders(
return

module_instance_ref = self.ref_id.split("_CH")[0]
module_instance = next(
mi
for mi in device_instance.module_instances
if mi.identifier == module_instance_ref
)
try:
module_instance = next(
mi
for mi in device_instance.module_instances
if mi.identifier == module_instance_ref
)
except StopIteration:
raise UnexpectedDataError(
f"ModuleInstance '{module_instance_ref}' not found for "
f"ChannelNode '{self.ref_id}' {self.name} of {device_instance}"
) from None
for argument in module_instance.arguments:
self.name = self.name.replace(f"{{{{{argument.name}}}}}", argument.value)

Expand Down Expand Up @@ -472,11 +487,18 @@ def _parse_base_number_argument(
# for SubModules the NumericArg item may use a BaseValue reference to an
# Argument of the base ModuleDef containing the base value for all its SubModules
result = 0
base_number_argument = next(
arg
for arg in module_instance.arguments
if arg.ref_id == base_number_argument_ref
)
try:
base_number_argument = next(
arg
for arg in module_instance.arguments
if arg.ref_id == base_number_argument_ref
)
except StopIteration:
raise UnexpectedDataError(
f"Base number argument {base_number_argument_ref} not found for "
f"ComObjectInstanceRef {self.ref_id=} {self.text=} "
f"of application {self.application_program_id_prefix}",
) from None

try:
# path (1) if value is a number, we are done
Expand All @@ -491,11 +513,18 @@ def _parse_base_number_argument(
num_arg is not None
and (base_value_ref := num_arg.base_value) is not None
):
base_module = next(
mi
for mi in module_instances
if mi.identifier == module_instance.base_module
)
try:
base_module = next(
mi
for mi in module_instances
if mi.identifier == module_instance.base_module
)
except StopIteration:
raise UnexpectedDataError(
f"Base ModuleInstance {module_instance.base_module} not found for "
f"ComObjectInstanceRef {self.ref_id=} {self.text=} "
f"of application {self.application_program_id_prefix}",
) from None
result += _parse_base_number_argument(
module_instance=base_module,
base_number_argument_ref=base_value_ref,
Expand All @@ -504,9 +533,18 @@ def _parse_base_number_argument(
base_number_argument, application.allocators
)

_module_instance = next(
mi for mi in module_instances if self.ref_id.startswith(f"{mi.identifier}_")
)
try:
_module_instance = next(
mi
for mi in module_instances
if self.ref_id.startswith(f"{mi.identifier}_")
)
except StopIteration:
raise UnexpectedDataError(
f"ModuleInstance not found for ComObjectInstanceRef {self.ref_id=} {self.text=} "
f"of application {self.application_program_id_prefix}",
) from None

com_object_number = self.number
self.number += _parse_base_number_argument(
_module_instance, self.base_number_argument_ref
Expand All @@ -522,12 +560,19 @@ def _base_number_from_allocator(
application_allocators: dict[str, Allocator],
) -> int:
"""Apply base number from allocator."""
allocator_object_base = next(
allocator
for allocator in application_allocators.values()
if allocator.identifier
== self.application_program_id_prefix + base_number_argument.value
)
try:
allocator_object_base = next(
allocator
for allocator in application_allocators.values()
if allocator.identifier
== self.application_program_id_prefix + base_number_argument.value
)
except StopIteration:
raise UnexpectedDataError(
f"Allocator with identifier {base_number_argument.value} not found for "
f"ComObjectInstanceRef {self.ref_id=} {self.text=} "
f"of application {self.application_program_id_prefix}",
) from None
if (allocator_size := base_number_argument.allocates) is None:
_LOGGER.warning(
"Base number allocator size not found for %s. Base number argument: %s",
Expand Down

0 comments on commit a4dbf41

Please sign in to comment.