Framework for composing algorithms I am implementing a game in python, which will give the user a sample encryption, and explain each step from plaintext to cipher. The task for the player is to decrypt a word by undoing each step shown above on the new word.

For this, I have created an abstract algorithm which requires an encrypt function, and a current development status. In the future I think it will require other properties such as explanation text or a difficulty level.

The abstract algorithm looks like this:

from abc import ABC, abstractmethod
from enum import Enum


class Status(Enum):
    """The current development status"""
    in_development = 0
    under_testing = 1
    production_ready = 2
    known_bugs = 3


class BaseAlgorithm(ABC):
    """This is the base on which all other algs are to be built"""
    @abstractmethod
    def encrypt(self, word):
        """Encrypt the word using an internal alg"""
        pass

    @abstractmethod
    def decrypt(self, word):
        """Decrypt the word using an internal alg
        If this is not overloaded, it is assumed that"""
        return self.encrypt(word)

    @property
    @abstractmethod
    def status(self):
        """The current progress and readiness of the alg"""
        pass


An example algorithm is implemented in rotate.py

from . import BaseAlgorithm, Status


class RotateAlg(BaseAlgorithm):
    """Takes a rotation amount  which it uses to encrypt or decrypt
    all words pass into it"""
    def __init__(self, offset):
        try:
            self.offset = int(offset)
        except ValueError:
            raise ValueError("the offset must be an integer") from None

        # Decides which direction is the default way to rotate
        self.rotate_left = True

    def encrypt(self, word):
        """Encrypts by rotating the word"""
        return self.rotate(word)

    def decrypt(self, word):
        """Decrypts by rotating the word back"""
        return self.rotate(word, inverse=True)

    def rotate(self, word, inverse=False):
        """Rotates the word to the left"""
        offset = self.offset
        # Inverse and rotate_left both flip the direction
        # If both are the same the direction remains unchanged
        if inverse == self.rotate_left:
            offset *= -1
        return rotate(word, offset)

    @property
    def status(self):
        return Status.in_development


def rotate(word, offset):
    """Rotates the word right by the amount specified"""
    offset %= len(word)
    return word[offset:] + word[:offset]


Finally how these algorithms would be composed together is a simple accumulating loop

def decode(cipher, encryptions):
    for encryption in reversed(encryptions):
        cipher = encryption.decrypt(cipher)
    return cipher


I feel like this is the wrong approach to take though, as there is a lot of boilerplate for what is essentially a function which is used in two ways. Is there a better design pattern to use in this scenario? I suspect a more functional approach would suit, such as passing the encryption functions into a class that binds them together. My concern is I lose the option of providing metadata with the function with that style.
For such a simple use case, I would drop the abc module in favor of NotImplementedError:

class BaseAlgorithm:
    """This is the base on which all other algorithms are to be built."""

    # Unless explicitly stated otherwise, algorithms are in development
    STATUS = Status.in_development

    def encrypt(self, word):
        """Encrypt the word using an internal algorithm."""
        raise NotImplementedError

    def decrypt(self, word):
        """Decrypt the word using an internal algorithm.

        If this is not overloaded, it is assumed that
        encrypting and decrypting are the same operation.
        """
        return self.encrypt(word)


Using such base, only encrypt is explicitly required to be overloaded. I also extracted the status from a method to a class constant as this is the kind of information that does not depend of any state about this class. It also allows to focus on the algorithm first and only change the status when it is ready. Lastly, I also expanded the docstrings a bit so they are more explicit; you may want to explain, in the class docstring, what a user is expected to do when inheriting from BaseAlgorithm.

As for RotateAlg:


you could name it Rotate as the Alg part is already implied from the base BaseAlgorithm class;
you could incorporate the rotate function directly in the rotate method as there shouldn't be much use for a function on their own;
you could handle the rotate_left logic better by using this information directly as a parameter to rotate.


This would become something along the lines of:

class Rotate(BaseAlgorithm):
    """Takes a rotation amount  which it uses to encrypt or decrypt
    all words pass into it"""
    def __init__(self, offset):
        try:
            self.offset = int(offset)
        except ValueError:
            raise ValueError("the offset must be an integer") from None

    def encrypt(self, word):
        """Encrypts by rotating the word"""
        return self.rotate(word, rotate_left=False)

    def decrypt(self, word):
        """Decrypts by rotating the word back"""
        return self.rotate(word, rotate_left=True)

    def rotate(self, word, rotate_left=False):
        """Rotates the word to the chosen direction"""
        offset = -self.offset if rotate_left else self.offset
        offset %= len(word)
        return word[offset:] + word[:offset]


And, unless you specify otherwise with:

class Rotate(BaseAlgorithm):
    STATUS = Status.under_testing
    ...


then Rotate.STATUS will still be Status.in_development.