3 from typing
import Set
, Dict
, Any
, Optional
4 from functools
import cached_property
7 class ConfigParseError(Exception):
15 def __init__(self
, config
: "Config", identifier
: str,
16 output_markdown_file
: str,
17 aliases
: Optional
[Set
[str]] = None,
18 email
: Optional
[str] = None):
20 self
.identifier
= identifier
21 self
.output_markdown_file
= output_markdown_file
24 self
.aliases
= aliases
28 def all_names(self
) -> Set
[str]:
29 retval
= self
.aliases
.copy()
30 retval
.add(self
.identifier
)
31 if self
.email
is not None:
32 retval
.add(self
.email
)
35 def __eq__(self
, other
):
36 return self
.identifier
== other
.identifier
39 return hash(self
.identifier
)
42 return (f
"Person(config=..., identifier={self.identifier!r}, "
43 f
"output_markdown_file={self.output_markdown_file!r}, "
44 f
"aliases={self.aliases!r}, email={self.email!r})")
48 def __init__(self
, config
: "Config",
49 identifier
: str, canonical_bug_id
: int):
51 self
.identifier
= identifier
52 self
.canonical_bug_id
= canonical_bug_id
54 def __eq__(self
, other
):
55 return self
.identifier
== other
.identifier
58 return hash(self
.identifier
)
61 return f
"Milestone(config=..., " \
62 f
"identifier={self.identifier!r}, " \
63 f
"canonical_bug_id={self.canonical_bug_id})"
67 def __init__(self
, bugzilla_url
: str, people
: Dict
[str, Person
],
68 milestones
: Dict
[str, Milestone
]):
69 self
.bugzilla_url
= bugzilla_url
71 self
.milestones
= milestones
74 return f
"Config(bugzilla_url={self.bugzilla_url!r}, " \
75 f
"people={self.people!r}, " \
76 f
"milestones={self.milestones!r})"
79 def bugzilla_url_stripped(self
):
80 return self
.bugzilla_url
.rstrip('/')
83 def all_names(self
) -> Dict
[str, Person
]:
84 # also checks for any name clashes and raises
85 # ConfigParseError if any are detected
86 retval
= self
.people
.copy()
88 for person
in self
.people
.values():
89 for name
in person
.all_names
:
90 other_person
= retval
.get(name
)
91 if other_person
is not None and other_person
is not person
:
92 alias_or_email
= "alias"
93 if name
== person
.email
:
94 alias_or_email
= "email"
95 if name
in self
.people
:
96 raise ConfigParseError(
97 f
"{alias_or_email} is not allowed to be the same "
98 f
"as any person's identifier: in person entry for "
99 f
"{person.identifier!r}: {name!r} is also the "
100 f
"identifier for person"
101 f
" {other_person.identifier!r}")
102 raise ConfigParseError(
103 f
"{alias_or_email} is not allowed to be the same as "
104 f
"another person's alias or email: in person entry "
105 f
"for {person.identifier!r}: {name!r} is also an alias"
106 f
" or email for person {other_person.identifier!r}")
107 retval
[name
] = person
111 def canonical_bug_ids(self
) -> Dict
[int, Milestone
]:
112 # also checks for any bug id clashes and raises
113 # ConfigParseError if any are detected
115 for milestone
in self
.milestones
.values():
116 other_milestone
= retval
.get(milestone
.canonical_bug_id
)
117 if other_milestone
is not None:
118 raise ConfigParseError(
119 f
"canonical_bug_id is not allowed to be the same as "
120 f
"another milestone's canonical_bug_id: in milestone "
121 f
"entry for {milestone.identifier!r}: "
122 f
"{milestone.canonical_bug_id} is also the "
123 f
"canonical_bug_id for milestone "
124 f
"{other_milestone.identifier!r}")
125 retval
[milestone
.canonical_bug_id
] = milestone
128 def _parse_person(self
, identifier
: str, value
: Any
) -> Person
:
129 def raise_aliases_must_be_list_of_strings():
130 raise ConfigParseError(
131 f
"`aliases` field in person entry for {identifier!r} must "
132 f
"be a list of strings")
134 if not isinstance(value
, dict):
135 raise ConfigParseError(
136 f
"person entry for {identifier!r} must be a table")
139 output_markdown_file
= None
140 for k
, v
in value
.items():
141 assert isinstance(k
, str)
143 if not isinstance(v
, list):
144 raise_aliases_must_be_list_of_strings()
146 if isinstance(alias
, str):
148 raise ConfigParseError(
149 f
"duplicate alias in person entry for "
150 f
"{identifier!r}: {alias!r}")
153 raise_aliases_must_be_list_of_strings()
155 if not isinstance(v
, str):
156 raise ConfigParseError(
157 f
"`email` field in person entry for {identifier!r} "
160 elif k
== "output_markdown_file":
161 if not isinstance(v
, str):
162 raise ConfigParseError(
163 f
"`output_markdown_file` field in person entry for "
164 f
"{identifier!r} must be a string")
165 output_markdown_file
= v
167 raise ConfigParseError(
168 f
"unknown field in person entry for {identifier!r}: `{k}`")
169 if output_markdown_file
is None:
170 raise ConfigParseError(f
"`output_markdown_file` field is missing in "
171 f
"person entry for {identifier!r}")
172 return Person(config
=self
, identifier
=identifier
,
173 output_markdown_file
=output_markdown_file
,
174 aliases
=aliases
, email
=email
)
176 def _parse_people(self
, people
: Any
):
177 if not isinstance(people
, dict):
178 raise ConfigParseError("`people` field must be a table")
179 for identifier
, value
in people
.items():
180 assert isinstance(identifier
, str)
181 self
.people
[identifier
] = self
._parse
_person
(identifier
, value
)
183 # self.all_names checks for name clashes and raises ConfigParseError
184 # if any are detected, so the following line is needed:
187 def _parse_milestone(self
, identifier
: str, value
: Any
) -> Milestone
:
188 if not isinstance(value
, dict):
189 raise ConfigParseError(
190 f
"milestones entry for {identifier!r} must be a table")
191 canonical_bug_id
= None
192 for k
, v
in value
.items():
193 assert isinstance(k
, str)
194 if k
== "canonical_bug_id":
195 if not isinstance(v
, int):
196 raise ConfigParseError(
197 f
"`canonical_bug_id` field in milestones entry for "
198 f
"{identifier!r} must be an integer")
201 raise ConfigParseError(f
"unknown field in milestones entry "
202 f
"for {identifier!r}: `{k}`")
203 if canonical_bug_id
is None:
204 raise ConfigParseError(f
"`canonical_bug_id` field is missing in "
205 f
"milestones entry for {identifier!r}")
206 return Milestone(config
=self
, identifier
=identifier
,
207 canonical_bug_id
=canonical_bug_id
)
209 def _parse_milestones(self
, milestones
: Any
):
210 if not isinstance(milestones
, dict):
211 raise ConfigParseError("`milestones` field must be a table")
212 for identifier
, value
in milestones
.items():
213 assert isinstance(identifier
, str)
214 self
.milestones
[identifier
] = \
215 self
._parse
_milestone
(identifier
, value
)
217 # self.canonical_bug_ids checks for bug id clashes and raises
218 # ConfigParseError if any are detected, so the following line
220 self
.canonical_bug_ids
223 def _from_toml(parsed_toml
: Dict
[str, Any
]) -> "Config":
227 for k
, v
in parsed_toml
.items():
228 assert isinstance(k
, str)
231 elif k
== "milestones":
233 elif k
== "bugzilla_url":
234 if not isinstance(v
, str):
235 raise ConfigParseError("`bugzilla_url` must be a string")
238 raise ConfigParseError(f
"unknown config entry: `{k}`")
240 if bugzilla_url
is None:
241 raise ConfigParseError("`bugzilla_url` field is missing")
243 config
= Config(bugzilla_url
=bugzilla_url
, people
={}, milestones
={})
246 raise ConfigParseError("`people` table is missing")
248 config
._parse
_people
(people
)
250 if milestones
is None:
251 raise ConfigParseError("`milestones` table is missing")
253 config
._parse
_milestones
(milestones
)
258 def from_str(text
: str) -> "Config":
260 parsed_toml
= toml
.loads(text
)
261 except toml
.TomlDecodeError
as e
:
262 new_err
= ConfigParseError(f
"TOML parse error: {e}")
263 raise new_err
.with_traceback(sys
.exc_info()[2])
264 return Config
._from
_toml
(parsed_toml
)
267 def from_file(file: Any
) -> "Config":
268 if isinstance(file, list):
269 raise TypeError("list is not a valid file or path")
271 parsed_toml
= toml
.load(file)
272 except toml
.TomlDecodeError
as e
:
273 new_err
= ConfigParseError(f
"TOML parse error: {e}")
274 raise new_err
.with_traceback(sys
.exc_info()[2])
275 return Config
._from
_toml
(parsed_toml
)