Metadata-Version: 2.1
Name: abstracts
Version: 0.0.12
Summary: Abstract class and interface definitions
Home-page: https://github.com/envoyproxy/pytooling/tree/main/abstracts
Author: Ryan Northey
Author-email: ryan@synca.io
Maintainer: Ryan Northey
Maintainer-email: ryan@synca.io
License: Apache Software License 2.0
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Testing
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Operating System :: OS Independent
Classifier: License :: OSI Approved :: Apache Software License
Requires-Python: >=3.5
Provides-Extra: lint
Requires-Dist: flake8 ; extra == 'lint'
Provides-Extra: publish
Requires-Dist: wheel ; extra == 'publish'
Provides-Extra: test
Requires-Dist: pytest ; extra == 'test'
Requires-Dist: pytest-coverage ; extra == 'test'
Requires-Dist: pytest-patches ; extra == 'test'
Provides-Extra: types
Requires-Dist: mypy ; extra == 'types'


abstracts
=========

Abstract class and interface definitions.

Create an ``abstract.Abstraction``
----------------------------------

An ``Abstraction`` is a ``metaclass`` for defining abstract classes.

Let's define an abstract ``AFoo`` class and give it an abstract ``do_foo``
method.

Like any python class, an ``Abstraction`` can have any name, but it may
be helpful to distinguish abstract classes from others by prefixing their
name with ``A``.

.. code-block:: pycon

   >>> import abc
   >>> import abstracts

   >>> class AFoo(metaclass=abstracts.Abstraction):
   ...
   ...     @abc.abstractmethod
   ...     def do_foo(self):
   ...         raise NotImplementedError

Abstract classes **cannot** be instantiated directly.

.. code-block:: pycon

   >>> AFoo()
   Traceback (most recent call last):
   ...
   TypeError: Can't instantiate abstract class AFoo with abstract method... do_foo

Create an ``implementer`` for an ``abstract.Abstraction``
---------------------------------------------------------

In order to make use of ``AFoo``, we need to create an implementer for it.

.. code-block:: pycon

   >>> @abstracts.implementer(AFoo)
   ... class Foo:
   ...     pass

The implementer **must** implement all of the abstract methods,
defined by its abstract classes.

.. code-block:: pycon

   >>> Foo()
   Traceback (most recent call last):
   ...
   TypeError: Can't instantiate abstract class Foo with abstract method... do_foo

   >>> @abstracts.implementer(AFoo)
   ... class Foo2:
   ...
   ...     def do_foo(self):
   ...         return "DID FOO"

   >>> Foo2()
   <__main__.Foo2 object at ...>

An implementer inherits from its ``Abstractions``
-------------------------------------------------

An ``implementer`` class is a subclass of its ``Abstraction``.

.. code-block:: pycon

   >>> issubclass(Foo2, AFoo)
   True

Likewise an instance of an implementer is an instance of its ``Abstraction``

.. code-block:: pycon

   >>> isinstance(Foo2(), AFoo)
   True

The ``Abstraction`` class can be seen in the class ``bases``, and the
methods of the ``Abstraction`` can be invoked by the implementer.

.. code-block:: pycon

   >>> import inspect
   >>> AFoo in inspect.getmro(Foo2)
   True

Create an ``implementer`` that implements multiple ``Abstraction`` s.
---------------------------------------------------------------------

An implementer can implement multiple abstractions.

Let's create a second abstraction.

.. code-block:: pycon

   >>> class ABar(metaclass=abstracts.Abstraction):
   ...
   ...     @abc.abstractmethod
   ...     def do_bar(self):
   ...         raise NotImplementedError

And now we can create an implementer that implememts both the ``AFoo`` and ``ABar``
``Abstraction`` s.

.. code-block:: pycon

   >>> @abstracts.implementer((AFoo, ABar))
   ... class FooBar:
   ...
   ...     def do_foo(self):
   ...         return "DID FOO"
   ...
   ...     def do_bar(self):
   ...         return "DID BAR"

   >>> FooBar()
   <__main__.FooBar object at ...>

Defining abstract properties
----------------------------

Properties can be defined in an abstract class, and just like with normal
methods, they must be implemented by any implementers.

.. code-block:: pycon

   >>> class AMover(metaclass=abstracts.Abstraction):
   ...
   ...     @property
   ...     @abc.abstractmethod
   ...     def speed(self):
   ...         return 5
   ...
   ...     @property
   ...     @abc.abstractmethod
   ...     def direction(self):
   ...         return "forwards"

Calling ``super()`` on an ``abstractmethod``
--------------------------------------------

Just like with pythons "Abstract Base Classes" you can call ``super()``
in an ``abstractmethod``, to invoke an abstract implementation.

.. code-block:: pycon

   >>> @abstracts.implementer(AMover)
   ... class Mover:
   ...
   ...     @property
   ...     def direction(self):
   ...         return "backwards"
   ...
   ...     @property
   ...     def speed(self):
   ...         return super().speed

This custom implementation of ``AMover`` **must** implement both ``speed`` and
``direction``, even if its implementation invokes the abstract implementation.

In this case it uses the default/abstract implementation of ``speed`` while providing
its own implementation of ``direction``.

.. code-block:: pycon

   >>> mover = Mover()
   >>> mover
   <__main__.Mover object at ...>

   >>> mover.speed
   5
   >>> mover.direction
   'backwards'

Defining an ``abstracts.Interface`` class
-----------------------------------------

