From bd1aa159d83fa0b246a0e9f8cf4cc1cc3c61303d Mon Sep 17 00:00:00 2001 From: allen Date: Thu, 23 Jul 2026 17:13:21 +0900 Subject: [PATCH 1/5] Treat present-null config value as empty mapping (#5451) A YAML mapping key present with an empty (null) value parses to `None`, which the config conversion layer could not distinguish from an absent key. Since sampler and resource-detector type dispatch selects a type via `is not None`, writing a leaf node as `always_on:` (rather than `always_on: {}`) produced an all-`None` config and failed with "Unsupported sampler type in config"; detectors written as `- service:` were silently skipped. Per the declarative-config spec a key present with a null value on a `dict[str, Any]`-typed node means "select this with an empty config", so coerce such a `None` to an empty mapping in `_convert_value`. Scalar and dataclass fields keep `None`, so an absent optional section stays unset. --- .changelog/5454.fixed | 1 + .../configuration/_conversion.py | 13 +++++-- .../tests/test_conversion.py | 27 +++++++++++++++ .../tests/test_resource.py | 12 +++++++ .../tests/test_tracer_provider.py | 34 +++++++++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 .changelog/5454.fixed diff --git a/.changelog/5454.fixed b/.changelog/5454.fixed new file mode 100644 index 00000000000..0534de623e3 --- /dev/null +++ b/.changelog/5454.fixed @@ -0,0 +1 @@ +`opentelemetry-configuration`: a declarative config key present with an empty (null) value on an object-typed node (e.g. `always_on:` or a `- service:` detector) is now treated the same as an explicit empty mapping (`always_on: {}`) instead of failing sampler type dispatch or silently skipping the detector. diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py index c388b966b7e..dcfd5984964 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py @@ -40,12 +40,19 @@ def _convert_value(value: Any, type_hint: Any) -> Any: dataclasses. Other values (primitives, enums, ``dict[str, Any]`` aliases) pass through unchanged. """ - if value is None: - return None - unwrapped = _unwrap_optional(type_hint) origin = get_origin(unwrapped) + if value is None: + # A mapping key present with an empty (null) YAML value parses to + # ``None``. For ``dict[str, Any]``-typed nodes the declarative-config + # spec treats a present null as "select this with an empty config" — + # e.g. ``always_on:`` is equivalent to ``always_on: {}``. Coerce it to + # an empty mapping so downstream ``is not None`` type dispatch selects + # it. Scalar and dataclass fields keep ``None`` (an absent optional + # section stays unset). + return {} if origin is dict else None + # list[X] — recurse on each element if origin is list and isinstance(value, list): args = get_args(unwrapped) diff --git a/opentelemetry-configuration/tests/test_conversion.py b/opentelemetry-configuration/tests/test_conversion.py index 6ae15c6e648..4d09ad36ea5 100644 --- a/opentelemetry-configuration/tests/test_conversion.py +++ b/opentelemetry-configuration/tests/test_conversion.py @@ -42,6 +42,13 @@ class _WithEnum: filter: ExemplarFilter | None = None +@dataclass +class _WithMapping: + # ``dict[str, Any]``-typed node, like the sampler/detector leaf configs. + option: dict[str, Any] | None = None + name: str | None = None + + class TestDictToDataclass(unittest.TestCase): def test_raises_on_non_dataclass(self): # _dict_to_dataclass is internal and assumes cls is a dataclass. @@ -108,3 +115,23 @@ def test_enum_value_already_enum_passes_through(self): {"filter": ExemplarFilter.trace_based}, _WithEnum ) self.assertIs(result.filter, ExemplarFilter.trace_based) + + def test_present_null_mapping_coerced_to_empty_dict(self): + # A key present with a null YAML value (e.g. ``option:``) on a + # dict-typed node means "select with empty config", so it must become + # an empty mapping rather than None. Regression test for #5451. + result = _dict_to_dataclass({"option": None}, _WithMapping) + self.assertEqual(result.option, {}) + + def test_explicit_empty_mapping_preserved(self): + result = _dict_to_dataclass({"option": {}}, _WithMapping) + self.assertEqual(result.option, {}) + + def test_absent_mapping_stays_none(self): + # An omitted optional section must remain unset, unlike a present null. + result = _dict_to_dataclass({"name": "x"}, _WithMapping) + self.assertIsNone(result.option) + + def test_present_null_scalar_stays_none(self): + result = _dict_to_dataclass({"name": None}, _WithMapping) + self.assertIsNone(result.name) diff --git a/opentelemetry-configuration/tests/test_resource.py b/opentelemetry-configuration/tests/test_resource.py index b822a37f8fa..c2147df09c8 100644 --- a/opentelemetry-configuration/tests/test_resource.py +++ b/opentelemetry-configuration/tests/test_resource.py @@ -7,6 +7,7 @@ import unittest from unittest.mock import MagicMock, patch +from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._exceptions import ConfigurationError from opentelemetry.configuration._resource import create_resource from opentelemetry.configuration.models import ( @@ -376,6 +377,17 @@ def test_explicit_service_name_overrides_env_var(self): resource = create_resource(config) self.assertEqual(resource.attributes[SERVICE_NAME], "explicit-svc") + def test_null_valued_detector_from_parsed_config(self): + # Regression test for #5451: a detector written as ``- service:`` + # (present, null) is parsed to ``{"service": None}``. It must run the + # detector just like ``- service: {}`` rather than being skipped. + config = _dict_to_dataclass( + {"detection_development": {"detectors": [{"service": None}]}}, + ResourceConfig, + ) + resource = create_resource(config) + self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) + def test_service_detector_not_run_when_absent(self): resource = create_resource(ResourceConfig()) self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes) diff --git a/opentelemetry-configuration/tests/test_tracer_provider.py b/opentelemetry-configuration/tests/test_tracer_provider.py index b8fe78f82a7..492f0a83d0c 100644 --- a/opentelemetry-configuration/tests/test_tracer_provider.py +++ b/opentelemetry-configuration/tests/test_tracer_provider.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch from opentelemetry import trace as trace_api +from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._tracer_provider import ( configure_tracer_provider, create_tracer_provider, @@ -249,6 +250,39 @@ def test_no_sampler_raises_configuration_error(self): with self.assertRaises(ConfigurationError): self._make_provider(SamplerConfig()) + def test_null_valued_always_on_from_parsed_config(self): + # Regression test for #5451: a leaf sampler written as ``always_on:`` + # (present, null) is parsed to ``None`` by the YAML loader. It must be + # treated the same as ``always_on: {}`` rather than failing type + # dispatch. + sampler_config = _dict_to_dataclass({"always_on": None}, SamplerConfig) + provider = self._make_provider(sampler_config) + self.assertIs(provider.sampler, ALWAYS_ON) + + def test_null_valued_parent_based_delegates_from_parsed_config(self): + # Regression test for #5451: the exact parent_based config from the + # issue, where every leaf delegate is written with a null value. + sampler_config = _dict_to_dataclass( + { + "parent_based": { + "root": {"always_on": None}, + "remote_parent_sampled": {"always_on": None}, + "remote_parent_not_sampled": {"always_off": None}, + "local_parent_sampled": {"always_on": None}, + "local_parent_not_sampled": {"always_off": None}, + } + }, + SamplerConfig, + ) + provider = self._make_provider(sampler_config) + sampler = provider.sampler + self.assertIsInstance(sampler, ParentBased) + self.assertIs(sampler._root, ALWAYS_ON) + self.assertIs(sampler._remote_parent_sampled, ALWAYS_ON) + self.assertIs(sampler._remote_parent_not_sampled, ALWAYS_OFF) + self.assertIs(sampler._local_parent_sampled, ALWAYS_ON) + self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF) + def test_user_defined_sampler_loaded_via_entry_point(self): mock_sampler = MagicMock(spec=Sampler) mock_class = MagicMock(return_value=mock_sampler) From a11cc5d3270a63b5fd36ecf61bc2b5cc1d8d5d39 Mon Sep 17 00:00:00 2001 From: allen Date: Fri, 24 Jul 2026 09:23:44 +0900 Subject: [PATCH 2/5] Extend present-null coercion to empty-constructible dataclasses (#5451) Address review feedback: the empty-value issue affects not only dict-aliased nodes (sampler/detector leaves) but any dataclass config that can be instantiated with no arguments, e.g. a metric `console:` exporter written without an explicit `console: {}`. _convert_value now also coerces a present null to a defaulted dataclass instance when the annotated type is a dataclass whose fields are all optional. Dataclasses with required fields, and scalar fields, keep None so an absent optional section stays unset and no TypeError is raised. Adds regression coverage for the null-valued metric console exporter and for the required-field guard. --- .changelog/5454.fixed | 2 +- .../configuration/_conversion.py | 44 +++++++++++++++---- .../tests/test_conversion.py | 28 +++++++++++- .../tests/test_meter_provider.py | 18 ++++++++ 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/.changelog/5454.fixed b/.changelog/5454.fixed index 0534de623e3..cc73265525f 100644 --- a/.changelog/5454.fixed +++ b/.changelog/5454.fixed @@ -1 +1 @@ -`opentelemetry-configuration`: a declarative config key present with an empty (null) value on an object-typed node (e.g. `always_on:` or a `- service:` detector) is now treated the same as an explicit empty mapping (`always_on: {}`) instead of failing sampler type dispatch or silently skipping the detector. +`opentelemetry-configuration`: a declarative config key present with an empty (null) value on an object-typed node (e.g. `always_on:`, a `- service:` detector, or a metric `console:` exporter) is now treated the same as an explicit empty config (`always_on: {}`) instead of failing type dispatch or silently skipping the node. Both `dict`-typed nodes and dataclasses constructible with no arguments are covered. diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py index dcfd5984964..3d37f2f5df2 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py @@ -12,7 +12,7 @@ from __future__ import annotations from collections.abc import Mapping -from dataclasses import fields, is_dataclass +from dataclasses import MISSING, fields, is_dataclass from enum import Enum from types import UnionType from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints @@ -33,6 +33,39 @@ def _unwrap_optional(type_hint: Any) -> Any: return type_hint +def _coerce_present_null(unwrapped: Any, origin: Any) -> Any: + """Map a present null value to the empty config its type implies. + + A mapping key present with an empty (null) YAML value parses to ``None``, + which is otherwise indistinguishable from an absent key. For object-typed + nodes the declarative-config spec treats a present null as "select this + with an empty config" — e.g. ``always_on:`` is equivalent to + ``always_on: {}`` and a ``console:`` exporter to ``console: {}``. + + ``dict[str, Any]`` aliases become an empty mapping and dataclasses that + can be built with no arguments become a defaulted instance, so downstream + ``is not None`` type dispatch selects them. Scalar fields — and + dataclasses with required fields, which cannot be defaulted — keep + ``None``, so an absent optional section stays unset. + """ + if origin is dict: + return {} + # A dataclass type annotation has no typing origin (``get_origin`` is + # ``None``); only instantiate when every field is optional. + if ( + origin is None + and isinstance(unwrapped, type) + and is_dataclass(unwrapped) + and all( + field.default is not MISSING + or field.default_factory is not MISSING + for field in fields(unwrapped) + ) + ): + return _dict_to_dataclass({}, unwrapped) + return None + + def _convert_value(value: Any, type_hint: Any) -> Any: """Convert a value according to its type hint. @@ -44,14 +77,7 @@ def _convert_value(value: Any, type_hint: Any) -> Any: origin = get_origin(unwrapped) if value is None: - # A mapping key present with an empty (null) YAML value parses to - # ``None``. For ``dict[str, Any]``-typed nodes the declarative-config - # spec treats a present null as "select this with an empty config" — - # e.g. ``always_on:`` is equivalent to ``always_on: {}``. Coerce it to - # an empty mapping so downstream ``is not None`` type dispatch selects - # it. Scalar and dataclass fields keep ``None`` (an absent optional - # section stays unset). - return {} if origin is dict else None + return _coerce_present_null(unwrapped, origin) # list[X] — recurse on each element if origin is list and isinstance(value, list): diff --git a/opentelemetry-configuration/tests/test_conversion.py b/opentelemetry-configuration/tests/test_conversion.py index 4d09ad36ea5..a1d3cc5d17e 100644 --- a/opentelemetry-configuration/tests/test_conversion.py +++ b/opentelemetry-configuration/tests/test_conversion.py @@ -49,6 +49,18 @@ class _WithMapping: name: str | None = None +@dataclass +class _RequiredField: + # A dataclass that cannot be instantiated with no arguments. + required: int + optional: str | None = None + + +@dataclass +class _WithRequiredHolder: + required_field: _RequiredField | None = None + + class TestDictToDataclass(unittest.TestCase): def test_raises_on_non_dataclass(self): # _dict_to_dataclass is internal and assumes cls is a dataclass. @@ -79,11 +91,23 @@ def test_converts_list_of_dataclasses(self): self.assertEqual(result.middle.items[0].value, 1) self.assertEqual(result.middle.items[1].value, 2) - def test_none_value_preserved(self): + def test_present_null_dataclass_coerced_to_empty_instance(self): + # A present-null value on a dataclass-typed node that can be built with + # no arguments (e.g. a metric ``console:`` exporter) becomes a + # defaulted instance, not None. Regression test for #5451. result = _dict_to_dataclass({"middle": None, "name": "test"}, _Outer) - self.assertIsNone(result.middle) + self.assertIsInstance(result.middle, _Middle) + self.assertIsNone(result.middle.inner) self.assertEqual(result.name, "test") + def test_present_null_dataclass_with_required_field_stays_none(self): + # A dataclass that cannot be instantiated with no arguments is left as + # None rather than raising a TypeError. + result = _dict_to_dataclass( + {"required_field": None}, _WithRequiredHolder + ) + self.assertIsNone(result.required_field) + def test_missing_optional_fields_default_to_none(self): result = _dict_to_dataclass({}, _Outer) self.assertIsNone(result.middle) diff --git a/opentelemetry-configuration/tests/test_meter_provider.py b/opentelemetry-configuration/tests/test_meter_provider.py index 5e1d7e686a6..691df236377 100644 --- a/opentelemetry-configuration/tests/test_meter_provider.py +++ b/opentelemetry-configuration/tests/test_meter_provider.py @@ -9,6 +9,7 @@ import unittest from unittest.mock import MagicMock, patch +from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration._meter_provider import ( configure_meter_provider, create_meter_provider, @@ -186,6 +187,23 @@ def test_console_exporter(self): self.assertIsInstance(reader, PeriodicExportingMetricReader) self.assertIsInstance(reader._exporter, ConsoleMetricExporter) + def test_null_valued_console_exporter_from_parsed_config(self): + # Regression test for #5451: a dataclass-typed exporter written as + # ``console:`` (present, null) must behave like ``console: {}`` rather + # than requiring an explicit empty mapping. + config = _dict_to_dataclass( + { + "readers": [ + {"periodic": {"exporter": {"console": None}}}, + ] + }, + MeterProviderConfig, + ) + provider = create_meter_provider(config) + reader = provider._metric_readers[0] + self.assertIsInstance(reader, PeriodicExportingMetricReader) + self.assertIsInstance(reader._exporter, ConsoleMetricExporter) + def test_periodic_reader_default_interval(self): config = self._make_periodic_config( PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) From c8da5349baf19aa2fdb63df04d4c776602eea3ff Mon Sep 17 00:00:00 2001 From: allen Date: Fri, 24 Jul 2026 09:29:09 +0900 Subject: [PATCH 3/5] Reuse dict/dataclass handling for present-null coercion (#5451) Adopt @emdneto's suggestion to substitute an empty mapping and let the existing dict/dataclass conversion path build it, instead of a separate coercion helper. Keep an "all fields optional" guard (_is_empty_constructible_dataclass): 21 config dataclasses have required fields (e.g. BatchSpanProcessor.exporter, PeriodicMetricReader.exporter), and coercing a present null into those via cls() would raise TypeError. Leaving them None routes to the existing "Unsupported type" ConfigurationError instead. --- .../configuration/_conversion.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py index 3d37f2f5df2..6eeaae49648 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_conversion.py @@ -33,37 +33,22 @@ def _unwrap_optional(type_hint: Any) -> Any: return type_hint -def _coerce_present_null(unwrapped: Any, origin: Any) -> Any: - """Map a present null value to the empty config its type implies. - - A mapping key present with an empty (null) YAML value parses to ``None``, - which is otherwise indistinguishable from an absent key. For object-typed - nodes the declarative-config spec treats a present null as "select this - with an empty config" — e.g. ``always_on:`` is equivalent to - ``always_on: {}`` and a ``console:`` exporter to ``console: {}``. - - ``dict[str, Any]`` aliases become an empty mapping and dataclasses that - can be built with no arguments become a defaulted instance, so downstream - ``is not None`` type dispatch selects them. Scalar fields — and - dataclasses with required fields, which cannot be defaulted — keep - ``None``, so an absent optional section stays unset. +def _is_empty_constructible_dataclass(unwrapped: Any) -> bool: + """True if ``unwrapped`` is a dataclass type instantiable with no args. + + A dataclass with only optional fields (all have a default or + default_factory) can be built as ``cls()``; one with a required field + cannot, and coercing a present null into it would raise ``TypeError``. """ - if origin is dict: - return {} - # A dataclass type annotation has no typing origin (``get_origin`` is - # ``None``); only instantiate when every field is optional. - if ( - origin is None - and isinstance(unwrapped, type) + return ( + isinstance(unwrapped, type) and is_dataclass(unwrapped) and all( field.default is not MISSING or field.default_factory is not MISSING for field in fields(unwrapped) ) - ): - return _dict_to_dataclass({}, unwrapped) - return None + ) def _convert_value(value: Any, type_hint: Any) -> Any: @@ -77,7 +62,20 @@ def _convert_value(value: Any, type_hint: Any) -> Any: origin = get_origin(unwrapped) if value is None: - return _coerce_present_null(unwrapped, origin) + # A mapping key present with an empty (null) YAML value parses to + # ``None``, which is otherwise indistinguishable from an absent key. + # For object-typed nodes the declarative-config spec treats a present + # null as "select this with an empty config" — e.g. ``always_on:`` is + # equivalent to ``always_on: {}`` and a metric ``console:`` exporter to + # ``console: {}``. Substitute an empty mapping and let the dict/ + # dataclass handling below build it, so downstream ``is not None`` type + # dispatch selects it. Scalar fields — and dataclasses with required + # fields, which cannot be defaulted — keep ``None``, so an absent + # optional section stays unset. + if origin is dict or _is_empty_constructible_dataclass(unwrapped): + value = {} + else: + return None # list[X] — recurse on each element if origin is list and isinstance(value, list): From 6c9bb0a3385a094fbbdc243d1f24f0a65141adec Mon Sep 17 00:00:00 2001 From: allen Date: Fri, 24 Jul 2026 10:24:01 +0900 Subject: [PATCH 4/5] Test the guard with the real jaeger_remote_development case (#5451) Replace the synthetic required-field dataclass fixture with the one real node that is schema-nullable yet has required fields: ExperimentalJaegerRemoteSampler (endpoint, initial_sampler). Unlike batch/periodic (schema type: object, so a null is rejected before conversion), jaeger_remote_development is schema type: [object, null], so jaeger_remote_development: passes validation and reaches the conversion layer. Coercing it would raise TypeError; the guard leaves it None. Adds an end-to-end loader test proving the schema accepts the null and conversion survives, alongside a sibling console: node that is still coerced to {}. --- .../tests/file/test_loader.py | 36 +++++++++++++++++++ .../tests/test_conversion.py | 23 ++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/opentelemetry-configuration/tests/file/test_loader.py b/opentelemetry-configuration/tests/file/test_loader.py index ea9689008b5..30d60a5477f 100644 --- a/opentelemetry-configuration/tests/file/test_loader.py +++ b/opentelemetry-configuration/tests/file/test_loader.py @@ -327,6 +327,42 @@ def test_typed_config_feeds_factory_function(self): self.assertIsInstance(processors[0], BatchSpanProcessor) self.assertIsInstance(processors[0].span_exporter, ConsoleSpanExporter) + def test_null_valued_required_field_node_survives_conversion(self): + # Regression test for #5451: ``jaeger_remote_development`` is nullable + # in the schema (so ``jaeger_remote_development:`` passes validation and + # is NOT rejected before conversion), yet its model has required + # fields. Coercing the present null into it would raise TypeError, so + # it must be left as None instead. The rest of the config still loads. + yaml = """ +file_format: '1.0' +tracer_provider: + processors: + - batch: + exporter: + console: + sampler: + jaeger_remote_development: +""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as fh: + fh.write(yaml) + path = fh.name + try: + config = load_config_file(path) + finally: + os.unlink(path) + + # Schema accepted the null, and conversion left the required-field + # node unset rather than crashing. + self.assertIsNone( + config.tracer_provider.sampler.jaeger_remote_development + ) + # A sibling nullable dict-typed node (console:) was still coerced. + self.assertEqual( + config.tracer_provider.processors[0].batch.exporter.console, {} + ) + class TestFileFormatValidation(unittest.TestCase): """Validate the file_format version per the configuration spec.""" diff --git a/opentelemetry-configuration/tests/test_conversion.py b/opentelemetry-configuration/tests/test_conversion.py index a1d3cc5d17e..061df050df0 100644 --- a/opentelemetry-configuration/tests/test_conversion.py +++ b/opentelemetry-configuration/tests/test_conversion.py @@ -11,6 +11,7 @@ from opentelemetry.configuration._common import _additional_properties from opentelemetry.configuration._conversion import _dict_to_dataclass from opentelemetry.configuration.models import ExemplarFilter +from opentelemetry.configuration.models import Sampler as SamplerConfig @dataclass @@ -49,18 +50,6 @@ class _WithMapping: name: str | None = None -@dataclass -class _RequiredField: - # A dataclass that cannot be instantiated with no arguments. - required: int - optional: str | None = None - - -@dataclass -class _WithRequiredHolder: - required_field: _RequiredField | None = None - - class TestDictToDataclass(unittest.TestCase): def test_raises_on_non_dataclass(self): # _dict_to_dataclass is internal and assumes cls is a dataclass. @@ -101,12 +90,14 @@ def test_present_null_dataclass_coerced_to_empty_instance(self): self.assertEqual(result.name, "test") def test_present_null_dataclass_with_required_field_stays_none(self): - # A dataclass that cannot be instantiated with no arguments is left as - # None rather than raising a TypeError. + # ``jaeger_remote_development`` is nullable in the schema but its model + # (ExperimentalJaegerRemoteSampler) has required fields, so it cannot + # be defaulted. A present null must stay None rather than raising a + # TypeError trying to instantiate it. Regression test for #5451. result = _dict_to_dataclass( - {"required_field": None}, _WithRequiredHolder + {"jaeger_remote_development": None}, SamplerConfig ) - self.assertIsNone(result.required_field) + self.assertIsNone(result.jaeger_remote_development) def test_missing_optional_fields_default_to_none(self): result = _dict_to_dataclass({}, _Outer) From c5b5acb7aa18c19cf9074f221463282599b40850 Mon Sep 17 00:00:00 2001 From: allen Date: Fri, 24 Jul 2026 18:25:46 +0900 Subject: [PATCH 5/5] Reuse the _load helper with an optional yaml arg (#5451) Address review: instead of re-implementing the tempfile write/load/unlink dance in the new test, give TestConfigLoaderEndToEnd._load an optional yaml parameter (defaulting to the class _YAML) and pass the jaeger config to it. --- .../tests/file/test_loader.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/opentelemetry-configuration/tests/file/test_loader.py b/opentelemetry-configuration/tests/file/test_loader.py index 30d60a5477f..2d047dcc14a 100644 --- a/opentelemetry-configuration/tests/file/test_loader.py +++ b/opentelemetry-configuration/tests/file/test_loader.py @@ -282,11 +282,11 @@ class TestConfigLoaderEndToEnd(unittest.TestCase): trace_id_ratio_based: {ratio: 0.5} """ - def _load(self) -> OpenTelemetryConfiguration: + def _load(self, yaml: str | None = None) -> OpenTelemetryConfiguration: with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False ) as fh: - fh.write(self._YAML) + fh.write(self._YAML if yaml is None else yaml) path = fh.name try: return load_config_file(path) @@ -333,7 +333,8 @@ def test_null_valued_required_field_node_survives_conversion(self): # is NOT rejected before conversion), yet its model has required # fields. Coercing the present null into it would raise TypeError, so # it must be left as None instead. The rest of the config still loads. - yaml = """ + config = self._load( + """ file_format: '1.0' tracer_provider: processors: @@ -343,15 +344,7 @@ def test_null_valued_required_field_node_survives_conversion(self): sampler: jaeger_remote_development: """ - with tempfile.NamedTemporaryFile( - mode="w", suffix=".yaml", delete=False - ) as fh: - fh.write(yaml) - path = fh.name - try: - config = load_config_file(path) - finally: - os.unlink(path) + ) # Schema accepted the null, and conversion left the required-field # node unset rather than crashing.