add milestones to Config
authorJacob Lifshay <programmerjake@gmail.com>
Wed, 9 Sep 2020 21:14:27 +0000 (14:14 -0700)
committerJacob Lifshay <programmerjake@gmail.com>
Wed, 9 Sep 2020 21:14:27 +0000 (14:14 -0700)
src/budget_sync/config.py
src/budget_sync/test/test_config.py

index 56162ba..2f0e0f7 100644 (file)
@@ -39,14 +39,30 @@ class Person:
             f"aliases={self.aliases!r}, email={self.email!r})"
 
 
+class Milestone:
+    def __init__(self, config: "Config",
+                 identifier: str, canonical_bug_id: int):
+        self.config = config
+        self.identifier = identifier
+        self.canonical_bug_id = canonical_bug_id
+
+    def __repr__(self):
+        return f"Milestone(config=..., " \
+            f"identifier={self.identifier!r}, " \
+            f"canonical_bug_id={self.canonical_bug_id})"
+
+
 class Config:
-    def __init__(self, bugzilla_url: str, people: Dict[str, Person]):
+    def __init__(self, bugzilla_url: str, people: Dict[str, Person],
+                 milestones: Dict[str, Milestone]):
         self.bugzilla_url = bugzilla_url
         self.people = people
+        self.milestones = milestones
 
     def __repr__(self):
         return f"Config(bugzilla_url={self.bugzilla_url!r}, " \
-            f"people={self.people!r})"
+            f"people={self.people!r}, " \
+            f"milestones={self.milestones!r})"
 
     @cached_property
     def all_names(self) -> Dict[str, Person]:
@@ -73,6 +89,24 @@ class Config:
                 retval[alias] = person
         return retval
 
