"""
Functions, classes and/or types which either **are**, or are related to Python
variable storage types (``dict``, ``tuple``, ``list``, ``set`` etc.)
Object-like Dictionaries (dict's)
---------------------------------
Have you ever wanted a dictionary that works like an object, where you can get/set dictionary keys using
attributes (``x.something``) as easily as you can with items (``x['something']``)?
We did. So we invented :class:`.DictObject`, a sub-class of the built-in :class:`dict`, making it compatible
with most functions/methods which expect a :class:`dict` (e.g. :meth:`json.dumps`).
You can create a new :class:`.DictObject` and use it just like a ``dict``, or you can convert an existing
``dict`` into a ``DictObject`` much like you'd cast any other builtin type.
It can also easily be cast back into a standard ``dict`` when needed, without losing any data.
Creating a new DictObject and using it
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Since :class:`.DictObject` is a subclass of the builtin :class:`dict`, you can instantiate a new
:class:`.DictObject` in the same way you would use the standard :class:`dict` class::
>>> d = DictObject(hello='world')
>>> d
{'hello': 'world'}
>>> d['hello']
'world'
>>> d.hello
'world'
>>> d.lorem = 'ipsum'
>>> d['orange'] = 'banana'
>>> d
{'hello': 'world', 'lorem': 'ipsum', 'orange': 'banana'}
Converting an existing dictionary (dict) into a DictObject
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can convert an existing ``dict`` into a :class:`.DictObject` in the same way you'd convert
any other object into a ``dict``::
>>> y = {"hello": "world", "example": 123}
>>> x = DictObject(y)
>>> x.example
123
>>> x['hello']
'world'
>>> x.hello = 'replaced'
>>> x
{'hello': 'replaced', 'example': 123}
It also works vice versa, you can convert a :class:`.DictObject` instance back into a :class:`dict` just as
easily as you converted the `dict` into a `DictObject`.
>>> z = dict(x)
>>> z
{'hello': 'replaced', 'example': 123}
Dict-able NamedTuple's
----------------------
While :func:`collections.namedtuple`'s can be useful, they have some quirks, such as not being able to access
fields by item/key (``x['something']``). They also expose a method ``._asdict()``, but cannot be directly casted
into a :class:`dict` using ``dict(x)``.
Our :func:`.dictable_namedtuple` collection is designed to fix these quirks.
What is a dictable_namedtuple and why use it?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Unlike the normal :func:`.namedtuple` types, ``dictable_namedtuple``s add extra convenience functionality:
* Can access fields via item/key: ``john['first_name']``
* Can convert instance into a dict simply by casting: ``dict(john)``
* Can set new items/attributes on an instance, even if they weren't previously defined.
* NOTE: You cannot edit an original namedtuple field defined on the type, those remain read only
There are three functions available for working with ``dictable_namedtuple`` classes/instances,
each for different purposes.
* :py:func:`.dictable_namedtuple` - Create a new ``dictable_namedtuple`` type for instantiation.
* :py:func:`.convert_dictable_namedtuple` - Convert an existing **namedtuple instance** (not a type/class) into
a ``dictable_namedtuple`` instance.
* :py:func:`.subclass_dictable_namedtuple` - Convert an existing **namedtuple type/class** (not an instance) into
a ``dictable_namedtuple`` type for instantiation.
Importing dictable_namedtuple functions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: python
from collections import namedtuple
from privex.helpers import dictable_namedtuple, convert_dictable_namedtuple, subclass_dictable_namedtuple
Creating a NEW dictable_namedtuple type and instance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you're **creating a new Named Tuple**, and you want it to support dictionary-like access, and
have it able to be converted into a dict simply through ``dict(my_namedtuple)``, then you want
:py:func:`.dictable_namedtuple`
.. code-block:: python
Person = dictable_namedtuple('Person', 'first_name last_name')
john = Person('John', 'Doe')
dave = Person(first_name='Dave', last_name='Smith')
print(dave['first_name']) # Prints: Dave
print(dave.first_name) # Prints: Dave
print(john[1]) # Prints: Doe
print(dict(john)) # Prints: {'first_name': 'John', 'last_name': 'Doe'}
Converting an existing namedtuple instance into a dictable_namedtuple instance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have **existing Named Tuple instances**, e.g. returned from a python library, then you can use
:py:func:`.convert_dictable_namedtuple` to convert them into ``dictable_namedtuple``'s and gain all the
functionality mentioned at the start of this section.
.. code-block:: python
Person = namedtuple('Person', 'first_name last_name') # This is an existing namedtuple "type" or "class"
john = Person('John', 'Doe') # This is an existing namedtuple instance
john.first_name # This works on a standard namedtuple. Returns: John
john[1] # This works on a standard namedtuple. Returns: Doe
john['first_name'] # However, this would throw a TypeError.
dict(john) # And this would throw a ValueError.
# We can now convert 'john' into a dictable_namedtuple, which will retain the functionality of a
# namedtuple, but add to the functionality by allowing dict-like key access, updating/creating new
# fields, as well as painlessly casting to a dictionary.
d_john = convert_dictable_namedtuple(john)
d_john.first_name # Returns: John
d_john[1] # Returns: Doe
d_john['first_name'] # Returns: 'John'
dict(d_john) # Returns: {'first_name': 'John', 'last_name': 'Doe'}
Converting an existing namedtuple type/class into a dictable_namedtuple type/class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have **existing Named Tuple type/class** then you can use :py:func:`.subclass_dictable_namedtuple`
to convert the type/class into a ``dictable_namedtuple`` type/class and gain all the functionality mentioned
at the start of this section. (**NOTE:** it's usually easier to just replace your ``namedtuple`` calls
with ``dictable_namedtuple``)
.. code-block:: python
Person = namedtuple('Person', 'first_name last_name') # This is an existing namedtuple "type" or "class"
# We can now convert the 'Person' type into a dictable_namedtuple type.
d_Person = subclass_dictable_namedtuple(Person)
# Then we can use this converted type to create instances of Person with dictable_namedtuple functionality.
john = d_Person('John', 'Doe')
john.first_name # Returns: John
john[1] # Returns: Doe
john['first_name'] # Returns: 'John'
dict(john) # Returns: {'first_name': 'John', 'last_name': 'Doe'}
**Copyright**::
+===================================================+
| © 2019 Privex Inc. |
| https://www.privex.io |
+===================================================+
| |
| Originally Developed by Privex Inc. |
| License: X11 / MIT |
| |
| Core Developer(s): |
| |
| (+) Chris (@someguy123) [Privex] |
| (+) Kale (@kryogenic) [Privex] |
| |
+===================================================+
Copyright 2019 Privex Inc. ( https://www.privex.io )
"""
import inspect
import sys
from collections import namedtuple, OrderedDict
from typing import Dict, Optional, NamedTuple, Union, Type
import logging
log = logging.getLogger(__name__)
[docs]class DictObject(dict):
"""
A very simple :class:`dict` wrapper, which allows you to read and write dictionary keys using attributes
(dot notation) PLUS standard item (key / square bracket notation) access.
**Example Usage (creating and using a new DictObject)**::
>>> d = DictObject(hello='world')
>>> d
{'hello': 'world'}
>>> d['hello']
'world'
>>> d.hello
'world'
>>> d.lorem = 'ipsum'
>>> d['orange'] = 'banana'
>>> d
{'hello': 'world', 'lorem': 'ipsum', 'orange': 'banana'}
**Example Usage (converting an existing dict)**::
>>> y = {"hello": "world", "example": 123}
>>> x = DictObject(y)
>>> x.example
123
>>> x['hello']
'world'
>>> x.hello = 'replaced'
>>> x
{'hello': 'replaced', 'example': 123}
"""
def __getattr__(self, item):
"""When an attribute is requested, e.g. ``x.something``, forward it to ``dict['something']``"""
if hasattr(super(), item):
return super().__getattribute__(item)
return super().__getitem__(item)
def __setattr__(self, key, value):
"""When an attribute is set, e.g. ``x.something = 'abcd'``, forward it to ``dict['something'] = 'abcd'``"""
if hasattr(super(), key):
return super().__setattr__(key, value)
return super().__setitem__(key, value)
[docs]class OrderedDictObject(OrderedDict):
"""
Ordered version of :class:`.DictObject` - dictionary with attribute access.
See :class:`.DictObject`
"""
def __getattr__(self, item):
"""When an attribute is requested, e.g. ``x.something``, forward it to ``dict['something']``"""
if hasattr(super(), item):
return super().__getattribute__(item)
return super().__getitem__(item)
def __setattr__(self, key, value):
"""When an attribute is set, e.g. ``x.something = 'abcd'``, forward it to ``dict['something'] = 'abcd'``"""
if hasattr(super(), key):
return super().__setattr__(key, value)
return super().__setitem__(key, value)
class MockDictObj(DictObject):
"""
This is a masqueraded :class:`.DictObject` made to look like the builtin :class:`dict` by
editing the class name, qualname and module.
It may improve compatibility when passing :class:`.DictObject` to certain third-party
functions/methods.
Note: this isn't enough to fool a ``type(x) is dict`` check.
"""
MockDictObj.__name__ = 'dict'
MockDictObj.__qualname__ = 'dict'
MockDictObj.__module__ = 'builtins'
[docs]def is_namedtuple(*objs) -> bool:
"""
Takes one or more objects as positional arguments, and returns ``True`` if ALL passed objects
are namedtuple instances
**Example usage**
First, create or obtain one or more NamedTuple objects::
>>> from collections import namedtuple
>>> Point, Person = namedtuple('Point', 'x y'), namedtuple('Person', 'first_name last_name')
>>> pt1, pt2 = Point(1.0, 5.0), Point(2.5, 1.5)
>>> john = Person('John', 'Doe')
We'll also create a ``tuple``, ``dict``, and ``str`` to show they're detected as invalid::
>>> normal_tuple, tst_dict, tst_str = (1, 2, 3,), dict(hello='world'), "hello world"
First we'll call :func:`.is_namedtuple` with our Person NamedTuple object ``john``::
>>> is_namedtuple(john)
True
As expected, the function shows ``john`` is in-fact a named tuple.
Now let's try it with our two Point named tuple's ``pt1`` and ``pt2``, plus our Person named tuple ``john``.
>>> is_namedtuple(pt1, john, pt2)
True
Since all three arguments were named tuples (even though pt1/pt2 and john are different types), the function
returns ``True``.
Now we'll test with a few objects that clearly aren't named tuple's::
>>> is_namedtuple(tst_str) # Strings aren't named tuples.
False
>>> is_namedtuple(normal_tuple) # A plain bracket tuple is not a named tuple.
False
>>> is_namedtuple(john, tst_dict) # ``john`` is a named tuple, but a dict isn't, thus False is returned.
False
Original source: https://stackoverflow.com/a/2166841
:param Any objs: The objects (as positional args) to check whether they are a NamedTuple
:return bool is_namedtuple: ``True`` if all passed ``objs`` are named tuples.
"""
if len(objs) == 0: raise AttributeError("is_namedtuple expects at least one argument")
for x in objs:
t = type(x)
b = t.__bases__
if tuple not in b: return False
f = getattr(t, '_fields', None)
if not isinstance(f, tuple): return False
if not all(type(n) == str for n in f): return False
return True
[docs]def convert_dictable_namedtuple(nt_instance, typename=None, module=None, **kwargs) -> Union[NamedTuple, Dict]:
"""
Convert an existing :func:`collections.namedtuple` instance into a dictable_namedtuple instance.
**Example**
First we create a namedtuple type ``Person``
>>> from collections import namedtuple
>>> Person = namedtuple('Person', 'first_name last_name')
Next we create an instance of ``Person`` called John Doe, and we can confirm it's a normal namedtuple, as we
can't access first_name by item/key.
>>> john = Person('John', 'Doe')
>>> john['first_name']
TypeError: tuple indices must be integers or slices, not str
Using :func:`.convert_dictable_namedtuple`, we can convert ``john`` from a normal ``namedtuple``, into
a ``dictable_namedtuple``.
This enables many convenience features (see :func:`.dictable_namedtuple` for more info)
such as easy casting to a :class:`dict`, and accessing fields by item/key (square brackets)::
>>> from privex.helpers import convert_dictable_namedtuple
>>> d_john = convert_dictable_namedtuple(john)
>>> d_john
Person(first_name='John', last_name='Doe')
>>> d_john['first_name']
'John'
>>> dict(d_john)
{'first_name': 'John', 'last_name': 'Doe'}
:param nt_instance: An instantiated namedtuple object (using a type returned from :func:`collections.namedtuple`)
:param str typename: Optionally, you can change the name of your instance's class, e.g. if you provide a ``Person``
instance, but you set this to ``Man``, then this will return a ``Man`` instance, like so:
``Man(first_name='John', last_name='Doe')``
:param str module: Optionally, you can change the module that the type class belongs to. Otherwise it will inherit the module path
from the class of your instance.
:key bool read_only: (Default: ``False``) If set to ``True``, the outputted dictable_namedtuple instance will not
allow new fields to be created via attribute / item setting.
:return dictable_namedtuple: The instance you passed ``nt_instance``, converted into a dictable_namedtuple
"""
nt_class = nt_instance.__class__
module = nt_class.__module__ if module is None else module
dnt_class = subclass_dictable_namedtuple(nt_class, typename=typename, module=module, **kwargs)
return dnt_class(**nt_instance._asdict())
[docs]def subclass_dictable_namedtuple(named_type: type, typename=None, module=None, **kwargs) -> type:
"""
Convert an existing :func:`collections.namedtuple` **type** into a dictable_namedtuple.
If you have an INSTANCE of a type (e.g. it has data attached), use :func:`.convert_dictable_namedtuple`
**Example**::
>>> from collections import namedtuple
>>> from privex.helpers import subclass_dictable_namedtuple
>>> # Create a namedtuple type called 'Person'
>>> orig_Person = namedtuple('Person', 'first_name last_name')
>>> # Convert the 'Person' type into a dictable_namedtuple
>>> Person = subclass_dictable_namedtuple(orig_Person)
>>> john = Person('John', 'Doe') # Create an instance of this dictable_namedtuple Person
>>> john['middle_name'] = 'Davis'
:param type named_type: A NamedTuple type returned from :func:`collections.namedtuple`
:param str typename: Optionally, you can change the name of your type, e.g. if you provide a ``Person``
class type, but you set this to ``Man``, then this will return a ``Man`` class type.
:param str module: Optionally, you can change the module that the type class belongs to. Otherwise it will
inherit the module path from ``named_type``.
:key bool read_only: (Default: ``False``) If set to ``True``, the outputted dictable_namedtuple type will not allow
new fields to be created via attribute / item setting.
:return type dictable_namedtuple: Your ``named_type`` converted into a dictable_namedtuple type class.
"""
typename = named_type.__name__ if typename is None else typename
module = named_type.__module__ if module is None else module
read_only = kwargs.pop('read_only', False)
_dt = make_dict_tuple(typename, ' '.join(named_type._fields), read_only=read_only)
if module is None:
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
if module is not None:
_dt.__module__ = module
return _dt
[docs]def make_dict_tuple(typename, field_names, *args, **kwargs):
"""
Generates a :func:`collections.namedtuple` type, with added / modified methods injected to make it
into a ``dictable_namedtuple``.
Note: You probably want to be using :func:`.dictable_namedtuple` instead of calling this directly.
"""
read_only = kwargs.pop('read_only', False)
module = kwargs.pop('module', None)
# Create a namedtuple type to use as a base
BaseNT = namedtuple(typename, field_names, **kwargs)
def __init__(self, *args, **kwargs):
self.__dict__['_extra_items'] = dict()
for i, a in enumerate(list(args)):
self.__dict__[self._fields[i]] = a
for k, a in kwargs.items():
self.__dict__[k] = a
def __iter__(self):
"""This ``__iter__`` method allows for casting a dictable_namedtuple instance using ``dict(my_nt)``"""
for k in self._fields: yield (k, getattr(self, k),)
def __getitem__(self, item):
"""Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc']`` or ``my_nt[0]``"""
if type(item) is int:
return self.__dict__[self._fields[item]]
return getattr(self, item)
def __getattr__(self, item):
"""Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd``"""
try:
_v = object.__getattribute__(self, '_extra_items')
return _v[item]
except (KeyError, AttributeError):
return object.__getattribute__(self, item)
def __setitem__(self, key, value):
"""Handles when a dictable_namedtuple instance is accessed like ``my_nt['abc'] = 'def'``"""
if hasattr(self, key):
return tuple.__setattr__(self, key, value)
if self._READ_ONLY:
raise KeyError(f"{self.__class__.__name__} is read only. You cannot set a non-existent field.")
self._extra_items[key] = value
if key not in self._fields:
tuple.__setattr__(self, '_fields', self._fields + (key,))
def __setattr__(self, key, value):
"""Handles when a dictable_namedtuple instance is accessed like ``my_nt.abcd = 'def'``"""
if key in ['_extra_items', '_fields'] or key in self._fields:
return tuple.__setattr__(self, key, value)
if self._READ_ONLY:
raise AttributeError(f"{self.__class__.__name__} is read only. You cannot set a non-existent field.")
self._extra_items[key] = value
if key not in self._fields:
tuple.__setattr__(self, '_fields', self._fields + (key,))
def _asdict(self):
"""
The original namedtuple ``_asdict`` doesn't work with our :meth:`.__iter__`, so we override it
for compatibility. Simply calls ``return dict(self)`` to convert the instance to a dict.
"""
return dict(self)
def __repr__(self):
_n = ', '.join(f"{name}='{getattr(self, name)}'" for name in self._fields)
return f"{self.__class__.__name__}({_n})"
# Inject our methods defined above into the namedtuple type BaseNT
BaseNT.__getattr__ = __getattr__
BaseNT.__getitem__ = __getitem__
BaseNT.__setitem__ = __setitem__
BaseNT.__setattr__ = __setattr__
BaseNT._asdict = _asdict
BaseNT.__repr__ = __repr__
BaseNT.__iter__ = __iter__
BaseNT.__init__ = __init__
BaseNT._READ_ONLY = read_only
# Create a class for BaseNT with tuple + object mixins, allowing things like __dict__ to function properly
# and allowing for tuple.__setattr__ / object.__getattribute__ calls.
class K(BaseNT, tuple, object):
pass
# Get the calling module so we can overwrite the module name of the class.
if module is None:
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
# Overwrite the type name + module to match the originally requested typename
K.__name__ = BaseNT.__name__
K.__qualname__ = BaseNT.__qualname__
K.__module__ = module
return K
[docs]def dictable_namedtuple(typename, field_names, *args, **kwargs) -> Union[Type[namedtuple], dict]:
"""
Creates a dictable_namedtuple type for instantiation (same usage as :func:`collections.namedtuple`) - unlike
namedtuple, dictable_namedtuple instances allow item (dict-like) field access, support writing and can be
painlessly converted into dictionaries via ``dict(my_namedtuple)``.
Named tuple instances created from ``dictable_namedtuple`` types are generally backwards compatible with
any code that expects a standard :func:`collections.namedtuple` type instance.
**Quickstart**
>>> from privex.helpers import dictable_namedtuple
>>> # Define a dictable_namedtuple type of 'Person', which has two fields - first_name and last_name
>>> p = dictable_namedtuple('Person', 'first_name last_name')
>>> john = p('John', 'Doe') # Alternatively you can do p(first_name='John', last_name='Doe')
>>> john.first_name # You can retrieve keys either via attributes (dot notation)
'John'
>>> john['last_name'] # Via named keys (square brackets)
'Doe'
>>> john[1] # Or, via indexed keys (square brackets, with integer keys)
'Doe'
>>> john.middle_name = 'Davis' # You can also update / set new keys via attribute/key/index
>>> dict(john) # Newly created keys will show up as normal in dict(your_object)
{'first_name': 'John', 'last_name': 'Doe', 'middle_name': 'Davis'}
>>> john # As well as in the representation in the REPL or when str() is called.
Person(first_name='John', last_name='Doe', middle_name='Davis')
This function adds / overrides the following methods on the generated namedtuple type:
* _asdict
* __iter__
* __getitem__
* __getattribute__
* __setitem__
* __setattr__
* __repr__
Extra functionality compared to the standard :func:`.namedtuple` generated classes:
* Can access fields via item/key: ``john['first_name']``
* Can convert instance into a dict simply by casting: ``dict(john)``
* Can set new items/attributes on an instance, even if they weren't previously defined.
``john['middle_name'] = 'Davis'`` or ``john.middle_name = 'Davis'``
**Example Usage**
First we'll create a named tuple typle called ``Person``, which takes two arguments, first_name and last_name.
>>> from privex.helpers import dictable_namedtuple
>>> Person = dictable_namedtuple('Person', 'first_name last_name')
Now we'll create an instance of ``Person`` called ``john``. These instances look like normal ``namedtuple``'s, and
should be generally compatible with any functions/methods which deal with named tuple's.
>>> john = Person('John', 'Doe') # Alternatively you can do Person(first_name='John', last_name='Doe')
>>> john
Person(first_name='John', last_name='Doe')
Unlike a normal ``namedtuple`` type instance, we can access fields by attribute (``.first_name``), index (``[0]``),
AND by item/key name (``['last_name']``).
>>> john.first_name
'John'
>>> john[0]
'John'
>>> john['last_name']
'Doe'
Another potentially useful feature, is that you can also update / create new fields, via your preferred method
of field notation (other than numbered indexes, since those don't include a field name)::
>>> john['middle_name'] = 'Davis'
>>> john.middle_name = 'Davis'
We can also convert ``john`` into a standard dictionary, with a simple ``dict(john)`` cast. You can see that
the new field we added (``middle_name``) is present in the dictionary serialized format.
>>> dict(john)
{'first_name': 'John', 'last_name': 'Doe', 'middle_name': 'Davis'}
:param str typename: The name used for the namedtuple type/class
:param str field_names: One or more field names separated by spaces, e.g. ``'id first_name last_name address'``
:key bool read_only: (Default: ``False``) If set to ``True``, the outputted dictable_namedtuple instance will not
allow new fields to be created via attribute / item setting.
:return Type[namedtuple] dict_namedtuple: A dict_namedtuple type/class which can be instantiated with the given
``field_names`` via positional or keyword args.
"""
module = kwargs.get('module', None)
read_only = kwargs.pop('read_only', False)
# As per namedtuple's comment block, we need to set __module__ to the frame
# where the named tuple is created, otherwise it can't be pickled properly.
# This also ensures that __module__ would match that of a normal namedtuple()
if module is None:
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
return make_dict_tuple(typename, field_names, module=module, read_only=read_only, *args, **kwargs)
[docs]class Dictable:
"""
A small abstract class for use with Python 3.7 dataclasses.
Allows dataclasses to be converted into a ``dict`` using the standard ``dict()`` function:
>>> @dataclass
>>> class SomeData(Dictable):
... a: str
... b: int
...
>>> mydata = SomeData(a='test', b=2)
>>> dict(mydata)
{'a': 'test', 'b': 2}
Also allows creating dataclasses from arbitrary dictionaries, while ignoring any extraneous dict keys.
If you create a dataclass using a ``dict`` and you have keys in your ``dict`` that don't exist in the dataclass,
it'll generally throw an error due to non-existent kwargs:
>>> mydict = dict(a='test', b=2, c='hello')
>>> sd = SomeData(**mydict)
TypeError: __init__() got an unexpected keyword argument 'c'
Using ``from_dict`` you can simply trim off any extraneous dict keys:
>>> sd = SomeData.from_dict(**mydict)
>>> sd.a, sd.b
('test', 2)
>>> sd.c
AttributeError: 'SomeData' object has no attribute 'c'
"""
def __iter__(self):
# Allow casting into dict()
for k, v in self.__dict__.items(): yield (k, v,)
[docs] @classmethod
def from_dict(cls, env):
# noinspection PyArgumentList
return cls(**{
k: v for k, v in env.items()
if k in inspect.signature(cls).parameters
})
[docs]class Mocker(object):
"""
This mock class is designed to be used either to act as a stand-in "noop" (no operation) object, which
could be used either as a drop-in replacement for a failed module / class import, or for certain unit tests.
If you need additional functionality such as methods having actual behaviour, you can set attributes on a
Mocker instance to either a lambda, or point them at a real function/method::
>>> m = Mocker()
>>> m.some_func = lambda a: a+1
>>> m.some_func(5)
6
**Example use case - fallback for unimportant module imports**
Below is a real world example of using :class:`.Mocker` and :py:func:`privex.helpers.decorators.mock_decorator`
to simulate :py:mod:`pytest` - allowing your tests to run under the standard :py:mod:`unittest` framework if
a user doesn't have pytest (as long as your tests aren't critically dependent on PyTest).
Try importing ``pytest`` then fallback to a mock pytest::
>>> try:
... import pytest
... except ImportError:
... from privex.helpers import Mocker, mock_decorator
... print('Failed to import pytest. Using privex.helpers.Mocker to fake pytest.')
... # Make pytest pretend to be the class 'module' (the class actually used for modules)
... pytest = Mocker.make_mock_class('module')
... # To make pytest.mark.skip work, we add the fake module 'mark', then set skip to `mock_decorator`
... pytest.add_mock_module('mark')
... pytest.mark.skip = mock_decorator
...
Since we added the mock module ``mark``, and set the attribute ``skip`` to point at ``mock_decorator``, the
test function ``test_something`` won't cause a syntax error. ``mock_decorator`` will just call test_something()
which doesn't do anything anyway::
>>> @pytest.mark.skip(reason="this test doesn't actually do anything...")
... def test_something():
... pass
>>>
>>> def test_other_thing():
... if True:
... return pytest.skip('cannot test test_other_thing because of an error')
...
>>>
**Generating "disguised" mock classes**
If you need the mock class to appear to have a certain class name and/or module path, you can generate
"disguised" mock classes using :py:meth:`.make_mock_class` like so:
>>> redis = Mocker.make_mock_class('Redis', module='redis')
>>> redis
<redis.Redis object at 0x7fd7402ea4a8>
**A :class:`.Mocker` instance has the following behaviour**
* Attributes that don't exist result in a function being returned, which accepts any arguments / keyword args,
and simply returns ``None``
Example::
>>> m = Mocker()
>>> repr(m.randomattr('hello', world=123))
'None'
* Arbitrary attributes ``x.something`` and items ``x['something']`` can be set on an instance, and they will
be similarly returned when they're accessed. Attributes and items share the same key/value's, so the
following examples are all accessing the same data::
Example::
>>> m = Mocker()
>>> m.example = 'hello'
>>> m['example'] = 'world'
>>> print(m.example)
world
>>> print(m['example'])
world
* You can add arbitrary "modules" to a Mocker instance. With only the ``name`` argument, :py:meth:`.add_mock_module`
will add a "module" under the instance, which is really just another :class:`.Mocker` instance.
Example::
>>> m = Mocker()
>>> m.add_mock_module('my_module')
>>> m.my_module.example = 'hello'
>>> print(m.my_module['example'], m.my_module.example)
hello hello
"""
mock_modules: dict
mock_attrs: dict
[docs] def __init__(self, modules: dict = None, attributes: dict = None):
self.mock_attrs = {} if attributes is None else attributes
self.mock_modules = {} if modules is None else modules
[docs] @classmethod
def make_mock_class(cls, name='Mocker', instance=True, **kwargs):
"""
Return a customized mock class or create an instance which appears to be named ``name``
Allows code which might check ``x.__class__.__name__`` to believe it's the correct object.
Using the kwarg ``module`` you can change the module that the class / instance appears to have been imported
from, allowing for quite deceiving fake classes and instances.
**Example usage**::
>>> redis = Mocker.make_mock_class('Redis', module='redis')
>>> # As seen below, the class appears to be called Redis, and even claims to be from the module `redis`
>>> redis
<redis.Redis object at 0x7fd7402ea4a8>
>>> print(f'Module: {redis.__module__} - Class Name: {redis.__class__.__name__}')
Module: redis - Class Name: Redis
**Creating methods/attributes dynamically**
You can set arbitrary attributes to point at a function, or just set them to a lambda::
>>> redis.exists = lambda key: 1
>>> redis.exists('hello')
1
>>> redis.hello() # Non-existent attributes just act as a function that eats any args and returns None
None
:param name: The name to write onto the mock class's ``__name__`` (and ``__qualname__`` if not specified)
:param bool instance: If ``True`` then the disguised mock class will be returned as an instance. Otherwise
the raw class itself will be returned for you to instantiate yourself.
:param kwargs: All kwargs (other than ``qualname``) are forwarded to ``__init__`` of the disguised class
if ``instance`` is True.
:key str qualname: Optionally specify the "qualified name" to insert into ``__qualname__``. If this isn't
specified, then ``name`` is used for qualname, which is fine for most cases anyway.
:key str module: Optionally override the module namespace that the class is supposedly from. If not specified,
then the class will just inherit this module (``privex.helpers.common``)
:return:
"""
qualname = kwargs.pop('qualname', name)
class OuterMocker(cls):
pass
OuterMocker.__name__ = name
OuterMocker.__qualname__ = qualname
if 'module' in kwargs:
OuterMocker.__module__ = kwargs['module']
return OuterMocker() if instance else OuterMocker
[docs] def add_mock_module(self, name: str, value=None, mock_attrs: dict = None, mock_modules: dict = None):
"""
Add a fake sub-module to this Mocker instance.
Example::
>>> m = Mocker()
>>> m.add_mock_module('my_module')
>>> m.my_module.example = 'hello'
>>> print(m.my_module['example'], m.my_module.example)
hello hello
:param str name: The name of the module to add.
:param value: Set the "module" to this object, instead of an instance of :class:`.Mocker`
:param dict mock_attrs: If ``value`` is ``None``, then this can optionally contain a dictionary of
attributes/items to pre-set on the Mocker instance.
:param dict mock_modules: If ``value`` is ``None``, then this can optionally contain a dictionary of
"modules" to pre-set on the Mocker instance.
"""
mock_attrs = {} if mock_attrs is None else mock_attrs
mock_modules = {} if mock_modules is None else mock_modules
self.mock_modules[name] = Mocker(modules=mock_modules, attributes=mock_attrs) if value is None else value
def __getattribute__(self, item):
try:
return super().__getattribute__(item)
except AttributeError:
pass
try:
if item in super().__getattribute__('mock_modules'):
return self.mock_modules[item]
except AttributeError:
pass
try:
if item in super().__getattribute__('mock_attrs'):
return self.mock_attrs[item]
except AttributeError:
pass
return lambda *args, **kwargs: None
def __setattr__(self, key, value):
if key in ['mock_attrs', 'mock_modules']:
return super().__setattr__(key, value)
m = super().__getattribute__('mock_attrs')
m[key] = value
def __getitem__(self, item):
return self.__getattribute__(item)
def __setitem__(self, key, value):
self.__setattr__(key, value)
@property
def __name__(self):
return self.__class__.__name__