diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1e33938a..adb07aa288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-api`: Fix AttributeError on invalid attribute type in Python 3.9 + ([#4905](https://github.com/open-telemetry/opentelemetry-python/pull/4905)) - `opentelemetry-exporter-otlp-proto-grpc`: Fix re-initialization of gRPC channel on UNAVAILABLE error ([#4825](https://github.com/open-telemetry/opentelemetry-python/pull/4825)) - `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 5116c2fdd8..a2995b9086 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -39,6 +39,11 @@ _logger = logging.getLogger(__name__) +def _get_type_name(type_: type) -> str: + """Get the name of a type, handling typing generics that may lack __name__.""" + return getattr(type_, "__name__", str(type_)) + + def _clean_attribute( key: str, value: types.AttributeValue, max_len: Optional[int] ) -> Optional[Union[types.AttributeValue, Tuple[Union[str, int, float], ...]]]: @@ -83,7 +88,7 @@ def _clean_attribute( element_type.__name__, key, [ - valid_type.__name__ + _get_type_name(valid_type) for valid_type in _VALID_ATTR_VALUE_TYPES ], ) @@ -113,7 +118,7 @@ def _clean_attribute( "sequence of those types", type(value).__name__, key, - [valid_type.__name__ for valid_type in _VALID_ATTR_VALUE_TYPES], + [_get_type_name(valid_type) for valid_type in _VALID_ATTR_VALUE_TYPES], ) return None @@ -190,7 +195,7 @@ def _clean_extended_attribute_value( # pylint: disable=too-many-branches except Exception: raise TypeError( f"Invalid type {type(value).__name__} for attribute value. " - f"Expected one of {[valid_type.__name__ for valid_type in _VALID_ANY_VALUE_TYPES]} or a " + f"Expected one of {[_get_type_name(valid_type) for valid_type in _VALID_ANY_VALUE_TYPES]} or a " "sequence of those types", ) diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 8cb6f35fbc..2a235ed937 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -16,11 +16,13 @@ import unittest from typing import MutableSequence +from unittest import mock from opentelemetry.attributes import ( BoundedAttributes, _clean_attribute, _clean_extended_attribute, + _clean_extended_attribute_value, ) @@ -182,6 +184,28 @@ def test_mapping(self): _clean_extended_attribute("headers", mapping, None), expected ) + def test_invalid_type_error_message(self): + """Test that invalid types produce proper TypeError, not AttributeError. + + Regression test for issue #4821 where typing.Mapping lacks __name__ in Python 3.9. + """ + + # Create a class that raises when converted to string + class InvalidType: + def __str__(self): + raise ValueError("Cannot convert to string") + + # This should raise TypeError with expected types listed, not AttributeError + with self.assertRaises(TypeError) as ctx: + _clean_extended_attribute_value(InvalidType(), None) + + # Verify the error message contains expected information and doesn't raise AttributeError + error_msg = str(ctx.exception) + self.assertIn("Invalid type", error_msg) + self.assertIn("InvalidType", error_msg) + # Ensure the message includes valid types without raising AttributeError + self.assertIn("Expected one of", error_msg) + class TestBoundedAttributes(unittest.TestCase): # pylint: disable=consider-using-dict-items @@ -294,7 +318,7 @@ def test_locking(self): # pylint: disable=no-self-use def test_extended_attributes(self): bdict = BoundedAttributes(extended_attributes=True, immutable=False) - with unittest.mock.patch( + with mock.patch( "opentelemetry.attributes._clean_extended_attribute", return_value="mock_value", ) as clean_extended_attribute_mock: