Repository: alteryx/woodwork
Pull request #1062 (2021-07-19 21:46:05)

Add Config.with_options and fix potential issues in test suite
Adds a new method to the `Config` object that enables setting config values temporarily inside of a with block.  Adding this functionality was necessary to fix a potential issue in the test suite.  The problem was that any test case that modifies the global config singleton with `ww.config.set_option` and then throws an `AssertionError` could cause the global config to be left in that state during the subsequent execution of further test cases.

davesque: Just saw that I forgot the release notes.  Will update.
codecov[bot]: # [Codecov](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) Report
> Merging [#1062](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (ef6e2d5) into [main](https://codecov.io/gh/alteryx/woodwork/commit/8103cb992090d7b54b22024f9e6ee6dc9e17bbcf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (8103cb9) will **decrease** coverage by `3.70%`.
> The diff coverage is `95.74%`.

[![Impacted file tree graph](https://codecov.io/gh/alteryx/woodwork/pull/1062/graphs/tree.svg?width=650&height=150&src=pr&token=KJCKMREBDP&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx)](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx)

```diff
@@             Coverage Diff             @@
##              main    #1062      +/-   ##
===========================================
- Coverage   100.00%   96.29%   -3.71%     
===========================================
  Files           46       46              
  Lines         7967     7989      +22     
===========================================
- Hits          7967     7693     -274     
- Misses           0      296     +296     
```


| [Impacted Files](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | Coverage Δ | |
|---|---|---|
| [woodwork/tests/type\_system/test\_ltype\_inference.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdHlwZV9zeXN0ZW0vdGVzdF9sdHlwZV9pbmZlcmVuY2UucHk=) | `90.37% <90.90%> (-9.63%)` | :arrow_down: |
| [woodwork/config.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvY29uZmlnLnB5) | `100.00% <100.00%> (ø)` | |
| [woodwork/tests/test\_config.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdGVzdF9jb25maWcucHk=) | `100.00% <100.00%> (ø)` | |
| [woodwork/tests/testing\_utils/table\_utils.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvdGVzdGluZ191dGlscy90YWJsZV91dGlscy5weQ==) | `80.00% <0.00%> (-20.00%)` | :arrow_down: |
| [woodwork/tests/conftest.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvY29uZnRlc3QucHk=) | `81.77% <0.00%> (-18.23%)` | :arrow_down: |
| [woodwork/serialize.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvc2VyaWFsaXplLnB5) | `87.96% <0.00%> (-12.04%)` | :arrow_down: |
| [woodwork/tests/accessor/test\_indexers.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvYWNjZXNzb3IvdGVzdF9pbmRleGVycy5weQ==) | `90.16% <0.00%> (-9.84%)` | :arrow_down: |
| [woodwork/indexers.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvaW5kZXhlcnMucHk=) | `92.10% <0.00%> (-7.90%)` | :arrow_down: |
| [woodwork/logical\_types.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvbG9naWNhbF90eXBlcy5weQ==) | `92.30% <0.00%> (-7.70%)` | :arrow_down: |
| [woodwork/tests/accessor/test\_serialization.py](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-d29vZHdvcmsvdGVzdHMvYWNjZXNzb3IvdGVzdF9zZXJpYWxpemF0aW9uLnB5) | `92.73% <0.00%> (-7.27%)` | :arrow_down: |
| ... and [16 more](https://codecov.io/gh/alteryx/woodwork/pull/1062/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx). Last update [8103cb9...ef6e2d5](https://codecov.io/gh/alteryx/woodwork/pull/1062?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx).

tamargrey: @davesque is this from a specific bug that was popping up or more a potential issue that we should take care to avoid running into?
davesque: @tamargrey I didn't confirm that this was causing any actual failures.  It's more that I wanted to eliminate it as a possible cause for some failures I was diagnosing.  I realized that those tests would leave the config in a weird state if they failed and that could definitely cause other tests to fail if they depended on behavior governed by those settings.  And it also seemed like a nice to have feature.
davesque: @tamargrey It's because some of the functionality I was updating to address #1051 caused failures in those tests.
tamargrey: @davesque cool--makes sense! Thanks for adding

Patch:
diff --git a/docs/source/guides/setting_config_options.ipynb b/docs/source/guides/setting_config_options.ipynb
index 87ee4ce..3eba975 100644
--- a/docs/source/guides/setting_config_options.ipynb
+++ b/docs/source/guides/setting_config_options.ipynb
@@ -41,7 +41,9 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "scrolled": true
+   },
    "outputs": [],
    "source": [
     "ww.config.set_option('categorical_threshold', 25)\n",
@@ -52,9 +54,39 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "As you can see from the output above, the value for the `categorical_threshold` config variable has been updated to `25`.\n",
+    "As you can see from the output above, the value for the `categorical_threshold` config variable has been updated to `25`."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Temporarily Updating Config Settings\n",
     "\n",
+    "Settings can also be temporarily updated in the context of a with block by using `ww.config.with_options`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "with ww.config.with_options(categorical_threshold=35):\n",
+    "    # Do something\n",
+    "    print(\"Temporary settings:\\n\")\n",
+    "    print(repr(ww.config), \"\\n\")\n",
+    "    \n",
+    "print(\"Restored settings:\\n\")\n",
+    "print(repr(ww.config))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "## Get Value for a Specific Config Variable\n",
+    "\n",
     "If you need access to the value that is set for a specific config variable you can access it with the `ww.config.get_option` function, passing in the name of the config variable for which you want the value."
    ]
   },
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 0329253..b772083 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -9,6 +9,8 @@ Future Release
           type (:pr:`1047`)
         * Add ability to calculate outliers and the statistical info required for box and whisker plots
           to ``WoodworkColumnAccessor`` (:pr:`1048`)
+        * Add ability to change config settings in a with block with
+          ``ww.config.with_options`` (:pr:`1062`)
     * Fixes
     * Changes
         * Entirely null columns are now inferred as the Unknown logical type (:pr:`1043`)
diff --git a/woodwork/config.py b/woodwork/config.py
index 3c6ef59..7670a77 100644
--- a/woodwork/config.py
+++ b/woodwork/config.py
@@ -1,3 +1,5 @@
+import contextlib
+
 CONFIG_DEFAULTS = {
     'categorical_threshold': 10,
     'numeric_categorical_threshold': -1,
@@ -25,6 +27,18 @@ class Config:
             raise KeyError(f"Invalid option specified: {key}")
         self._data[key] = self._defaults[key]
 
+    @contextlib.contextmanager
+    def with_options(self, **options):
+        old_options = {k: self.get_option(k) for k in options}
+
+        for k, v in options.items():
+            self.set_option(k, v)
+        try:
+            yield
+        finally:
+            for k, v in old_options.items():
+                self.set_option(k, v)
+
     def __repr__(self):
         output_string = "Woodwork Global Config Settings\n"
         output_string += "-" * (len(output_string) - 1)


Test patch:
diff --git a/woodwork/tests/test_config.py b/woodwork/tests/test_config.py
index bb8772c..f0a2648 100644
--- a/woodwork/tests/test_config.py
+++ b/woodwork/tests/test_config.py
@@ -42,6 +42,29 @@ def test_reset_option(config):
     assert config.get_option('option1') == 'value1'
 
 
+def test_with_options(config):
+    old_value_1 = config.get_option("option1")
+    old_value_2 = config.get_option("option2")
+
+    with_value_1 = "some_other_value_1"
+    with_value_2 = "some_other_value_2"
+
+    with config.with_options(option1=with_value_1, option2=with_value_2):
+        assert config.get_option("option1") == with_value_1
+        assert config.get_option("option2") == with_value_2
+
+    assert config.get_option("option1") == old_value_1
+    assert config.get_option("option2") == old_value_2
+
+    # Make sure that config gets reset when exceptions occur
+    try:
+        with config.with_options(option1="lkjasdlfkjasdf"):
+            raise RuntimeError("something went wrong!")
+    except RuntimeError:
+        pass
+    assert config.get_option("option1") == old_value_1
+
+
 def test_invalid_option_warnings(config):
     error_msg = 'Invalid option specified: invalid_option'
 
diff --git a/woodwork/tests/type_system/test_ltype_inference.py b/woodwork/tests/type_system/test_ltype_inference.py
index 08f3565..214be32 100644
--- a/woodwork/tests/type_system/test_ltype_inference.py
+++ b/woodwork/tests/type_system/test_ltype_inference.py
@@ -114,27 +114,25 @@ def test_categorical_inference(categories):
 
 
 def test_categorical_integers_inference(integers):
-    ww.config.set_option('numeric_categorical_threshold', 10)
-    dtypes = ['int8', 'int16', 'int32', 'int64', 'intp', 'int', 'Int64']
-    if ks and isinstance(integers[0], ks.Series):
-        dtypes = get_koalas_dtypes(dtypes)
-    for series in integers:
-        for dtype in dtypes:
-            inferred_type = ww.type_system.infer_logical_type(series.astype(dtype))
-            assert isinstance(inferred_type, Categorical)
-    ww.config.reset_option('numeric_categorical_threshold')
+    with ww.config.with_options(numeric_categorical_threshold=10):
+        dtypes = ['int8', 'int16', 'int32', 'int64', 'intp', 'int', 'Int64']
+        if ks and isinstance(integers[0], ks.Series):
+            dtypes = get_koalas_dtypes(dtypes)
+        for series in integers:
+            for dtype in dtypes:
+                inferred_type = ww.type_system.infer_logical_type(series.astype(dtype))
+                assert isinstance(inferred_type, Categorical)
 
 
 def test_categorical_double_inference(doubles):
-    ww.config.set_option('numeric_categorical_threshold', 10)
-    dtypes = ['float', 'float32', 'float64', 'float_']
-    if ks and isinstance(doubles[0], ks.Series):
-        dtypes = get_koalas_dtypes(dtypes)
-    for series in doubles:
-        for dtype in dtypes:
-            inferred_type = ww.type_system.infer_logical_type(series.astype(dtype))
-            assert isinstance(inferred_type, Categorical)
-    ww.config.reset_option('numeric_categorical_threshold')
+    with ww.config.with_options(numeric_categorical_threshold=10):
+        dtypes = ['float', 'float32', 'float64', 'float_']
+        if ks and isinstance(doubles[0], ks.Series):
+            dtypes = get_koalas_dtypes(dtypes)
+        for series in doubles:
+            for dtype in dtypes:
+                inferred_type = ww.type_system.infer_logical_type(series.astype(dtype))
+                assert isinstance(inferred_type, Categorical)
 
 
 def test_timedelta_inference(timedeltas):
@@ -173,13 +171,12 @@ def test_unknown_inference_with_threshhold(long_strings):
     if ks and isinstance(long_strings[0], ks.Series):
         dtypes = get_koalas_dtypes(dtypes)
 
-    ww.config.set_option('categorical_threshold', 19)
-    for dtype in dtypes:
-        inferred_type = ww.type_system.infer_logical_type(long_strings[0].astype(dtype))
-        assert isinstance(inferred_type, Unknown)
-        inferred_type = ww.type_system.infer_logical_type(long_strings[1].astype(dtype))
-        assert isinstance(inferred_type, Categorical)
-    ww.config.reset_option('categorical_threshold')
+    with ww.config.with_options(categorical_threshold=19):
+        for dtype in dtypes:
+            inferred_type = ww.type_system.infer_logical_type(long_strings[0].astype(dtype))
+            assert isinstance(inferred_type, Unknown)
+            inferred_type = ww.type_system.infer_logical_type(long_strings[1].astype(dtype))
+            assert isinstance(inferred_type, Categorical)
 
 
 def test_pdna_inference(pdnas):