Immutable and concealed attributes for classes, modules, and namespaces.
Project description
API Documentation (stable) | API Documentation (current) | Code of Conduct | Contribution Guide
Overview
Enables the creation of classes, modules, and namespaces on which all attributes are immutable and for which non-public attributes are concealed. Immutability increases code safety by discouraging monkey-patching and preventing accidental or deliberate changes to state. Concealment means that functions, such as dir, can report a subset of attributes that are intended for programmers to use without exposing internals.
Salient Features
A module class, which enforces immutability and concealment upon module attributes. This module class can replace the standard Python module class with a single line of code in a module definition.
A factory (metaclass) that creates classes, enforcing immutability and concealment upon their attributes. (Just attributes on the classes, themsleves, are immutable and concealed and not attributes on the instances of the classes.)
A factory that creates namespaces, enforcing immutability and concealment upon their attributes.
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:
>>> import lockup >>> 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:
>>> 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.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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file lockup-1.0.2.tar.gz
.
File metadata
- Download URL: lockup-1.0.2.tar.gz
- Upload date:
- Size: 19.0 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
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2a547136020da78ae6d4b1891b9c28160fbaee0e26d47cb6fb968d98e3b84027 |
|
MD5 | 5b38cdcb8ea47d8eb8995629e5d3fbc4 |
|
BLAKE2b-256 | c690227b04056bf361620af718fa84bf352cf6e87c944fc8dffcc045a36b5a13 |
File details
Details for the file lockup-1.0.2-py3-none-any.whl
.
File metadata
- Download URL: lockup-1.0.2-py3-none-any.whl
- Upload date:
- Size: 16.6 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
Algorithm | Hash digest | |
---|---|---|
SHA256 | d1f02cbaa2d30b43622aeec93f5d06af4addead0c62bca08e7581f6cc10cf9e4 |
|
MD5 | 4c9bb3dd0b6fa9c4dd2dc54e12fc676e |
|
BLAKE2b-256 | 6255979611dfd8329b4d8943c6e1fc0659a90618b5ba2570b7afcd3f6d4a4a8b |