Skip to content

emcd/python-lockup

Repository files navigation

lockup

Project Version

PyPI - Status

Tests Status

Code Coverage

Python Versions

Project License

API Documentation (stable) | API Documentation (current) | Code of Conduct | Contribution Guide

Overview

Enables the creation of classes, modules, and namespaces on which the following properties are true:

  • All attributes are immutable. Immutability increases code safety by discouraging monkey-patching and preventing changes to state, accidental or otherwise.

    >>> import getpass
    >>> def steal_password( prompt = 'Password: ', stream = None ):
    ...     pwned = getpass.getpass( prompt = prompt, stream = stream )
    ...     # Send host address, username, and password to Dark Web collector.
    ...     return pwned
    ...
    >>> import lockup
    >>> lockup.reclassify_module( getpass )
    >>> getpass.getpass = steal_password
    Traceback (most recent call last):
    ...
    lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'getpass' on module 'getpass'.
    >>> import lockup
    >>> ns = lockup.create_namespace( some_constant = 6 )
    >>> ns.some_constant = 13
    Traceback (most recent call last):
    ...
    lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'some_constant' on class 'lockup.Namespace'.
  • Non-public attributes are concealed. Concealment means that the dir function will report a subset of attributes that are intended for programmers to use... without exposing internals.

    >>> import lockup
    >>> class Demo( metaclass = lockup.Class ):
    ...     _foo = 'Semi-private class variable.'
    ...     hello = 'Public class variable.'
    ...     def __len__( self ): return 1
    ...
    >>> dir( Demo )
    ['hello']

In addition to the above, the package also provides the ability to apprehend "fugitive" exceptions attempting to cross API boundaries. Various auxiliary functionalities are provided as well; these are used internally within the package but are deemed useful enough for public consumption. Please see the documentation for more details.

Quick Tour

Let us consider the mutable os module from the Python standard library and how we can alter "constants" that may be used in many places:

>>> import os
>>> type( os )
<class 'module'>
>>> os.O_RDONLY
0
>>> os.O_RDONLY = os.O_RDWR
>>> os.O_RDONLY
2
>>> os.O_RDONLY = 0

Now, let us see what protection it gains from becoming immutable:

>>> import os
>>> import lockup
>>> lockup.reclassify_module( os )
>>> type( os )
<class 'lockup.module.Module'>
>>> # How? https://docs.python.org/3/reference/datamodel.html#customizing-module-attribute-access
>>> os.O_RDONLY = os.O_RDWR
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'O_RDONLY' on module 'os'.
>>> del os.O_RDONLY
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to delete indelible attribute 'O_RDONLY' on module 'os'.

Let us monkey-patch a mutable class:

>>> class A:
...     def expected_functionality( self ): return 42
...
>>> a = A( )
>>> a.expected_functionality( )
42
>>> def monkey_patch( self ):
...     return 'I selfishly change behavior upon which other consumers depend.'
...
>>> A.expected_functionality = monkey_patch
>>> a = A( )
>>> a.expected_functionality( )
'I selfishly change behavior upon which other consumers depend.'

Now, let us try to monkey-patch an immutable class:

>>> import lockup
>>> class B( metaclass = lockup.Class ):
...     def expected_functionality( self ): return 42
...
>>> b = B( )
>>> b.expected_functionality( )
42
>>> def monkey_patch( self ):
...     return 'I selfishly change behavior upon which other consumers depend.'
...
>>> B.expected_functionality = monkey_patch
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'expected_functionality' on class ...
>>> del B.expected_functionality
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to delete indelible attribute 'expected_functionality' on class ...

Note

Only class attributes are immutable. Instances of immutable classes will have mutable attributes without additional intervention beyond the scope of this package.

An alternative to types.SimpleNamespace is provided. First, let us observe the behaviors on a standard namespace:

>>> import types
>>> sn = types.SimpleNamespace( run = lambda: 42 )
>>> sn
namespace(run=<function <lambda> at ...>)
>>> sn.run( )
42
>>> type( sn )
<class 'types.SimpleNamespace'>
>>> sn.__dict__
{'run': <function <lambda> at ...>}
>>> type( sn.run )
<class 'function'>
>>> sn.run = lambda: 666
>>> sn.run( )
666
>>> sn( )  # doctest: +SKIP
Traceback (most recent call last):
...
TypeError: 'types.SimpleNamespace' object is not callable

Now, let us compare those behaviors to an immutable namespace:

>>> import lockup
>>> ns = lockup.create_namespace( run = lambda: 42 )
>>> ns
NamespaceClass( 'Namespace', ('object',), { ... } )
>>> ns.run( )
42
>>> type( ns )
<class 'lockup.factories.NamespaceClass'>
>>> ns.__dict__
mappingproxy({...})
>>> type( ns.run )
<class 'function'>
>>> ns.run = lambda: 666
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'run' on class 'lockup.Namespace'.
>>> ns.__dict__[ 'run' ] = lambda: 666
Traceback (most recent call last):
...
TypeError: 'mappingproxy' object does not support item assignment
>>> ns( )
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleOperation: Impermissible instantiation of class 'lockup.Namespace'.

Also of note is that we can define namespace classes directly, allowing us to capture imports for internal use in a module without publicly exposing them as part of the module API, for example:

>>> import lockup
>>> class __( metaclass = lockup.NamespaceClass ):
...     from os import O_RDONLY, O_RDWR
...
>>> __.O_RDONLY
0

The above technique is used internally within this package itself.

Interception

If a particular exceptional condition is not anticipated in Python code, a "fugitive" exception can escape across the boundary of a published API. If you have told the consumers of the API that it will only emit certain classes of exceptions, then consumers might not handle exceptions outside of the expected classes, i.e., fugitive exceptions. If you apprehend all fugitives at the API boundary, then you can guarantee to your consumers that they will only need to anticipate certain classes of exceptions.

Here is an example with an interceptor, which includes fugitive exception apprehension, that this package uses internally:

>>> from lockup.exceptions import InvalidState
>>> from lockup.interception import our_interceptor
>>> @our_interceptor
... def divide_by_zero( number ): return number / 0
...
>>> try: divide_by_zero( 42 )
... except InvalidState as exc:
...     type( exc ), type( exc.__cause__ ), str( exc )
...
(<class 'lockup.exceptions.InvalidState'>, <class 'ZeroDivisionError'>, "Apprehension of fugitive exception of class 'builtins.ZeroDivisionError' at boundary of function 'divide_by_zero' on module '__main__'.")

As can be seen, the ZeroDivisionError is in the custody of an exception that is of an expected class.

You can create your own interceptors with custom fugitive apprehension behaviors using the create_interception_decorator function.

Compatibility

This package has been verified to work on the following Python implementations:

  • CPython
    • Complete functionality.
    • Support for interpreters compiled with Py_TRACE_REFS definition.
  • PyPy
    • Complete functionality except for reflection.
    • Reflection is a no-op if assert_implementation is False.
  • Pyston

    • Complete functionality.

    Warning

    Support for Pyston may disappear in the future as the maintainers have decided to invest in a JIT module for CPython rather than a separate implementation.

It likely works on others as well, but please report if it does not.

...than the required minimum

GitHub last commit

pre-commit

Security Status

Static Analysis Status

PyPI - Implementation

PyPI - Wheel

About

Immutable and concealed attributes for Python classes, modules, and namespaces.

Resources

License

Stars

Watchers

Forks

Languages