├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── contextvars └── __init__.py ├── pytest.ini ├── setup.py └── tests ├── __init__.py └── test_basics.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [Makefile] 8 | indent_style = tab 9 | 10 | [*.{py,pyx,pxd,pxi,yml,h}] 11 | indent_size = 4 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *._* 2 | *.pyc 3 | *.pyo 4 | *.so 5 | *~ 6 | .#* 7 | .DS_Store 8 | /build 9 | __pycache__/ 10 | /*.egg 11 | /*.egg-info 12 | /dist 13 | /.cache 14 | /.pytest_cache 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 3.5 5 | - 3.6 6 | 7 | script: 8 | - pip install -e . 9 | - python setup.py test 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present MagicStack Inc. http://magic.io 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright (c) 2015-present MagicStack Inc. http://magic.io 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include contextvars *.py 2 | recursive-include tests *.py 3 | include LICENSE README.rst 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/MagicStack/contextvars.svg?branch=master 2 | :target: https://travis-ci.org/MagicStack/contextvars 3 | 4 | 5 | PEP 567 Backport 6 | ================ 7 | 8 | This package implements a backport of Python 3.7 ``contextvars`` 9 | module (see PEP 567) for Python 3.6. 10 | 11 | **Important:** at this moment this package does not provide an 12 | asyncio event loop with PEP 567 support yet. Stay tuned for updates. 13 | 14 | 15 | Original "contextvars" Package 16 | ============================== 17 | 18 | This package replaces the old "contextvars" PyPI package which 19 | repository is available `here `_. 20 | 21 | 22 | Documentation 23 | ============= 24 | 25 | Read the official ``contextvars`` module documentation here: 26 | https://docs.python.org/3.7/library/contextvars.html 27 | 28 | 29 | `PEP 567 `_ also provides 30 | a comprehensive overview of the API and explains all design choices. 31 | 32 | 33 | Installation 34 | ============ 35 | 36 | .. code-block:: bash 37 | 38 | $ pip install contextvars 39 | 40 | 41 | Usage 42 | ===== 43 | 44 | .. code-block:: python 45 | 46 | import contextvars 47 | 48 | my_var = contextvars.ContextVar('my_var') 49 | 50 | # ... 51 | 52 | 53 | Listing as a Dependency 54 | ======================= 55 | 56 | The good news is that the standard library always takes the 57 | precedence over site packages, so even if a local ``contextvars`` 58 | module is installed, the one from the standard library will be used. 59 | Therefore you can simply list "contextvars" in your 60 | ``requirements.txt`` or ``setup.py`` files. 61 | 62 | Another option is to use `"platform specific dependencies" 63 | `_ setuptools feature: 65 | 66 | .. code-block:: python 67 | 68 | import setuptools 69 | 70 | setuptools.setup( 71 | name="Project", 72 | ... 73 | install_requires=[ 74 | 'contextvars;python_version<"3.7"' 75 | ] 76 | ) 77 | 78 | 79 | License 80 | ======= 81 | 82 | Apache 2.0. 83 | -------------------------------------------------------------------------------- /contextvars/__init__.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | import threading 3 | 4 | import immutables 5 | 6 | 7 | __all__ = ('ContextVar', 'Context', 'Token', 'copy_context') 8 | 9 | 10 | _NO_DEFAULT = object() 11 | 12 | 13 | class ContextMeta(type(collections.abc.Mapping)): 14 | 15 | # contextvars.Context is not subclassable. 16 | 17 | def __new__(mcls, names, bases, dct): 18 | cls = super().__new__(mcls, names, bases, dct) 19 | if cls.__module__ != 'contextvars' or cls.__name__ != 'Context': 20 | raise TypeError("type 'Context' is not an acceptable base type") 21 | return cls 22 | 23 | 24 | class Context(collections.abc.Mapping, metaclass=ContextMeta): 25 | 26 | def __init__(self): 27 | self._data = immutables.Map() 28 | self._prev_context = None 29 | 30 | def run(self, callable, *args, **kwargs): 31 | if self._prev_context is not None: 32 | raise RuntimeError( 33 | 'cannot enter context: {} is already entered'.format(self)) 34 | 35 | self._prev_context = _get_context() 36 | try: 37 | _set_context(self) 38 | return callable(*args, **kwargs) 39 | finally: 40 | _set_context(self._prev_context) 41 | self._prev_context = None 42 | 43 | def copy(self): 44 | new = Context() 45 | new._data = self._data 46 | return new 47 | 48 | def __getitem__(self, var): 49 | if not isinstance(var, ContextVar): 50 | raise TypeError( 51 | "a ContextVar key was expected, got {!r}".format(var)) 52 | return self._data[var] 53 | 54 | def __contains__(self, var): 55 | if not isinstance(var, ContextVar): 56 | raise TypeError( 57 | "a ContextVar key was expected, got {!r}".format(var)) 58 | return var in self._data 59 | 60 | def __len__(self): 61 | return len(self._data) 62 | 63 | def __iter__(self): 64 | return iter(self._data) 65 | 66 | 67 | class ContextVarMeta(type): 68 | 69 | # contextvars.ContextVar is not subclassable. 70 | 71 | def __new__(mcls, names, bases, dct): 72 | cls = super().__new__(mcls, names, bases, dct) 73 | if cls.__module__ != 'contextvars' or cls.__name__ != 'ContextVar': 74 | raise TypeError("type 'ContextVar' is not an acceptable base type") 75 | return cls 76 | 77 | def __getitem__(cls, name): 78 | return 79 | 80 | 81 | class ContextVar(metaclass=ContextVarMeta): 82 | 83 | def __init__(self, name, *, default=_NO_DEFAULT): 84 | if not isinstance(name, str): 85 | raise TypeError("context variable name must be a str") 86 | self._name = name 87 | self._default = default 88 | 89 | @property 90 | def name(self): 91 | return self._name 92 | 93 | def get(self, default=_NO_DEFAULT): 94 | ctx = _get_context() 95 | try: 96 | return ctx[self] 97 | except KeyError: 98 | pass 99 | 100 | if default is not _NO_DEFAULT: 101 | return default 102 | 103 | if self._default is not _NO_DEFAULT: 104 | return self._default 105 | 106 | raise LookupError 107 | 108 | def set(self, value): 109 | ctx = _get_context() 110 | data = ctx._data 111 | try: 112 | old_value = data[self] 113 | except KeyError: 114 | old_value = Token.MISSING 115 | 116 | updated_data = data.set(self, value) 117 | ctx._data = updated_data 118 | return Token(ctx, self, old_value) 119 | 120 | def reset(self, token): 121 | if token._used: 122 | raise RuntimeError("Token has already been used once") 123 | 124 | if token._var is not self: 125 | raise ValueError( 126 | "Token was created by a different ContextVar") 127 | 128 | if token._context is not _get_context(): 129 | raise ValueError( 130 | "Token was created in a different Context") 131 | 132 | ctx = token._context 133 | if token._old_value is Token.MISSING: 134 | ctx._data = ctx._data.delete(token._var) 135 | else: 136 | ctx._data = ctx._data.set(token._var, token._old_value) 137 | 138 | token._used = True 139 | 140 | def __repr__(self): 141 | r = ''.format(id(self)) 145 | 146 | 147 | class TokenMeta(type): 148 | 149 | # contextvars.Token is not subclassable. 150 | 151 | def __new__(mcls, names, bases, dct): 152 | cls = super().__new__(mcls, names, bases, dct) 153 | if cls.__module__ != 'contextvars' or cls.__name__ != 'Token': 154 | raise TypeError("type 'Token' is not an acceptable base type") 155 | return cls 156 | 157 | 158 | class Token(metaclass=TokenMeta): 159 | 160 | MISSING = object() 161 | 162 | def __init__(self, context, var, old_value): 163 | self._context = context 164 | self._var = var 165 | self._old_value = old_value 166 | self._used = False 167 | 168 | @property 169 | def var(self): 170 | return self._var 171 | 172 | @property 173 | def old_value(self): 174 | return self._old_value 175 | 176 | def __repr__(self): 177 | r = '=0.9', 21 | ], 22 | license='Apache License, Version 2.0', 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | 'Programming Language :: Python :: 3 :: Only', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Operating System :: POSIX', 29 | 'Operating System :: MacOS :: MacOS X', 30 | 'Operating System :: Microsoft :: Windows', 31 | 'License :: OSI Approved :: Apache Software License', 32 | 'Intended Audience :: Developers', 33 | ], 34 | include_package_data=True, 35 | test_suite='tests.suite', 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | import unittest 4 | import unittest.runner 5 | 6 | 7 | def suite(): 8 | test_loader = unittest.TestLoader() 9 | test_suite = test_loader.discover( 10 | os.path.dirname(__file__), pattern='test_*.py') 11 | return test_suite 12 | 13 | 14 | if __name__ == '__main__': 15 | runner = unittest.runner.TextTestRunner() 16 | result = runner.run(suite()) 17 | sys.exit(not result.wasSuccessful()) 18 | -------------------------------------------------------------------------------- /tests/test_basics.py: -------------------------------------------------------------------------------- 1 | # Tests are copied from cpython/Lib/test/test_context.py 2 | # License: PSFL 3 | # Copyright: 2018 Python Software Foundation 4 | 5 | 6 | import concurrent.futures 7 | import functools 8 | import random 9 | import time 10 | import unittest 11 | 12 | import contextvars 13 | 14 | 15 | def isolated_context(func): 16 | """Needed to make reftracking test mode work.""" 17 | @functools.wraps(func) 18 | def wrapper(*args, **kwargs): 19 | ctx = contextvars.Context() 20 | return ctx.run(func, *args, **kwargs) 21 | return wrapper 22 | 23 | 24 | class ContextTest(unittest.TestCase): 25 | def test_context_var_new_1(self): 26 | with self.assertRaises(TypeError): 27 | contextvars.ContextVar() 28 | 29 | with self.assertRaisesRegex(TypeError, 'must be a str'): 30 | contextvars.ContextVar(1) 31 | 32 | c = contextvars.ContextVar('a') 33 | self.assertNotEqual(hash(c), hash('a')) 34 | 35 | def test_context_var_new_2(self): 36 | self.assertIsNone(contextvars.ContextVar[int]) 37 | 38 | @isolated_context 39 | def test_context_var_repr_1(self): 40 | c = contextvars.ContextVar('a') 41 | self.assertIn('a', repr(c)) 42 | 43 | c = contextvars.ContextVar('a', default=123) 44 | self.assertIn('123', repr(c)) 45 | 46 | lst = [] 47 | c = contextvars.ContextVar('a', default=lst) 48 | lst.append(c) 49 | self.assertIn('...', repr(c)) 50 | self.assertIn('...', repr(lst)) 51 | 52 | t = c.set(1) 53 | self.assertIn(repr(c), repr(t)) 54 | self.assertNotIn(' used ', repr(t)) 55 | c.reset(t) 56 | self.assertIn(' used ', repr(t)) 57 | 58 | def test_context_subclassing_1(self): 59 | with self.assertRaisesRegex(TypeError, 'not an acceptable base type'): 60 | class MyContextVar(contextvars.ContextVar): 61 | # Potentially we might want ContextVars to be subclassable. 62 | pass 63 | 64 | with self.assertRaisesRegex(TypeError, 'not an acceptable base type'): 65 | class MyContext(contextvars.Context): 66 | pass 67 | 68 | with self.assertRaisesRegex(TypeError, 'not an acceptable base type'): 69 | class MyToken(contextvars.Token): 70 | pass 71 | 72 | def test_context_new_1(self): 73 | with self.assertRaises(TypeError): 74 | contextvars.Context(1) 75 | with self.assertRaises(TypeError): 76 | contextvars.Context(1, a=1) 77 | with self.assertRaises(TypeError): 78 | contextvars.Context(a=1) 79 | contextvars.Context(**{}) 80 | 81 | def test_context_typerrors_1(self): 82 | ctx = contextvars.Context() 83 | 84 | with self.assertRaisesRegex(TypeError, 'ContextVar key was expected'): 85 | ctx[1] 86 | with self.assertRaisesRegex(TypeError, 'ContextVar key was expected'): 87 | 1 in ctx 88 | with self.assertRaisesRegex(TypeError, 'ContextVar key was expected'): 89 | ctx.get(1) 90 | 91 | def test_context_get_context_1(self): 92 | ctx = contextvars.copy_context() 93 | self.assertIsInstance(ctx, contextvars.Context) 94 | 95 | def test_context_run_1(self): 96 | ctx = contextvars.Context() 97 | 98 | with self.assertRaisesRegex(TypeError, 'missing 1 required'): 99 | ctx.run() 100 | 101 | def test_context_run_2(self): 102 | ctx = contextvars.Context() 103 | 104 | def func(*args, **kwargs): 105 | kwargs['spam'] = 'foo' 106 | args += ('bar',) 107 | return args, kwargs 108 | 109 | for f in (func, functools.partial(func)): 110 | # partial doesn't support FASTCALL 111 | 112 | self.assertEqual(ctx.run(f), (('bar',), {'spam': 'foo'})) 113 | self.assertEqual(ctx.run(f, 1), ((1, 'bar'), {'spam': 'foo'})) 114 | 115 | self.assertEqual( 116 | ctx.run(f, a=2), 117 | (('bar',), {'a': 2, 'spam': 'foo'})) 118 | 119 | self.assertEqual( 120 | ctx.run(f, 11, a=2), 121 | ((11, 'bar'), {'a': 2, 'spam': 'foo'})) 122 | 123 | a = {} 124 | self.assertEqual( 125 | ctx.run(f, 11, **a), 126 | ((11, 'bar'), {'spam': 'foo'})) 127 | self.assertEqual(a, {}) 128 | 129 | def test_context_run_3(self): 130 | ctx = contextvars.Context() 131 | 132 | def func(*args, **kwargs): 133 | 1 / 0 134 | 135 | with self.assertRaises(ZeroDivisionError): 136 | ctx.run(func) 137 | with self.assertRaises(ZeroDivisionError): 138 | ctx.run(func, 1, 2) 139 | with self.assertRaises(ZeroDivisionError): 140 | ctx.run(func, 1, 2, a=123) 141 | 142 | @isolated_context 143 | def test_context_run_4(self): 144 | ctx1 = contextvars.Context() 145 | ctx2 = contextvars.Context() 146 | var = contextvars.ContextVar('var') 147 | 148 | def func2(): 149 | self.assertIsNone(var.get(None)) 150 | 151 | def func1(): 152 | self.assertIsNone(var.get(None)) 153 | var.set('spam') 154 | ctx2.run(func2) 155 | self.assertEqual(var.get(None), 'spam') 156 | 157 | cur = contextvars.copy_context() 158 | self.assertEqual(len(cur), 1) 159 | self.assertEqual(cur[var], 'spam') 160 | return cur 161 | 162 | returned_ctx = ctx1.run(func1) 163 | self.assertEqual(ctx1, returned_ctx) 164 | self.assertEqual(returned_ctx[var], 'spam') 165 | self.assertIn(var, returned_ctx) 166 | 167 | def test_context_run_5(self): 168 | ctx = contextvars.Context() 169 | var = contextvars.ContextVar('var') 170 | 171 | def func(): 172 | self.assertIsNone(var.get(None)) 173 | var.set('spam') 174 | 1 / 0 175 | 176 | with self.assertRaises(ZeroDivisionError): 177 | ctx.run(func) 178 | 179 | self.assertIsNone(var.get(None)) 180 | 181 | def test_context_run_6(self): 182 | ctx = contextvars.Context() 183 | c = contextvars.ContextVar('a', default=0) 184 | 185 | def fun(): 186 | self.assertEqual(c.get(), 0) 187 | self.assertIsNone(ctx.get(c)) 188 | 189 | c.set(42) 190 | self.assertEqual(c.get(), 42) 191 | self.assertEqual(ctx.get(c), 42) 192 | 193 | ctx.run(fun) 194 | 195 | def test_context_run_7(self): 196 | ctx = contextvars.Context() 197 | 198 | def fun(): 199 | with self.assertRaisesRegex(RuntimeError, 'is already entered'): 200 | ctx.run(fun) 201 | 202 | ctx.run(fun) 203 | 204 | @isolated_context 205 | def test_context_getset_1(self): 206 | c = contextvars.ContextVar('c') 207 | with self.assertRaises(LookupError): 208 | c.get() 209 | 210 | self.assertIsNone(c.get(None)) 211 | 212 | t0 = c.set(42) 213 | self.assertEqual(c.get(), 42) 214 | self.assertEqual(c.get(None), 42) 215 | self.assertIs(t0.old_value, t0.MISSING) 216 | self.assertIs(t0.old_value, contextvars.Token.MISSING) 217 | self.assertIs(t0.var, c) 218 | 219 | t = c.set('spam') 220 | self.assertEqual(c.get(), 'spam') 221 | self.assertEqual(c.get(None), 'spam') 222 | self.assertEqual(t.old_value, 42) 223 | c.reset(t) 224 | 225 | self.assertEqual(c.get(), 42) 226 | self.assertEqual(c.get(None), 42) 227 | 228 | c.set('spam2') 229 | with self.assertRaisesRegex(RuntimeError, 'has already been used'): 230 | c.reset(t) 231 | self.assertEqual(c.get(), 'spam2') 232 | 233 | ctx1 = contextvars.copy_context() 234 | self.assertIn(c, ctx1) 235 | 236 | c.reset(t0) 237 | with self.assertRaisesRegex(RuntimeError, 'has already been used'): 238 | c.reset(t0) 239 | self.assertIsNone(c.get(None)) 240 | 241 | self.assertIn(c, ctx1) 242 | self.assertEqual(ctx1[c], 'spam2') 243 | self.assertEqual(ctx1.get(c, 'aa'), 'spam2') 244 | self.assertEqual(len(ctx1), 1) 245 | self.assertEqual(list(ctx1.items()), [(c, 'spam2')]) 246 | self.assertEqual(list(ctx1.values()), ['spam2']) 247 | self.assertEqual(list(ctx1.keys()), [c]) 248 | self.assertEqual(list(ctx1), [c]) 249 | 250 | ctx2 = contextvars.copy_context() 251 | self.assertNotIn(c, ctx2) 252 | with self.assertRaises(KeyError): 253 | ctx2[c] 254 | self.assertEqual(ctx2.get(c, 'aa'), 'aa') 255 | self.assertEqual(len(ctx2), 0) 256 | self.assertEqual(list(ctx2), []) 257 | 258 | @isolated_context 259 | def test_context_getset_2(self): 260 | v1 = contextvars.ContextVar('v1') 261 | v2 = contextvars.ContextVar('v2') 262 | 263 | t1 = v1.set(42) 264 | with self.assertRaisesRegex(ValueError, 'by a different'): 265 | v2.reset(t1) 266 | 267 | @isolated_context 268 | def test_context_getset_3(self): 269 | c = contextvars.ContextVar('c', default=42) 270 | ctx = contextvars.Context() 271 | 272 | def fun(): 273 | self.assertEqual(c.get(), 42) 274 | with self.assertRaises(KeyError): 275 | ctx[c] 276 | self.assertIsNone(ctx.get(c)) 277 | self.assertEqual(ctx.get(c, 'spam'), 'spam') 278 | self.assertNotIn(c, ctx) 279 | self.assertEqual(list(ctx.keys()), []) 280 | 281 | t = c.set(1) 282 | self.assertEqual(list(ctx.keys()), [c]) 283 | self.assertEqual(ctx[c], 1) 284 | 285 | c.reset(t) 286 | self.assertEqual(list(ctx.keys()), []) 287 | with self.assertRaises(KeyError): 288 | ctx[c] 289 | 290 | ctx.run(fun) 291 | 292 | @isolated_context 293 | def test_context_getset_4(self): 294 | c = contextvars.ContextVar('c', default=42) 295 | ctx = contextvars.Context() 296 | 297 | tok = ctx.run(c.set, 1) 298 | 299 | with self.assertRaisesRegex(ValueError, 'different Context'): 300 | c.reset(tok) 301 | 302 | @isolated_context 303 | def test_context_getset_5(self): 304 | c = contextvars.ContextVar('c', default=42) 305 | c.set([]) 306 | 307 | def fun(): 308 | c.set([]) 309 | c.get().append(42) 310 | self.assertEqual(c.get(), [42]) 311 | 312 | contextvars.copy_context().run(fun) 313 | self.assertEqual(c.get(), []) 314 | 315 | def test_context_copy_1(self): 316 | ctx1 = contextvars.Context() 317 | c = contextvars.ContextVar('c', default=42) 318 | 319 | def ctx1_fun(): 320 | c.set(10) 321 | 322 | ctx2 = ctx1.copy() 323 | self.assertEqual(ctx2[c], 10) 324 | 325 | c.set(20) 326 | self.assertEqual(ctx1[c], 20) 327 | self.assertEqual(ctx2[c], 10) 328 | 329 | ctx2.run(ctx2_fun) 330 | self.assertEqual(ctx1[c], 20) 331 | self.assertEqual(ctx2[c], 30) 332 | 333 | def ctx2_fun(): 334 | self.assertEqual(c.get(), 10) 335 | c.set(30) 336 | self.assertEqual(c.get(), 30) 337 | 338 | ctx1.run(ctx1_fun) 339 | 340 | @isolated_context 341 | def test_context_threads_1(self): 342 | cvar = contextvars.ContextVar('cvar') 343 | 344 | def sub(num): 345 | for i in range(10): 346 | cvar.set(num + i) 347 | time.sleep(random.uniform(0.001, 0.05)) 348 | self.assertEqual(cvar.get(), num + i) 349 | return num 350 | 351 | tp = concurrent.futures.ThreadPoolExecutor(max_workers=10) 352 | try: 353 | results = list(tp.map(sub, range(10))) 354 | finally: 355 | tp.shutdown() 356 | self.assertEqual(results, list(range(10))) 357 | --------------------------------------------------------------------------------