ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.86k stars 303 forks source link

Building a container with a list of derived classes #762

Open gitisz opened 11 months ago

gitisz commented 11 months ago

Thank you for this clean DI framework!

I have a CLI that I am attempting to refactor, where in it I have a concept of Services. This is a list of configurable objects, but all should be derived from a common base object.

The documentation points to providers.List, which seemed promising except it requires arguments to be coded.

Here is a picture of our configuration:

        self.file = {
            "Cli": {
                "Version": "latest",
                "Resources": {
                    "Qa": {
                        "Failover": {
                            "Activities": [
                                "qa-activity-one",
                                "qa-activity-two",
                            ],
                            "Services": [
                                {
                                    "CloudWatchService": {
                                        "Name": "qa-cloudwatch-service-1"
                                    }
                                },
                                {"S3Service": {"Name": "qa-s3-service-1"}},
                            ],
                            "Configuration": {},
                        }
                    },
                    "Prod": {
                        "Failover": {
                            "Activities": [
                                "prod-activity-one",
                                "prod-activity-two",
                            ],
                            "Services": [
                                {
                                    "CloudWatchService": {
                                        "Name": "prod-cloudwatch-service-1"
                                    }
                                },
                                {"S3Service": {"Name": "prod-s3-service-1"}},
                            ],
                            "Configuration": {},
                        }
                    },
                },
            }
        }

From it, Services is an array of different service types.

class BaseService:
    def __init__(self, Name):
        self.Name = Name

    def perform(self):
        pass

class CloudWatchService(BaseService):
    def __init__(self, Name):
        self.Name = Name

    def perform(self):
        logger.debug("CloudWatchService")

class S3Service(BaseService):
    def __init__(self, Name):
        self.Name = Name

    def perform(self):
        logger.debug("S3Service")

class Failover:
    """
    Contains Activities, Services, and Configuration, while also responsible for providing EffectiveActivities.
    """

    def __init__(self, Activities, Services, Configuration):
        self.Activities = Activities
        self.Services = Services
        self.Configuration = Configuration

class Environment:
    """
    Provides for a selectable environment, such as Dev, Qa, and Prod containing the full graph of Failover configuration.
    """

    def __init__(self, Failover):
        self.Failover = Failover

class Resources:
    def __init__(self, Environment):
        self.Environment = Environment

class Cli:
    def __init__(self, Version, Resources):
        self.Version = Version
        self.Resources = Resources

When creating a container, this works, except the Services are still a dictionary and not converted to classes:

class Container(containers.DynamicContainer):
    def getFailoverProvider(environment, resources):
        activities = resources[environment].Failover.Activities
        configuration = resources[environment].Failover.Configuration
        services = providers.Factory(list, resources[environment].Failover.Services)
        failover = providers.Singleton(
            Failover,
            Activities=activities,
            Configuration=configuration,
            Services=services
        )
        return failover
    config = providers.Configuration()
    environment = providers.Selector(
        config.environment,
        Qa=providers.Singleton(
            Environment, Failover=getFailoverProvider("Qa", config.OneCli.Resources)
        ),
        Prod=providers.Singleton(
            Environment, Failover=getFailoverProvider("Prod", config.OneCli.Resources)
        ),
    )
    resources = providers.Singleton(Resources, Environment=environment)
    Cli = providers.Singleton(OneCli, config.OneCli.Version, resources)

Here is my test:

class FailoverTest(unittest.TestCase):
    def setUp(self):
        logger = logging.getLogger()
        logger.level = logging.INFO
        stream_handler = logging.StreamHandler(sys.stdout)
        logger.addHandler(stream_handler)

        self.file = {
            "Cli": {
                "Version": "latest",
                "Resources": {
                    "Qa": {
                        "Failover": {
                            "Activities": [
                                "qa-activity-one",
                                "qa-activity-two",
                            ],
                            "Services": [
                                {
                                    "CloudWatchService": {
                                        "Name": "qa-cloudwatch-service-1"
                                    }
                                },
                                {"S3Service": {"Name": "qa-s3-service-1"}},
                            ],
                            "Configuration": {},
                        }
                    },
                    "Prod": {
                        "Failover": {
                            "Activities": [
                                "prod-activity-one",
                                "prod-activity-two",
                            ],
                            "Services": [
                                {
                                    "CloudWatchService": {
                                        "Name": "prod-cloudwatch-service-1"
                                    }
                                },
                                {"S3Service": {"Name": "prod-s3-service-1"}},
                            ],
                            "Configuration": {},
                        }
                    },
                },
            }
        }

    def test_qa_failover_init(self):
        # Arrange
        container = Container()
        container.config.from_dict(self.file)
        container.config.environment.from_value("Qa")

        # Act
        logger.debug(container.config)
        cli = container.Cli()

        # Assert
        self.assertIsInstance(cli, Cli)
        self.assertTrue(oneCli.Version == "latest")

        self.assertIsInstance(cli.Resources, Resources)
        self.assertIsNotNone(cli.Resources)

        self.assertIsInstance(cli.Resources.Environment, Environment)
        self.assertIsNotNone(cli.Resources.Environment)
        self.assertIn(
            "qa-activity-one", cli.Resources.Environment.Failover.Activities
        )

        # I don't want this: name = cli.Resources.Environment.Failover.Services[0]['CloudWatchService']['Name']
        # I want this: name = cli.Resources.Environment.Failover.Services[0].CloudWatchService.Name

How can create list of derived objects based from the configuration loaded at runtime?