diff --git a/CHANGELOG.md b/CHANGELOG.md index e048783a204866246ff68e054efdadb395b847b1..e069212ae9f12b3cea5e9b5b4bd51906bf42c85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -# v2.21.4 +# v2.22.0rc1 * moar tests for CPManager -* added tainting \ No newline at end of file +* added tainting +* added Environments \ No newline at end of file diff --git a/docs/coding/environment.rst b/docs/coding/environment.rst new file mode 100644 index 0000000000000000000000000000000000000000..46fe521079a5f2c7e9a14ed00d1506fc9672bed6 --- /dev/null +++ b/docs/coding/environment.rst @@ -0,0 +1,21 @@ +Sometimes, you have a bunch of arguments to functions +such as the database connection object, and would +prefer them not to be passed by an argument, but configured +through Satella's Environments themselves: + +.. autoclass:: satella.coding.Context + :members: + +You can think of them as a stack of cards each carrying a set of variable names. The variable, if not present +on the current card, will be checked upwards for parent of this card. Each thread, by default, gets a +separate hierarchy. Removing a variable on one card will not affect it's parent, however the variable will remain +inaccessible for the duration of this context. Use them like this: + +.. code-block:: python + + with Context() as ctxt: + ctxt.value = 55 + with Context() as new_ctxt: + new_ctxt.value = 66 + assert ctxt.value == 66 + assert ctxt.value == 55 diff --git a/docs/index.rst b/docs/index.rst index 2ffd07412087b8384ad899c24d8bcfb46e6f50f0..1baf94281037b357ac2541a3ffacb8825a5f1b72 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Visit the project's page at GitHub_! configuration/schema configuration/sources coding/ctxt_managers + coding/environment coding/functions coding/futures coding/structures diff --git a/satella/__init__.py b/satella/__init__.py index 68faeb8bf0bc18a23e81f9b28c929371d2c42b51..9015163e78de0fadeee4903d6861cdef1a2e1f84 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.21.4a2' +__version__ = '2.22.0rc2' diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py index b8963fa4d7c99dd537db82e6d03f5a7db76d3aca..612957d321d6b21dc52a7360b7a4219211612f49 100644 --- a/satella/coding/__init__.py +++ b/satella/coding/__init__.py @@ -17,6 +17,7 @@ from .misc import update_if_not_none, update_key_if_none, update_attr_if_none, q update_key_if_not_none, source_to_function, update_key_if_true, \ get_arguments, call_with_arguments, chain_callables, Closeable, contains, \ enum_value +from .environment import Context from .overloading import overload, class_or_instancemethod from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \ raises_exception, reraise_as @@ -24,7 +25,7 @@ from .expect_exception import expect_exception from .deep_compare import assert_equal, InequalityReason, Inequal __all__ = [ - 'EmptyContextManager', + 'EmptyContextManager', 'Context', 'assert_equal', 'InequalityReason', 'Inequal', 'Closeable', 'contains', 'enum_value', 'reraise_as', 'expect_exception', diff --git a/satella/coding/environment.py b/satella/coding/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..c0482a1d344dcc9156e5ec8f4309b61ad45cc906 --- /dev/null +++ b/satella/coding/environment.py @@ -0,0 +1,105 @@ +from __future__ import annotations +import typing as tp +import threading +from satella.coding.typing import V + +from .misc import Closeable + + +local = threading.local() + + +THEY_HATIN = object() + + +class Context: + """ + New layer of environment. Can have it's own variables, or can hoist them onto the parent. + """ + + def __init__(self, parent: tp.Optional[Context] = None, **variables): + self.parent = parent + self.variables = {} + self.bool = None + + def __str__(self): + return str(id(self)) + + def push_up(self, item: str) -> None: + """ + Advance current variable to the top of the card stack. + + :param item: variable name + """ + var = self.variables.pop(item) + self.parent.variables[item] = var + + def __getattr__(self, item: str): + if item in self.variables: + v = self.variables[item] + if v is not THEY_HATIN: + return v + raise AttributeError() + if self.parent is not None: + return getattr(self.parent, item) + raise AttributeError() + + def __enter__(self): + global local + try: + parent = local.thread_context + except AttributeError: + parent = None + ctxt = Context(parent=parent) + if ctxt is not parent: + ctxt.parent = parent + local.thread_context = ctxt + return ctxt + + def __setattr__(self, key: str, value: V): + """ + Set a value + """ + if key in ('parent', 'variables', 'bool'): + return super().__setattr__(key, value) + else: + self.variables[key] = value + + def __delattr__(self, item: str) -> None: + self.variables[item] = THEY_HATIN + + def does_exist(self, val: str) -> bool: + """ + Does a given value exist on stack for this call of function? + """ + if val in self.variables: + if self.variables[val] is THEY_HATIN: + return False + return True + else: + if self.parent is None: + return False + return self.parent.does_exist(val) + + def __exit__(self, exc_type, exc_val, exc_tb) -> bool: + if self.parent is None: + try: + del local.thread_context + except AttributeError: + pass + else: + local.thread_context = self.parent + return False + + @staticmethod + def get() -> Context: + """ + Return a local context for this thread + """ + global local + try: + return local.thread_context + except AttributeError: + ctxt = Context() + local.thread_context = ctxt + return ctxt diff --git a/satella/debug/test_environment.py b/satella/debug/test_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..c6c743466e18bd7ce4d306feae5037c495b997c3 --- /dev/null +++ b/satella/debug/test_environment.py @@ -0,0 +1,31 @@ +import unittest + +from satella.coding.environment import Context + + +class TestEnvs(unittest.TestCase): + + def test_envs(self): + ctxt = Context.get() + ctxt.value = 5 + self.assertEqual(ctxt.value, 5) + with Context() as new_ctxt: + self.assertEqual(new_ctxt.value, 5) + + def test_delete_envs(self): + ctxt = Context.get() + ctxt.value = 5 + self.assertEqual(ctxt.value, 5) + with Context() as new_ctxt: + del new_ctxt.value + self.assertRaises(AttributeError, lambda: new_ctxt.value) + self.assertEqual(ctxt.value, 5) + + def test_nesting(self): + with Context() as ctxt: + ctxt.value = 55 + with Context() as new_ctxt: + new_ctxt.value = 66 + assert new_ctxt.value == 66 + assert ctxt.value == 55 +