Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-opentelemetry-api for 
openSUSE:Factory checked in at 2026-03-30 18:30:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-opentelemetry-api (Old)
 and      /work/SRC/openSUSE:Factory/.python-opentelemetry-api.new.1999 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-opentelemetry-api"

Mon Mar 30 18:30:17 2026 rev:22 rq:1343630 version:1.40.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-opentelemetry-api/python-opentelemetry-api.changes
        2026-01-06 17:41:10.961268384 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-opentelemetry-api.new.1999/python-opentelemetry-api.changes
      2026-03-30 18:31:11.610806423 +0200
@@ -1,0 +2,20 @@
+Mon Mar 23 22:08:22 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.40.0:
+  * bump semantic-conventions to v1.40.0
+  * Add stale PR GitHub Action
+  * Allow loading all resource detectors by setting
+    `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` to `*`
+  * Regenerate opentelemetry-proto code with v1.9.0 release
+  * Add python 3.14 support
+  * Silence events API warnings for internal users
+  * opentelemetry-sdk: make it possible to override the default
+    processors in the SDK configurator
+  * Prevent possible endless recursion from happening in
+    `SimpleLogRecordProcessor.on_emit`, (#4799) and (#4867).
+  * Implement span start/end metrics
+  * Add environment variable carriers to API
+  * Add experimental composable rule based sampler
+  * Make ConcurrentMultiSpanProcessor fork safe
+
+-------------------------------------------------------------------

Old:
----
  opentelemetry_api-1.39.1.tar.gz

New:
----
  opentelemetry_api-1.40.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-opentelemetry-api.spec ++++++
--- /var/tmp/diff_new_pack.PgKAM5/_old  2026-03-30 18:31:12.298835018 +0200
+++ /var/tmp/diff_new_pack.PgKAM5/_new  2026-03-30 18:31:12.302835184 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-opentelemetry-api
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -27,7 +27,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-opentelemetry-api%{?psuffix}
-Version:        1.39.1
+Version:        1.40.0
 Release:        0
 Summary:        OpenTelemetry Python API
 License:        Apache-2.0
@@ -51,7 +51,7 @@
 %endif
 %if %{with test}
 BuildRequires:  %{python_module opentelemetry-api = %{version}}
-BuildRequires:  %{python_module opentelemetry-test-utils = 0.60b0}
+BuildRequires:  %{python_module opentelemetry-test-utils = 0.61b0}
 BuildRequires:  %{python_module pytest}
 %endif
 %python_subpackages

++++++ opentelemetry_api-1.39.1.tar.gz -> opentelemetry_api-1.40.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/PKG-INFO 
new/opentelemetry_api-1.40.0/PKG-INFO
--- old/opentelemetry_api-1.39.1/PKG-INFO       2020-02-02 01:00:00.000000000 
+0100
+++ new/opentelemetry_api-1.40.0/PKG-INFO       2020-02-02 01:00:00.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: opentelemetry-api
-Version: 1.39.1
+Version: 1.40.0
 Summary: OpenTelemetry Python API
 Project-URL: Homepage, 
https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api
 Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python
@@ -17,6 +17,7 @@
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Classifier: Typing :: Typed
 Requires-Python: >=3.9
 Requires-Dist: importlib-metadata<8.8.0,>=6.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/pyproject.toml 
new/opentelemetry_api-1.40.0/pyproject.toml
--- old/opentelemetry_api-1.39.1/pyproject.toml 2020-02-02 01:00:00.000000000 
+0100
+++ new/opentelemetry_api-1.40.0/pyproject.toml 2020-02-02 01:00:00.000000000 
+0100
@@ -22,6 +22,7 @@
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
     "Typing :: Typed",
 ]
 dependencies = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/context/__init__.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/context/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/context/__init__.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/context/__init__.py  
2020-02-02 01:00:00.000000000 +0100
@@ -160,6 +160,7 @@
 # FIXME This is a temporary location for the suppress instrumentation key.
 # Once the decision around how to suppress instrumentation is made in the
 # spec, this key should be moved accordingly.
+_ON_EMIT_RECURSION_COUNT_KEY = create_key("on_emit_recursion_count")
 _SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation")
 _SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key(
     "suppress_http_instrumentation"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/__init__.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/__init__.py
--- 
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/__init__.py    
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/__init__.py    
    2020-02-02 01:00:00.000000000 +0100
@@ -118,7 +118,7 @@
 
         Args:
             name: The name of the instrumenting module.
-                ``__name__`` may not be used as this can result in
+                ``__name__`` should be avoided as this can result in
                 different meter names if the meters are in different files.
                 It is better to use a fixed string that can be imported where
                 needed and used consistently as the name of the meter.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/instrument.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/instrument.py
--- 
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/instrument.py  
    2020-02-02 01:00:00.000000000 +0100
+++ 
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/instrument.py  
    2020-02-02 01:00:00.000000000 +0100
@@ -183,7 +183,14 @@
         attributes: Optional[Attributes] = None,
         context: Optional[Context] = None,
     ) -> None:
