From 8eb46b0b02badf4d1ebb5123ea594c70ae81e9b8 Mon Sep 17 00:00:00 2001 From: hariharan077 Date: Wed, 10 Jun 2026 22:42:29 +0530 Subject: [PATCH] fix(config): align additional_properties typing Declare generated additional_properties fields as instance dataclass fields, matching the @_additional_properties decorator runtime behavior and _ComponentConfig protocol. Skip non-init dataclass fields when resolving configured resource detectors so the new generated field is not treated as a detector. Assisted-by: ChatGPT --- opentelemetry-sdk/codegen/dataclass.jinja2 | 4 +- .../sdk/_configuration/_common.py | 8 ++- .../sdk/_configuration/_resource.py | 13 +++-- .../sdk/_configuration/models.py | 52 ++++++++++++++----- .../tests/_configuration/test_common.py | 24 +++++++-- 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/opentelemetry-sdk/codegen/dataclass.jinja2 b/opentelemetry-sdk/codegen/dataclass.jinja2 index 17b8aecdb50..79e8bcdc1e5 100644 --- a/opentelemetry-sdk/codegen/dataclass.jinja2 +++ b/opentelemetry-sdk/codegen/dataclass.jinja2 @@ -3,7 +3,7 @@ JSON Schema additionalProperties. When a schema type allows additional properties (e.g. Sampler, SpanExporter), this template adds: - @_additional_properties decorator (captures user-defined kwargs) - - additional_properties: ClassVar[dict[str, Any]] annotation (for type checkers) + - additional_properties instance field matching the decorator runtime behavior The template checks two context variables set by datamodel-codegen: - additionalProperties: set when the schema value is a boolean (true/false) @@ -69,5 +69,5 @@ class {{ class_name }}: {%- endif %} {%- endfor -%} {%- if has_additional %} - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field(default_factory=dict, init=False) {%- endif -%} diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py index 65729cbb8e3..28f0fc9ca47 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py @@ -30,7 +30,7 @@ def _additional_properties(cls): """ original_init = cls.__init__ original_sig = inspect.signature(original_init) - known_fields = frozenset(f.name for f in dataclasses.fields(cls)) + known_fields = frozenset(f.name for f in dataclasses.fields(cls) if f.init) def _init(self, **kwargs): known = {k: v for k, v in kwargs.items() if k in known_fields} @@ -81,10 +81,8 @@ class _ComponentConfig(Protocol): for ``**kwargs`` splatting to the user-defined component class) or ``None`` (when the YAML uses ``my_plugin:`` or ``my_plugin: null``). - Note: the generated models declare ``additional_properties`` as a - ``ClassVar`` even though the decorator assigns it as an instance - attribute at runtime. This is tolerated by pyright in ``standard`` - mode but flagged in ``strict`` mode. See #5268. + The generated models declare ``additional_properties`` as an instance + field to match the decorator's runtime assignment. """ additional_properties: dict[str, dict[str, Any] | None] diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py index 15d9e945e37..b1cc5df4dc0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py @@ -172,17 +172,20 @@ def _run_detectors( The detected_attrs dict is updated in-place; later detectors overwrite earlier ones for the same key. """ - for name in dataclasses.fields(detector_config): - value = getattr(detector_config, name.name, None) + for field in dataclasses.fields(detector_config): + if not field.init: + continue + + value = getattr(detector_config, field.name, None) if value is None: continue - if name.name in _RESOURCE_DETECTOR_REGISTRY: + if field.name in _RESOURCE_DETECTOR_REGISTRY: detected_attrs.update( - _RESOURCE_DETECTOR_REGISTRY[name.name](value) + _RESOURCE_DETECTOR_REGISTRY[field.name](value) ) else: cls = load_entry_point( - "opentelemetry_resource_detector", name.name + "opentelemetry_resource_detector", field.name ) detected_attrs.update(cls(**(value or {})).detect().attributes) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/models.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/models.py index 1c97e3536ab..ad690160175 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/models.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/models.py @@ -4,9 +4,9 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum -from typing import Any, ClassVar, TypeAlias +from typing import Any, TypeAlias from opentelemetry.sdk._configuration._common import _additional_properties @@ -359,7 +359,9 @@ class SpanExporter: otlp_grpc: OtlpGrpcExporter | None = None otlp_file_development: ExperimentalOtlpFileExporter | None = None console: ConsoleExporter | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) class SpanKind(Enum): @@ -511,7 +513,9 @@ class ExperimentalResourceDetector: host: ExperimentalHostResourceDetector | None = None process: ExperimentalProcessResourceDetector | None = None service: ExperimentalServiceResourceDetector | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -537,21 +541,27 @@ class LogRecordExporter: otlp_grpc: OtlpGrpcExporter | None = None otlp_file_development: ExperimentalOtlpFileExporter | None = None console: ConsoleExporter | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @_additional_properties @dataclass class MetricProducer: opencensus: OpenCensusMetricProducer | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @_additional_properties @dataclass class PullMetricExporter: prometheus_development: ExperimentalPrometheusMetricExporter | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -568,7 +578,9 @@ class PushMetricExporter: otlp_grpc: OtlpGrpcMetricExporter | None = None otlp_file_development: ExperimentalOtlpFileMetricExporter | None = None console: ConsoleMetricExporter | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -586,7 +598,9 @@ class SimpleSpanProcessor: class SpanProcessor: batch: BatchSpanProcessor | None = None simple: SimpleSpanProcessor | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @_additional_properties @@ -596,7 +610,9 @@ class TextMapPropagator: baggage: BaggagePropagator | None = None b3: B3Propagator | None = None b3multi: B3MultiPropagator | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -662,7 +678,9 @@ class ExperimentalResourceDetection: class LogRecordProcessor: batch: BatchLogRecordProcessor | None = None simple: SimpleLogRecordProcessor | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -731,7 +749,9 @@ class OpenTelemetryConfiguration: resource: Resource | None = None instrumentation_development: ExperimentalInstrumentation | None = None distribution: Distribution | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -773,7 +793,9 @@ class ExperimentalComposableSampler: ) probability: ExperimentalComposableProbabilitySampler | None = None rule_based: ExperimentalComposableRuleBasedSampler | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass @@ -802,7 +824,9 @@ class Sampler: parent_based: ParentBasedSampler | None = None probability_development: ExperimentalProbabilitySampler | None = None trace_id_ratio_based: TraceIdRatioBasedSampler | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) @dataclass diff --git a/opentelemetry-sdk/tests/_configuration/test_common.py b/opentelemetry-sdk/tests/_configuration/test_common.py index 46469f15d2b..730ec05873e 100644 --- a/opentelemetry-sdk/tests/_configuration/test_common.py +++ b/opentelemetry-sdk/tests/_configuration/test_common.py @@ -3,9 +3,9 @@ import inspect import unittest -from dataclasses import dataclass +from dataclasses import dataclass, field, fields from types import SimpleNamespace -from typing import Any, ClassVar +from typing import Any from unittest.mock import MagicMock, patch from opentelemetry.sdk._configuration._common import ( @@ -213,7 +213,9 @@ def setUp(self): class _SampleConfig: known_field: dict | None = None another_field: str | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) self.cls = _SampleConfig @@ -265,11 +267,23 @@ class TestGeneratedModelsHaveAdditionalProperties(unittest.TestCase): def _assert_supports_additional_properties(self, model_cls): # pylint: disable=unexpected-keyword-arg obj = model_cls(_test_plugin_key={}) + other = model_cls() self.assertTrue( hasattr(obj, "additional_properties"), f"{model_cls.__name__} missing additional_properties attribute", ) self.assertIn("_test_plugin_key", obj.additional_properties) + self.assertEqual(other.additional_properties, {}) + self.assertIsNot( + obj.additional_properties, other.additional_properties + ) + + additional_properties_field = next( + field + for field in fields(model_cls) + if field.name == "additional_properties" + ) + self.assertFalse(additional_properties_field.init) def test_sampler(self): self._assert_supports_additional_properties(Sampler) @@ -299,7 +313,9 @@ def setUp(self): class _Config: builtin_a: dict | None = None builtin_b: str | None = None - additional_properties: ClassVar[dict[str, Any]] + additional_properties: dict[str, dict[str, Any] | None] = field( + default_factory=dict, init=False + ) self.cls = _Config self.registry = {