From 011c8d2266d2ef20ff3cd385504efdd03bf0ebdc Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Mon, 2 Nov 2020 13:11:47 -0500 Subject: [PATCH 01/16] TST Adds failing test --- sklearn/tests/test_config.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index eec349861258c..6d7a5a16b4684 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -1,5 +1,9 @@ +import time +from joblib import Parallel + from sklearn import get_config, set_config, config_context from sklearn.utils._testing import assert_raises +from sklearn.utils.fixes import delayed def test_config_context(): @@ -72,3 +76,20 @@ def test_set_config(): # No unknown arguments assert_raises(TypeError, set_config, do_something_else=True) + + +def test_config_threadsafe(): + """Test that the global config is threadsafe.""" + + def set_assume_finite(assume_finite, sleep_dur): + with config_context(assume_finite=assume_finite): + time.sleep(sleep_dur) + return get_config()['assume_finite'] + + booleans = [False, True] + sleep_seconds = [0.5, 1] + items = Parallel(backend='threading', n_jobs=2)( + delayed(set_assume_finite)(value, sleep_dur) + for value, sleep_dur in zip(booleans, sleep_seconds)) + + assert items == [False, True] From 59b4a8e2299d22b3487a97a0be860ddb3b798127 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Mon, 2 Nov 2020 13:58:16 -0500 Subject: [PATCH 02/16] ENH Makes copies of global configuration per thread --- sklearn/_config.py | 17 ++++++++++++----- sklearn/tests/test_config.py | 21 +++++++++++++-------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index feb5e86287c38..b54c27bdf8b07 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -2,6 +2,7 @@ """ import os from contextlib import contextmanager as contextmanager +import threading _global_config = { 'assume_finite': bool(os.environ.get('SKLEARN_ASSUME_FINITE', False)), @@ -9,6 +10,7 @@ 'print_changed_only': True, 'display': 'text', } +_threadlocal = threading.local() def get_config(): @@ -24,7 +26,9 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ - return _global_config.copy() + if not hasattr(_threadlocal, 'global_config'): + _threadlocal.global_config = _global_config.copy() + return _threadlocal.global_config.copy() def set_config(assume_finite=None, working_memory=None, @@ -72,14 +76,17 @@ def set_config(assume_finite=None, working_memory=None, config_context : Context manager for global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ + if not hasattr(_threadlocal, 'global_config'): + _threadlocal.global_config = _global_config.copy() + if assume_finite is not None: - _global_config['assume_finite'] = assume_finite + _threadlocal.global_config['assume_finite'] = assume_finite if working_memory is not None: - _global_config['working_memory'] = working_memory + _threadlocal.global_config['working_memory'] = working_memory if print_changed_only is not None: - _global_config['print_changed_only'] = print_changed_only + _threadlocal.global_config['print_changed_only'] = print_changed_only if display is not None: - _global_config['display'] = display + _threadlocal.global_config['display'] = display @contextmanager diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index 6d7a5a16b4684..5843f07048a24 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -1,5 +1,7 @@ import time + from joblib import Parallel +import pytest from sklearn import get_config, set_config, config_context from sklearn.utils._testing import assert_raises @@ -78,17 +80,20 @@ def test_set_config(): assert_raises(TypeError, set_config, do_something_else=True) -def test_config_threadsafe(): - """Test that the global config is threadsafe.""" +def set_assume_finite(assume_finite, sleep_dur): + with config_context(assume_finite=assume_finite): + time.sleep(sleep_dur) + return get_config()['assume_finite'] - def set_assume_finite(assume_finite, sleep_dur): - with config_context(assume_finite=assume_finite): - time.sleep(sleep_dur) - return get_config()['assume_finite'] + +@pytest.mark.parametrize("backend", + ["loky", "multiprocessing", "threading"]) +def test_config_threadsafe(backend): + """Test that the global config is threadsafe.""" booleans = [False, True] - sleep_seconds = [0.5, 1] - items = Parallel(backend='threading', n_jobs=2)( + sleep_seconds = [0.1, 0.2] + items = Parallel(backend=backend, n_jobs=2)( delayed(set_assume_finite)(value, sleep_dur) for value, sleep_dur in zip(booleans, sleep_seconds)) From f6a135786cf8b5af849dd59c664a343ae4a9e328 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Mon, 2 Nov 2020 14:30:14 -0500 Subject: [PATCH 03/16] TST Skip loky tests for joblib version < 0.12 --- sklearn/tests/test_config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index 5843f07048a24..a49caae5b4082 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -1,11 +1,13 @@ import time from joblib import Parallel +import joblib import pytest from sklearn import get_config, set_config, config_context from sklearn.utils._testing import assert_raises from sklearn.utils.fixes import delayed +from sklearn.utils.fixes import parse_version def test_config_context(): @@ -91,8 +93,13 @@ def set_assume_finite(assume_finite, sleep_dur): def test_config_threadsafe(backend): """Test that the global config is threadsafe.""" + if (parse_version(joblib.__version__) < parse_version('0.12') + and backend == 'loky'): + pytest.skip('loky backend does not exist in joblib <0.12') + booleans = [False, True] sleep_seconds = [0.1, 0.2] + items = Parallel(backend=backend, n_jobs=2)( delayed(set_assume_finite)(value, sleep_dur) for value, sleep_dur in zip(booleans, sleep_seconds)) From 99a4ce16f29ee05d6094db1b8fe4ec4242d2865e Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Thu, 26 Nov 2020 13:30:06 -0500 Subject: [PATCH 04/16] ENH Small refactor --- sklearn/_config.py | 12 ++++++++---- sklearn/tests/test_config.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index b54c27bdf8b07..68242080b4080 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -13,6 +13,12 @@ _threadlocal = threading.local() +def _set_threadlocal(): + """Set the configuration that is local to the thread.""" + if not hasattr(_threadlocal, 'global_config'): + _threadlocal.global_config = _global_config.copy() + + def get_config(): """Retrieve current values for configuration set by :func:`set_config` @@ -26,8 +32,7 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ - if not hasattr(_threadlocal, 'global_config'): - _threadlocal.global_config = _global_config.copy() + _set_threadlocal() return _threadlocal.global_config.copy() @@ -76,8 +81,7 @@ def set_config(assume_finite=None, working_memory=None, config_context : Context manager for global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - if not hasattr(_threadlocal, 'global_config'): - _threadlocal.global_config = _global_config.copy() + _set_threadlocal() if assume_finite is not None: _threadlocal.global_config['assume_finite'] = assume_finite diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index a49caae5b4082..9b040e2376ff4 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -95,7 +95,7 @@ def test_config_threadsafe(backend): if (parse_version(joblib.__version__) < parse_version('0.12') and backend == 'loky'): - pytest.skip('loky backend does not exist in joblib <0.12') + pytest.skip('loky backend does not exist in joblib <0.12') # noqa booleans = [False, True] sleep_seconds = [0.1, 0.2] From 966498d956dae0253a91049d340a451d8bd3da9a Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Thu, 26 Nov 2020 17:28:06 -0500 Subject: [PATCH 05/16] DOC Update docstring of test --- sklearn/tests/test_config.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index 9b040e2376ff4..86e376e10eb2a 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -1,4 +1,5 @@ import time +from concurrent.futures import ThreadPoolExecutor from joblib import Parallel import joblib @@ -83,6 +84,7 @@ def test_set_config(): def set_assume_finite(assume_finite, sleep_dur): + """Return the value of assume_finite after waiting `sleep_dur`.""" with config_context(assume_finite=assume_finite): time.sleep(sleep_dur) return get_config()['assume_finite'] @@ -90,18 +92,39 @@ def set_assume_finite(assume_finite, sleep_dur): @pytest.mark.parametrize("backend", ["loky", "multiprocessing", "threading"]) -def test_config_threadsafe(backend): - """Test that the global config is threadsafe.""" +def test_config_threadsafe_joblib(backend): + """Test that the global config is threadsafe with all joblib backends. + Two jobs are spawned and sets assume_finite to two different values. + When the job with a duration 0.1s completes, the assume_finite value + should be the same as the value passed to the function. In other words, + it is not influenced by the other job setting assume_finite to True. + """ if (parse_version(joblib.__version__) < parse_version('0.12') and backend == 'loky'): pytest.skip('loky backend does not exist in joblib <0.12') # noqa - booleans = [False, True] + assume_finite_bools = [False, True] sleep_seconds = [0.1, 0.2] items = Parallel(backend=backend, n_jobs=2)( - delayed(set_assume_finite)(value, sleep_dur) - for value, sleep_dur in zip(booleans, sleep_seconds)) + delayed(set_assume_finite)(assume_finite, sleep_dur) + for assume_finite, sleep_dur + in zip(assume_finite_bools, sleep_seconds)) + + assert items == [False, True] + + +def test_config_threadsafe(): + """Uses threads directly to test that the global config does not change + between threads. Same test as `test_config_threadsafe_joblib` by with + `ThreadPoolExecutor`.""" + + assume_finite_bools = [False, True] + sleep_seconds = [0.1, 0.2] + + with ThreadPoolExecutor(max_workers=2) as e: + items = [output for output in + e.map(set_assume_finite, assume_finite_bools, sleep_seconds)] assert items == [False, True] From df291d9861baa6ddd9661a5dfcfac03b7f4de85a Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Thu, 26 Nov 2020 17:31:53 -0500 Subject: [PATCH 06/16] DOC Update names --- sklearn/tests/test_config.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index 86e376e10eb2a..b611808b7ec4d 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -83,10 +83,10 @@ def test_set_config(): assert_raises(TypeError, set_config, do_something_else=True) -def set_assume_finite(assume_finite, sleep_dur): - """Return the value of assume_finite after waiting `sleep_dur`.""" +def set_assume_finite(assume_finite, sleep_duration): + """Return the value of assume_finite after waiting `sleep_duration`.""" with config_context(assume_finite=assume_finite): - time.sleep(sleep_dur) + time.sleep(sleep_duration) return get_config()['assume_finite'] @@ -104,13 +104,13 @@ def test_config_threadsafe_joblib(backend): and backend == 'loky'): pytest.skip('loky backend does not exist in joblib <0.12') # noqa - assume_finite_bools = [False, True] - sleep_seconds = [0.1, 0.2] + assume_finites = [False, True] + sleep_durations = [0.1, 0.2] items = Parallel(backend=backend, n_jobs=2)( delayed(set_assume_finite)(assume_finite, sleep_dur) for assume_finite, sleep_dur - in zip(assume_finite_bools, sleep_seconds)) + in zip(assume_finites, sleep_durations)) assert items == [False, True] @@ -120,11 +120,11 @@ def test_config_threadsafe(): between threads. Same test as `test_config_threadsafe_joblib` by with `ThreadPoolExecutor`.""" - assume_finite_bools = [False, True] - sleep_seconds = [0.1, 0.2] + assume_finites = [False, True] + sleep_durations = [0.1, 0.2] with ThreadPoolExecutor(max_workers=2) as e: items = [output for output in - e.map(set_assume_finite, assume_finite_bools, sleep_seconds)] + e.map(set_assume_finite, assume_finites, sleep_durations)] assert items == [False, True] From ba93b54fb729e4919d7b484cbb15938c8e1b9542 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Fri, 9 Apr 2021 09:29:39 -0400 Subject: [PATCH 07/16] DOC Updates changelog and docstring about threadsafeness --- doc/whats_new/v1.0.rst | 5 +++++ sklearn/_config.py | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/whats_new/v1.0.rst b/doc/whats_new/v1.0.rst index 602d4b1246878..e254aadc23f4e 100644 --- a/doc/whats_new/v1.0.rst +++ b/doc/whats_new/v1.0.rst @@ -76,6 +76,11 @@ Changelog - For :class:`tree.ExtraTreeRegressor`, `criterion="mse"` is deprecated, use `"squared_error"` instead which is now the default. +:mod:`sklearn.base` +................... + +- |Fix| :func:`config_context` is now threadsafe. :pr:`18736` by `Thomas Fan`_. + :mod:`sklearn.calibration` .......................... diff --git a/sklearn/_config.py b/sklearn/_config.py index 68242080b4080..eb613dc29909e 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -131,8 +131,7 @@ def config_context(**new_config): Notes ----- All settings, not just those presently modified, will be returned to - their previous values when the context manager is exited. This is not - thread-safe. + their previous values when the context manager is exited. Examples -------- From ea516c44c899e008c0f79abbd1bba40a09f3835f Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Fri, 9 Apr 2021 09:33:04 -0400 Subject: [PATCH 08/16] DOC Update test's docstring --- sklearn/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index b611808b7ec4d..580cae0c31842 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -117,7 +117,7 @@ def test_config_threadsafe_joblib(backend): def test_config_threadsafe(): """Uses threads directly to test that the global config does not change - between threads. Same test as `test_config_threadsafe_joblib` by with + between threads. Same test as `test_config_threadsafe_joblib` but with `ThreadPoolExecutor`.""" assume_finites = [False, True] From 6fe06197004124472490dd5d3eddfe2eb1a46b00 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Fri, 9 Apr 2021 09:36:55 -0400 Subject: [PATCH 09/16] ENH Cleaner code --- sklearn/_config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index eb613dc29909e..9fba4d32d7748 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -13,10 +13,11 @@ _threadlocal = threading.local() -def _set_threadlocal(): +def _get_threadlocal(): """Set the configuration that is local to the thread.""" if not hasattr(_threadlocal, 'global_config'): _threadlocal.global_config = _global_config.copy() + return _threadlocal.global_config def get_config(): @@ -32,8 +33,7 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ - _set_threadlocal() - return _threadlocal.global_config.copy() + return _get_threadlocal() def set_config(assume_finite=None, working_memory=None, @@ -81,16 +81,16 @@ def set_config(assume_finite=None, working_memory=None, config_context : Context manager for global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - _set_threadlocal() + local_config = _get_threadlocal() if assume_finite is not None: - _threadlocal.global_config['assume_finite'] = assume_finite + local_config['assume_finite'] = assume_finite if working_memory is not None: - _threadlocal.global_config['working_memory'] = working_memory + local_config['working_memory'] = working_memory if print_changed_only is not None: - _threadlocal.global_config['print_changed_only'] = print_changed_only + local_config['print_changed_only'] = print_changed_only if display is not None: - _threadlocal.global_config['display'] = display + local_config['display'] = display @contextmanager From 91d18c116d71cf74b2cfb8e36eee22eb61b8ec14 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Fri, 9 Apr 2021 09:37:36 -0400 Subject: [PATCH 10/16] DOC Better docstring --- sklearn/_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index 9fba4d32d7748..56ab18482a07e 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -14,7 +14,8 @@ def _get_threadlocal(): - """Set the configuration that is local to the thread.""" + """Get the configuration that is local to the thread. If the configuration + does not exist, copy the default global configuration.""" if not hasattr(_threadlocal, 'global_config'): _threadlocal.global_config = _global_config.copy() return _threadlocal.global_config From 276c720cd30a8bfb6717326422097fc2c6ff3836 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Sat, 10 Apr 2021 08:20:39 -0400 Subject: [PATCH 11/16] WIP Fixes merge error --- sklearn/tests/test_config.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sklearn/tests/test_config.py b/sklearn/tests/test_config.py index 23f341a1036aa..6d458088a37a8 100644 --- a/sklearn/tests/test_config.py +++ b/sklearn/tests/test_config.py @@ -6,7 +6,6 @@ import pytest from sklearn import get_config, set_config, config_context -from sklearn.utils._testing import assert_raises from sklearn.utils.fixes import delayed from sklearn.utils.fixes import parse_version @@ -83,8 +82,8 @@ def test_set_config(): assert get_config()['assume_finite'] is False # No unknown arguments -<<<<<<< HEAD - assert_raises(TypeError, set_config, do_something_else=True) + with pytest.raises(TypeError): + set_config(do_something_else=True) def set_assume_finite(assume_finite, sleep_duration): @@ -132,7 +131,3 @@ def test_config_threadsafe(): e.map(set_assume_finite, assume_finites, sleep_durations)] assert items == [False, True] -======= - with pytest.raises(TypeError): - set_config(do_something_else=True) ->>>>>>> upstream/main From 1b28965376158f13da51816cbae49e544312edf6 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Sat, 10 Apr 2021 08:24:18 -0400 Subject: [PATCH 12/16] CLN Less lines of code --- sklearn/_config.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index 56ab18482a07e..3530d2b7beb41 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -13,14 +13,6 @@ _threadlocal = threading.local() -def _get_threadlocal(): - """Get the configuration that is local to the thread. If the configuration - does not exist, copy the default global configuration.""" - if not hasattr(_threadlocal, 'global_config'): - _threadlocal.global_config = _global_config.copy() - return _threadlocal.global_config - - def get_config(): """Retrieve current values for configuration set by :func:`set_config` @@ -34,7 +26,11 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ - return _get_threadlocal() + # Get the configuration that is local to the thread. If the configuration + # does not exist, copy the default global configuration. + if not hasattr(_threadlocal, 'global_config'): + _threadlocal.global_config = _global_config.copy() + return _threadlocal.global_config def set_config(assume_finite=None, working_memory=None, @@ -82,7 +78,7 @@ def set_config(assume_finite=None, working_memory=None, config_context : Context manager for global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - local_config = _get_threadlocal() + local_config = get_config() if assume_finite is not None: local_config['assume_finite'] = assume_finite From 319f007b3f62a7dfa766765ff32e972fcf7cf390 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Sat, 10 Apr 2021 08:29:29 -0400 Subject: [PATCH 13/16] CLN Better copy logic --- sklearn/_config.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index 3530d2b7beb41..ba584717373db 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -13,6 +13,14 @@ _threadlocal = threading.local() +def _get_threadlocal_config(): + """Get the configuration that is local to the thread. If the configuration + does not exist, copy the default global configuration.""" + if not hasattr(_threadlocal, 'global_config'): + _threadlocal.global_config = _global_config.copy() + return _threadlocal.global_config + + def get_config(): """Retrieve current values for configuration set by :func:`set_config` @@ -26,11 +34,7 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ - # Get the configuration that is local to the thread. If the configuration - # does not exist, copy the default global configuration. - if not hasattr(_threadlocal, 'global_config'): - _threadlocal.global_config = _global_config.copy() - return _threadlocal.global_config + return _get_threadlocal_config().copy() def set_config(assume_finite=None, working_memory=None, @@ -78,7 +82,7 @@ def set_config(assume_finite=None, working_memory=None, config_context : Context manager for global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - local_config = get_config() + local_config = _get_threadlocal_config() if assume_finite is not None: local_config['assume_finite'] = assume_finite @@ -148,7 +152,7 @@ def config_context(**new_config): set_config : Set global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - old_config = get_config().copy() + old_config = _get_threadlocal_config().copy() set_config(**new_config) try: From 450a4568264c6a5852e5078a7cfe775e2ffac188 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Sat, 10 Apr 2021 08:34:29 -0400 Subject: [PATCH 14/16] CLN Do not need copy in config_context --- sklearn/_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index ba584717373db..feaf541ea3b9d 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -152,7 +152,7 @@ def config_context(**new_config): set_config : Set global scikit-learn configuration. get_config : Retrieve current values of the global configuration. """ - old_config = _get_threadlocal_config().copy() + old_config = get_config() set_config(**new_config) try: From 957b3c045686edfbfe5b8ed3276d6048880406a8 Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Tue, 27 Apr 2021 10:56:07 -0400 Subject: [PATCH 15/16] DOC Adds comment about mutable --- sklearn/_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sklearn/_config.py b/sklearn/_config.py index feaf541ea3b9d..b9aa9cf51d3b9 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -14,7 +14,7 @@ def _get_threadlocal_config(): - """Get the configuration that is local to the thread. If the configuration + """Get a threadlocal **mutable** configuration. If the configuration does not exist, copy the default global configuration.""" if not hasattr(_threadlocal, 'global_config'): _threadlocal.global_config = _global_config.copy() From 8ef81f73d86b10c83c2769bf2e4a64b4a97edf1d Mon Sep 17 00:00:00 2001 From: "Thomas J. Fan" Date: Tue, 27 Apr 2021 10:58:14 -0400 Subject: [PATCH 16/16] DOC Adds comment for copy --- sklearn/_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sklearn/_config.py b/sklearn/_config.py index b9aa9cf51d3b9..e81d50849db05 100644 --- a/sklearn/_config.py +++ b/sklearn/_config.py @@ -34,6 +34,8 @@ def get_config(): config_context : Context manager for global scikit-learn configuration. set_config : Set global scikit-learn configuration. """ + # Return a copy of the threadlocal configuration so that users will + # not be able to modify the configuration with the returned dict. return _get_threadlocal_config().copy()