-        pass
+        """Records an increment to the counter.
+
+        Args:
+            amount: The amount to increment the counter by. Must be 
non-negative.
+            attributes: Optional set of attributes to associate with the 
measurement.
+            context: Optional context to associate with the measurement. If not
+                provided, the current context is used.
+        """
 
 
 class NoOpCounter(Counter):
@@ -234,7 +241,18 @@
         attributes: Optional[Attributes] = None,
         context: Optional[Context] = None,
     ) -> None:
-        pass
+        """Records an increment or decrement to the counter.
+
+        Unlike `Counter`, the ``amount`` may be negative, allowing the
+        instrument to track values that go up and down (e.g. number of
+        active requests, queue depth).
+
+        Args:
+            amount: The amount to add to the counter. May be positive or 
negative.
+            attributes: Optional set of attributes to associate with the 
measurement.
+            context: Optional context to associate with the measurement. If not
+                provided, the current context is used.
+        """
 
 
 class NoOpUpDownCounter(UpDownCounter):
@@ -376,7 +394,20 @@
         attributes: Optional[Attributes] = None,
         context: Optional[Context] = None,
     ) -> None:
-        pass
+        """Records a measurement.
+
+        Used to report measurements that are likely to be statistically
+        meaningful, such as request durations, payload sizes, or any value
+        for which a distribution (e.g. percentiles) is useful.
+
+        Args:
+            amount: The measurement to record. Should be non-negative in most
+                cases; negative values are only meaningful when the histogram
+                is used to track signed deltas.
+            attributes: Optional set of attributes to associate with the 
measurement.
+            context: Optional context to associate with the measurement. If not
+                provided, the current context is used.
+        """
 
 
 class NoOpHistogram(Histogram):
@@ -486,7 +517,18 @@
         attributes: Optional[Attributes] = None,
         context: Optional[Context] = None,
     ) -> None:
-        pass
+        """Records the current value of the gauge.
+
+        The gauge reports the last recorded value when observed. It is
+        intended for non-additive measurements where only the current
+        value matters (e.g. CPU utilisation percentage, room temperature).
+
+        Args:
+            amount: The current value to record.
+            attributes: Optional set of attributes to associate with the 
measurement.
+            context: Optional context to associate with the measurement. If not
+                provided, the current context is used.
+        """
 
 
 class NoOpGauge(Gauge):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/propagators/_envcarrier.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/propagators/_envcarrier.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/propagators/_envcarrier.py   
1970-01-01 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/propagators/_envcarrier.py   
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,99 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import typing
+from collections.abc import MutableMapping
+
+from opentelemetry.propagators.textmap import Getter, Setter
+
+
+class EnvironmentGetter(Getter[typing.Mapping[str, str]]):
+    """Getter implementation for extracting context and baggage from 
environment variables.
+
+    EnvironmentGetter creates a case-insensitive lookup from the current 
environment
+    variables at initialization time and provides simple data access without 
validation.
+
+    Per the OpenTelemetry specification, environment variables are treated as 
immutable
+    within a process. For environments where context-carrying environment 
variables
+    change between logical requests (e.g., AWS Lambda's _X_AMZN_TRACE_ID), 
create a
+    new EnvironmentGetter instance at the start of each request.
+
+    Example usage:
+        getter = EnvironmentGetter()
+        traceparent = getter.get({}, "traceparent")
+    """
+
+    def __init__(self):
+        # Create case-insensitive lookup from current environment
+        # Per spec: "creates an in-memory copy of the current environment 
variables"
+        self.carrier: typing.Dict[str, str] = {
+            k.lower(): v for k, v in os.environ.items()
+        }
+
+    def get(
+        self, carrier: typing.Mapping[str, str], key: str
+    ) -> typing.Optional[typing.List[str]]:
+        """Get a value from the environment carrier for the given key.
+
+        Args:
+            carrier: Not used; maintained for interface compatibility with 
Getter[CarrierT]
+            key: The key to look up (case-insensitive)
+
+        Returns:
+            A list with a single string value if the key exists, None 
otherwise.
+        """
+        val = self.carrier.get(key.lower())
+        if val is None:
+            return None
+        if isinstance(val, typing.Iterable) and not isinstance(val, str):
+            return list(val)
+        return [val]
+
+    def keys(self, carrier: typing.Mapping[str, str]) -> typing.List[str]:
+        """Get all keys from the environment carrier.
+
+        Args:
+            carrier: Not used; maintained for interface compatibility with 
Getter[CarrierT]
+
+        Returns:
+            List of all environment variable keys (lowercase).
+        """
+        return list(self.carrier.keys())
+
+
+class EnvironmentSetter(Setter[MutableMapping[str, str]]):
+    """Setter implementation for building environment variable dictionaries.
+
+    EnvironmentSetter builds a dictionary of environment variables that
+    can be passed to utilities like subprocess.run()
+
+    Example usage:
+        setter = EnvironmentSetter()
+        env_vars = {}
+        setter.set(env_vars, "traceparent", "00-trace-id-span-id-01")
+        subprocess.run(myCommand, env=env_vars)
+    """
+
+    def set(
+        self, carrier: MutableMapping[str, str], key: str, value: str
+    ) -> None:
+        """Set a value in the carrier dictionary for the given key.
+
+        Args:
+            carrier: Dictionary to store environment variables
+            key: The key to set (will be converted to uppercase)
+            value: The value to set
+        """
+        carrier[key.upper()] = value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/trace/__init__.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/trace/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/trace/__init__.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/trace/__init__.py    
2020-02-02 01:00:00.000000000 +0100
@@ -208,7 +208,7 @@
         Args:
             instrumenting_module_name: The uniquely identifiable name for 
