Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 149 additions & 34 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,95 +1,210 @@
=========
#########
I Promise
=========
#########

.. image:: https://badge.fury.io/py/ipromise.svg
:target: https://badge.fury.io/py/ipromise
:alt: ipromise badge

**I Promise** provides a Python base class, and decorators for
specifying promises relating to inheritance.

This repository provides a Python base class, and various decorators for specifying promises relating to inheritance.
It provides three inheritance patterns:

* implementing,
* overriding, and
* augmenting.

Base class
==========
Checking promises depends on inheritance from the base class ``AbstractBaseClass``. Unlike the standard library's similar class ``abc.ABCMeta``, ``AbstractBaseClass`` does not bring in any metaclasses. This is thanks to Python 3.6's PEP 487, which added ``__init_subclass__``.
Using the inheritance patterns can ensure an inheritance hierarchy
is used as intended by forcing a run-time failure when not.

----

.. contents::

----

**********
Installing
**********

.. code-block:: shell

pip install ipromise

***
Use
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this section add anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It helps to distinguish installation. Also, the table of contents is more sensible. I can remove if you don't like it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see the table of contents, but okay let's just keep it then. I'll remove it finally if it doesn't look good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the statement .. contents::

https://docutils.sourceforge.io/docs/ref/rst/directives.html#table-of-contents

I added VS Code extension reStructuredText to view the README.rst.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks.

***

Base class ``AbstractBaseClass``
================================

Checking promises depends on inheritance from the base class
``AbstractBaseClass``. Unlike the standard library's similar class
``abc.ABCMeta``, the ``AbstractBaseClass`` does not bring in any metaclasses.
This is thanks to Python 3.6 `PEP 487
<https://peps.python.org/pep-0487/>`_
which added ``__init_subclass__``.

Implementing
============
*Implementing* is the pattern whereby an inheriting class's method implements an abstract method from a base class method.

*Implementing* is the pattern whereby an inheriting class's method implements an
abstract method from a base class method.

It is declared using the decorators:

* ``abc.abstractmethod`` from the standard library, and
* ``implements``, which indicates that a method implements an abstract method in a base class
* ``implements``, which indicates that a method implements an abstract method in
a base class.

For example:
From ``samples/implements.py`` :

.. code-block:: python

class HasAbstractMethod(AbstractBaseClass):
@abstractmethod
def f(self):
raise NotImplementedError
from ipromise import AbstractBaseClass, implements
import abc

class MyInterface(AbstractBaseClass):
@abc.abstractmethod
def f(self):
raise NotImplementedError("You forgot to implement f()")

class ImplementsAbstractMethod(HasAbstractMethod):
@implements(HasAbstractMethod)
class MyImplementation(MyInterface):
@implements(MyInterface)
def f(self):
print("MyImplementation().f()")
return 0

MyImplementation().f()


Overriding
==========
*Overriding* is the pattern whereby an inheriting class's method replaces the implementation of a base class method.
It is declared using the decorator ``overrides``, which marks the overriding method.

An overriding method could call super, but does not have to:
*Overriding* is the pattern whereby an inheriting class's method replaces the
implementation of a base class method.
It is declared using the decorator ``overrides``, which marks the overriding
method.

An overriding method could call ``super`` but it is not required.

From ``samples/overrides.py`` :

.. code-block:: python

class HasRegularMethod(AbstractBaseClass):
from ipromise import AbstractBaseClass, overrides

class MyClass(AbstractBaseClass):
def f(self):
print("MyClass().f()")
return 1


class OverridesRegularMethod(HasRegularMethod):
@overrides(HasRegularMethod)
class MyClassButBetter(MyClass):
@overrides(MyClass)
def f(self):
print("MyClassButBetter().f()")
return 2

MyClass().f()
MyClassButBetter().f()

Augmenting
==========
*Augmenting* is a special case of *overriding* whereby the inheriting class's method not only *overrides* the base class method, but *extends* its functionality.

*Augmenting* is a special case of *overriding* whereby the inheriting class's
method not only *overrides* the base class method, but *extends* its
functionality.
This means that it must delegate to *super* in all code paths.
This pattern is typical in multiple inheritance.

We hope that Python linters will be able to check for the super call.

Augmenting is declared using two decorators:

* ``augments`` indicates that this method must call super within its definition and thus augments the behavior of the base class method, and
* ``must_agugment`` indicates that child classes that define this method must decorate their method overriddes with ``augments``.
* ``augments`` indicates that this method must call super within its definition
and thus augments the behavior of the base class method, and
* ``must_augment`` indicates that child classes that define this method must
decorate their method overriddes with ``augments``.

For example:
From ``samples/augments.py`` :

.. code-block:: python

class HasMustAugmentMethod(AbstractBaseClass):
from ipromise import AbstractBaseClass, must_augment, augments
import abc

class MyClass(AbstractBaseClass):
@must_augment
def f(self):
# must_augment prevents this behavior from being lost.
print("MyClass().f()")
self.times_f_called += 1
return 0


class AugmentsMethod(HasMustAugmentMethod):
@augments(HasMustAugmentMethod)
class MyClassAgumentedOnce(MyClass):
@augments(MyClass)
def f(self, extra=0, **kwargs):
print("MyClassAgumentedOnce().f()")
return super().f(**kwargs) + extra


class AugmentsMethodFurther(AugmentsMethod):
@augments(HasMustAugmentMethod)
class MyClassAgumentedOnceAgain(MyClassAgumentedOnce):
@augments(MyClass)
def f(self, **kwargs):
print("f has been called")
print("MyClassAgumentedOnceAgain().f()")
return super().f(**kwargs)

MyClassAgumentedOnce().f()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these lines add anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example call is more important in the implements example where the user shoudl not call MyInterface().f().

For consistency, I added calls in the other examples. Some users may be new to inheritance concepts. So they may not understand what can and cannot be called. I wanted to make the examples explicit and consistent, if a little redundant.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that makes sense.

MyClassAgumentedOnceAgain().f()

***********
Development
***********

Pull Requests can be submitted on github.

poetry
======

The poetry development environment can be started with the typical
poetry commands:

.. code-block:: text

poetry install
poetry shell

Tools
=====

Tool commands should be run at the project top-level directory.

mypy
----

.. code-block:: text

mypy ipromise test

flake8
------

.. code-block:: text

pflake8 ipromise test

pytest
------

.. code-block:: text

pytest -c pyproject.toml test

rst-lint
--------

Only necessary for ``README.rst`` changes.

.. code-block:: text

rst-lint --level info README.rst
Loading