# 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.

# FIXME find a better way to avoid all those "Expression has type "Any"" errors
# type: ignore

"""
Simple configuration manager

This is a configuration manager for OpenTelemetry. It reads configuration
values from environment variables prefixed with ``OPENTELEMETRY_PYTHON_`` whose
characters are only alphanumeric characters and unserscores, except for the
first character after ``OPENTELEMETRY_PYTHON_`` which must not be a number.

For example, these environment variables will be read:

1. ``OPENTELEMETRY_PYTHON_SOMETHING``
2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_``
3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE``
4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else``
4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2``

These won't:

1. ``OPENTELEMETRY_PYTH_SOMETHING``
2. ``OPENTELEMETRY_PYTHON_2_SOMETHING_AND__ELSE``
3. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE``

The values stored in the environment variables can be found in an instance of
``opentelemetry.configuration.Configuration``. This class can be instantiated
freely because instantiating it returns always the same object.

For example, if the environment variable
``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then
``Configuration().meter_provider == "my_meter_provider"`` would be ``True``.

Non defined attributes will always return ``None``. This is intended to make it
easier to use the ``Configuration`` object in actual code, because it won't be
necessary to check for the attribute to be defined first.

Environment variables used by OpenTelemetry
-------------------------------------------

1. OPENTELEMETRY_PYTHON_METER_PROVIDER
2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER

The value of these environment variables should be the name of the entry point
that points to the class that implements either provider. This OpenTelemetry
API package provides one entry point for each, which can be found in the
setup.py file::

    entry_points={
        ...
        "opentelemetry_meter_provider": [
            "default_meter_provider = "
            "opentelemetry.metrics:DefaultMeterProvider"
        ],
        "opentelemetry_tracer_provider": [
            "default_tracer_provider = "
            "opentelemetry.trace:DefaultTracerProvider"
        ],
    }

To use the meter provider above, then the
``OPENTELEMETRY_PYTHON_METER_PROVIDER`` should be set to
"default_meter_provider" (this is not actually necessary since the
OpenTelemetry API provided providers are the default ones used if no
configuration is found in the environment variables).
"""

from os import environ
from re import fullmatch


class Configuration:
    _instance = None

    __slots__ = []

    def __new__(cls) -> "Configuration":
        if Configuration._instance is None:

            for key, value in environ.items():

                match = fullmatch(
                    r"OPENTELEMETRY_PYTHON_([A-Za-z_][\w_]*)", key
                )

                if match is not None:

                    key = match.group(1)

                    setattr(Configuration, "_{}".format(key), value)
                    setattr(
                        Configuration,
                        key,
                        property(
                            fget=lambda cls, key=key: getattr(
                                cls, "_{}".format(key)
                            )
                        ),
                    )

                Configuration.__slots__.append(key)

            Configuration.__slots__ = tuple(Configuration.__slots__)

            Configuration._instance = object.__new__(cls)

        return cls._instance

    def __getattr__(self, name):
        return None

opentelemetry-api/src/opentelemetry/configuration/__init__.py
# 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.

# FIXME find a better way to avoid all those "Expression has type "Any"" errors
# type: ignore

"""
Simple configuration manager

This is a configuration manager for OpenTelemetry. It reads configuration
values from environment variables prefixed with ``OPENTELEMETRY_PYTHON_`` whose
characters are only alphanumeric characters and unserscores, except for the
first character after ``OPENTELEMETRY_PYTHON_`` which must not be a number.

For example, these environment variables will be read:

1. ``OPENTELEMETRY_PYTHON_SOMETHING``
2. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_``
3. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND__ELSE``
4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else``
4. ``OPENTELEMETRY_PYTHON_SOMETHING_ELSE_AND_else2``

These won't:

1. ``OPENTELEMETRY_PYTH_SOMETHING``
2. ``OPENTELEMETRY_PYTHON_2_SOMETHING_AND__ELSE``
3. ``OPENTELEMETRY_PYTHON_SOMETHING_%_ELSE``

The values stored in the environment variables can be found in an instance of
``opentelemetry.configuration.Configuration``. This class can be instantiated
freely because instantiating it returns always the same object.

For example, if the environment variable
``OPENTELEMETRY_PYTHON_METER_PROVIDER`` value is ``my_meter_provider``, then
``Configuration().meter_provider == "my_meter_provider"`` would be ``True``.

Non defined attributes will always return ``None``. This is intended to make it
easier to use the ``Configuration`` object in actual code, because it won't be
necessary to check for the attribute to be defined first.

Environment variables used by OpenTelemetry
-------------------------------------------

1. OPENTELEMETRY_PYTHON_METER_PROVIDER
2. OPENTELEMETRY_PYTHON_TRACER_PROVIDER

The value of these environment variables should be the name of the entry point
that points to the class that implements either provider. This OpenTelemetry
API package provides one entry point for each, which can be found in the
setup.py file::

    entry_points={
        ...
        "opentelemetry_meter_provider": [
            "default_meter_provider = "
            "opentelemetry.metrics:DefaultMeterProvider"
        ],
        "opentelemetry_tracer_provider": [
            "default_tracer_provider = "
            "opentelemetry.trace:DefaultTracerProvider"
        ],
    }

To use the meter provider above, then the
``OPENTELEMETRY_PYTHON_METER_PROVIDER`` should be set to
"default_meter_provider" (this is not actually necessary since the
OpenTelemetry API provided providers are the default ones used if no
configuration is found in the environment variables).
"""

from os import environ
from re import fullmatch


class Configuration:
    _instance = None

    __slots__ = []

    def __new__(cls) -> "Configuration":
        if Configuration._instance is None:

            for key, value in environ.items():

                match = fullmatch(
                    r"OPENTELEMETRY_PYTHON_([A-Za-z_][\w_]*)", key
                )

                if match is not None:

                    key = match.group(1)

                    setattr(Configuration, "_{}".format(key), value)
                    setattr(
                        Configuration,
                        key,
                        property(
                            fget=lambda cls, key=key: getattr(
                                cls, "_{}".format(key)
                            )
                        ),
                    )

                Configuration.__slots__.append(key)

            Configuration.__slots__ = tuple(Configuration.__slots__)

            Configuration._instance = object.__new__(cls)

        return cls._instance

    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

opentelemetry-api/src/opentelemetry/configuration/__init__.py
diff --git a/ext/opentelemetry-ext-flask/tests/base_test.py b/ext/opentelemetry-ext-flask/tests/base_test.py
index 7147afd7193..42341826df0 100644
--- 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
index 04f43d6e642..b94c7b33d6b 100644
--- 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
index 1075f808cb8..4e17f25fdc1 100644
--- 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/src/opentelemetry/configuration/__init__.py b/opentelemetry-api/src/opentelemetry/configuration/__init__.py
index 57b1c324c6c..ad546b0b864 100644
--- 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
diff --git a/opentelemetry-api/tests/configuration/test_configuration.py b/opentelemetry-api/tests/configuration/test_configuration.py
index 9688ec28b6c..c736c972622 100644
--- 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

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.
