--- /dev/null
+import toml
+import sys
+from typing import Set, Dict, Any
+
+
+class ConfigParseError(Exception):
+ pass
+
+
+class Person:
+ def __init__(self, config: "Config", identifier: str):
+ self.config = config
+ self.identifier = identifier
+
+ def __eq__(self, other):
+ return self.identifier == other.identifier
+
+ def __hash__(self):
+ return hash(self.identifier)
+
+ def __repr__(self):
+ return f"Person(config=..., identifier={self.identifier!r})"
+
+
+class Config:
+ def __init__(self, bugzilla_url: str, people: Dict[str, Person], aliases: Dict[str, Person] = None):
+ self.bugzilla_url = bugzilla_url
+ self.people = people
+ if aliases is None:
+ aliases = {}
+ for i in people:
+ aliases[i.identifier] = i
+ self.aliases = aliases
+
+ def __repr__(self):
+ return f"Config(bugzilla_url={self.bugzilla_url!r}, " \
+ f"people={self.people!r}, aliases={self.aliases!r})"
+
+ @staticmethod
+ def _parse_people(people: Any) -> Dict[str, Person]:
+ raise NotImplementedError()
+
+ @staticmethod
+ def _parse_aliases(people: Dict[str, Person], aliases: Any) -> Dict[str, Person]:
+ if not isinstance(aliases, dict):
+ raise ConfigParseError("`aliases` entry must be a table")
+ retval = {}
+ raise NotImplementedError()
+ return retval
+
+ @staticmethod
+ def _from_toml(parsed_toml: Dict[str, Any]) -> "Config":
+ people = None
+ aliases = None
+ bugzilla_url = None
+ for k, v in parsed_toml.items():
+ if k == "people":
+ people = Config._parse_people(v)
+ elif k == "aliases":
+ aliases = v
+ elif k == "bugzilla_url":
+ if not isinstance(v, str):
+ raise ConfigParseError("`bugzilla_url` must be a string")
+ bugzilla_url = v
+ else:
+ raise ConfigParseError(f"unknown config entry: `{k}`")
+
+ if people is None:
+ raise ConfigParseError("`people` key is missing")
+
+ if bugzilla_url is None:
+ raise ConfigParseError("`bugzilla_url` key is missing")
+
+ if aliases is not None:
+ aliases = Config._parse_aliases(people, aliases)
+
+ return Config(bugzilla_url=bugzilla_url,
+ people=people,
+ aliases=aliases)
+
+ @staticmethod
+ def from_str(text: str) -> "Config":
+ try:
+ parsed_toml = toml.loads(text)
+ except toml.TomlDecodeError as e:
+ new_err = ConfigParseError(f"TOML parse error: {e}")
+ raise new_err.with_traceback(sys.exc_info()[2])
+ return Config._from_toml(parsed_toml)
+
+ @staticmethod
+ def from_file(file: Any) -> "Config":
+ if isinstance(file, list):
+ raise TypeError("list is not a valid file or path")
+ try:
+ parsed_toml = toml.load(file)
+ except toml.TomlDecodeError as e:
+ new_err = ConfigParseError(f"TOML parse error: {e}")
+ raise new_err.with_traceback(sys.exc_info()[2])
+ return Config._from_toml(parsed_toml)
--- /dev/null
+import unittest
+from budget_sync.config import Config, ConfigParseError
+
+
+class TestConfig(unittest.TestCase):
+ def test_config_parsing(self):
+ def check_error(text: str, expected_error_text: str):
+ with self.assertRaises(ConfigParseError) as e:
+ Config.from_str(text)
+ self.assertEqual(str(e.exception), expected_error_text)
+
+ def check(text: str, expected_repr_text: str):
+ self.assertEqual(repr(Config.from_str(text)), expected_repr_text)
+
+ raise NotImplementedError("finish adding test cases")
+
+
+if __name__ == "__main__":
+ unittest.main()