Repository: open-telemetry/opentelemetry-python

Issue:
Add a standard way to "reset" a Configuration object for testing
It is a common occurrence in tests that the global `Configuration` object needs to be "reset" between tests. This means that its attributes need to be set back to their original values. Since the `Configuration` object is immutable by design, some additional, non-production available mechanism is needed to perform this action.

The need for this feature was mentioned in a [conversation](https://github.com/open-telemetry/opentelemetry-python/pull/630#discussion_r418343720) in #630.


Discussion:

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py
--- a/opentelemetry-api/src/opentelemetry/configuration/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/configuration/__init__.py
@@ -122,3 +122,20 @@ def __new__(cls) -> "Configuration":
 
     def __getattr__(self, name):
         return None
+
+    @classmethod
+    def _reset(cls):
+        """
+        This method "resets" the global configuration attributes
+
+        It is not intended to be used by production code but by testing code
+        only.
+        """
+
+        for slot in cls.__slots__:
+            if slot in cls.__dict__.keys():
+                delattr(cls, slot)
+                delattr(cls, "_{}".format(slot))
+
+        cls.__slots__ = []
+        cls._instance = None


Test patch:
diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/ext/opentelemetry-ext-flask/tests/base_test.py
--- a/ext/opentelemetry-ext-flask/tests/base_test.py
+++ b/ext/opentelemetry-ext-flask/tests/base_test.py
@@ -19,6 +19,7 @@
 from werkzeug.wrappers import BaseResponse
 
 from opentelemetry import trace
+from opentelemetry.configuration import Configuration
 
 
 def expected_attributes(override_attributes):
@@ -40,6 +41,10 @@ def expected_attributes(override_attributes):
 
 
 class InstrumentationTest:
+    def setUp(self):  # pylint: disable=invalid-name
+        super().setUp()  # pylint: disable=no-member
+        Configuration._reset()  # pylint: disable=protected-access
+
     @staticmethod
     def _hello_endpoint(helloid):
         if helloid == 500:
diff --git a/ext/opentelemetry-ext-flask/tests/test_automatic.py b/ext/opentelemetry-ext-flask/tests/test_automatic.py
--- a/ext/opentelemetry-ext-flask/tests/test_automatic.py
+++ b/ext/opentelemetry-ext-flask/tests/test_automatic.py
@@ -16,7 +16,6 @@
 from werkzeug.test import Client
 from werkzeug.wrappers import BaseResponse
 
-from opentelemetry.configuration import Configuration
 from opentelemetry.ext.flask import FlaskInstrumentor
 from opentelemetry.test.test_base import TestBase
 from opentelemetry.test.wsgitestutil import WsgiTestBase
@@ -29,8 +28,6 @@ class TestAutomatic(InstrumentationTest, TestBase, WsgiTestBase):
     def setUp(self):
         super().setUp()
 
-        Configuration._instance = None  # pylint: disable=protected-access
-        Configuration.__slots__ = []  # pylint: disable=protected-access
         FlaskInstrumentor().instrument()
 
         self.app = flask.Flask(__name__)
diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py
--- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py
+++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py
@@ -14,7 +14,6 @@
 
 from flask import Flask
 
-from opentelemetry.configuration import Configuration
 from opentelemetry.ext.flask import FlaskInstrumentor
 from opentelemetry.test.test_base import TestBase
 from opentelemetry.test.wsgitestutil import WsgiTestBase
@@ -27,8 +26,6 @@ class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase):
     def setUp(self):
         super().setUp()
 
-        Configuration._instance = None  # pylint: disable=protected-access
-        Configuration.__slots__ = []  # pylint: disable=protected-access
         self.app = Flask(__name__)
 
         FlaskInstrumentor().instrument_app(self.app)
diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py
--- a/opentelemetry-api/tests/configuration/test_configuration.py
+++ b/opentelemetry-api/tests/configuration/test_configuration.py
@@ -20,14 +20,10 @@
 
 
 class TestConfiguration(TestCase):
-    def setUp(self):
-        # This is added here to force a reload of the whole Configuration
-        # class, resetting its internal attributes so that each tests starts
-        # with a clean class.
-        from opentelemetry.configuration import Configuration  # type: ignore
-
     def tearDown(self):
-        from opentelemetry.configuration import Configuration  # type: ignore
+        # This call resets the attributes of the Configuration class so that
+        # each test is executed in the same conditions.
+        Configuration._reset()
 
     def test_singleton(self):
         self.assertIsInstance(Configuration(), Configuration)
@@ -72,3 +68,23 @@ def test_slots(self):
 
     def test_getattr(self):
         self.assertIsNone(Configuration().XYZ)
+
+    def test_reset(self):
+        environ_patcher = patch.dict(
+            "os.environ",  # type: ignore
+            {"OPENTELEMETRY_PYTHON_TRACER_PROVIDER": "tracer_provider"},
+        )
+
+        environ_patcher.start()
+
+        self.assertEqual(
+            Configuration().TRACER_PROVIDER, "tracer_provider"
+        )  # pylint: disable=no-member
+
+        environ_patcher.stop()
+
+        Configuration._reset()
+
+        self.assertIsNone(
+            Configuration().TRACER_PROVIDER
+        )  # pylint: disable=no-member