Skip to main content

Config-file strategy pattern enabler: easily create Pydantic-friendly Enums with a function to call for each member

Project description

ͱ Start with the "why"

Consider this config file:

aggregation: mean

Not good

from numpy import mean, median
import yaml

config = yaml.safe_load('aggregation: mean')
runner = eval(config['aggregation']) # OUCH executable YAML

Much better, but still hella wobbly

import numpy as np, yaml

dispatcher = {"mean": np.mean, "median": np.median}

config = yaml.safe_load('aggregation: mean')
runner = dispatcher[config['aggregation']]

Tidier

import numpy as np, yaml
from pydantic import BaseModel, field_validator

dispatcher = {"mean": np.mean, "median": np.median}

class Config(BaseModel):
    aggregation: str

    @field_validator('aggregation')
    @classmethod
    def agg_must_be_valid(cls, v: str) -> str:
        if v not in dispatcher:
            raise ValueError('Invalid aggregation')
        return v.title()

config = Config(yaml.safe_load('aggregation: mean'))
runner = dispatcher[config.aggregation]

Very much better

import numpy as np, yaml
from pydantic import BaseModel
from dispatcher import Dispatcher

# shortcut utility that creates a DispatchEnum object
Aggregation = Dispatcher(
    mean = np.mean,
    median = np.median
)
class Config:
    aggregation: Aggregation = Aggregation.MEAN

config = Config(yaml.safe_load('aggregation: mean'))

runner = config.aggregation # ding
# now the "aggregation" YAML field is parsed by pydantic into 
# an Aggregation object derived from an Enum that's also callable!
runner0 = lambda xs: config.aggregation(xs) # ding ding ding ding.

The "what"

This code provides a DispatchEnum class that subclasses from Enum but holds an additional value for each member. This is most useful in combination with Pydantic, which is able to parse Enum-valued fields received as strings, i.e.

class Parity(Enum):
    ODD = "odd"
    EVEN = "even"

class Parser(BaseModel):
     check_parity: Parity

config = Parser({"check_parity": "odd" })
print(config.check_parity) # prints Parity.ODD

With DispatchEnum we're able to assign an additional property to each Enum member:

class Parity(DispatchEnum):
    ODD = "odd"
    EVEN = "even"

Parity.assign(Parity.ODD, lambda x: x % 2 == 1)
Parity.assign(Parity.EVEN, lambda x: x % 2 == 0)
# or
Parity.from_dict({"ODD": lambda x: x % 2 == 1, "EVEN": lambda x: x % 2 == 0})

print(Parity.ODD(2)) # prints False

Therefore DispatchEnumis both a "dispatcher" (mapping a string identifier to a function) and an Enum (enabling Pydantic goodness).

For further convenience, the Dispatcher function creates a DispatchEnum filling in member names:

Aggregation = Dispatcher(
    mean = np.mean,
    median = np.median
)
# does the same as 

class Aggregation(DispatchEnum):
    MEAN: "mean"
    MEDIAN: "median"
Aggregation.from_dict({"mean": np.mean, "median": np.median})

Installation

Right now you should download dispatch.py and vendor it in. Soonishly a more mature version will be hitting PyPI too.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dispatcher-enum-0.1.0.tar.gz (7.0 kB view hashes)

Uploaded Source

Built Distribution

dispatcher_enum-0.1.0-py3-none-any.whl (7.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page