instrumentation
                 scope, such as instrumentation library, package, module or 
class name.
-                ``__name__`` may not be used as this can result in
+                ``__name__`` should be avoided as this can result in
                 different tracer names if the tracers are in different files.
                 It is better to use a fixed string that can be imported where
                 needed and used consistently as the name of the tracer.
@@ -471,7 +471,21 @@
         record_exception: bool = True,
         set_status_on_exception: bool = True,
     ) -> "Span":
-        return INVALID_SPAN
+        current_span = get_current_span(context)
+        if isinstance(current_span, NonRecordingSpan):
+            return current_span
+        parent_span_context = current_span.get_span_context()
+        if parent_span_context is not None and not isinstance(
+            parent_span_context, SpanContext
+        ):
+            logger.warning(
+                "Invalid span context for %s: %s",
+                current_span,
+                parent_span_context,
+            )
+            return INVALID_SPAN
+
+        return NonRecordingSpan(context=parent_span_context)
 
     @_agnosticcontextmanager
     def start_as_current_span(
@@ -486,7 +500,23 @@
         set_status_on_exception: bool = True,
         end_on_exit: bool = True,
     ) -> Iterator["Span"]:
-        yield INVALID_SPAN
+        span = self.start_span(
+            name=name,
+            context=context,
+            kind=kind,
+            attributes=attributes,
+            links=links,
+            start_time=start_time,
+            record_exception=record_exception,
+            set_status_on_exception=set_status_on_exception,
+        )
+        with use_span(
+            span,
+            end_on_exit=end_on_exit,
+            record_exception=record_exception,
+            set_status_on_exception=set_status_on_exception,
+        ) as span:
+            yield span
 
 
 @deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/util/_importlib_metadata.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/util/_importlib_metadata.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/util/_importlib_metadata.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/util/_importlib_metadata.py  
2020-02-02 01:00:00.000000000 +0100
@@ -36,7 +36,7 @@
     return original_entry_points()
 
 
