Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
"tests",
"dpytools"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
Expand Down
94 changes: 69 additions & 25 deletions dpytools/config/config.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,82 @@
from typing import Dict
from __future__ import annotations

import os
from typing import Any, Dict, List

from .properties.base import BaseProperty
from .properties.intproperty import IntegerProperty
from .properties.string import StringProperty

class Config:

def __init__(self):
self._properties_to_validate: List[BaseProperty] = []

@staticmethod
def from_env(config_dict: Dict[str, BaseProperty]):
# TODO = read in and populate property classes as
# per the example in the main readme.
# You need to populate with dot notation in mind so:
#
# StringProperty("fieldname", "fieldvalue")
#
# should be accessed on Config/self, so:
#
# value = config.fieldvalue.value
# i.e
# config.fieldvalue = StringProperty("fieldname", "fieldvalue")
#
# Worth looking at the __setattr_ dunder method and a loop
# for how to do this.
#
# Do track the BaseProperty's that you add ready for
# assert_valid_config call.
...
def from_env(config_dict: Dict[str, Dict[str, Any]]) -> Config:

config = Config()

for env_var_name, value in config_dict.items():

value_for_property = os.environ.get(env_var_name, None)
assert value_for_property is not None, f'Required envionrment value "{env_var_name}" could not be found.'

if value["class"] == StringProperty:
if value["kwargs"]:
regex = value["kwargs"].get("regex")
min_len = value["kwargs"].get("min_len")
max_len = value["kwargs"].get("max_len")
else:
regex = None
min_len = None
max_len = None

stringprop = StringProperty(
_name = value["property"],
_value = value_for_property,
regex = regex,
min_len = min_len,
max_len = max_len
)

prop_name = value["property"]
setattr(config, prop_name, stringprop)
config._properties_to_validate.append(stringprop)

elif value["class"] == IntegerProperty:
if value["kwargs"]:
min_val = value["kwargs"].get("min_val")
max_val = value["kwargs"].get("max_val")
else:
min_val = None
max_val = None

intprop = IntegerProperty(
_name = value["property"],
_value = value_for_property,
min_val = min_val,
max_val = max_val
)

prop_name = value["property"]
setattr(config, prop_name, intprop)
config._properties_to_validate.append(intprop)

else:
prop_type = value["class"]
raise TypeError(f"Unsupported property type specified via 'property' field, got {prop_type}. Should be of type StringProperty or IntegerProperty")

return config


def assert_valid_config(self):
"""
Assert that then Config class has the properties that
provided properties.
"""
for property in self._properties_to_validate:
property.type_is_valid()
property.secondary_validation()

# For each of the properties you imbided above, run
# self.type_is_valid()
# self.secondary_validation()


self._properties_to_validate = []
3 changes: 2 additions & 1 deletion dpytools/config/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .string import StringProperty
from .string import StringProperty
from .intproperty import IntegerProperty
25 changes: 16 additions & 9 deletions dpytools/config/properties/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@

@dataclass
class BaseProperty(metaclass=ABCMeta):
name: str
value: Any
_name: str
_value: Any

# TODO: getter
# allow someone to get the property
@property
def name(self):
return self._name

# TODO: setter
# categorically disallow anyone from
# changing a property after the class
# has been instantiated.
# Refuse to do it, and log an error.
@name.setter
def name(self, value):
raise ValueError(f"Trying to change name property to value {value} but you cannot change a property name after instantiation.")

@property
def value(self):
return self._value

@value.setter
def value(self, value):
raise ValueError(f"Trying to change value to {value} but you cannot change a property value after instantiation.")

@abstractmethod
def type_is_valid(self):
Expand Down
32 changes: 32 additions & 0 deletions dpytools/config/properties/intproperty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Optional
from dataclasses import dataclass
from .base import BaseProperty

@dataclass
class IntegerProperty(BaseProperty):
min_val: Optional[int]
max_val: Optional[int]

def type_is_valid(self):
"""
Validate that the property looks like
its of the correct type
"""
try:
int(self._value)
except Exception as err:
raise Exception(f"Cannot cast {self._name} value {self._value} to integer.") from err

def secondary_validation(self):
"""
Non type based validation you might want to
run against a configuration value of this kind.
"""
if not self._value:
raise ValueError(f"Integer value for {self._name} does not exist.")

if self.min_val and self._value < self.min_val:
raise ValueError(f"Integer value for {self._name} is lower than allowed minimum.")

