Skip to main content

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

Project description

Python Versions Project Version Tests Status https://codecov.io/gh/emcd/python-lockup/branch/master/graph/badge.svg?token=PA9QI9RL63 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 accidental or deliberate changes to state.

    >>> import math
    >>> import lockup
    >>> lockup.reclassify_module( math )
    >>> math.pi = math.e
    Traceback (most recent call last):
    ...
    lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'pi' on module 'math'.
    >>> 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 functions, such as dir, can report a subset of attributes that are intended for programmers to use (without directly exposing internals).

    >>> import lockup
    >>> dir( lockup )
    ['Class', 'Module', 'NamespaceClass', 'base', 'create_namespace', 'exceptions', 'reclassify_module']
    >>> len( dir( lockup ) )
    7
    >>> len( lockup.__dict__ )  # doctest: +SKIP
    18

Quick Tour

Module

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
>>> os.EX_OK
0
>>> del os.EX_OK
>>> os.EX_OK
Traceback (most recent call last):
...
AttributeError: module 'os' has no attribute 'EX_OK'
>>> os.EX_OK = 0
>>> type( os )
<class 'module'>

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

>>> import os
>>> import lockup
>>> lockup.reclassify_module( os )
>>> del os.EX_OK
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to delete indelible attribute 'EX_OK' on module 'os'.
>>> os.EX_OK = 255
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to assign immutable attribute 'EX_OK' on module 'os'.
>>> type( os )
<class 'lockup.Module'>

Class Factory

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 ...
>>> type( B )
<class 'lockup.Class'>
>>> del type( B ).__setattr__
Traceback (most recent call last):
...
lockup.exceptions.ImpermissibleAttributeOperation: Attempt to delete indelible attribute '__setattr__' on class 'lockup.Class'.
>>> issubclass( type( B ), type )
True

Namespace Factory

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'>
>>> dir( sn )
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'run']
>>> sn.__dict__
{'run': <function <lambda> at ...>}
>>> type( sn.run )
<class 'function'>
>>> sn.run = lambda: 666
>>> sn.run( )
666
>>> sn( )
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.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.

Exceptions

Exceptions can be intercepted with appropriate builtin exception classes or with package exception classes:

>>> import os
>>> import lockup
>>> from lockup.exceptions import InvalidOperation
>>> os.O_RDONLY
0
>>> lockup.reclassify_module( os )
>>> try: os.O_RDONLY = 15
... except AttributeError as exc:
...     type( exc ).mro( )
...
[<class 'lockup.exceptions.ImpermissibleAttributeOperation'>, <class 'lockup.exceptions.ImpermissibleOperation'>, <class 'lockup.exceptions.InvalidOperation'>, <class 'lockup.exceptions.Exception0'>, <class 'TypeError'>, <class 'AttributeError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>]
>>> try: os.does_not_exist
... except InvalidOperation as exc:
...     type( exc ).mro( )
...
[<class 'lockup.exceptions.InaccessibleAttribute'>, <class 'lockup.exceptions.InaccessibleEntity'>, <class 'lockup.exceptions.InvalidOperation'>, <class 'lockup.exceptions.Exception0'>, <class 'AttributeError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>]

Changelog

v1.0.4

Documentation

  • Add missing links in README.

  • Replace example in README with one that is clearer and able to be doctested.

v1.0.3

Documentation

  • Improve presentation of overview in README.

  • Properly fence Python code blocks in README for correct rendering on PyPI.

v1.0.2

Documentation

  • Provide direct link in README to stable API documentation.

v1.0.1

Documentation

  • Improve wording of introduction in README.

  • Add badges in README for supported Python versions, current release, code coverage, and license.

  • Provide direct links in README to current API documentation, code of conduct, and contribution guide.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lockup-1.0.5.tar.gz (19.5 kB view details)

Uploaded Source

Built Distribution

lockup-1.0.5-py3-none-any.whl (17.0 kB view details)

Uploaded Python 3

File details

Details for the file lockup-1.0.5.tar.gz.

File metadata

  • Download URL: lockup-1.0.5.tar.gz
  • Upload date:
  • Size: 19.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.8.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.6.15

File hashes

Hashes for lockup-1.0.5.tar.gz
Algorithm Hash digest
SHA256 657c0e8b2ed4a88c89878e19116debdca0f2b53f530a2e6b5bfb4de130f50915
MD5 12cf8385c473de5d323d481292cbdaf6
BLAKE2b-256 2dba63945ce70fd4d8edcb4658ab1b1b298e60e6f7c8808bdd33eb0c4a0eae21

See more details on using hashes here.

File details

Details for the file lockup-1.0.5-py3-none-any.whl.

File metadata

  • Download URL: lockup-1.0.5-py3-none-any.whl
  • Upload date:
  • Size: 17.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.8.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.6.15

File hashes

Hashes for lockup-1.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 5e09ae12d0a34b11f6f80c3c8f84f6e123db0004c9fe435fd1b4487484c6ce3e
MD5 232813e04bbc8ede6f47223727132c18
BLAKE2b-256 d56615631e3b5dd00bc8d55ead2c7d1b0ab0744c493564402ca3b97c91a432cd

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page