2

I am trying to register classes that are in different files to the factory class. The factory class has a dictionary called "registry" which hold/maps the a user defined name to the registering class. My issue is that if my factory class and registering classes are in the same .py file everything works as expected but the moment I move the registering classes into their own .py files and import the factory class to apply the register decorator (as described in the question & article below) the "registry" dictionary stays empty, which means that the classes are not getting registered.

They way I am registering these classes is via a decorator. My code looks very much like what we see here:

I would like to know:

  • What why keeping them in the same file work while splitting them out doest
  • How can I make the separate file approach work ?

Hopefully the code samples in the articles clarify what I am trying to do and struggling with.

cryp
  • 2,285
  • 3
  • 26
  • 33
  • you need to import the child modules files somewhere for the code that registers them to run, this is the only answer you can get without giving a reproducible minimal example. – Ahmed AEK Sep 23 '22 at 14:50
  • 1
    Ahmen AEK - does this https://stackoverflow.com/questions/70854464/registering-classes-to-factory-with-classes-in-different-files not give you minimal example? – cryp Sep 23 '22 at 15:07
  • does "you need to import the child modules files somewhere for the code that registers them to run" not answer your question ? and yes that's not a minimal reproducible example. – Ahmed AEK Sep 23 '22 at 15:09

1 Answers1

2

I'm currently exploring a similar problem, and I think I may have found a solution. It is a bit of a 'hack' though, so take it with a grain of salt.

What why keeping them in the same file work while splitting them out doest

In order to make your classes self-register in the factory while keeping their definition in single .py files, we have to somehow force the loading of the classes in the .py files.

How can I make the separate file approach work?

In my case, I've came across this problem when trying to implement a 'Simple Factory', with self-registering subclasses to avoid having to modify the typical 'if/else' idiom in the factory's get() method.

I'll use a simple example, starting with the decorator method you've mentioned.

Example with decorators

Let's say we have a ShoeFactory as shown below, in which we register different 'classes' of shoes:

# file shoe.py

class ShoeFactory:
    _shoe_classes = {}

    @classmethod
    def get(cls, shoe_type:str):
        try:
            return cls._shoe_classes[shoe_type]()
        except KeyError:
            raise ValueError(f"unknown product type : {shoe_type}")

    @classmethod
    def register(cls, shoe_type:str):
        def inner_wrapper(wrapped_class):
            cls._shoe_classes[shoe_type] = wrapped_class
            return wrapped_class
        return inner_wrapper

Examples of shoe classes:

# file sandal.py

from shoe import ShoeFactory

@ShoeFactory.register('Sandal')
class Sandal:
    def __init__(self):
        print("i'm a sandal")
# file croc.py

from shoe import ShoeFactory

@ShoeFactory.register('Croc')
class Croc:
    def __init__(self):
        print("i'm a croc")

In order to make Sandal self-register in the ShoeFactory while keeping its definition in a single .py file, we have to somehow force the loading of the Sandal class in .py file.

I've done this in 3 steps:

  1. Keeping all class implementations in a specific folder, e.g., structuring the files as follows:
.
└- shoe.py     # file with the ShoeFactory class
└─ shoes/
  └- __init__.py
  └- croc.py
  └- sandal.py
  1. Adding the following statement to the end of the shoe.py file, which will take care of loading and registering each individual class:
from shoes import *
  1. Add a piece of code like the snippet below to your __init__.py within the shoes/ foder, so that to dynamically load all classes [1]:
from inspect import isclass
from pkgutil import iter_modules
from pathlib import Path
from importlib import import_module

# iterate through the modules in the current package
package_dir = Path(__file__).resolve().parent
for (_, module_name, _) in iter_modules([package_dir]):

    # import the module and iterate through its attributes
    module = import_module(f"{__name__}.{module_name}")
    for attribute_name in dir(module):
        attribute = getattr(module, attribute_name)

        if isclass(attribute):            
            # Add the class to this package's variables
            globals()[attribute_name] = attribute

If we follow this approach, I get the following results when running some test code as follows:

# file shoe_test.py

from shoe import ShoeFactory

if __name__ == "__main__":
    croc = ShoeFactory.get('Croc')
    sandal = ShoeFactory.get('Sandal')

$ python shoe_test.py
i'm a croc
i'm a sandal

Example with __init_subclass__()

I've personally followed a slighly different approach for my simple factory design, which does not use decorators.

I've defined a RegistrableShoe base class, and then used a __init_subclass__() approach to do the self-registering ([1] item 49, [2]).

I think the idea is that when Python finds the definition of a subclass of RegistrableShoe, the __init_subclass__() method is ran, which in turn registers the subclass in the factory.

This approach requires the following changes when compared to the example above:

  1. Added a RegistrableShoe base class to the shoe.py file, and re-factored ShoeFactory a bit:
# file shoe.py


class RegistrableShoe():
    def __init_subclass__(cls, shoe_type:str):
        ShoeFactory.register(shoe_type, shoe_class=cls)


class ShoeFactory:
    _shoe_classes = {}

    @classmethod
    def get(cls, shoe_type:str):
        try:
            return cls._shoe_classes[shoe_type]()
        except KeyError:
            raise ValueError(f"unknown product type : {shoe_type}")

    @classmethod
    def register(cls, shoe_type:str, shoe_class:RegistrableShoe):
        cls._shoe_classes[shoe_type] = shoe_class

from shoes import *

  1. Changed the concrete shoe classes to derive from the RegistrableShoe base class and pass a shoe_type parameter:
# file croc.py

from shoe import RegistrableShoe

class Croc(RegistrableShoe, shoe_type='Croc'):
    def __init__(self):
        print("i'm a croc")
# file sandal.py

from shoe import RegistrableShoe

class Sandal(RegistrableShoe, shoe_type='Sandal'):
    def __init__(self):
        print("i'm a sandal")
fortune_pickle
  • 141
  • 2
  • 8