Repository: sdv-dev/Copulas (language: python)

Issue:
Add functionality to find version add-on
### Problem Description

_As an enterprise user, it would be nice to see the version of public Copulas that is being used as well as the enterprise version._

The actual functionality for this will be added in SDV enterprise, however, this library will need to know to add that functionality to the proper name space.

### Expected behavior

- In this[ file](https://github.com/sdv-dev/Copulas/blob/master/copulas/__init__.py), add and call a function that uses an entry point called `copulas_module` to add the `version` module to the namespace.
- Add any necessary logging and error checking
- Be careful not to override anything in the globals that is needed

### Additional context
The code can look something like the following:

```python
def _add_versioning():
    for entry_point in pkg_resources.iter_entry_points('copulas_module'):
        try:
            version_module = entry_point.load()
        except Exception:
            message = f'Failed to load "{entry_point.name}" from "{entry_point.module_name}". '
            continue

        if 'version' not in globals():
            globals()['version'] = version_module

_add_versioning()
```

In this case, the loaded entry point should return the module itself, so we can just add it directly to the `globals()` under the name `version`. This should enable the following:
```python
>>> copulas.version.public
0.8.0
```


Discussion / hints:

Fix patch:
diff --git a/copulas/__init__.py b/copulas/__init__.py
index f62e8ad9..af55a826 100644
--- a/copulas/__init__.py
+++ b/copulas/__init__.py
@@ -13,6 +13,10 @@
 import numpy as np
 import pandas as pd
 
+from copulas._addons import _find_addons
+
+_find_addons(group='copulas_modules', parent_globals=globals())
+
 EPSILON = np.finfo(np.float32).eps
 
 
diff --git a/copulas/_addons.py b/copulas/_addons.py
new file mode 100644
index 00000000..3f2f1761
--- /dev/null
+++ b/copulas/_addons.py
@@ -0,0 +1,26 @@
+"""Copulas add-ons functionality."""
+import warnings
+
+from pkg_resources import iter_entry_points
+
+
+def _find_addons(group, parent_globals):
+    """Find and load add-ons based on the given group.
+
+    Args:
+        group (str):
+            The name of the entry points group to load.
+        parent_globals (dict):
+            The caller's global scope. Modules will be added
+            to the parent's global scope through their name.
+    """
+    for entry_point in iter_entry_points(group=group):
+        try:
+            module = entry_point.load()
+        except Exception:
+            msg = f'Failed to load "{entry_point.name}" from "{entry_point.module}".'
+            warnings.warn(msg)
+            continue
+
+        if entry_point.name not in parent_globals:
+            parent_globals[entry_point.name] = module


Test patch:
diff --git a/tests/unit/test__addons.py b/tests/unit/test__addons.py
new file mode 100644
index 00000000..b02a5104
--- /dev/null
+++ b/tests/unit/test__addons.py
@@ -0,0 +1,45 @@
+from unittest.mock import Mock, patch
+
+from copulas._addons import _find_addons
+
+
+@patch('copulas._addons.iter_entry_points')
+def test__find_versions(entry_points_mock):
+    """Test loading an add-on."""
+    # Setup
+    entry_point = Mock()
+    entry_point.name = 'entry_name'
+    entry_point.load.return_value = 'entry_point'
+    entry_points_mock.return_value = [entry_point]
+    test_dict = {}
+
+    # Run
+    _find_addons(group='group', parent_globals=test_dict)
+
+    # Assert
+    entry_points_mock.assert_called_once_with(group='group')
+    assert test_dict['entry_name'] == 'entry_point'
+
+
+@patch('copulas._addons.warnings.warn')
+@patch('copulas._addons.iter_entry_points')
+def test__find_versions_bad_addon(entry_points_mock, warning_mock):
+    """Test failing to load an add-on generates a warning."""
+    # Setup
+    def entry_point_error():
+        raise ValueError()
+
+    bad_entry_point = Mock()
+    bad_entry_point.name = 'bad_entry_point'
+    bad_entry_point.module = 'bad_module'
+    bad_entry_point.load.side_effect = entry_point_error
+    entry_points_mock.return_value = [bad_entry_point]
+    test_dict = {}
+    msg = 'Failed to load "bad_entry_point" from "bad_module".'
+
+    # Run
+    _find_addons(group='group', parent_globals=test_dict)
+
+    # Assert
+    entry_points_mock.assert_called_once_with(group='group')
+    warning_mock.assert_called_once_with(msg)