-def entry_points(**params):
+def entry_points(**params) -> EntryPoints:
     """Replacement for importlib_metadata.entry_points that caches getting all 
the entry points.
 
     That part can be very slow, and OTel uses this function many times."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/src/opentelemetry/version/__init__.py 
new/opentelemetry_api-1.40.0/src/opentelemetry/version/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/version/__init__.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/version/__init__.py  
2020-02-02 01:00:00.000000000 +0100
@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "1.39.1"
+__version__ = "1.40.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/tests/propagators/test__envcarrier.py 
new/opentelemetry_api-1.40.0/tests/propagators/test__envcarrier.py
--- old/opentelemetry_api-1.39.1/tests/propagators/test__envcarrier.py  
1970-01-01 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/propagators/test__envcarrier.py  
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,551 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# type: ignore
+
+import os
+import unittest
+from unittest.mock import Mock, patch
+
+from opentelemetry import trace
+from opentelemetry.baggage import get_all, set_baggage
+from opentelemetry.baggage.propagation import W3CBaggagePropagator
+from opentelemetry.context import Context, get_current
+from opentelemetry.propagators._envcarrier import (
+    EnvironmentGetter,
+    EnvironmentSetter,
+)
+from opentelemetry.trace.propagation.tracecontext import (
+    TraceContextTextMapPropagator,
+)
+
+
+class TestEnvironmentGetter(unittest.TestCase):
+    """Unit tests for EnvironmentGetter."""
+
+    def test_get_existing_env_var(self):
+        """Test retrieving an existing environment variable."""
+        with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+            getter = EnvironmentGetter()
+            result = getter.get({}, "test_key")
+            self.assertEqual(result, ["test_value"])
+
+    def test_get_case_insensitive(self):
+        """Test case insensitive lookup for environment variables."""
+        with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+            getter = EnvironmentGetter()
+            self.assertEqual(getter.get({}, "test_key"), ["test_value"])
+            self.assertEqual(getter.get({}, "TEST_KEY"), ["test_value"])
+            self.assertEqual(getter.get({}, "Test_Key"), ["test_value"])
+
+    def test_get_nonexistent_env_var(self):
+        """Test retrieving a non-existent environment variable."""
+        with patch.dict(os.environ, {}, clear=True):
+            getter = EnvironmentGetter()
+            result = getter.get({}, "nonexistent_key")
+            self.assertIsNone(result)
+
+    def test_get_empty_value(self):
+        """Test retrieving an environment variable with empty value."""
+        with patch.dict(os.environ, {"EMPTY_KEY": ""}):
+            getter = EnvironmentGetter()
+            result = getter.get({}, "empty_key")
+            self.assertEqual(result, [""])
+
+    def test_get_with_special_characters(self):
+        """Test environment variables with special characters."""
+        with patch.dict(
+            os.environ, {"TEST_KEY": "value with spaces and !@#$%"}
+        ):
+            getter = EnvironmentGetter()
+            result = getter.get({}, "test_key")
+            self.assertEqual(result, ["value with spaces and !@#$%"])
+
+    def test_keys(self):
+        """Test getting all environment variable keys."""
+        test_env = {"KEY1": "value1", "KEY2": "value2", "key3": "value3"}
+        with patch.dict(os.environ, test_env, clear=True):
+            getter = EnvironmentGetter()
+            keys = getter.keys({})
+            expected_keys = {"key1", "key2", "key3"}
+            self.assertEqual(set(keys), expected_keys)
+
+    def test_keys_empty_environment(self):
+        """Test getting keys when environment is empty."""
+        with patch.dict(os.environ, {}, clear=True):
+            getter = EnvironmentGetter()
+            keys = getter.keys({})
+            self.assertEqual(keys, [])
+
+    def test_uses_snapshot_not_carrier_parameter(self):
+        """Test that getter uses internal snapshot, not carrier parameter.
+
+        The carrier parameter exists for interface compatibility with
+        Getter[CarrierT], but EnvironmentGetter reads from os.environ at
+        initialization, creating an immutable snapshot.
+        """
+        with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+            getter = EnvironmentGetter()
+            # Both return same value from snapshot, carrier is ignored
+            result1 = getter.get({}, "test_key")
+            result2 = getter.get({"test_key": "different"}, "test_key")
+            self.assertEqual(result1, ["test_value"])
+            self.assertEqual(result2, ["test_value"])
+
+    def test_snapshot_immutability(self):
+        """Test that getter snapshot doesn't see changes after 
initialization."""
+        with patch.dict(os.environ, {}, clear=True):
+            getter = EnvironmentGetter()
+            self.assertIsNone(getter.get({}, "test_key"))
+
+            # Add environment variable after initialization
+            os.environ["TEST_KEY"] = "new_value"
+
+            # Getter should still not see the new value
+            self.assertIsNone(getter.get({}, "test_key"))
+
+
+class TestEnvironmentSetter(unittest.TestCase):
+    """Unit tests for EnvironmentSetter."""
+
+    def test_set_basic(self):
+        """Test setting a value in carrier dictionary."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        setter.set(carrier, "test_key", "test_value")
+        self.assertEqual(carrier, {"TEST_KEY": "test_value"})
+
+    def test_set_multiple_values(self):
+        """Test setting multiple values in the same carrier."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        setter.set(carrier, "key1", "value1")
+        setter.set(carrier, "key2", "value2")
+        setter.set(carrier, "key3", "value3")
+        expected = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"}
+        self.assertEqual(carrier, expected)
+
+    def test_set_overwrites_existing(self):
+        """Test that setting a key overwrites existing value."""
+        setter = EnvironmentSetter()
+        carrier = {"TEST_KEY": "old_value"}
+        setter.set(carrier, "test_key", "new_value")
+        self.assertEqual(carrier, {"TEST_KEY": "new_value"})
+
+    def test_set_uppercases_keys(self):
+        """Test that keys are normalized to uppercase."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        setter.set(carrier, "lowercase_key", "value1")
+        setter.set(carrier, "UPPERCASE_KEY", "value2")
+        setter.set(carrier, "MiXeD_cAsE_kEy", "value3")
+        expected = {
+            "LOWERCASE_KEY": "value1",
+            "UPPERCASE_KEY": "value2",
+            "MIXED_CASE_KEY": "value3",
+        }
+        self.assertEqual(carrier, expected)
+
+    def test_set_special_characters_in_value(self):
+        """Test setting values with special characters."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        setter.set(carrier, "test_key", "value with spaces and !@#$%^&*()")
+        self.assertEqual(
+            carrier, {"TEST_KEY": "value with spaces and !@#$%^&*()"}
+        )
+
+    def test_set_empty_value(self):
+        """Test setting an empty value."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        setter.set(carrier, "empty_key", "")
+        self.assertEqual(carrier, {"EMPTY_KEY": ""})
+
+    def test_does_not_modify_os_environ(self):
+        """Test that setter does not modify os.environ."""
+        setter = EnvironmentSetter()
+        carrier = {}
+        original_environ = dict(os.environ)
+        setter.set(carrier, "test_key", "test_value")
+        self.assertEqual(dict(os.environ), original_environ)
+        self.assertEqual(carrier, {"TEST_KEY": "test_value"})
+
+
+class TestEnvironmentCarrierWithTraceContext(unittest.TestCase):
+    """Integration tests with W3C TraceContext propagator."""
+
+    TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736
+    SPAN_ID = 0x00F067AA0BA902B7
+
+    def setUp(self):
+        self.propagator = TraceContextTextMapPropagator()
+
+    def _extract_with_env(self, env_vars):
+        """Helper: Extract context from environment variables."""
+        with patch.dict(os.environ, env_vars, clear=True):
+            getter = EnvironmentGetter()
+            return self.propagator.extract({}, getter=getter)
+
+    def _inject_to_env(self, context):
+        """Helper: Inject context into environment dict."""
+        setter = EnvironmentSetter()
+        env_dict = {}
+        self.propagator.inject(env_dict, context=context, setter=setter)
+        return env_dict
+
+    def test_extract_valid_traceparent(self):
+        """Test extracting valid traceparent from environment."""
+        traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+        ctx = self._extract_with_env({"TRACEPARENT": traceparent})
+
+        span_context = trace.get_current_span(ctx).get_span_context()
+        self.assertEqual(span_context.trace_id, self.TRACE_ID)
+        self.assertEqual(span_context.span_id, self.SPAN_ID)
+        self.assertTrue(span_context.trace_flags.sampled)
+        self.assertTrue(span_context.is_remote)
+
+    def test_extract_traceparent_not_sampled(self):
+        """Test extracting traceparent with sampled=false."""
+        traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-00"
+        ctx = self._extract_with_env({"TRACEPARENT": traceparent})
+
+        span_context = trace.get_current_span(ctx).get_span_context()
+        self.assertFalse(span_context.trace_flags.sampled)
+
+    def test_extract_with_tracestate(self):
+        """Test extracting traceparent with tracestate."""
+        traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+        tracestate = "vendor1=value1,vendor2=value2"
+
+        ctx = self._extract_with_env(
+            {"TRACEPARENT": traceparent, "TRACESTATE": tracestate}
+        )
+
+        span_context = trace.get_current_span(ctx).get_span_context()
+        self.assertEqual(span_context.trace_state.get("vendor1"), "value1")
+        self.assertEqual(span_context.trace_state.get("vendor2"), "value2")
+
+    def test_extract_invalid_traceparent(self):
+        """Test that invalid traceparent formats are handled gracefully.
+
+        Per W3C Trace Context spec, invalid traceparent should be ignored.
+        """
+        invalid_traceparents = [
+            "invalid-format",
+            "00-00000000000000000000000000000000-1234567890123456-00",  # zero 
trace_id
+            "00-12345678901234567890123456789012-0000000000000000-00",  # zero 
span_id
+            "00-12345678901234567890123456789012-1234567890123456-00-extra",  
# extra data
+        ]
+
+        for invalid_tp in invalid_traceparents:
+            with self.subTest(traceparent=invalid_tp):
+                ctx = self._extract_with_env({"TRACEPARENT": invalid_tp})
+                span = trace.get_current_span(ctx)
+                self.assertEqual(
+                    span.get_span_context(), trace.INVALID_SPAN_CONTEXT
+                )
+
+    def test_extract_missing_traceparent(self):
+        """Test extraction with missing TRACEPARENT."""
+        ctx = self._extract_with_env({})
+        span = trace.get_current_span(ctx)
+        self.assertIsInstance(span.get_span_context(), trace.SpanContext)
+
+    def test_extract_preserves_context_on_invalid_traceparent(self):
+        """Test that invalid traceparent preserves original context."""
+        orig_ctx = Context({"my_key": "my_value"})
+
+        with patch.dict(os.environ, {"TRACEPARENT": "invalid"}, clear=True):
+            getter = EnvironmentGetter()
+            ctx = self.propagator.extract({}, context=orig_ctx, getter=getter)
+
+        self.assertDictEqual(ctx, orig_ctx)
+
+    def test_inject_valid_span_context(self):
+        """Test injecting valid span context to environment dict."""
+        span_context = trace.SpanContext(
+            trace_id=self.TRACE_ID,
+            span_id=self.SPAN_ID,
+            is_remote=False,
+            trace_flags=trace.TraceFlags(0x01),
+        )
+        ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+
+        env_dict = self._inject_to_env(ctx)
+
+        expected_traceparent = (
+            f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+        )
+        self.assertEqual(env_dict["TRACEPARENT"], expected_traceparent)
+
+    def test_inject_does_not_include_empty_tracestate(self):
+        """Test that empty tracestate is not injected.
+
+        Per W3C spec: vendors SHOULD avoid sending empty tracestate.
+        """
+        span_context = trace.SpanContext(
+            trace_id=self.TRACE_ID,
+            span_id=self.SPAN_ID,
+            is_remote=False,
+        )
+        ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+
+        env_dict = self._inject_to_env(ctx)
+
+        self.assertIn("TRACEPARENT", env_dict)
+        self.assertNotIn("TRACESTATE", env_dict)
+
+    def test_inject_invalid_context(self):
+        """Test that invalid context is not propagated."""
+        ctx = trace.set_span_in_context(trace.INVALID_SPAN)
+        env_dict = self._inject_to_env(ctx)
+        self.assertNotIn("TRACEPARENT", env_dict)
+
+    def test_roundtrip_preserves_traceparent(self):
+        """Test that traceparent survives extract->inject->extract cycle."""
+        original_traceparent = (
+            f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+        )
+
+        # Extract from environment
+        ctx1 = self._extract_with_env({"TRACEPARENT": original_traceparent})
+
+        # Inject to new environment dict
+        env_dict = self._inject_to_env(ctx1)
+        self.assertEqual(env_dict["TRACEPARENT"], original_traceparent)
+
+        # Extract again from injected dict
+        ctx2 = self._extract_with_env(env_dict)
+
+        # Verify both contexts have same span context
+        span1 = trace.get_current_span(ctx1).get_span_context()
+        span2 = trace.get_current_span(ctx2).get_span_context()
+        self.assertEqual(span1.trace_id, span2.trace_id)
+        self.assertEqual(span1.span_id, span2.span_id)
+        self.assertEqual(span1.trace_flags, span2.trace_flags)
+
+    def test_roundtrip_preserves_tracestate(self):
+        """Test that tracestate survives roundtrip."""
+        env_vars = {
+            "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+            "TRACESTATE": "vendor1=value1,vendor2=value2",
+        }
+
+        ctx1 = self._extract_with_env(env_vars)
+        env_dict = self._inject_to_env(ctx1)
+
+        self.assertIn("TRACESTATE", env_dict)
+        self.assertIn("vendor1=value1", env_dict["TRACESTATE"])
+        self.assertIn("vendor2=value2", env_dict["TRACESTATE"])
+
+    def test_case_handling(self):
+        """Test case handling for W3C headers."""
+        test_env = {
+            "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+            "TRACESTATE": "vendor=value",
+        }
+
+        with patch.dict(os.environ, test_env, clear=True):
+            getter = EnvironmentGetter()
+            # Propagator uses lowercase keys
+            self.assertIsNotNone(getter.get({}, "traceparent"))
+            self.assertIsNotNone(getter.get({}, "tracestate"))
+
+    @patch("opentelemetry.trace.INVALID_SPAN_CONTEXT")
+    @patch("opentelemetry.trace.get_current_span")
+    def test_fields(self, mock_get_current_span, mock_invalid_span_context):
+        """Test that propagator.fields matches injected keys."""
+        # pylint: disable=import-outside-toplevel
+        from opentelemetry.trace.span import TraceState  # noqa: PLC0415
+
+        mock_get_current_span.configure_mock(
+            return_value=Mock(
+                **{
+                    "get_span_context.return_value": Mock(
+                        **{
+                            "trace_id": 1,
+                            "span_id": 2,
+                            "trace_flags": 3,
+                            "trace_state": TraceState([("a", "b")]),
+                        }
+                    )
+                }
+            )
+        )
+
+        mock_setter = Mock()
+        self.propagator.inject({}, setter=mock_setter)
+
+        inject_fields = set()
+        for mock_call in mock_setter.mock_calls:
+            inject_fields.add(mock_call[1][1])
+
+        self.assertEqual(inject_fields, self.propagator.fields)
+
+
+class TestEnvironmentCarrierWithBaggage(unittest.TestCase):
+    """Integration tests with W3C Baggage propagator."""
+
+    def setUp(self):
+        self.propagator = W3CBaggagePropagator()
+
+    def _extract_with_env(self, env_vars):
+        """Helper: Extract baggage from environment variables."""
+        with patch.dict(os.environ, env_vars, clear=True):
+            getter = EnvironmentGetter()
+            return self.propagator.extract({}, getter=getter)
+
+    def _inject_to_env(self, context):
+        """Helper: Inject baggage into environment dict."""
+        setter = EnvironmentSetter()
+        env_dict = {}
+        self.propagator.inject(env_dict, context=context, setter=setter)
+        return env_dict
+
+    def test_extract_baggage(self):
+        """Test extracting baggage from BAGGAGE environment variable."""
+        ctx = self._extract_with_env(
+            {"BAGGAGE": "key1=value1,key2=value2,key3=value3"}
+        )
+
+        baggage = get_all(ctx)
+        self.assertEqual(
+            baggage, {"key1": "value1", "key2": "value2", "key3": "value3"}
+        )
+
+    def test_extract_empty_baggage(self):
+        """Test extracting empty baggage."""
+        ctx = self._extract_with_env({"BAGGAGE": ""})
+        baggage = get_all(ctx)
+        self.assertEqual(baggage, {})
+
+    def test_extract_missing_baggage(self):
+        """Test extraction with missing BAGGAGE."""
+        ctx = self._extract_with_env({})
+        baggage = get_all(ctx)
+        self.assertEqual(baggage, {})
+
+    def test_inject_baggage(self):
+        """Test injecting baggage into environment dict."""
+        ctx = get_current()
+        ctx = set_baggage("deployment", "production", context=ctx)
+        ctx = set_baggage("service", "api-gateway", context=ctx)
+
+        env_dict = self._inject_to_env(ctx)
+
+        self.assertIn("BAGGAGE", env_dict)
+        baggage_value = env_dict["BAGGAGE"]
+        self.assertIn("deployment=production", baggage_value)
+        self.assertIn("service=api-gateway", baggage_value)
+
+    def test_inject_no_baggage(self):
+        """Test that empty baggage is not injected."""
+        ctx = get_current()
+        env_dict = self._inject_to_env(ctx)
+        self.assertNotIn("BAGGAGE", env_dict)
+
+    def test_roundtrip_baggage(self):
+        """Test baggage roundtrip."""
+        ctx1 = get_current()
+        ctx1 = set_baggage("user_id", "12345", context=ctx1)
+        ctx1 = set_baggage("session_id", "abc-def", context=ctx1)
+
+        env_dict = self._inject_to_env(ctx1)
+        ctx2 = self._extract_with_env(env_dict)
+
+        baggage1 = get_all(ctx1)
+        baggage2 = get_all(ctx2)
+        self.assertEqual(baggage1, baggage2)
+
+    @patch("opentelemetry.baggage.propagation.get_all")
+    @patch("opentelemetry.baggage.propagation._format_baggage")
+    def test_fields(self, mock_format_baggage, mock_get_all):
+        """Test that propagator.fields matches injected keys."""
+        mock_setter = Mock()
+        self.propagator.inject({}, setter=mock_setter)
+
+        inject_fields = set()
+        for mock_call in mock_setter.mock_calls:
+            inject_fields.add(mock_call[1][1])
+
+        self.assertEqual(inject_fields, self.propagator.fields)
+
+
+class TestEnvironmentCarrierWithCompositePropagator(unittest.TestCase):
+    """Integration tests with multiple propagators."""
+
+    TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736
+    SPAN_ID = 0x00F067AA0BA902B7
+
+    def setUp(self):
+        # pylint: disable=import-outside-toplevel
+        from opentelemetry.propagators.composite import (  # noqa: PLC0415
+            CompositePropagator,
+        )
+
+        self.propagator = CompositePropagator(
+            [TraceContextTextMapPropagator(), W3CBaggagePropagator()]
+        )
+
+    def test_extract_all_w3c_headers(self):
+        """Test extracting both traceparent and baggage."""
+        env_vars = {
+            "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+            "TRACESTATE": "vendor=value",
+            "BAGGAGE": "user_id=12345,session_id=abc-def",
+        }
+
+        with patch.dict(os.environ, env_vars, clear=True):
+            getter = EnvironmentGetter()
+            ctx = self.propagator.extract({}, getter=getter)
+
+        # Verify traceparent was extracted
+        span_context = trace.get_current_span(ctx).get_span_context()
+        self.assertEqual(span_context.trace_id, self.TRACE_ID)
+        self.assertEqual(span_context.span_id, self.SPAN_ID)
+
+        # Verify baggage was extracted
+        baggage = get_all(ctx)
+        self.assertEqual(baggage["user_id"], "12345")
+        self.assertEqual(baggage["session_id"], "abc-def")
+
+    def test_inject_all_w3c_headers(self):
+        """Test injecting both traceparent and baggage."""
+        span_context = trace.SpanContext(
+            trace_id=self.TRACE_ID,
+            span_id=self.SPAN_ID,
+            is_remote=False,
+            trace_flags=trace.TraceFlags(0x01),
+        )
+        ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+        ctx = set_baggage("deployment", "production", context=ctx)
+
+        setter = EnvironmentSetter()
+        env_dict = {}
+        self.propagator.inject(env_dict, context=ctx, setter=setter)
+
+        # Verify both were injected with uppercase keys
+        self.assertIn("TRACEPARENT", env_dict)
+        self.assertIn("BAGGAGE", env_dict)
+        self.assertIn("deployment=production", env_dict["BAGGAGE"])
+
+    def test_empty_environment(self):
+        """Test behavior with completely empty environment."""
+        with patch.dict(os.environ, {}, clear=True):
+            getter = EnvironmentGetter()
+            ctx = self.propagator.extract({}, getter=getter)
+
+            # Should not crash, return valid context
+            self.assertIsInstance(ctx, Context)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/opentelemetry_api-1.39.1/tests/test_implementation.py 
new/opentelemetry_api-1.40.0/tests/test_implementation.py
--- old/opentelemetry_api-1.39.1/tests/test_implementation.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/test_implementation.py   2020-02-02 
01:00:00.000000000 +0100
@@ -17,6 +17,63 @@
 from opentelemetry import trace
 
 
