3
3
Basic support for hierarchies.
4
4
"""
5
5
6
+ from collections .abc import Hashable , Mapping
7
+ from typing import (
8
+ Any ,
9
+ Callable ,
10
+ Generic ,
11
+ Iterable ,
12
+ Iterator ,
13
+ Optional ,
14
+ TypeVar ,
15
+ )
16
+
6
17
# Default modules need to import the PyDelphin version
7
18
from delphin .__about__ import __version__ # noqa: F401
8
19
from delphin .exceptions import PyDelphinException
@@ -12,12 +23,23 @@ class HierarchyError(PyDelphinException):
12
23
"""Raised for invalid operations on hierarchies."""
13
24
14
25
15
- def _norm_id (id ):
26
+ H = TypeVar ("H" , bound = Hashable )
27
+ # generic types
28
+ Identifiers = Iterable [H ]
29
+ HierarchyMap = Mapping [H , Identifiers ]
30
+ DataMap = Mapping [H , Any ]
31
+ # explicit types
32
+ HierarchyDict = dict [H , tuple [H , ...]]
33
+ DataDict = dict [H , Any ]
34
+ IdentifierNormalizer = Callable [[H ], H ]
35
+
36
+
37
+ def _norm_id (id : H ) -> H :
16
38
"""Default id normalizer does nothing."""
17
39
return id
18
40
19
41
20
- class MultiHierarchy :
42
+ class MultiHierarchy ( Generic [ H ]) :
21
43
"""
22
44
A Multiply-inheriting Hierarchy.
23
45
@@ -30,6 +52,10 @@ class MultiHierarchy:
30
52
data. Data for identifiers may be get and set individually with
31
53
dictionary key-access.
32
54
55
+ While MultiHierarchy can model non-string hierarchies, the data
56
+ type of all node identifiers must be hashable and consistent
57
+ within the hierarchy.
58
+
33
59
>>> h = MultiHierarchy('*top*', {'food': '*top*',
34
60
... 'utensil': '*top*'})
35
61
>>> th.update({'fruit': 'food', 'apple': 'fruit'})
@@ -72,8 +98,19 @@ class MultiHierarchy:
72
98
top: the hierarchy's top node identifier
73
99
"""
74
100
75
- def __init__ (self , top , hierarchy = None , data = None ,
76
- normalize_identifier = None ):
101
+ _top : H
102
+ _hier : HierarchyDict
103
+ _loer : dict [H , set [H ]]
104
+ _data : DataDict
105
+ _norm : IdentifierNormalizer
106
+
107
+ def __init__ (
108
+ self ,
109
+ top : H ,
110
+ hierarchy : Optional [HierarchyMap ] = None ,
111
+ data : Optional [DataMap ] = None ,
112
+ normalize_identifier : Optional [IdentifierNormalizer ] = None ,
113
+ ):
77
114
if not normalize_identifier :
78
115
self ._norm = _norm_id
79
116
elif not callable (normalize_identifier ):
@@ -89,17 +126,19 @@ def __init__(self, top, hierarchy=None, data=None,
89
126
self .update (hierarchy , data )
90
127
91
128
@property
92
- def top (self ):
129
+ def top (self ) -> H :
93
130
return self ._top
94
131
95
- def __eq__ (self , other ) :
132
+ def __eq__ (self , other : Any ) -> bool :
96
133
if not isinstance (other , self .__class__ ):
97
134
return NotImplemented
98
- return (self ._top == other ._top
99
- and self ._hier == other ._hier
100
- and self ._data == other ._data )
135
+ return (
136
+ self ._top == other ._top
137
+ and self ._hier == other ._hier
138
+ and self ._data == other ._data
139
+ )
101
140
102
- def __getitem__ (self , identifier ) :
141
+ def __getitem__ (self , identifier : H ) -> Any :
103
142
identifier = self ._norm (identifier )
104
143
data = None
105
144
try :
@@ -109,31 +148,37 @@ def __getitem__(self, identifier):
109
148
raise
110
149
return data
111
150
112
- def __setitem__ (self , identifier , data ) :
151
+ def __setitem__ (self , identifier : H , data : Any ) -> None :
113
152
identifier = self ._norm (identifier )
114
153
if identifier not in self :
115
154
raise HierarchyError (
116
155
f'cannot set data; not in hierarchy: { identifier } ' )
117
156
self ._data [identifier ] = data
118
157
119
- def __iter__ (self ):
120
- return iter (identifier for identifier in self ._hier
121
- if identifier != self ._top )
158
+ def __iter__ (self ) -> Iterator [H ]:
159
+ return iter (
160
+ identifier for identifier in self ._hier
161
+ if identifier != self ._top
162
+ )
122
163
123
- def __contains__ (self , identifier ) :
164
+ def __contains__ (self , identifier : H ) -> bool :
124
165
return self ._norm (identifier ) in self ._hier
125
166
126
- def __len__ (self ):
167
+ def __len__ (self ) -> int :
127
168
return len (self ._hier ) - 1 # ignore top
128
169
129
- def items (self ):
170
+ def items (self ) -> Iterable [ tuple [ H , Any ]] :
130
171
"""
131
172
Return the (identifier, data) pairs excluding the top node.
132
173
"""
133
174
value = self .__getitem__
134
175
return [(identifier , value (identifier )) for identifier in self ]
135
176
136
- def update (self , subhierarchy = None , data = None ):
177
+ def update (
178
+ self ,
179
+ subhierarchy : Optional [HierarchyMap ] = None ,
180
+ data : Optional [DataMap ] = None ,
181
+ ) -> None :
137
182
"""
138
183
Incorporate *subhierarchy* and *data* into the hierarchy.
139
184
@@ -166,7 +211,7 @@ def update(self, subhierarchy=None, data=None):
166
211
loer = dict (self ._loer )
167
212
168
213
while subhierarchy :
169
- eligible = _get_eligible (hier , subhierarchy )
214
+ eligible : list [ H ] = _get_eligible (hier , subhierarchy )
170
215
171
216
for identifier in eligible :
172
217
parents = subhierarchy .pop (identifier )
@@ -181,22 +226,22 @@ def update(self, subhierarchy=None, data=None):
181
226
self ._loer = loer
182
227
self ._data .update (data )
183
228
184
- def parents (self , identifier ) :
229
+ def parents (self , identifier : H ) -> tuple [ H , ...] :
185
230
"""Return the immediate parents of *identifier*."""
186
231
identifier = self ._norm (identifier )
187
232
return self ._hier [identifier ]
188
233
189
- def children (self , identifier ) :
234
+ def children (self , identifier : H ) -> set [ H ] :
190
235
"""Return the immediate children of *identifier*."""
191
236
identifier = self ._norm (identifier )
192
237
return self ._loer [identifier ]
193
238
194
- def ancestors (self , identifier ) :
239
+ def ancestors (self , identifier : H ) -> set [ H ] :
195
240
"""Return the ancestors of *identifier*."""
196
241
identifier = self ._norm (identifier )
197
242
return _ancestors (identifier , self ._hier )
198
243
199
- def descendants (self , identifier ) :
244
+ def descendants (self , identifier : H ) -> set [ H ] :
200
245
"""Return the descendants of *identifier*."""
201
246
identifier = self ._norm (identifier )
202
247
xs = set ()
@@ -205,7 +250,7 @@ def descendants(self, identifier):
205
250
xs .update (self .descendants (child ))
206
251
return xs
207
252
208
- def subsumes (self , a , b ) :
253
+ def subsumes (self , a : H , b : H ) -> bool :
209
254
"""
210
255
Return `True` if node *a* subsumes node *b*.
211
256
@@ -234,7 +279,7 @@ def subsumes(self, a, b):
234
279
a , b = norm (a ), norm (b )
235
280
return a == b or b in self .descendants (a )
236
281
237
- def compatible (self , a , b ) :
282
+ def compatible (self , a : H , b : H ) -> bool :
238
283
"""
239
284
Return `True` if node *a* is compatible with node *b*.
240
285
@@ -262,7 +307,11 @@ def compatible(self, a, b):
262
307
b_lineage = self .descendants (b ).union ([b ])
263
308
return len (a_lineage .intersection (b_lineage )) > 0
264
309
265
- def validate_update (self , subhierarchy , data ):
310
+ def validate_update (
311
+ self ,
312
+ subhierarchy : Optional [HierarchyMap ],
313
+ data : Optional [DataMap ],
314
+ ) -> tuple [HierarchyDict , DataDict ]:
266
315
"""
267
316
Check if the update can apply to the current hierarchy.
268
317
@@ -277,58 +326,71 @@ def validate_update(self, subhierarchy, data):
277
326
ids = set (self ._hier ).intersection (subhierarchy )
278
327
if ids :
279
328
raise HierarchyError (
280
- 'already in hierarchy: {}' .format (', ' .join (ids )))
329
+ 'already in hierarchy: {}' .format (', ' .join (map ( str , ids ) )))
281
330
282
331
ids = set (data ).difference (set (self ._hier ).union (subhierarchy ))
283
332
if ids :
284
333
raise HierarchyError (
285
334
'cannot update data; not in hierarchy: {}'
286
- .format (', ' .join (ids )))
335
+ .format (', ' .join (map ( str , ids ) )))
287
336
return subhierarchy , data
288
337
289
338
290
- def _ancestors (id , hier ) :
339
+ def _ancestors (id : H , hier : dict [ H , tuple [ H , ...]]) -> set [ H ] :
291
340
xs = set ()
292
341
for parent in hier [id ]:
293
342
xs .add (parent )
294
343
xs .update (_ancestors (parent , hier ))
295
344
return xs
296
345
297
346
298
- def _normalize_update (norm , subhierarchy , data ):
299
- sub = {}
347
+ def _normalize_update (
348
+ norm : IdentifierNormalizer ,
349
+ subhierarchy : Optional [HierarchyMap ],
350
+ data : Optional [DataMap ],
351
+ ) -> tuple [HierarchyDict , DataDict ]:
352
+ sub : HierarchyDict = {}
353
+ parents : Identifiers
300
354
if subhierarchy :
301
355
for id , parents in subhierarchy .items ():
302
356
if isinstance (parents , str ):
303
357
parents = parents .split ()
304
358
id = norm (id )
305
359
parents = tuple (map (norm , parents ))
306
360
sub [id ] = parents
307
- dat = {}
361
+ dat : DataDict = {}
308
362
if data :
309
363
dat = {norm (id ): obj for id , obj in data .items ()}
310
364
return sub , dat
311
365
312
366
313
- def _get_eligible (hier , sub ):
367
+ def _get_eligible (
368
+ hier : HierarchyDict ,
369
+ sub : HierarchyDict ,
370
+ ) -> list [H ]:
314
371
eligible = [id for id , parents in sub .items ()
315
372
if all (parent in hier for parent in parents )]
316
373
if not eligible :
317
374
raise HierarchyError (
318
375
'disconnected or cyclic hierarchy; remaining: {}'
319
- .format (', ' .join (sub )))
376
+ .format (', ' .join (map ( str , sub ) )))
320
377
return eligible
321
378
322
379
323
- def _validate_parentage (id , parents , hier ):
324
- ancestors = set ()
380
+ def _validate_parentage (
381
+ id : H ,
382
+ parents : tuple [H , ...],
383
+ hier : HierarchyDict ,
384
+ ) -> None :
385
+ ancestors : set [H ] = set ()
325
386
for parent in parents :
326
387
ancestors .update (_ancestors (parent , hier ))
327
- redundant = ancestors .intersection (parents )
388
+ redundant = sorted ( map ( str , ancestors .intersection (parents )) )
328
389
if redundant :
329
390
raise HierarchyError (
330
391
'{} has redundant parents: {}'
331
- .format (id , ', ' .join (sorted (redundant ))))
392
+ .format (id , ', ' .join (redundant ))
393
+ )
332
394
333
395
334
396
# single-parented hierarchy might be something like this:
0 commit comments