pyprotect is a python module that provides API to restrict visibility or mutability of selected Python object attributes in a robust manner.
The key functions in the pyprotect module API - private() and protect() wrap the python object (like a Proxy) to restrict visibility or mutability of selected attributes of the wrapped object, while allowing the wrapping object to behave virtually identical to the wrapped object.
- Can wrap virtually any Python object - instances, classes (types), modules, methods, classmethods, instancemethods, staticmethods, partials, lambdas.
- Tested on Python 2.7 and Python 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11
- Tested on the following distributions with the latest versions of python2, python3, pypy3, pypy shipped by the respective distributions:
- Ubuntu Jammy 22.04
- Arch linux (20220101)
- Fedora 37
- Alpine Linux 3.15 (Alpine 3.16 does not have python2-dev)
- Has extensive unit (functional) tests - in tests directory.
- Quick start
- Classes
- Features of key classes
- API
- Calling wrap operations multiple times
- Python rules for attributes of type 'property':
- What kind of python objects can be wrapped?
- Work in progress
- Changelog
freeze(o: object) -> Frozen:
- If o is immutable (e.g. int , string), returns o UNCHANGED
- If o is Wrapped, returns o UNCHANGED if object WRAPPPED INSIDE o is immutable, returns Frozen otherwise
- If o is Frozen, returns o UNCHANGED
- If o is FrozenPrivate, FrozenProtected or FrozenPrivacyDict, returns o UNCHANGED
- If o is Private, returns FrozenPrivate
- If o is Protected, returns FrozenProtected
- Otherwise, returns Frozen
Object returned prevents modification of ANY attribute
private(o: object, frozen: bool = False) -> object:
- If frozen is False:
- If o is an instance of Private, returns o UNCHANGED
- If 'o' is an instance of Protected, returns o UNCHANGED
- If frozen is True:
- If o is an instance of Private, returns freeze(o) (FrozenPrivate)
- If o is an instance of Protected, returns freeze(o) (FrozenProtected)
- Otherwise: If frozen is True, returns FrozenPrivate; returns Private otherwise
protect(
o: object frozen: bool = False,
dynamic: bool = True,
hide_private: bool = False,
ro_data: bool = False,
ro_method: bool = True,
ro: List[str] = [],
rw: List[str] = [],
hide: List[str] = []
) -> object:
# o-->object to be wrapped
Returns-->Instance of FrozenProtected if frozen; Instance of Protected otherwise
If protect() is called on an object 'o' that is an instance of Protected, protect() will merge the protect() rules, enforcing the most restrictive combination among the two sets of protect() options:
- hide and hide_private are OR-ed
- ro_method, ro_data and ro are OR-ed
- rw is AND-ed, but rw of second protect overrides ro* of second protect but not the first protect.
In short, by calling protect() a second time (or multiple times):
- Additoinal attributes can be hidden
- Additional attributes can be made read-only
- No previously hidden attribute will become visible
- No previously read-only attribute will become mutable
Options: protect method arguments
Option | Type | Default | Description | Overrides |
---|---|---|---|---|
frozen | bool | False | If True, no attributes can be changed, added or deleted | |
hide_private | bool | False | If True, private vars of the form _var will be hidden |
|
ro_data | bool | False | Data (non-method) attributes will be immutable Can override selectively with rw |
|
ro_method | bool | True | Method (callable) attributes will be immutable Can override selectively with rw |
|
ro | list of str | [ ] | Attributes that will be immutable Can override selectively with rw |
|
rw | list of str | [ ] | Attributes that will be mutable | ro_data ro_method ro |
hide | list of str | [ ] |
Visibility and mutability of attributes with protect() method
Option | Attribute Type | Restricts Visibility | Restricts Mutability |
---|---|---|---|
frozen | Any | NO | YES |
hide_private | Private attributes | YES | YES (Indirect) |
ro_data | Data attributes | NO | YES |
ro_method | Method attributes | NO | YES |
ro | ANY | NO | YES |
rw | ANY | NO | YES |
hide | ANY | YES | YES (Indirect) |
- Visibility: No additional restrictions
- Mutability: No additional restrictions
- Visibility: Does not additionally restrict visibility of any attributes in wrapped object accessed through wrapping object
- Mutability: Prevents modification of ANY attribute
- Visibility:
- Cannot access traditionally 'private' mangled python attributes
- Cannot modify traditionally private attributes (form 'var')
- Attributes not part of dir(wrapped_object) are not visible
- Mutability:
- The following attributes of wrapped object are NEVER writeable:
__class__
,__dict__
,__delattr__
,__setattr__
,__slots__
,__getattribute__
- Traditional (mangled) Python private vars are ALWAYS hidden
- Private vars (form _var) will be read-only
- Attributes cannot be added or removed
- Attributes that are properties are ALWAYS visible AND WRITABLE (except if 'frozen' is used)
- Properties indicate an intention of class author to expose them
- Whether they are actually writable depends on whether class author implemented property.setter
- The following attributes of wrapped object are NEVER writeable:
- Visibility: Same as Private
- Mutability: Prevents modification of ANY attribute
- Features of Private PLUS allows further restriction of:
- Which attributes are VISIBLE
- Which attributes are WRITEABLE
- Default settings:
- Features of Private - see above
- dynamic == True Attribute additions, deletions, type changes automatically visible
- ro_method == True: Method attributes will be read-only
- All other non-private data attributes are read-write
- Features of Protected PLUS prevents modification of ANY attribute
freeze(o: object) -> Frozen:
- If o is immutable (e.g. int , string), returns o UNCHANGED
- If o is Wrapped, returns o UNCHANGED if object WRAPPPED INSIDE o is immutable, returns Frozen otherwise
- If o is Frozen, returns o UNCHANGED
- If o is FrozenPrivate, FrozenProtected or FrozenPrivacyDict, returns o UNCHANGED
- If o is Private, returns FrozenPrivate
- If o is Protected, returns FrozenProtected
- Otherwise, returns Frozen
Object returned prevents modification of ANY attribute
private(o: object, frozen: bool = False) -> object:
- If frozen is False:
- If o is an instance of Private, returns o UNCHANGED
- If 'o' is an instance of Protected, returns o UNCHANGED
- If frozen is True:
- If o is an instance of Private, returns freeze(o) (FrozenPrivate)
- If o is an instance of Protected, returns freeze(o) (FrozenProtected)
- Otherwise: If frozen is True, returns FrozenPrivate; returns Private otherwise
protect(
o: object frozen: bool = False,
dynamic: bool = True,
hide_private: bool = False,
ro_data: bool = False,
ro_method: bool = True,
ro: List[str] = [],
rw: List[str] = [],
hide: List[str] = []
) -> object:
# o-->object to be wrapped
Returns-->Instance of FrozenProtected if frozen; Instance of Protected otherwise
If protect() is called on an object 'o' that is an instance of Protected, protect() will merge the protect() rules, enforcing the most restrictive combination among the two sets of protect() options:
- hide and hide_private are OR-ed
- ro_method, ro_data and ro are OR-ed
- rw is AND-ed, but rw of second protect overrides ro* of second protect but not the first protect.
In short, by calling protect() a second time (or multiple times):
- Additoinal attributes can be hidden
- Additional attributes can be made read-only
- No previously hidden attribute will become visible
- No previously read-only attribute will become mutable
Options: protect method arguments
Option | Type | Default | Description | Overrides |
---|---|---|---|---|
frozen | bool | False | If True, no attributes can be changed, added or deleted | |
hide_private | bool | False | If True, private vars of the form _var will be hidden |
|
ro_data | bool | False | Data (non-method) attributes will be immutable Can override selectively with rw |
|
ro_method | bool | True | Method (callable) attributes will be immutable Can override selectively with rw |
|
ro | list of str | [ ] | Attributes that will be immutable Can override selectively with rw |
|
rw | list of str | [ ] | Attributes that will be mutable | ro_data ro_method ro |
hide | list of str | [ ] |
Visibility and mutability of attributes with protect() method
Option | Attribute Type | Restricts Visibility | Restricts Mutability |
---|---|---|---|
frozen | Any | NO | YES |
hide_private | Private attributes | YES | YES (Indirect) |
ro_data | Data attributes | NO | YES |
ro_method | Method attributes | NO | YES |
ro | ANY | NO | YES |
rw | ANY | NO | YES |
hide | ANY | YES | YES (Indirect) |
wrap(o: object) -> Wrapped:
- Should behave just like the wrapped object, except following attributes cannot be modified:
__getattribute__
,__delattr__
,__setattr__
,__slots__
- Explicitly does NOT support pickling, and will raise pickle.PicklingError
- Does NOT protect CLASS (or
__class__
) of wrapped object from modification - Does NOT protect
__dict__
or__slots__
Useful for testing if wrapping is failing for a particular type of object
isfrozen(x: object) -> bool
x was created using freeze() or private(o, frozen=True) or protect(o, frozen=True)
isimmutable(x: object) -> bool
x is known to be immutable
isprivate(x: object) -> bool
x was created using private()
isprotected(x: object) -> bool
x was created using protect()
contains(w: object, o: object) -> bool
If w is a wrapped object (iswrapped(w) is True), returns whether w wraps o Otherwise unconditionally returns False
help_protected(x: object) -> None
If x wraps o, executes help(o) Otherwise executes h_elp(x)_
id_protected(x: object) -> int
if x is a wrapped object (iswrapped(x) is True) and x wraps o, returns id(o) Otherwise returns id(x)
isinstance_protected(x: object, t: type) -> bool
If x is a wrapped object (iswrapped(x) is True) and x wraps o, returns isinstance(o, t) Otherwise returns isinstance(x, t)
isreadonly(x: object, a: str) -> bool
If x is a wrapped object - with iswrapped(x) == True - and x wraps o, isreadonly(x, a) returns whether rules of wrapper make attribute a read-only when accessed through x This represents rule of wrapped object - does not guarantee that_o_ has attribute_a_ or that setting attribute a in object o will not raise any exception If x is not a wrapped object (iswrapped(x) is False) , unconditionally returns False
instance_of_protected(x: object, o: object) -> bool
If x is a wrapped object - with iswrapped(x) == True - and x wraps o, instance_of_protected(x, o) returns True if and only if isinstance(x, type(o)) If x is not a wrapped object - iswrapped(x) == False - instance_of_protected(x, o) returns isinstance(x, o)
isvisible(x: object, a: str) -> bool
Returns False if and only if iswrapped(x) is True AND x makes attribute a invisible if present in wrapped object
This represents rule of wrapped object - does not guarantee that wrapped object has attribute a or that accessing attribute a in object x will not raise any exception
If x is not a wrapped object, unconditionally returns False
same_class_protected(c: type, w: object) -> bool
If iswrapped(w) and w wraps o: Returns (c is type(o))
Otherwise: returns (c is type(w))
subclass_of_protected(x: object, w: object) -> bool
If iswrapped(w) and w wraps o: Returns issubclass(x, type(o))
Otherwise: returns issubclass(x, w)
immutable_builtin_attributes() -> Set[str]
Returns-->set of str: attributes in builtins that are immutable Used in unit tests
always_delegated_attributes() -> set(str)
Attributes that are always delegated to wrapped object
attribute_protected() -> str
Name of special attribute in Wrapped objects
hidden_pickle_attributes
hidden_pickle_attributes() -> set(str)
Attributes that are never visible in object 'o' if iswrapped(o) - to disallow pickling
never_writeable() -> set(str)
Attributes that are never writeable in object o if iswrapped(o)
never_writeable_private() -> set(str)
Attributes that are never writeable in object o if isprivate(o)
In the table below:
- The left-most column shows starting state.
- The top row shows operation applied to the starting state.
- The intersecting cell shows the result.
- UNCH represents operation returning the starting state unchanged
Operation 🡆 On type 🡇 |
wrap | freeze | private | private + frozen |
protect | protect + frozen |
---|---|---|---|---|---|---|
Ordinary object iswrapped(x) is False |
Wrapped | Frozen (2) |
Private | FrozenPrivate | Protected | FrozenProtected |
Wrapped | UNCH | Frozen (2) |
Private | FrozenPrivate | Protected | FrozenProtected |
Frozen | Wrapped (2) |
UNCH (2) |
FrozenPrivate | FrozenPrivate | FrozenProtected | FrozenProtected |
Private | UNCH | FrozenPrivate | UNCH | FrozenPrivate | Protected | FrozenProtected |
FrozenPrivate | UNCH | UNCH | UNCH | UNCH | FrozenProtected | FrozenProtected |
Protected | UNCH | FrozenProtected | UNCH | FrozenProtected | Protected (1) |
FrozenProtected (1) |
FrozenProtected | UNCH | UNCH | UNCH | UNCH | FrozenProtected (1) |
FrozenProtected (1) |
- protect() applied twice will merge the protect() rules, enforcing the most restrictive combination among the two sets of protect() options:
- hide and hide_private are OR-ed
- ro_method, ro_data and ro are OR-ed
- rw is AND-ed, but rw of second protect overrides ro* of second protect but not the first protect.
In short, by calling protect() a second time (or multiple times): - Additoinal attributes can be hidden - Additional attributes can be made read-only but: - No previously hidden attribute will become visible - No previously read-only attribute will become mutable
- If x is an immutable object (e.g. int, str ...) having isimmutable(x) == True, freeze(x) returns x and iswrapped(freeze(x)) will be False.
For all other objects x, having isimmutable(x) == False, freeze(x) will return a Frozen object having iswrapped(freeze(x)) == True
For all other wrapped objects w, created with private(x) or protect(x), freeze(w) will always return a Wrapped object with iswrapped(w) == True
- Properties are defined in the CLASS, and cannot be changed in the object INSTANCE
- Properties cannot be DELETED
- Properties cannot be WRITTEN to unless property has a 'setter' method defined in the CLASS
- These rules are implemented by the python language (interpreter) and Protected class does not enforce or check
Pretty much anything. pyprotect only mediates attribute access using object.__getattribute__
, object.__setattr__
and object.__delatr__
. If these methods work on your object, your object can be wrapped
- Uploading to pypi.org
- Test cases required
- Project name on github changed to
pyprotect
to match the name of the module. This has been long-pending. The old github link redirects to the new project name.
- Started signing commits with my GPG key and displaying 'verified' for signed commits
A number of parameters to protect() have been discontinued. See list and reasons below, as well as how to achieve the same effect without thos parameters (sometimes, it takes more work). Most of them would be realistically useful very rarely, and / or do not align with what I call 'idiomatic python'.
hide_all, hide_method, hide_dunder
So-called 'special methods' in Python serve an important functional roles - especially in emulating containers (tuples, lists, sets, dicts), emulating numeric types supporting arithmetic operators, numeric comparisons etc. If such specific 'dunder methods' were hidden, it would definitely affect the behavior of the wrapped object. hide_dunder
would hide all such special methods. hide_method
would in addition hide all methods in the objest. hide_all
would hide all object attributes, making the object virtually useless, and the option useful in testing (if at all).
hide_data In most cases, hiding all non-method data attributes will make the object less useful / cripple the expected usage of the object. Specific use-cases can be achieved using the 'hide' parameter.
ro_dunder Seems like it can be replaced by using ro_method' Mostly, in 'idiomatic python', methods of a class / instance are not mutated from outside the class / instance. This expected 'idiomatic python' behavior can be achieved with 'ro_method'.
ro_all Is unnecessary, since 'frozen' can be used instead.
add In my opinion, 'add' does not align with 'idiomatic python'. While Python allows users of a class / instance adding attributes to the class / instance, that is not the expected style. Based on this, I have decided to promote that 'idiomatic style', and prevent adding / deleting any attributes in a Private or Protected object.
show It is unnecessary, since all attributes are visible by default in Python. Only 'hide' or 'hide_private' will hide any attributes.
This leaves the following (incidentally also reducing testing load):
- Visibility:
- hide_private
- hide
- Mutability:
- ro_data
- ro_method
- ro
- rw
- frozen
- Behavior:
- dynamic