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]:
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(
# 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")
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
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"]
"'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 = ""
"""
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 = ""
check_error(
"""
bugzilla_url = ""
+ [milestones]
[people."person1"]
[people."person2"]
aliases = ["person1"]
check_error(
"""
bugzilla_url = ""
+ [milestones]
[people."person1"]
aliases = ["a"]
[people."person2"]
"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"]
'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:
"^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()