Auto registering class registry I built this auto registering registry to look into a package and import all modules and register any classes that are a subclass of a base class. I use it as a registry for a model's choices in Django and to be able to do a getattr on the class to get properties in a service that processes some data based on choices selected and dynamically finds the right class. 

Reasoning behind this methodology is because we would have an unknown number of stat classes that will be created in the future. I wanted to avoid forgetting to register them and to hide the implementation details of the choices on the model (The model is absolutely massive). So I wanted a folder that just takes care of that for us. 

I am trying to determine if this is breaking any best practices, is a good design, safe, efficient and not doing anything downright stupid. (Maybe the dynamic import).

Base class for using it other places if need be

class AutoRegClassRegistry(Singleton):
    PACKAGE = None
    BASE_CLS = None

    def __init__(self, force_import=True, *args, **kwargs):
        super(AutoRegClassRegistry, self).__init__(*args, **kwargs)

        self.force_import = force_import
        if self.PACKAGE:
            package_dir = os.path.dirname(self.PACKAGE.__file__)
            for module in [name for _, name, _ in pkgutil.iter_modules([package_dir])]:
                module = "{}.{}".format(self.PACKAGE.__name__, module)
                if self.force_import:
                    _ = __import__(module)
                for name, obj in inspect.getmembers(sys.modules[module], inspect.isclass):
                    if name == self.BASE_CLS.__name__:
                        continue
                    if issubclass(obj, self.BASE_CLS):
                        self.register(obj)

    def register(self):
        raise NotImplementedError("You must inherit from this class and override the register func")


Project structure

- init.py (this ClientStats class in here)
- base.py
- autoregister
    - init.py
    - hourly.py
    - daily.py
    - weekly.py
    - monthly.py


Implementation

class ClientStats(AutoRegClassRegistry):
    """
    A registry for client stats that also behaves as a tuple when used as a choices field. You can also do a getattr on
    this class to get the class, description or short of a stat.
    """
    PACKAGE = auto_register
    BASE_CLS = BaseStat

    def __init__(self, *args, **kwargs):
        self.__choices = []
        super(ClientStats, self).__init__(*args, **kwargs)

    def __getitem__(self, key):
        return self.__choices[key]

    def __iter__(self):
        return iter(self.__choices)

    def register(self, cls):
        cls_short = getattr(cls, "SHORT", None)
        if not cls_short:
            raise AttributeError("CLS {} doesn't have attribute 'SHORT' which is required to register to ClientStats")
        cls_desc = getattr(cls, "DESCRIPTION", None)
        if not cls_desc:
            raise AttributeError(
                "CLS {} doesn't have attribute 'DESCRIPTION' which is required to register to ClientStats")
        if hasattr(self, cls_short):
            raise AttributeError("ClientStats already has a {} registered".format(cls_short))
        setattr(self, cls_short, cls_short)
        setattr(self, "{}_DESC".format(cls_short), cls_desc)
        setattr(self, "{}_CLS".format(cls_short), cls)
        self.__choices.append((cls_short, getattr(self, "{}_DESC".format(cls_short))))

CLIENT_STATS = ClientStats()


Example class that gets registered

class DailyStatExample(BaseStat):
    SHORT = "DSE"
    DESCRIPTION = "Example stat class"


Sample usage of the ClientStats class:

models.py

class ExampleModel(models.Model):
    client_stats = MultiSelectSlugField(choices=CLIENT_STATS)


script.py

stats = ExampleModel.objects.get(pk=1).client_stats
for stat in stats:
    klass = getattr(CLIENT_STATS, "{}_CLS".format(stat)
    klass_inst = klass()
    klass.calculate(arg1, arg2)
Why

for module in [name for _, name, _ in pkgutil.iter_modules([package_dir])]:


and not

for _, module, _ in pkgutil.iter_modules([package_dir]):


? If it's about iterating eagerly, using list would work better than a list comprehension.

_ = __import__(module)


is also weird; why not just __import__(module)?

It makes sense for AutoRegClassRegistry to be an ABC, but by no means is this a requirement. It's worth noting that

def register(self):
    raise NotImplementedError("You must inherit from this class and override the register func")


has the wrong number of arguments, so will give an unhelpful error instead.

Don't use name mangling for self.__choices = [] unless there's good reason; _choices works just as well.

cls_short = getattr(cls, "SHORT", None)
if not cls_short:
    raise AttributeError("CLS {} doesn't have attribute 'SHORT' which is required to register to ClientStats")


seems like it breaks EAFTP;

cls_short = cls.SHORT


would work just as well and

try:
    cls_short = cls.SHORT
except AttributeError:
    raise AttributeError("CLS {} doesn't have attribute 'SHORT' which is required to register to ClientStats")


would be more descriptive of intent. The same happens immediately after.

The whole

if hasattr(self, cls_short):
    raise AttributeError("ClientStats already has a {} registered".format(cls_short))
setattr(self, cls_short, cls_short)
setattr(self, "{}_DESC".format(cls_short), cls_desc)
setattr(self, "{}_CLS".format(cls_short), cls)


looks really backwards, though; why not just put this in a simple data structure? Why does it have to be through messy attribute accesses.



This whole thing feels really dynamic, and dynamism should be a last resort. But I've never touched Django, so I can't really say whether what you're doing is justified or not.

As I said, I've never touched Django so I can't really say whether this could work, but I'd be tempted to do something more like

DailyStatExample = ClientStats.new("DSE", "Example stat class")


or

@ClientStats.register("DSE", "Example stat class")
class DailyStatExample:
    ...