+class RecordingSpan(trace.Span):
+    def __init__(self, context: trace.SpanContext) -> None:
+        self._context = context
+
+    def get_span_context(self) -> trace.SpanContext:
+        return self._context
+
+    def is_recording(self) -> bool:
+        return True
+
+    def end(self, end_time=None) -> None:
+        pass
+
+    def set_attributes(self, attributes) -> None:
+        pass
+
+    def set_attribute(self, key, value) -> None:
+        pass
+
+    def add_event(
+        self,
+        name: str,
+        attributes=None,
+        timestamp=None,
+    ) -> None:
+        pass
+
+    def add_link(
+        self,
+        context,
+        attributes=None,
+    ) -> None:
+        pass
+
+    def update_name(self, name) -> None:
+        pass
+
+    def set_status(
+        self,
+        status,
+        description=None,
+    ) -> None:
+        pass
+
+    def record_exception(
+        self,
+        exception,
+        attributes=None,
+        timestamp=None,
+        escaped=False,
+    ) -> None:
+        pass
+
+    def __repr__(self) -> str:
+        return f"RecordingSpan({self._context!r})"
+
+
 class TestAPIOnlyImplementation(unittest.TestCase):
     """
     This test is in place to ensure the API is returning values that
@@ -36,18 +93,45 @@
         tracer_provider = trace.NoOpTracerProvider()
         tracer = tracer_provider.get_tracer(__name__)
         with tracer.start_span("test") as span:
-            self.assertEqual(
-                span.get_span_context(), trace.INVALID_SPAN_CONTEXT
-            )
-            self.assertEqual(span, trace.INVALID_SPAN)
+            self.assertFalse(span.get_span_context().is_valid)
             self.assertIs(span.is_recording(), False)
             with tracer.start_span("test2") as span2:
-                self.assertEqual(
-                    span2.get_span_context(), trace.INVALID_SPAN_CONTEXT
-                )
-                self.assertEqual(span2, trace.INVALID_SPAN)
+                self.assertFalse(span2.get_span_context().is_valid)
                 self.assertIs(span2.is_recording(), False)
 
+    def test_default_tracer_context_propagation_recording_span(self):
+        tracer_provider = trace.NoOpTracerProvider()
+        tracer = tracer_provider.get_tracer(__name__)
+        span_context = trace.SpanContext(
+            2604504634922341076776623263868986797,
+            5213367945872657620,
+            False,
+            trace.TraceFlags(0x01),
+        )
+        ctx = trace.set_span_in_context(RecordingSpan(context=span_context))
+        with tracer.start_span("test", context=ctx) as span:
+            self.assertTrue(span.get_span_context().is_valid)
+            self.assertEqual(span.get_span_context(), span_context)
+            self.assertIs(span.is_recording(), False)
+
+    def test_default_tracer_context_propagation_non_recording_span(self):
+        tracer_provider = trace.NoOpTracerProvider()
+        tracer = tracer_provider.get_tracer(__name__)
+        ctx = trace.set_span_in_context(trace.INVALID_SPAN)
+        with tracer.start_span("test", context=ctx) as span:
+            self.assertFalse(span.get_span_context().is_valid)
+            self.assertIs(span, trace.INVALID_SPAN)
+
+    def test_default_tracer_context_propagation_with_invalid_context(self):
+        tracer_provider = trace.NoOpTracerProvider()
+        tracer = tracer_provider.get_tracer(__name__)
+        ctx = trace.set_span_in_context(
+            RecordingSpan(context="invalid_context")  # type: 
ignore[reportArgumentType]
+        )
+        with tracer.start_span("test", context=ctx) as span:
+            self.assertFalse(span.get_span_context().is_valid)
+            self.assertIs(span, trace.INVALID_SPAN)
+
     def test_span(self):
         with self.assertRaises(TypeError):
             # pylint: disable=abstract-class-instantiated
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/tests/trace/test_proxy.py 
new/opentelemetry_api-1.40.0/tests/trace/test_proxy.py
--- old/opentelemetry_api-1.39.1/tests/trace/test_proxy.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/trace/test_proxy.py      2020-02-02 
01:00:00.000000000 +0100
@@ -96,8 +96,8 @@
             return trace.get_current_span()
 
         # call function before configuring tracing provider, should
-        # return INVALID_SPAN from the NoOpTracer
-        self.assertEqual(my_function(), trace.INVALID_SPAN)
+        # return NonRecordingSpan from the NoOpTracer
+        self.assertFalse(my_function().is_recording())
 
         # configure tracing provider
         trace.set_tracer_provider(TestProvider())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/tests/trace/test_tracer.py 
new/opentelemetry_api-1.40.0/tests/trace/test_tracer.py
--- old/opentelemetry_api-1.39.1/tests/trace/test_tracer.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/trace/test_tracer.py     2020-02-02 
01:00:00.000000000 +0100
@@ -79,5 +79,5 @@
     def test_get_current_span(self):
         with self.tracer.start_as_current_span("test") as span:
             get_current_span().set_attribute("test", "test")
-            self.assertEqual(span, INVALID_SPAN)
+            self.assertFalse(span.is_recording())
             self.assertFalse(hasattr("span", "attributes"))

Reply via email to