# ͱ Start with the "why"
DispatchEnum is *the* Pythonic way to deal with the "strategy in config" pattern, where
we want choices in implementation details ("strategies") to be available outside
Python code proper.
Consider this cfg file:
```yaml
aggregation: mean
length: square
```
We see this typically when we want to allow for different aggregating functions (mean, median...) to be
used in a functionality that meaningfully accepts them.
## Not good
```py
from numpy import mean, median, abs # rock the global namespace!
import yaml
square = lambda x: x*x
def excess(lst, cfg):
agg = eval(cfg['aggregation'])(lst) # OUCH executable YAML
return [eval(cfg['length'])(val - agg) for val in lst] # OOF right in the feels
cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # prints [1,0,1]
```
## Much better, but still hella wobbly
```py
import numpy as np, yaml
agg_dispatcher = {"mean": np.mean, "median": np.median}
len_dispatcher = {"square": lambda x: x*x, "abs": np.abs}
def excess(lst, cfg):
agg = agg_dispatcher[cfg['aggregation']](lst)
return [len_dispatcher[cfg['length']](val - agg) for val in lst]
cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # same as above
```
## Safer with Pydantic but drowning in boilerplate
```py
import numpy as np, yaml
from pydantic import BaseModel, field_validator
agg_dispatcher = {"mean": np.mean, "median": np.median}
len_dispatcher = {"square": lambda x: x*x, "abs": np.abs}
class Config(BaseModel):
aggregation: str
length: str
@field_validator('aggregation')
@classmethod
def agg_must_be_valid(cls, v: str) -> str:
if v not in agg_dispatcher:
raise ValueError('Invalid aggregation')
return v
@field_validator('length')
@classmethod
def len_must_be_valid(cls, v: str) -> str:
if v not in len_dispatcher:
raise ValueError('Invalid length')
return v
def excess(lst, cfg):
agg = agg_dispatcher[cfg.aggregation](lst)
return [len_dispatcher[cfg.length](val - agg) for val in lst]
cfg = yaml.safe_load(config.yaml)
print(excess([1,2,3], cfg)) # same as above
```
## Class and quality
```py
import numpy as np, yaml
from pydantic import BaseModel
from dispatcher import Dispatcher
# shortcut utility that creates a DispatchEnum object
AggregationStrategy = Dispatcher(
mean = np.mean,
median = np.median
)
LengthStrategy = Dispatcher(
square = lambda x: x*x,
abs = np.abs
)
class Config:
aggregation: AggregationStrategy = AggregationStrategy.MEAN
length: LengthStrategy
cfg = Config(yaml.safe_load(config.yaml))
def excess(lst, cfg):
agg = cfg.aggregation(lst) # ding ding ding ding
return [cfg.length(val - agg) for val in lst]
```
# 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.
```py
class Parity(Enum):
ODD = "odd"
EVEN = "even"
class Parser(BaseModel):
check_parity: Parity
cfg = Parser({"check_parity": "odd" })
print(cfg.check_parity) # prints Parity.ODD
```
With `DispatchEnum` we're able to assign an additional property to each Enum member:
```py
class Parity(DispatchEnum):
ODD = "odd"
EVEN = "even"
Parity.from_dict({"ODD": lambda x: x % 2 == 1, "EVEN": lambda x: x % 2 == 0})
print(Parity.ODD(2)) # prints False
```
Therefore `DispatchEnum`is 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:
```py
AggregationStrategy = Dispatcher(
mean = np.mean,
median = np.median
)
```
which is shorthand for
```py
class AggregationStrategy(DispatchEnum):
MEAN: "mean"
MEDIAN: "median"
AggregationStrategy.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.
Raw data
{
"_id": null,
"home_page": "https://github.com/asemic-horizon/dispatcher",
"name": "dispatcher-enum",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": null,
"author": "Diego Navarro",
"author_email": "the.electric.me@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/e3/7c/51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86/dispatcher-enum-0.1.1.tar.gz",
"platform": null,
"description": "# \u0371 Start with the \"why\" \n\nDispatchEnum is *the* Pythonic way to deal with the \"strategy in config\" pattern, where\nwe want choices in implementation details (\"strategies\") to be available outside \nPython code proper.\n\nConsider this cfg file:\n```yaml\naggregation: mean\nlength: square\n```\n\nWe see this typically when we want to allow for different aggregating functions (mean, median...) to be\nused in a functionality that meaningfully accepts them.\n\n## Not good\n\n```py\nfrom numpy import mean, median, abs # rock the global namespace!\nimport yaml\n\nsquare = lambda x: x*x\n\ndef excess(lst, cfg):\n agg = eval(cfg['aggregation'])(lst) # OUCH executable YAML\n return [eval(cfg['length'])(val - agg) for val in lst] # OOF right in the feels\n\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # prints [1,0,1] \n```\n\n## Much better, but still hella wobbly\n\n```py\nimport numpy as np, yaml\n\nagg_dispatcher = {\"mean\": np.mean, \"median\": np.median}\nlen_dispatcher = {\"square\": lambda x: x*x, \"abs\": np.abs}\n\ndef excess(lst, cfg):\n agg = agg_dispatcher[cfg['aggregation']](lst)\n return [len_dispatcher[cfg['length']](val - agg) for val in lst]\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # same as above \n```\n\n## Safer with Pydantic but drowning in boilerplate\n\n```py\nimport numpy as np, yaml\nfrom pydantic import BaseModel, field_validator\n\nagg_dispatcher = {\"mean\": np.mean, \"median\": np.median}\nlen_dispatcher = {\"square\": lambda x: x*x, \"abs\": np.abs}\n\nclass Config(BaseModel):\n aggregation: str\n length: str\n\n @field_validator('aggregation')\n @classmethod\n def agg_must_be_valid(cls, v: str) -> str:\n if v not in agg_dispatcher:\n raise ValueError('Invalid aggregation')\n return v\n\n @field_validator('length')\n @classmethod\n def len_must_be_valid(cls, v: str) -> str:\n if v not in len_dispatcher:\n raise ValueError('Invalid length')\n return v\n\ndef excess(lst, cfg):\n agg = agg_dispatcher[cfg.aggregation](lst)\n return [len_dispatcher[cfg.length](val - agg) for val in lst]\n\ncfg = yaml.safe_load(config.yaml)\nprint(excess([1,2,3], cfg)) # same as above \n\n```\n\n## Class and quality\n```py\nimport numpy as np, yaml\nfrom pydantic import BaseModel\nfrom dispatcher import Dispatcher\n\n# shortcut utility that creates a DispatchEnum object\nAggregationStrategy = Dispatcher(\n mean = np.mean,\n median = np.median\n)\nLengthStrategy = Dispatcher(\n square = lambda x: x*x,\n abs = np.abs\n) \nclass Config:\n aggregation: AggregationStrategy = AggregationStrategy.MEAN\n length: LengthStrategy \n\ncfg = Config(yaml.safe_load(config.yaml))\ndef excess(lst, cfg):\n agg = cfg.aggregation(lst) # ding ding ding ding\n return [cfg.length(val - agg) for val in lst]\n\n```\n\n# The \"what\"\n\nThis code provides a `DispatchEnum` class that subclasses from Enum but holds an\nadditional value for each member. This is most useful in combination with Pydantic,\nwhich is able to parse Enum-valued fields received as strings, i.e.\n\n```py\nclass Parity(Enum):\n ODD = \"odd\"\n EVEN = \"even\"\n\nclass Parser(BaseModel):\n check_parity: Parity\n\ncfg = Parser({\"check_parity\": \"odd\" })\nprint(cfg.check_parity) # prints Parity.ODD\n```\n\nWith `DispatchEnum` we're able to assign an additional property to each Enum member:\n\n```py\nclass Parity(DispatchEnum):\n ODD = \"odd\"\n EVEN = \"even\"\n\nParity.from_dict({\"ODD\": lambda x: x % 2 == 1, \"EVEN\": lambda x: x % 2 == 0})\nprint(Parity.ODD(2)) # prints False\n```\n\nTherefore `DispatchEnum`is both a \"dispatcher\" (mapping a string identifier to a function)\nand an `Enum` (enabling Pydantic goodness).\n\nFor further convenience, the `Dispatcher` function creates a DispatchEnum filling in member names:\n\n```py\nAggregationStrategy = Dispatcher(\n mean = np.mean,\n median = np.median\n)\n```\nwhich is shorthand for \n```py\nclass AggregationStrategy(DispatchEnum):\n MEAN: \"mean\"\n MEDIAN: \"median\"\nAggregationStrategy.from_dict({\"mean\": np.mean, \"median\": np.median})\n```\n\n# Installation\n\nRight now you should download `dispatch.py` and vendor it in. Soonishly a more mature\nversion will be hitting PyPI too.\n\n",
"bugtrack_url": null,
"license": null,
"summary": "Config-file strategy pattern enabler: easily create Pydantic-friendly Enums with a function to call for each member",
"version": "0.1.1",
"project_urls": {
"Homepage": "https://github.com/asemic-horizon/dispatcher"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "e37c51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86",
"md5": "dbf20fecd450afa1b83100d731af4a2a",
"sha256": "264d463f060ec77ab766157f98506fd083a09690099e3240b813a142e96d70b9"
},
"downloads": -1,
"filename": "dispatcher-enum-0.1.1.tar.gz",
"has_sig": false,
"md5_digest": "dbf20fecd450afa1b83100d731af4a2a",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 7367,
"upload_time": "2024-06-15T19:10:20",
"upload_time_iso_8601": "2024-06-15T19:10:20.093432Z",
"url": "https://files.pythonhosted.org/packages/e3/7c/51ace662361b79ebe15ede87af8cce903bd2045225c88bc14725b86deb86/dispatcher-enum-0.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-06-15 19:10:20",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "asemic-horizon",
"github_project": "dispatcher",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "dispatcher-enum"
}