From 7c15147d6b243b768fc98cd17970b9b218ca64ea Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 17 Jun 2026 22:17:52 -0400 Subject: [PATCH 1/5] opentelemetry-exporter-http-transport: enable entry-point loading of transport implementations --- .../pyproject.toml | 8 ++- .../exporter/http/transport/__init__.py | 66 +++++++++++++++++++ .../exporter/http/transport/_base.py | 26 +++++++- .../exporter/http/transport/_requests.py | 11 ++++ .../exporter/http/transport/_urllib3.py | 11 ++++ .../test-requirements.in | 1 + .../test-requirements.latest.txt | 5 ++ .../test-requirements.oldest.txt | 5 ++ .../tests/test_load_transport.py | 51 ++++++++++++++ .../tests/test_requests_transport.py | 43 ++++++++++++ .../tests/test_urllib3_transport.py | 42 ++++++++++++ uv.lock | 4 ++ 12 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py diff --git a/exporter/opentelemetry-exporter-http-transport/pyproject.toml b/exporter/opentelemetry-exporter-http-transport/pyproject.toml index e2ecb426cbb..40ca4774a50 100644 --- a/exporter/opentelemetry-exporter-http-transport/pyproject.toml +++ b/exporter/opentelemetry-exporter-http-transport/pyproject.toml @@ -25,7 +25,9 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -dependencies = [] +dependencies = [ + "opentelemetry-api ~= 1.15", +] [project.optional-dependencies] urllib3 = [ @@ -39,6 +41,10 @@ requests = [ Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-http-transport" Repository = "https://github.com/open-telemetry/opentelemetry-python" +[project.entry-points.opentelemetry_http_transport] +urllib3 = "opentelemetry.exporter.http.transport._urllib3:Urllib3HTTPTransport" +requests = "opentelemetry.exporter.http.transport._requests:RequestsHTTPTransport" + [tool.hatch.version] path = "src/opentelemetry/exporter/http/transport/version/__init__.py" diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py index e57cf4aba95..6231d9a8b23 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py @@ -1,2 +1,68 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + + from opentelemetry.exporter.http.transport._base import BaseHTTPTransport + + +def _load_requests_transport() -> type[BaseHTTPTransport]: + # pylint: disable-next=import-outside-toplevel,import-error + from opentelemetry.exporter.http.transport._requests import ( # noqa: PLC0415 + RequestsHTTPTransport, + ) + + return RequestsHTTPTransport + + +def _load_urllib3_transport() -> type[BaseHTTPTransport]: + # pylint: disable-next=import-outside-toplevel,import-error + from opentelemetry.exporter.http.transport._urllib3 import ( # noqa: PLC0415 + Urllib3HTTPTransport, + ) + + return Urllib3HTTPTransport + + +_KNOWN_TRANSPORTS: dict[str, Callable[[], type[BaseHTTPTransport]]] = { + "requests": _load_requests_transport, + "urllib3": _load_urllib3_transport, +} + + +def _load_http_transport_class(name: str) -> type[BaseHTTPTransport]: + """Return the transport class registered under *name*. + + Checks the built-in transport registry first to avoid the overhead of an + entry-point scan for built-in transports. Falls back to standard entry-point + discovery for user supplied transports registered under the + ``opentelemetry_http_transport`` group. + + :param name: Entry point name, e.g. ``"requests"`` or ``"urllib3"``. + :returns: The :class:`~opentelemetry.exporter.http.transport._base.BaseHTTPTransport` + subclass registered under *name*. + :raises ValueError: If no transport is registered under *name*. + """ + if name in _KNOWN_TRANSPORTS: + return _KNOWN_TRANSPORTS[name]() + # pylint: disable-next=import-outside-toplevel,import-error + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, + ) + + ep = next( + iter(entry_points(group="opentelemetry_http_transport", name=name)), + None, + ) + if not ep: + raise ValueError( + f"No HTTP transport registered under name {name!r}. " + "Install the corresponding extra or register an entry point " + "under the 'opentelemetry_http_transport' group." + ) + return ep.load() diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py index 20602ac3d4e..fab96bf6648 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from dataclasses import dataclass -from typing import Any +from typing import Any, Self @dataclass(frozen=True, slots=True) @@ -57,6 +57,30 @@ def json(self) -> Any: class BaseHTTPTransport(ABC): """Abstract HTTP transport interface used by HTTP exporters.""" + @classmethod + @abstractmethod + def create( + cls, + verify: bool | str, + cert: str | tuple[str, str] | None, + **kwargs: Any, + ) -> Self: + """Create a new transport instance. + + :param verify: Controls TLS certificate verification. ``True`` verifies + the server certificate against the system CA bundle, ``False`` + disables verification, a string is interpreted as a path to a CA + bundle file to use instead of the system bundle. + :param cert: Client certificate for mutual TLS. Pass a string path to a + PEM-encoded certificate file (the file must contain both the + certificate and the key), or a ``(cert_file, key_file)`` tuple when + the private key is in a separate file. Pass ``None`` to disable + client side authentication. + :param kwargs: Additional keyword arguments forwarded to the transport + implementation. + :returns: A new transport instance. + """ + @abstractmethod def request( self, diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py index 637fd93b511..a2ec5ee8ff6 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py @@ -15,6 +15,8 @@ ) if TYPE_CHECKING: + from typing import Self + import requests from requests import Response @@ -60,6 +62,15 @@ def headers(self) -> Mapping[str, str]: class RequestsHTTPTransport(BaseHTTPTransport): + @classmethod + def create( + cls, + verify: bool | str, + cert: str | tuple[str, str] | None, + **kwargs: Any, + ) -> Self: + return cls(verify=verify, cert=cert, **kwargs) + def __init__( self, *, diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py index 57a68bd9677..4fed0114b9f 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py @@ -16,6 +16,8 @@ ) if TYPE_CHECKING: + from typing import Self + from urllib3 import BaseHTTPResponse @@ -65,6 +67,15 @@ def json(self) -> Any: class Urllib3HTTPTransport(BaseHTTPTransport): + @classmethod + def create( + cls, + verify: bool | str, + cert: str | tuple[str, str] | None, + **kwargs: Any, + ) -> Self: + return cls(verify=verify, cert=cert, **kwargs) + def __init__( self, *, diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.in b/exporter/opentelemetry-exporter-http-transport/test-requirements.in index e8b1ac224dd..c9de583437a 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.in +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.in @@ -3,4 +3,5 @@ mocket==3.14.1 packaging==26.2 pluggy==1.6.0 pytest==7.4.4 +-e opentelemetry-api -e exporter/opentelemetry-exporter-http-transport[urllib3,requests] diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt b/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt index b1ec00aea69..c2d364fb3dd 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt @@ -2,6 +2,10 @@ # uv pip compile --python 3.10 --universal --resolution highest exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in +-e opentelemetry-api + # via + # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in + # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests charset-normalizer==3.4.7 @@ -44,6 +48,7 @@ typing-extensions==4.15.0 # via # exceptiongroup # mocket + # opentelemetry-api urllib3==2.7.0 # via # mocket diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt b/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt index c9b33dc117a..63a0f998c88 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt @@ -2,6 +2,10 @@ # uv pip compile --python 3.10 --universal --resolution lowest-direct exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in +-e opentelemetry-api + # via + # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in + # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests chardet==3.0.4 @@ -44,6 +48,7 @@ typing-extensions==4.15.0 # via # exceptiongroup # mocket + # opentelemetry-api urllib3==1.26.20 # via # mocket diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py new file mode 100644 index 00000000000..2c903691506 --- /dev/null +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py @@ -0,0 +1,51 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# pylint: disable=import-error + +import unittest +from unittest.mock import MagicMock, patch + +from opentelemetry.exporter.http.transport import _load_http_transport_class + +from opentelemetry.exporter.http.transport._requests import ( + RequestsHTTPTransport, +) + +from opentelemetry.exporter.http.transport._urllib3 import ( + Urllib3HTTPTransport, +) + +_ENTRY_POINTS_TARGET = "opentelemetry.util._importlib_metadata.entry_points" + + +# pylint: disable=no-self-use +class TestLoadHTTPTransportClass(unittest.TestCase): + def test_returns_requests_transport(self): + self.assertIs(_load_http_transport_class("requests"), RequestsHTTPTransport) + + def test_returns_urllib3_transport(self): + self.assertIs(_load_http_transport_class("urllib3"), Urllib3HTTPTransport) + + def test_known_transport_does_not_call_entry_points(self): + with patch(_ENTRY_POINTS_TARGET) as mock_ep: + _load_http_transport_class("requests") + _load_http_transport_class("urllib3") + self.assertFalse(mock_ep.called) + + def test_unknown_transport_calls_entry_points(self): + class _CustomTransport: + pass + + mock_ep = MagicMock() + mock_ep.load.return_value = _CustomTransport + with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn: + result = _load_http_transport_class("custom") + self.assertEqual( + mock_fn.call_args.kwargs, + {"group": "opentelemetry_http_transport", "name": "custom"}, + ) + self.assertIs(result, _CustomTransport) + + def test_unknown_transport_raises_value_error(self): + with patch(_ENTRY_POINTS_TARGET, return_value=[]): + self.assertRaises(ValueError, _load_http_transport_class, "nonexistent") diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_requests_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_requests_transport.py index 3d0b11ecc70..33a6115e9c2 100644 --- a/exporter/opentelemetry-exporter-http-transport/tests/test_requests_transport.py +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_requests_transport.py @@ -261,3 +261,46 @@ def test_close_closes_session(self): transport = RequestsHTTPTransport(session=mock_session) transport.close() mock_session.close.assert_called_once() + + def test_create_returns_transport_instance(self): + mock_session = MagicMock(spec=requests.Session) + transport = RequestsHTTPTransport.create( + True, None, session=mock_session + ) + self.assertIsInstance(transport, RequestsHTTPTransport) + + def test_create_forwards_verify(self): + cases = [ + (True, True), + (False, False), + ("/path/to/ca.pem", "/path/to/ca.pem"), + ] + for verify, expected in cases: + with self.subTest(verify=verify): + mock_session = MagicMock(spec=requests.Session) + RequestsHTTPTransport.create( + verify, None, session=mock_session + ) + self.assertEqual(mock_session.verify, expected) + + def test_create_forwards_cert(self): + cases = [ + "/path/to/cert.pem", + ("/path/to/cert.pem", "/path/to/key.pem"), + ] + for cert in cases: + with self.subTest(cert=cert): + mock_session = MagicMock(spec=requests.Session) + RequestsHTTPTransport.create(True, cert, session=mock_session) + self.assertEqual(mock_session.cert, cert) + + def test_create_forwards_session_kwarg(self): + mock_session = MagicMock(spec=requests.Session) + mock_session.request.return_value = MagicMock( + status_code=200, reason="OK" + ) + transport = RequestsHTTPTransport.create( + True, None, session=mock_session + ) + transport.request("POST", _TEST_URL) + mock_session.request.assert_called_once() diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_urllib3_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_urllib3_transport.py index 03e4171ea75..18cd23afa2e 100644 --- a/exporter/opentelemetry-exporter-http-transport/tests/test_urllib3_transport.py +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_urllib3_transport.py @@ -303,3 +303,45 @@ def test_close_clears_pool(self): transport = Urllib3HTTPTransport() transport.close() mock_pm.return_value.clear.assert_called_once() + + def test_create_returns_transport_instance(self): + with patch("urllib3.PoolManager"): + transport = Urllib3HTTPTransport.create(True, None) + self.assertIsInstance(transport, Urllib3HTTPTransport) + + def test_create_forwards_verify(self): + cases = [ + (True, "CERT_REQUIRED", None), + (False, "CERT_NONE", None), + ("/path/to/ca.pem", "CERT_REQUIRED", "/path/to/ca.pem"), + ] + for verify, expected_cert_reqs, expected_ca_certs in cases: + with self.subTest(verify=verify): + with patch("urllib3.PoolManager") as mock_pm: + Urllib3HTTPTransport.create(verify, None) + kwargs = mock_pm.call_args.kwargs + self.assertEqual(kwargs["cert_reqs"], expected_cert_reqs) + if expected_ca_certs is not None: + self.assertEqual(kwargs["ca_certs"], expected_ca_certs) + else: + self.assertNotIn("ca_certs", kwargs) + + def test_create_forwards_cert(self): + cases = [ + ("/path/to/cert.pem", "/path/to/cert.pem", None), + ( + ("/path/to/cert.pem", "/path/to/key.pem"), + "/path/to/cert.pem", + "/path/to/key.pem", + ), + ] + for cert, expected_cert_file, expected_key_file in cases: + with self.subTest(cert=cert): + with patch("urllib3.PoolManager") as mock_pm: + Urllib3HTTPTransport.create(True, cert) + kwargs = mock_pm.call_args.kwargs + self.assertEqual(kwargs["cert_file"], expected_cert_file) + if expected_key_file is not None: + self.assertEqual(kwargs["key_file"], expected_key_file) + else: + self.assertNotIn("key_file", kwargs) diff --git a/uv.lock b/uv.lock index e307a066394..ec806543d86 100644 --- a/uv.lock +++ b/uv.lock @@ -845,6 +845,9 @@ wheels = [ [[package]] name = "opentelemetry-exporter-http-transport" source = { editable = "exporter/opentelemetry-exporter-http-transport" } +dependencies = [ + { name = "opentelemetry-api" }, +] [package.optional-dependencies] requests = [ @@ -856,6 +859,7 @@ urllib3 = [ [package.metadata] requires-dist = [ + { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "requests", marker = "extra == 'requests'", specifier = "~=2.25" }, { name = "urllib3", marker = "extra == 'urllib3'", specifier = ">=1.26" }, ] From 71f6d7b65f7e04d38ecc09fbbd223c21bdd37105 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 17 Jun 2026 22:24:00 -0400 Subject: [PATCH 2/5] add changelog fragment --- .changelog/5320.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5320.added diff --git a/.changelog/5320.added b/.changelog/5320.added new file mode 100644 index 00000000000..ab719308295 --- /dev/null +++ b/.changelog/5320.added @@ -0,0 +1 @@ +`opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations From 7e1051e6bc8be6b11a9e26d485be602e81cd45aa Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 17 Jun 2026 22:27:06 -0400 Subject: [PATCH 3/5] fix Python 3.10 typing import error --- .../opentelemetry/exporter/http/transport/_base.py | 9 +++++++-- .../tests/test_load_transport.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py index fab96bf6648..6887b68891b 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py @@ -1,11 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import json from abc import ABC, abstractmethod -from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Self +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any, Self @dataclass(frozen=True, slots=True) diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py index 2c903691506..a61d04a211c 100644 --- a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py @@ -6,11 +6,9 @@ from unittest.mock import MagicMock, patch from opentelemetry.exporter.http.transport import _load_http_transport_class - from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) - from opentelemetry.exporter.http.transport._urllib3 import ( Urllib3HTTPTransport, ) @@ -21,10 +19,14 @@ # pylint: disable=no-self-use class TestLoadHTTPTransportClass(unittest.TestCase): def test_returns_requests_transport(self): - self.assertIs(_load_http_transport_class("requests"), RequestsHTTPTransport) + self.assertIs( + _load_http_transport_class("requests"), RequestsHTTPTransport + ) def test_returns_urllib3_transport(self): - self.assertIs(_load_http_transport_class("urllib3"), Urllib3HTTPTransport) + self.assertIs( + _load_http_transport_class("urllib3"), Urllib3HTTPTransport + ) def test_known_transport_does_not_call_entry_points(self): with patch(_ENTRY_POINTS_TARGET) as mock_ep: @@ -48,4 +50,6 @@ class _CustomTransport: def test_unknown_transport_raises_value_error(self): with patch(_ENTRY_POINTS_TARGET, return_value=[]): - self.assertRaises(ValueError, _load_http_transport_class, "nonexistent") + self.assertRaises( + ValueError, _load_http_transport_class, "nonexistent" + ) From e581987fbc9303713fcac65a9ce7bb3b12cc0a4f Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 17 Jun 2026 22:31:47 -0400 Subject: [PATCH 4/5] fix typecheck errors --- .../src/opentelemetry/exporter/http/transport/_base.py | 4 +++- .../src/opentelemetry/exporter/http/transport/_requests.py | 7 ++++--- .../src/opentelemetry/exporter/http/transport/_urllib3.py | 7 ++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py index 6887b68891b..1951cb720d8 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py @@ -10,7 +10,9 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Any, Self + from typing import Any + + from typing_extensions import Self @dataclass(frozen=True, slots=True) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py index a2ec5ee8ff6..e63ee5e2e4b 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py @@ -4,9 +4,8 @@ from __future__ import annotations import functools -from collections.abc import Mapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( @@ -15,10 +14,12 @@ ) if TYPE_CHECKING: - from typing import Self + from collections.abc import Mapping + from typing import Any import requests from requests import Response + from typing_extensions import Self @functools.cache diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py index 4fed0114b9f..633092773c6 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py @@ -5,9 +5,8 @@ import functools import json -from collections.abc import Mapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( @@ -16,8 +15,10 @@ ) if TYPE_CHECKING: - from typing import Self + from collections.abc import Mapping + from typing import Any + from typing_extensions import Self from urllib3 import BaseHTTPResponse From dc758873c128de9488277fb08fb77933b34cd27b Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 18 Jun 2026 21:21:31 -0400 Subject: [PATCH 5/5] check that returned class is a subclass of BaseHTTPTransport --- .../exporter/http/transport/__init__.py | 13 ++++++++++++- .../exporter/http/transport/_requests.py | 3 ++- .../exporter/http/transport/_urllib3.py | 3 ++- .../tests/test_load_transport.py | 17 +++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py index 6231d9a8b23..85552503dca 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py @@ -65,4 +65,15 @@ def _load_http_transport_class(name: str) -> type[BaseHTTPTransport]: "Install the corresponding extra or register an entry point " "under the 'opentelemetry_http_transport' group." ) - return ep.load() + cls = ep.load() + # pylint: disable-next=import-outside-toplevel,import-error + from opentelemetry.exporter.http.transport._base import ( # noqa: PLC0415 + BaseHTTPTransport, + ) + + if not isinstance(cls, type) or not issubclass(cls, BaseHTTPTransport): + raise TypeError( + f"Transport {name!r} loaded from entry point does not subclass " + f"BaseHTTPTransport (got {cls!r})." + ) + return cls diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py index e63ee5e2e4b..b7b51d4acc8 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py @@ -68,7 +68,7 @@ def create( cls, verify: bool | str, cert: str | tuple[str, str] | None, - **kwargs: Any, + **kwargs: dict[str, Any], ) -> Self: return cls(verify=verify, cert=cert, **kwargs) @@ -78,6 +78,7 @@ def __init__( verify: bool | str = True, cert: str | tuple[str, str] | None = None, session: requests.Session | None = None, + **kwargs: dict[str, Any], ) -> None: # pylint: disable-next=import-outside-toplevel import requests # noqa: PLC0415 diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py index 633092773c6..64fb04db299 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py @@ -73,7 +73,7 @@ def create( cls, verify: bool | str, cert: str | tuple[str, str] | None, - **kwargs: Any, + **kwargs: dict[str, Any], ) -> Self: return cls(verify=verify, cert=cert, **kwargs) @@ -82,6 +82,7 @@ def __init__( *, verify: bool | str = True, cert: str | tuple[str, str] | None = None, + **kwargs: dict[str, Any], ) -> None: # pylint: disable-next=import-outside-toplevel import urllib3 # noqa: PLC0415 diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py index a61d04a211c..6e2baadc017 100644 --- a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch from opentelemetry.exporter.http.transport import _load_http_transport_class +from opentelemetry.exporter.http.transport._base import BaseHTTPTransport from opentelemetry.exporter.http.transport._requests import ( RequestsHTTPTransport, ) @@ -38,6 +39,7 @@ def test_unknown_transport_calls_entry_points(self): class _CustomTransport: pass + BaseHTTPTransport.register(_CustomTransport) mock_ep = MagicMock() mock_ep.load.return_value = _CustomTransport with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn: @@ -48,6 +50,21 @@ class _CustomTransport: ) self.assertIs(result, _CustomTransport) + def test_entry_point_non_subclass_raises_type_error(self): + class _NotATransport: + pass + + mock_ep = MagicMock() + mock_ep.load.return_value = _NotATransport + with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]): + self.assertRaises(TypeError, _load_http_transport_class, "bad") + + def test_entry_point_non_class_raises_type_error(self): + mock_ep = MagicMock() + mock_ep.load.return_value = object() + with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]): + self.assertRaises(TypeError, _load_http_transport_class, "bad") + def test_unknown_transport_raises_value_error(self): with patch(_ENTRY_POINTS_TARGET, return_value=[]): self.assertRaises(