add email to Person.all_names and Config.all_names
[utils.git] / src / budget_sync / config.py
index 56162ba717f164f59eaddad65927588ec8d48016..28b584ca33468fd449d66e6c0c5a8fe0dd14834f 100644 (file)
@@ -13,10 +13,12 @@ class Person:
     email: Optional[str]
 
     def __init__(self, config: "Config", identifier: str,
+                 output_markdown_file: str,
                  aliases: Optional[Set[str]] = None,
                  email: Optional[str] = None):
         self.config = config
         self.identifier = identifier
+        self.output_markdown_file = output_markdown_file
         if aliases is None:
             aliases = set()
         self.aliases = aliases
@@ -26,6 +28,8 @@ class Person:
     def all_names(self) -> Set[str]:
         retval = self.aliases.copy()
         retval.add(self.identifier)
+        if self.email is not None:
+            retval.add(self.email)
         return retval
 
     def __eq__(self, other):
@@ -35,18 +39,45 @@ class Person:
         return hash(self.identifier)
 
     def __repr__(self):
-        return f"Person(config=..., identifier={self.identifier!r}, " \
-            f"aliases={self.aliases!r}, email={self.email!r})"
+        return (f"Person(config=..., identifier={self.identifier!r}, "
+                f"output_markdown_file={self.output_markdown_file!r}, "
+                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 __eq__(self, other):
+        return self.identifier == other.identifier
+
+    def __hash__(self):
+        return hash(self.identifier)
+
+    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 bugzilla_url_stripped(self):
+        return self.bugzilla_url.rstrip('/')
 
     @cached_property
     def all_names(self) -> Dict[str, Person]:
@@ -55,22 +86,43 @@ class Config:
         retval = self.people.copy()
 
         for person in self.people.values():
-            for alias in person.aliases:
-                other_person = retval.get(alias)
-                if other_person is not None:
-                    if alias in self.people:
+            for name in person.all_names:
+                other_person = retval.get(name)
+                if other_person is not None and other_person is not person:
+                    alias_or_email = "alias"
+                    if name == person.email:
+                        alias_or_email = "email"
+                    if name in self.people:
                         raise ConfigParseError(
-                            f"alias is not allowed to be the same as any"
-                            f" person's identifier: in person entry for "
-                            f"{person.identifier!r}: {alias!r} is also the "
+                            f"{alias_or_email} is not allowed to be the same "
+                            f"as any person's identifier: in person entry for "
+                            f"{person.identifier!r}: {name!r} is also the "
                             f"identifier for person"
                             f" {other_person.identifier!r}")
                     raise ConfigParseError(
-                        f"alias is not allowed to be the same as another"
-                        f" person's alias: in person entry for "
-                        f"{person.identifier!r}: {alias!r} is also an alias "
-                        f"for person {other_person.identifier!r}")
-                retval[alias] = person
+                        f"{alias_or_email} is not allowed to be the same as "
+                        f"another person's alias or email: in person entry "
+                        f"for {person.identifier!r}: {name!r} is also an alias"
+                        f" or email for person {other_person.identifier!r}")
+                retval[name] = 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:
@@ -84,6 +136,7 @@ class Config:
                 f"person entry for {identifier!r} must be a table")
         aliases = set()
         email = None
+        output_markdown_file = None
         for k, v in value.items():
             assert isinstance(k, str)
             if k == "aliases":
@@ -104,10 +157,20 @@ class Config:
                         f"`email` field in person entry for {identifier!r} "
                         f"must be a string")
                 email = v
+            elif k == "output_markdown_file":
+                if not isinstance(v, str):
+                    raise ConfigParseError(
+                        f"`output_markdown_file` field in person entry for "
+                        f"{identifier!r} must be a string")
+                output_markdown_file = v
             else:
                 raise ConfigParseError(
                     f"unknown field in person entry for {identifier!r}: `{k}`")
+        if output_markdown_file is None:
+            raise ConfigParseError(f"`output_markdown_file` field is missing in "
+                                   f"person entry for {identifier!r}")
         return Person(config=self, identifier=identifier,
+                      output_markdown_file=output_markdown_file,
                       aliases=aliases, email=email)
 
     def _parse_people(self, people: Any):
@@ -121,14 +184,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 +240,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