Skip to content

Commit

Permalink
Add a test using dynamic subclasses and bump min python to 3.9
Browse files Browse the repository at this point in the history
Python 3.8 doesn't have the dict union operator | and you can't make union types using Union[type1, type2] so I bumped the minimum python version to 3.9
  • Loading branch information
TomHodson authored and jameshawkes committed Feb 4, 2024
1 parent 8c1ff18 commit ed0cd57
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 2 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "Apache-2.0"
repository = "https://github.com/ecmwf/conflator/"

[tool.poetry.dependencies]
python = ">3.8"
python = ">3.9"
pydantic = "^2.5.3"
argparse = "^1.4.0"
rich-argparse = "^1.4.0"
Expand Down
99 changes: 99 additions & 0 deletions tests/test_subclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from unittest.mock import patch
import pytest
from conflator import CLIArg, Conflator, EnvVar, ConfigModel
from annotated_types import Annotated
from typing import Literal, Union
from pydantic import Field, ConfigDict
import yaml

from dataclasses import dataclass, field

class Action(ConfigModel):
model_config = ConfigDict(extra='forbid')
name: str

class Source(Action):
"Produces messages"
name: Literal["Source"]
start_from: str

class Process(Action):
"Processes messages"
name: Literal["Process"]
should_flub_widgets: bool

class Sink(Action):
"Consumes messages"
name: Literal["Sink"]
emit_aviso_notification: bool

@dataclass
class Subclasses:
"""Get all the subclasses for given class
returns {name : class}
deduplicated by name
cached
"""

cache: dict = field(default_factory=dict)

def get(self, target):
try:
return self.cache[target.__name__]
except KeyError:
# Deduplicate classes by __name__
deduped = list({subcls.__name__: subcls for subcls in target.__subclasses__()}.values())
subclasses = {target.__name__: target} | {k: v for subcls in deduped for k, v in self.get(subcls).items()}
subclasses[target.__name__] = target
self.cache[target.__name__] = subclasses
return subclasses

# Get all the subclasses of Action and remove Action itself
action_subclasses = tuple(set(Subclasses().get(Action).values()) - set([Action,]))

# Constuct a union type out of the subclasses
# Field(discriminator="name") tells pydantic to look at the name
# field to decide what type of the union to attempt to parse
action_subclasses_union = Annotated[
Union[action_subclasses],
Field(discriminator="name")]

class Config(ConfigModel):
actions: list[action_subclasses_union] = Field(discriminator='name')


def test_subclasses():
config = yaml.safe_load("""
actions:
- name: "Source"
start_from: 01031997
# should_flub_widgets: True # extra key
- name: Process
should_flub_widgets: True
- name: Sink
emit_aviso_notification: False
""")
conflator = Conflator("test-app", Config, cli = False, **config)
config = conflator.load()

assert config.actions[0].name == "Source"


def test_subclasses_extra_key():
with pytest.raises(SystemExit):
config = yaml.safe_load("""
actions:
- name: "Source"
start_from: 01031997
should_flub_widgets: True # extra key
- name: Process
should_flub_widgets: True
- name: Sink
emit_aviso_notification: False
""")
conflator = Conflator("test-app", Config, cli = False, **config)
config = conflator.load()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
env_list = py{38,39,310,311}
env_list = py{39,310,311}
minversion = 4.12.1

[testenv]
Expand Down

0 comments on commit ed0cd57

Please sign in to comment.