+    @cached_property
+    def canonical_bug_ids(self) -> Dict[int, Milestone]:
+        # also checks for any bug id clashes and raises
+        # ConfigParseError if any are detected
+        retval = {}
+        for milestone in self.milestones.values():
+            other_milestone = retval.get(milestone.canonical_bug_id)
+            if other_milestone is not None:
+                raise ConfigParseError(
+                    f"canonical_bug_id is not allowed to be the same as "
+                    f"another milestone's canonical_bug_id: in milestone "
+                    f"entry for {milestone.identifier!r}: "
+                    f"{milestone.canonical_bug_id} is also the "
+                    f"canonical_bug_id for milestone "
+                    f"{other_milestone.identifier!r}")
+            retval[milestone.canonical_bug_id] = milestone
+        return retval
+
     def _parse_person(self, identifier: str, value: Any) -> Person:
         def raise_aliases_must_be_list_of_strings():
             raise ConfigParseError(
@@ -121,14 +155,52 @@ class Config:
         # if any are detected, so the following line is needed:
         self.all_names
 
+    def _parse_milestone(self, identifier: str, value: Any) -> Milestone:
+        if not isinstance(value, dict):
+            raise ConfigParseError(
+                f"milestones entry for {identifier!r} must be a table")
+        canonical_bug_id = None
+        for k, v in value.items():
+            assert isinstance(k, str)
+            if k == "canonical_bug_id":
+                if not isinstance(v, int):
+                    raise ConfigParseError(
+                        f"`canonical_bug_id` field in milestones entry for "
+                        f"{identifier!r} must be an integer")
+                canonical_bug_id = v
+            else:
+                raise ConfigParseError(f"unknown field in milestones entry "
+                                       f"for {identifier!r}: `{k}`")
+        if canonical_bug_id is None:
+            raise ConfigParseError(f"`canonical_bug_id` field is missing in "
+                                   f"milestones entry for {identifier!r}")
+        return Milestone(config=self, identifier=identifier,
+                         canonical_bug_id=canonical_bug_id)
+
+    def _parse_milestones(self, milestones: Any):
+        if not isinstance(milestones, dict):
+            raise ConfigParseError("`milestones` field must be a table")
+        for identifier, value in milestones.items():
+            assert isinstance(identifier, str)
+            self.milestones[identifier] = \
+                self._parse_milestone(identifier, value)
+
+        # self.canonical_bug_ids checks for bug id clashes and raises
+        # ConfigParseError if any are detected, so the following line
+        # is needed:
+        self.canonical_bug_ids
+
     @staticmethod
     def _from_toml(parsed_toml: Dict[str, Any]) -> "Config":
         people = None
         bugzilla_url = None
+        milestones = None
         for k, v in parsed_toml.items():
             assert isinstance(k, str)
             if k == "people":
                 people = v
+            elif k == "milestones":
+                milestones = v
             elif k == "bugzilla_url":
                 if not isinstance(v, str):
                     raise ConfigParseError("`bugzilla_url` must be a string")
@@ -139,13 +211,18 @@ class Config:
         if bugzilla_url is None:
             raise ConfigParseError("`bugzilla_url` field is missing")
 
-        config = Config(bugzilla_url=bugzilla_url, people={})
+        config = Config(bugzilla_url=bugzilla_url, people={}, milestones={})
 
         if people is None:
             raise ConfigParseError("`people` table is missing")
         else:
             config._parse_people(people)
 
+        if milestones is None:
+            raise ConfigParseError("`milestones` table is missing")
+        else:
+            config._parse_milestones(milestones)
+
         return config
 
     @staticmethod
index c16416f..3b203a1 100644 (file)
@@ -72,9 +72,16 @@ class TestConfig(unittest.TestCase):
             aliases = ["a", "a"]
             """,
             "duplicate alias in person entry for 'person1': 'a'")
+        check_error(
+            """
+            bugzilla_url = ""
+            [people]
+            """,
+            "`milestones` table is missing")
         check(
             """
             bugzilla_url = ""
+            [milestones]
             [people."person1"]
             aliases = ["a"]
             [people."person2"]
@@ -84,7 +91,7 @@ class TestConfig(unittest.TestCase):
             "'person1': Person(config=..., identifier='person1', "
             "aliases={'a'}, email=None), "
             "'person2': Person(config=..., identifier='person2', "
-            "aliases={'b'}, email=None)})")
+            "aliases={'b'}, email=None)}, milestones={})")
         check_error(
             """
             bugzilla_url = ""
@@ -96,17 +103,19 @@ class TestConfig(unittest.TestCase):
             """
             bugzilla_url = ""
             [people]
+            [milestones]
             """,
-            "Config(bugzilla_url='', people={})")
+            "Config(bugzilla_url='', people={}, milestones={})")
         check(
             """
             bugzilla_url = ""
+            [milestones]
             [people."person1"]
             email = "email@example.com"
             """,
             "Config(bugzilla_url='', people={"
             "'person1': Person(config=..., identifier='person1', "
-            "aliases=set(), email='email@example.com')})")
+            "aliases=set(), email='email@example.com')}, milestones={})")
         check_error(
             """
             bugzilla_url = ""
@@ -117,6 +126,7 @@ class TestConfig(unittest.TestCase):
         check_error(
             """
             bugzilla_url = ""
+            [milestones]
             [people."person1"]
             [people."person2"]
             aliases = ["person1"]
@@ -127,6 +137,7 @@ class TestConfig(unittest.TestCase):
         check_error(
             """
             bugzilla_url = ""
+            [milestones]
             [people."person1"]
             aliases = ["a"]
             [people."person2"]
@@ -135,11 +146,63 @@ class TestConfig(unittest.TestCase):
             "alias is not allowed to be the same as another person's alias: "
             "in person entry for 'person2': 'a' is also an alias for person "
             "'person1'")
+        check_error(
+            """
+            bugzilla_url = ""
+            [milestones]
+            "abc" = 1
+            [people]
+            """,
+            "milestones entry for 'abc' must be a table")
+        check_error(
+            """
+            bugzilla_url = ""
+            [milestones]
+            "abc" = { canonical_bug_id = "abc" }
+            [people]
+            """,
+            "`canonical_bug_id` field in milestones entry for 'abc' must "
+            "be an integer")
+        check_error(
+            """
+            bugzilla_url = ""
+            [milestones]
+            "abc" = { blah = "def" }
+            [people]
+            """,
+            "unknown field in milestones entry for 'abc': `blah`")
+        check_error(
+            """
+            bugzilla_url = ""
+            [milestones]
+            "abc" = {}
+            [people]
+            """,
+            "`canonical_bug_id` field is missing in milestones entry for 'abc'")
+        check_error(
+            """
+            bugzilla_url = ""
+            milestones = 1
+            [people]
+            """,
+            "`milestones` field must be a table")
+        check_error(
+            """
+            bugzilla_url = ""
+            [milestones]
+            "abc" = { canonical_bug_id = 1 }
+            "def" = { canonical_bug_id = 1 }
+            [people]
+            """,
+            "canonical_bug_id is not allowed to be the same as another "
+            "milestone's canonical_bug_id: in milestone entry for 'def': "
+            "1 is also the canonical_bug_id for milestone 'abc'")
 
     def test_all_names(self):
         config = Config.from_str(
             """
             bugzilla_url = ""
+            [milestones]
             [people."person1"]
             aliases = ["person1_alias1", "alias1"]
             [people."person2"]
@@ -157,6 +220,23 @@ class TestConfig(unittest.TestCase):
                              'alias2': person2,
                          })
 
+    def test_canonical_bug_ids(self):
+        config = Config.from_str(
+            """
+            bugzilla_url = ""
+            [people]
+            [milestones]
+            "Milestone 1" = { canonical_bug_id = 1 }
+            "Milestone 2" = { canonical_bug_id = 2 }
+            """)
+        milestone1 = config.milestones['Milestone 1']
+        milestone2 = config.milestones['Milestone 2']
+        self.assertEqual(config.canonical_bug_ids,
+                         {
+                             1: milestone1,
+                             2: milestone2,
+                         })
+
     def test_from_file(self):
         def load(text):
             with io.StringIO(text) as file:
@@ -171,6 +251,21 @@ class TestConfig(unittest.TestCase):
                 "^TOML parse error: Empty value is invalid"):
             load("""bad-toml=""")
 
+        self.assertEqual(str(load(
+            """
+            bugzilla_url = "https://bugzilla.example.com/"
+            [people."person1"]
+            email = "person1@example.com"
+            aliases = ["alias1"]
+            [milestones]
+            "Milestone 1" = { canonical_bug_id = 123 }
+            """)),
+            "Config(bugzilla_url='https://bugzilla.example.com/', "
+            "people={'person1': Person(config=..., identifier='person1', "
+            "aliases={'alias1'}, email='person1@example.com')}, "
+            "milestones={'Milestone 1': Milestone(config=..., "
+            "identifier='Milestone 1', canonical_bug_id=123)})")
+
 
 if __name__ == "__main__":
     unittest.main()