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
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):
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]:
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:
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":
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):
# 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