Check unique value when define concrete class for abstract variable in python

Question:

Suppose that I have this architecture for my classes:

# abstracts.py
import abc
class AbstractReader(metaclass=abc.ABCMeta):
  
    @classmethod
    def get_reader_name(cl):
        return cls._READER_NAME

    @classmethod
    @property
    @abc.abstractmethod
    def _READER_NAME(cls):
        raise NotImplementedError

# concretes.py
from .abstracts import AbstractReader
class ReaderConcreteNumber1(AbstractReader):
    _READER_NAME = "NAME1"

class ReaderConcreteNumber2(AbstractReader):
    _READER_NAME = "NAME2"

Also I have a manager classes that find concrete classes by _READER_NAME variable. So I need to define unique _READER_NAME for each of my concrete classes.

how do I check that NAME1 and NAME2 are unique when concrete classes are going to define?

Asked By: Whale 52Hz

||

Answers:

This is a very special case, but it can be solved with a singleton pattern.

To ease things for our selfes we first create a singleton annotation

# anotations.py

def singleton(clazz):
    """Singleton annotator ensures the annotated class is a singleton"""

    class ClassW(clazz):
        """Creates a new sealed class from the object to create."""
        _instance = None

        def __new__(cls, *args, **kwargs):
            if ClassW._instance is None:
                ClassW._instance = super(ClassW, cls).__new__(clazz, *args, **kwargs)
                ClassW._instance._sealed = False

            return ClassW._instance

        def __init__(self, *args, **kwargs):
            if self._sealed:
                return

            super(ClassW, self).__init__(*args, **kwargs)
            self._sealed = True

    ClassW.__name__ = clazz.__name__
    return ClassW

Now we construct a singleton Registry class, to register our classes with and to do the checking.

# registry.py

from .annotations import singleton 

@singleton
class ReaderRegistry:
    """
    Singleton class to register processing readers

    ### Usage
    To register a block call the register function with an ID and the class object.
        ReaderRegistry().register('FooName', FooReader)
    The class for the block can then be obtained via
        Registry()['FooName']

    """
    registry = {}

    def register(self, key: str, clazz: Type[Block]) -> None:
        """Register a new reader. Names must be unique within the registry"""
        if key in self:
            raise f"Reader with key {key} already registered."
        self.registry[key] = clazz

    def __contains__(self, key: str) -> bool:
        return key in self.registry.keys()

    def __getitem__(self, key: str) -> Type[Block]:
        return self.registry[key]

Now, you can

#concretes.py 

from .abstracts import AbstractReader
from .registry import ReaderRegistry

class ReaderConcreteNumber1(AbstractReader):
    _READER_NAME = "NAME1"

# Note that this is OUTSIDE and AFTER the class definition, 
# e.g. end of the file. 
RederRegistry().register(ReaderConcreteNumber1._READER_NAME , ReaderConcreteNumber1)

If a reader with such a name exists in the registry already there will be an exception thrown once the file is imported. Now, you can just lookip the classes to construct by theire naume in the registry, e.g.

if reader _namenot in ReaderRegistry():
    raise f"Block [{reader _name}] is not known."

reader = ReaderRegistry()[reader _name]
Answered By: Cpt.Hook

You can create a metaclass with a constructor that uses a set to keep track of the name of each instantiating class and raises an exception if a given name already exists in the set:

class UniqueName(type):
    names = set()

    def __new__(metacls, cls, bases, classdict):
        name = classdict['_READER_NAME']
        if name in metacls.names:
            raise ValueError(f"Class with name '{name}' already exists.")
        metacls.names.add(name)
        return super().__new__(metacls, cls, bases, classdict)

And make it the metaclass of your AbstractReader class. since Python does not allow a class to have multiple metaclasses, you would need to make AbstractReader inherit from abc.ABCMeta instead of having it as a metaclass:

class AbstractReader(abc.ABCMeta, metaclass=UniqueName):
    ... # your original code here

Or if you want to use ABCMeta as metaclass in your AbstractReader, just override ABCMeta class and set child ABC as metaclass in AbstractReader:

class BaseABCMeta(abc.ABCMeta):
    """
    Check unique name for _READER_NAME variable
    """
    _readers_name = set()

    def __new__(mcls, name, bases, namespace, **kwargs):
        reader_name = namespace['_READER_NAME']
        if reader_name in mcls._readers_name:
            raise ValueError(f"Class with name '{reader_name}' already exists. ")
        mcls._readers_name.add(reader_name)

        return super().__new__(mcls, name, bases, namespace, **kwargs)

class AbstractReader(metaclass=BaseABCMeta):
    # Your codes ...

So that:

class ReaderConcreteNumber1(AbstractReader):
    _READER_NAME = "NAME1"

class ReaderConcreteNumber2(AbstractReader):
    _READER_NAME = "NAME1"

would produce:

ValueError: Class with name 'NAME1' already exists.

Demo: https://replit.com/@blhsing/MerryEveryInternet

Answered By: blhsing