Source code for ideas.examples.constants

"""
This import hook makes it possible to define constants, that is variables
which cannot have their initial value changed.

In this import hook, constants are identified in two ways:

1. names in all ``UPPERCASE_LETTERS``, which is a common Python convention

2. variables that were declared to be constant by the inclusion
   of a ``Final`` type hint.
"""


import types

from ideas import import_hook
from ideas.console import CONSOLE_NAME
import token_utils

shorten_path = import_hook.shorten_path
CONSTANTS = {}
DECLARED_FINAL = {}


def make_class(on_prevent_change=True):
    class ModuleWithConstants(types.ModuleType):
        """Special module type that prevents variables identified as constants
        to have their value changed.

        This is used to replace a module __class__ from the default as follows:

            module.__class__ = ModuleWithConstants

        This example only prints a message when an attempt is made to change
        the value of a constant. Alternatively, this could be logged or an
        exception could be raised.
        """

        def __setattr__(self, key, value):
            if (
                key in CONSTANTS[self.__file__]
                or key == key.upper()
                or key in DECLARED_FINAL[self.__file__]
            ):
                if on_prevent_change:
                    if callable(on_prevent_change):
                        on_prevent_change(  # noqa
                            filename=self.__file__, key=key, value=value, kind="set"
                        )
                    return
            super().__setattr__(key, value)

        def __delattr__(self, key):
            if key in CONSTANTS[self.__file__]:
                if on_prevent_change:
                    if callable(on_prevent_change):
                        on_prevent_change(  # noqa
                            filename=self.__file__, key=key, kind="delete"
                        )
                    return
            super().__delattr__(key)

    return ModuleWithConstants


[docs]class FinalDict(dict): """A ``FinalDict`` is a ``dict`` subclass which ensures that constants are not over-written. Constants are identified in two ways: 1. names in all ``UPPERCASE_LETTERS``, which is a common Python convention 2. variables that were declared to be constant by the inclusion of a ``Final`` type hint. Note: We only override methods which could result in changing the value of a constant. """ def __init__(self, module_filename, on_prevent_change=True): """Initialises the instance""" self.__file__ = module_filename self.on_prevent_change = on_prevent_change super().__init__() def __setitem__(self, key, value): """Sets self[key] to value. If key is identified as a constant, it prevents changing its value after initial assignment. """ if key in CONSTANTS[self.__file__]: if self.on_prevent_change: if callable(self.on_prevent_change): self.on_prevent_change( filename=self.__file__, key=key, value=value, kind="set" ) return if key == key.upper() or key in DECLARED_FINAL[self.__file__]: CONSTANTS[self.__file__][key] = value return super().__setitem__(key, value) def __delitem__(self, key): """Deletes self[key] unless key is identified as a constant""" if key in CONSTANTS[self.__file__]: if self.on_prevent_change: if callable(self.on_prevent_change): self.on_prevent_change( filename=self.__file__, key=key, kind="delete" ) return return return super().__delitem__(key)
[docs] def setdefault(self, key, default=None): """Insert key with a value of default if key is not in the dictionary. It prevents changes if the key is identified as a constant. """ if key in CONSTANTS[self.__file__]: if self.on_prevent_change: if callable(self.on_prevent_change): self.on_prevent_change( filename=self.__file__, key=key, value=default, kind="set" ) return if key == key.upper() or key in DECLARED_FINAL[self.__file__]: CONSTANTS[self.__file__][key] = default return super().setdefault(key, default)
[docs] def pop(self, key): """D.pop(key) -> value, remove specified key and return the corresponding value, unless the key is identified as a constant. If key is not found, d is returned if given, otherwise KeyError is raised """ if key in CONSTANTS[self.__file__]: if self.on_prevent_change: if callable(self.on_prevent_change): self.on_prevent_change( filename=self.__file__, key=key, kind="delete" ) return CONSTANTS[self.__file__][key] return super().pop(key)
[docs] def update(self, mapping_or_iterable=(), **kwargs): """Updates the content of the dict from a mapping or an iterable, or from a list of keywords arguments. Keys identified as constants are prevented from changing. """ if hasattr(mapping_or_iterable, "keys"): for key in mapping_or_iterable: self.__setitem__(key, mapping_or_iterable[key]) else: for key, value in mapping_or_iterable: self.__setitem__(key, value) for key in kwargs: self.__setitem__(key, kwargs[key])
[docs]def transform_source(source, filename=None, **_kwargs): """Identifies simple assignments with a Final type hint, returning the source unchanged. The pattern we are looking for is:: |python_identifier : Final ... where ``|`` indicates the beginning of a line. """ if filename not in CONSTANTS: CONSTANTS[filename] = {} if filename not in DECLARED_FINAL: DECLARED_FINAL[filename] = set([]) for tokens in token_utils.get_lines(source): # a line of tokens can start with DEDENT tokens ... if token_utils.get_number(tokens) > 3: index = token_utils.get_first_index(tokens) first_token = tokens[index] if ( first_token.start_col == 0 and first_token.is_identifier() and tokens[index + 1] == ":" and tokens[index + 2] == "Final" ): DECLARED_FINAL[filename].add(first_token.string) return source
[docs]def exec_(code_object, filename=None, module=None, callback_params=None, **kwargs): """Executes a code_object in a local instance of a ``FinalDict``. The argument ``globals_`` is assumed to be the original module's ``__dict__``. A module's ``__dict__`` is a read-only object; therefore, we cannot execute code in it while expecting to be able to prevent changes to variables intended to remain constants. However, we can do so indirectly. Instead of executing code using the module's ``__dict__`` directly, we start by making a copy of its content into a ``FinalDict`` instance. We execute the code in that instance, and use its value after execution to update the module's ``__dict__``. """ locals_ = FinalDict( filename, on_prevent_change=callback_params["on_prevent_change"] ) locals_.update(module.__dict__) exec(code_object, locals_) module.__dict__.update(locals_)
[docs]def on_change_print(filename=None, key=None, value=None, kind=None): """Function called by default when an attempt is made to change the value of a constant. """ if kind == "set": print( "You cannot change the value of %s.%s to %s" % (shorten_path(filename), key, value) ) elif kind == "delete": print("You cannot delete %s in module %s." % (key, shorten_path(filename))) else: raise NotImplementedError
[docs]def add_hook(on_prevent_change=None, **_kwargs): """Creates and adds the import hook in sys.meta_path When an attempt is made to change the value of a constant, ``on_prevent_change`` is called. By default, this function just prints a warning about what was done. This could be replaced by a function that logs the results or one that raises an exception, etc. """ if on_prevent_change is None: on_prevent_change = on_change_print callback_params = {"on_prevent_change": on_prevent_change} module_class = make_class(**callback_params) console_dict = FinalDict(CONSOLE_NAME, on_prevent_change=on_prevent_change) CONSTANTS[CONSOLE_NAME] = {} hook = import_hook.create_hook( module_class=module_class, transform_source=transform_source, exec_=exec_, callback_params=callback_params, console_dict=console_dict, hook_name=__name__, ) return hook