if self.max_val and self._value > self.max_val:
raise ValueError(f"Integer value for {self._name} is higher than allowed maximum.")
25 changes: 15 additions & 10 deletions dpytools/config/properties/string.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Optional

from dataclasses import dataclass
from .base import BaseProperty

import re

@dataclass
class StringProperty(BaseProperty):
regex: Optional[str]
min_len: Optional[int]
Expand All @@ -14,26 +16,29 @@ def type_is_valid(self):
its of the correct type
"""
try:
str(self.value)
str(self._value)
except Exception as err:
raise Exception(f"Cannot cast {self.name} value {self.value} to string.") from err
raise Exception(f"Cannot cast {self.name} value {self._value} to string.") from err

def secondary_validation_passed(self):
def secondary_validation(self):
"""
Non type based validation you might want to
run against a configuration value of this kind.
"""
if len(self.value) == 0:

if len(self._value) == 0:
raise ValueError(f"Str value for {self.name} is an empty string")

if self.regex:
# TODO - confirm the value matches the regex
...
regex_search = re.search(self.regex, self._value)
if not regex_search:
raise ValueError(f"Str value for {self.name} does not match the given regex.")

if self.min_len:
# TODO - confirm the string matches of exceeds the minimum length
...
if len(self._value) < self.min_len:
raise ValueError(f"Str value for {self.name} is shorter than minimum length {self.min_len}")

if self.max_len:
# TODO - confirm the value matches or is less than the max length
...
if len(self._value) > self.max_len:
raise ValueError(f"Str value for {self.name} is longer than maximum length {self.max_len}")
116 changes: 116 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from _pytest.monkeypatch import monkeypatch
import pytest

from dpytools.config.config import Config
from dpytools.config.properties.string import StringProperty
from dpytools.config.properties.intproperty import IntegerProperty

def test_config_loader(monkeypatch):
"""
Tests that a config object can be created and its attributes
dynamically generated from an input config dictionary with the
expected contents.
"""

# Assigning environment variable values for config dictionary values
monkeypatch.setenv("SOME_STRING_ENV_VAR", "Some string value")
monkeypatch.setenv("SOME_URL_ENV_VAR", "https://test.com/some-url")
monkeypatch.setenv("SOME_INT_ENV_VAR", "6")

config_dictionary = {
"SOME_STRING_ENV_VAR": {
"class": StringProperty,
"property": "name1",
"kwargs": {
"regex": "string value",
"min_len": 10
},
},
"SOME_URL_ENV_VAR": {
"class": StringProperty,
"property": "name2",
"kwargs": {
"regex": "https://.*",
"max_len": 100
},
},
"SOME_INT_ENV_VAR": {
"class": IntegerProperty,
"property": "name3",
"kwargs": {
"min_val": 5,
"max_val": 27
}
},
}

config = Config.from_env(config_dictionary)

# Assertions

assert config.name1.name == "name1"
assert config.name1.value == "Some string value"
assert config.name1.min_len == 10
assert config.name1.regex == "string value"

assert config.name2.name == "name2"
assert config.name2.value == "https://test.com/some-url"
assert config.name2.regex == "https://.*"
assert config.name2.max_len == 100

assert config.name3.name == "name3"
assert config.name3.min_val == 5
assert config.name3.max_val == 27


def test_config_loader_no_values_error():
"""
Tests that an exception will be raised when a config object
is created using the from_env() method but the environment
variable values have not been assigned (values are None).
"""

# No environment variable values assigned in this test

config_dictionary = {
"SOME_STRING_ENV_VAR": {
"class": StringProperty,
"property": "name1",
"kwargs": {
"min_len": 10
},
}
}

with pytest.raises(Exception) as e:

config = Config.from_env(config_dictionary)

assert 'Required environment value "SOME_STRING_ENV_VAR" could not be found.' in str(e.value)


def test_config_loader_incorrect_type_error(monkeypatch):
"""
Tests that a TypeError will be raised when a config object
is created using the from_env() method but the type of an
attribute being created is not either a StringProperty or IntegerProperty.
"""

monkeypatch.setenv("SOME_STRING_ENV_VAR", "Some string value")

config_dictionary = {
"SOME_STRING_ENV_VAR": {
"class": int,
"property": "name1",
"kwargs": {
"min_val": 10,

},
}
}

with pytest.raises(TypeError) as e:

config = Config.from_env(config_dictionary)

assert "Unsupported property type specified via 'property' field, got <class 'int'>. Should be of type StringProperty or IntegerProperty" in str(e.value)
Loading