An ``Interface`` is much like an ``Abstraction``, but with a few differences.

An ``Interface`` can only define methods with the ``@interfacemethod`` decorator.

It cannot define normal methods or methods with the ``@abstractmethod``, only methods
with ``@interfacemethod``.

An ``@interfacemethod`` if invoked will always raise an ``NotImplementedError``, and
therefore cannot be used as an abstract implementation.

Lets add an ``Interface`` class that we can use.

In the way that it may be helpful to distinguish an ``Abstraction`` from other
types of classes, it may be also useful to distinguish an ``Interface`` by
using an ``I`` prefix when naming them.

.. code-block:: pycon

   >>> class IGeared(metaclass=abstracts.Interface):
   ...
   ...     @property
   ...     @abstracts.interfacemethod
   ...     def number_of_gears(self):
   ...         # Raising an error is ~superfluous as the decorator will raise
   ...         # anyway if the method is invoked.
   ...         raise NotImplementedError

Implementing an ``Interface``
-----------------------------

Just like with an ``Abstraction``, an ``Interface`` can be implemented using
the ``@implementer`` decorator.

An implementer, can implement a combination of ``Abstractions`` and
``Interfaces``.

.. code-block:: pycon

   >>> @abstracts.implementer((AMover, IGeared))
   ... class Bicycle:
   ...
   ...     @property
   ...     def direction(self):
   ...         return super().direction
   ...
   ...     @property
   ...     def speed(self):
   ...         return super().speed
   ...
   ...     @property
   ...     def number_of_gears(self):
   ...         return 7

   >>> Bicycle().number_of_gears
   7

An implementer does **not** inherit from its ``Interfaces``
-----------------------------------------------------------

An ``implementer`` class is a subclass of its ``Interfaces``.

.. code-block:: pycon

   >>> issubclass(Bicycle, AMover)
   True
   >>> issubclass(Bicycle, IGeared)
   True

Likewise an instance of an implementer is an instance of its ``Interfaces``

.. code-block:: pycon

   >>> isinstance(Bicycle(), AMover)
   True
   >>> isinstance(Bicycle(), IGeared)
   True

Unlike with ``Abstractions`` it does **not** however, inherit from its ``Interfaces``.

.. code-block:: pycon

   >>> AMover in inspect.getmro(Bicycle)
   True

   >>> IGeared in inspect.getmro(Bicycle)
   False

``@interfacemethods`` can never be invoked
------------------------------------------

The key thing to remember is that you cannot call ``super()`` on any
``@interfacemethod``, or directly invoke it.

If it was defined as part of an ``Interface`` you will receive an
``AttributeError``, as the implementation does not inherit directly from the
interface.

.. code-block:: pycon

   >>> @abstracts.implementer((AMover, IGeared))
   ... class BrokenBicycle:
   ...
   ...     @property
   ...     def direction(self):
   ...         return super().direction
   ...
   ...     @property
   ...     def speed(self):
   ...         return super().speed
   ...
   ...     @property
   ...     def number_of_gears(self):
   ...         return super().number_of_gears

   >>> BrokenBicycle().number_of_gears
   Traceback (most recent call last):
   ...
   AttributeError: 'super' object has no attribute 'number_of_gears'

.. warning::

   Misuse of this class can have `unintended consequences <https://www.dailymotion.com/video/x2howud>`_

If you invoke ``super()`` on an ``@interfacemethod`` defined as part of an
``Abstraction`` it will raise ``NotImplementedError``.

As an ``Interface`` can only hold this type of method, you can never invoke
any of its methods. Doing so directly will raising a ``NotImplementedError``.

.. code-block:: pycon

   >>> IGeared.number_of_gears.__get__(Bicycle())
   Traceback (most recent call last):
   ...
   NotImplementedError

Combining ``@abstractmethod`` and ``@interfacemethod`` in an ``Abstraction``
----------------------------------------------------------------------------

As ``Interfaces`` are "pure", they cannot use ``@abstractmethod`` or contain any implementation.

An ``Abstraction`` on the other hand can combine both.

Lets create a pure ``Interface`` that represents a "shed".

.. code-block:: pycon

   >>> class IShed(metaclass=abstracts.Interface):
   ...
   ...     @property
   ...     @abstracts.interfacemethod
   ...     def size(self):
   ...         raise NotImplementedError

We can use this interface to create an ``ABikeShed`` ``Abstraction``

.. code-block:: pycon

   >>> class ABikeShed(IShed, metaclass=abstracts.Abstraction):
   ...
   ...     @property
   ...     @abstracts.interfacemethod
   ...     def max_bike_size(self):
   ...         raise NotImplementedError
   ...
   ...     @abc.abstractmethod
   ...     def get_capacity(self):
   ...         return int(self.size / self.max_bike_size)

We can now create an implementation.

It will need to define both the ``size`` and the ``max_bike_size``,
as these are ``interfacemethods``.

It can, however, make use of the abstract implementation of ``get_capacity``,
even if it must be defined.

.. code-block:: pycon

   >>> @abstracts.implementer(ABikeShed)
   ... class BikeShed:
   ...
   ...     @property
   ...     def max_bike_size(self):
   ...         return 7
   ...
   ...     @property
   ...     def size(self):
   ...         return 161
   ...
   ...     def get_capacity(self):
   ...         return super().get_capacity()

   >>> bikeshed = BikeShed()
   >>> bikeshed.get_capacity()
   23


