Skip to content

Commit 2229ca5

Browse files
Fix AttributeError in __repr__/__str__ for partially initialized objects (#118)
When objects fail during `__init__`, debuggers and exception handlers call `__repr__()` or `__str__()` on the partially constructed instance. These methods were accessing attributes not yet set, causing cascading `AttributeError` that obscured the original initialization error. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pavel-kirienko <3298404+pavel-kirienko@users.noreply.github.com> Co-authored-by: Pavel Kirienko <pavel.kirienko@gmail.com>
1 parent 8c41bcf commit 2229ca5

10 files changed

Lines changed: 90 additions & 34 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# PyDSDL
22

3-
[![Build status](https://ci.appveyor.com/api/projects/status/lurx5gihhcl9wq1w/branch/master?svg=true)](https://ci.appveyor.com/project/Zubax/pydsdl/branch/master)
4-
[![Documentation Status](https://readthedocs.org/projects/pydsdl/badge/?version=latest)](https://pydsdl.readthedocs.io/en/latest/?badge=latest)
3+
[![Documentation](https://readthedocs.org/projects/pydsdl/badge/?version=latest)](https://pydsdl.readthedocs.io/en/latest/?badge=latest)
54
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pydsdl)](https://pypi.org/project/pydsdl/)
65
[![Forum](https://img.shields.io/discourse/https/forum.opencyphal.org/users.svg)](https://forum.opencyphal.org)
76

pydsdl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys as _sys
88
from pathlib import Path as _Path
99

10-
__version__ = "1.24.2"
10+
__version__ = "1.24.3"
1111
__version_info__ = tuple(map(int, __version__.split(".")[:3]))
1212
__license__ = "MIT"
1313
__author__ = "OpenCyphal"

pydsdl/_expression/_any.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ def __str__(self) -> str:
5454
raise NotImplementedError # pragma: no cover
5555

5656
def __repr__(self) -> str:
57-
return self.TYPE_NAME + "(" + str(self) + ")"
57+
try:
58+
return self.TYPE_NAME + "(" + str(self) + ")"
59+
except (AttributeError, NotImplementedError): # pragma: no cover
60+
return "%s(UNINITIALIZED)" % self.__class__.__name__
5861

5962
# Unary operators.
6063
def _logical_not(self) -> "Boolean":

pydsdl/_expression/_container.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ def __eq__(self, other: object) -> bool:
8383
return NotImplemented
8484

8585
def __str__(self) -> str:
86-
return "{%s}" % ", ".join(map(str, self._value)) # This is recursive.
86+
try:
87+
return "{%s}" % ", ".join(map(str, self._value)) # This is recursive.
88+
except (AttributeError, TypeError): # pragma: no cover
89+
return "Set(UNINITIALIZED)"
8790

8891
#
8992
# Set algebra implementation.

pydsdl/_expression/_primitive.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ def __eq__(self, other: object) -> bool:
4747
return NotImplemented # pragma: no cover
4848

4949
def __str__(self) -> str:
50-
return "true" if self._value else "false"
50+
try:
51+
return "true" if self._value else "false"
52+
except AttributeError: # pragma: no cover
53+
return "Boolean(UNINITIALIZED)"
5154

5255
def __bool__(self) -> bool: # For use in expressions without accessing "native_value"
5356
return self._value
@@ -107,7 +110,10 @@ def __eq__(self, other: object) -> bool:
107110
return NotImplemented # pragma: no cover
108111

109112
def __str__(self) -> str:
110-
return str(self._value)
113+
try:
114+
return str(self._value)
115+
except AttributeError: # pragma: no cover
116+
return "Rational(UNINITIALIZED)"
111117

112118
#
113119
# Unary operators.
@@ -216,7 +222,10 @@ def __eq__(self, other: object) -> bool:
216222
return NotImplemented # pragma: no cover
217223

218224
def __str__(self) -> str:
219-
return repr(self._value)
225+
try:
226+
return repr(self._value)
227+
except AttributeError: # pragma: no cover
228+
return "String(UNINITIALIZED)"
220229

221230
def _add(self, right: _any.Any) -> "String":
222231
if isinstance(right, String):

pydsdl/_serializable/_array.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,16 @@ def enumerate_elements_with_offsets(
9898
yield index, offset
9999

100100
def __str__(self) -> str:
101-
return "%s[%d]" % (self.element_type, self.capacity)
101+
try:
102+
return "%s[%d]" % (self.element_type, self.capacity)
103+
except AttributeError: # pragma: no cover
104+
return "FixedLengthArrayType(UNINITIALIZED)"
102105

103106
def __repr__(self) -> str:
104-
return "FixedLengthArrayType(element_type=%r, capacity=%r)" % (self.element_type, self.capacity)
107+
try:
108+
return "FixedLengthArrayType(element_type=%r, capacity=%r)" % (self.element_type, self.capacity)
109+
except AttributeError: # pragma: no cover
110+
return "FixedLengthArrayType(UNINITIALIZED)"
105111

106112

107113
def _unittest_fixed_array() -> None:
@@ -178,10 +184,16 @@ def length_field_type(self) -> UnsignedIntegerType:
178184
return self._length_field_type
179185

180186
def __str__(self) -> str:
181-
return "%s[<=%d]" % (self.element_type, self.capacity)
187+
try:
188+
return "%s[<=%d]" % (self.element_type, self.capacity)
189+
except AttributeError: # pragma: no cover
190+
return "VariableLengthArrayType(UNINITIALIZED)"
182191

183192
def __repr__(self) -> str:
184-
return "VariableLengthArrayType(element_type=%r, capacity=%r)" % (self.element_type, self.capacity)
193+
try:
194+
return "VariableLengthArrayType(element_type=%r, capacity=%r)" % (self.element_type, self.capacity)
195+
except AttributeError: # pragma: no cover
196+
return "VariableLengthArrayType(UNINITIALIZED)"
185197

186198

187199
def _unittest_variable_array() -> None:

pydsdl/_serializable/_attribute.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,16 @@ def __eq__(self, other: object) -> bool:
5353

5454
def __str__(self) -> str:
5555
"""Returns the normalized DSDL representation of the attribute."""
56-
return ("%s %s" % (self.data_type, self.name)).strip()
56+
try:
57+
return ("%s %s" % (self.data_type, self.name)).strip()
58+
except AttributeError: # pragma: no cover
59+
return "%s(UNINITIALIZED)" % self.__class__.__name__
5760

5861
def __repr__(self) -> str:
59-
return "%s(data_type=%r, name=%r)" % (self.__class__.__name__, self.data_type, self.name)
62+
try:
63+
return "%s(data_type=%r, name=%r)" % (self.__class__.__name__, self.data_type, self.name)
64+
except AttributeError: # pragma: no cover
65+
return "%s(UNINITIALIZED)" % self.__class__.__name__
6066

6167

6268
class Field(Attribute):
@@ -145,10 +151,16 @@ def __eq__(self, other: object) -> bool:
145151

146152
def __str__(self) -> str:
147153
"""Returns the normalized DSDL representation of the constant and its value."""
148-
return "%s %s = %s" % (self.data_type, self.name, self.value)
154+
try:
155+
return "%s %s = %s" % (self.data_type, self.name, self.value)
156+
except AttributeError: # pragma: no cover
157+
return "%s(UNINITIALIZED)" % self.__class__.__name__
149158

150159
def __repr__(self) -> str:
151-
return "Constant(data_type=%r, name=%r, value=%r)" % (self.data_type, self.name, self._value)
160+
try:
161+
return "Constant(data_type=%r, name=%r, value=%r)" % (self.data_type, self.name, self._value)
162+
except AttributeError: # pragma: no cover
163+
return "Constant(UNINITIALIZED)"
152164

153165

154166
def _unittest_attribute() -> None:

pydsdl/_serializable/_composite.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -353,22 +353,28 @@ def __getitem__(self, attribute_name: str) -> Attribute:
353353

354354
def __str__(self) -> str:
355355
"""Returns a string like ``uavcan.node.Heartbeat.1.0``."""
356-
return "%s.%d.%d" % (self.full_name, self.version.major, self.version.minor)
356+
try:
357+
return "%s.%d.%d" % (self.full_name, self.version.major, self.version.minor)
358+
except AttributeError: # pragma: no cover
359+
return "%s(UNINITIALIZED)" % self.__class__.__name__
357360

358361
def __repr__(self) -> str:
359-
return (
360-
"%s(name=%r, version=%r, fields=%r, constants=%r, alignment_requirement=%r, "
361-
"deprecated=%r, fixed_port_id=%r)"
362-
) % (
363-
self.__class__.__name__,
364-
self.full_name,
365-
self.version,
366-
self.fields,
367-
self.constants,
368-
self.alignment_requirement,
369-
self.deprecated,
370-
self.fixed_port_id,
371-
)
362+
try:
363+
return (
364+
"%s(name=%r, version=%r, fields=%r, constants=%r, alignment_requirement=%r, "
365+
"deprecated=%r, fixed_port_id=%r)"
366+
) % (
367+
self.__class__.__name__,
368+
self.full_name,
369+
self.version,
370+
self.fields,
371+
self.constants,
372+
self.alignment_requirement,
373+
self.deprecated,
374+
self.fixed_port_id,
375+
)
376+
except AttributeError: # pragma: no cover
377+
return "%s(UNINITIALIZED)" % self.__class__.__name__
372378

373379

374380
class UnionType(CompositeType):
@@ -651,7 +657,10 @@ def _check_aggregation(self, aggregate: "SerializableType") -> typing.Optional[A
651657
return super()._check_aggregation(aggregate)
652658

653659
def __repr__(self) -> str:
654-
return "%s(inner=%r, extent=%r)" % (self.__class__.__name__, self.inner_type, self.extent)
660+
try:
661+
return "%s(inner=%r, extent=%r)" % (self.__class__.__name__, self.inner_type, self.extent)
662+
except AttributeError: # pragma: no cover
663+
return "%s(UNINITIALIZED)" % self.__class__.__name__
655664

656665

657666
class ServiceType(CompositeType):

pydsdl/_serializable/_primitive.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ def __str__(self) -> str: # pragma: no cover
100100
raise NotImplementedError
101101

102102
def __repr__(self) -> str:
103-
return "%s(bit_length=%r, cast_mode=%r)" % (self.__class__.__name__, self.bit_length, self.cast_mode)
103+
try:
104+
return "%s(bit_length=%r, cast_mode=%r)" % (self.__class__.__name__, self.bit_length, self.cast_mode)
105+
except AttributeError: # pragma: no cover
106+
return "%s(UNINITIALIZED)" % self.__class__.__name__
104107

105108

106109
class BooleanType(PrimitiveType):

pydsdl/_serializable/_void.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ def alignment_requirement(self) -> int:
5050
return 1
5151

5252
def __str__(self) -> str:
53-
return "void%d" % self.bit_length
53+
try:
54+
return "void%d" % self.bit_length
55+
except AttributeError: # pragma: no cover
56+
return "VoidType(UNINITIALIZED)"
5457

5558
def __repr__(self) -> str:
56-
return "VoidType(bit_length=%d)" % self.bit_length
59+
try:
60+
return "VoidType(bit_length=%d)" % self.bit_length
61+
except AttributeError: # pragma: no cover
62+
return "VoidType(UNINITIALIZED)"
5763

5864

5965
def _unittest_void() -> None:

0 commit comments

Comments
 (0)