1+ from __future__ import annotations
2+
13import logging
24import time
35import xml .etree .ElementTree as ET
6+ from typing import cast , Optional , Sequence , Union
47
58from lib .commands import ssh , SSHCommandFailed
69from lib .common import wait_for
10+ from lib .typing import AnswerfileDict , Self , SimpleAnswerfileDict
711
812class AnswerFile :
9- def __init__ (self , kind , / ):
13+ def __init__ (self , kind : str , / ) -> None :
1014 from data import BASE_ANSWERFILES
11- defn = BASE_ANSWERFILES [kind ]
15+ defn : SimpleAnswerfileDict = BASE_ANSWERFILES [kind ]
1216 self .defn = self ._normalize_structure (defn )
1317
14- def write_xml (self , filename ) :
18+ def write_xml (self , filename : str ) -> None :
1519 etree = ET .ElementTree (self ._defn_to_xml_et (self .defn ))
1620 etree .write (filename )
1721
1822 # chainable mutators for lambdas
1923
20- def top_append (self , * defs ):
24+ def top_append (self , * defs : Union [SimpleAnswerfileDict , None , ValueError ]) -> Self :
25+ assert not isinstance (self .defn ['CONTENTS' ], str ), "a toplevel CONTENTS must be a list"
2126 for defn in defs :
2227 if defn is None :
2328 continue
2429 self .defn ['CONTENTS' ].append (self ._normalize_structure (defn ))
2530 return self
2631
27- def top_setattr (self , attrs ) :
32+ def top_setattr (self , attrs : "dict[str, str]" ) -> Self :
2833 assert 'CONTENTS' not in attrs
29- self .defn .update (attrs )
34+ self .defn .update (cast ( AnswerfileDict , attrs ) )
3035 return self
3136
3237 # makes a mutable deep copy of all `contents`
3338 @staticmethod
34- def _normalize_structure (defn ) :
39+ def _normalize_structure (defn : Union [ SimpleAnswerfileDict , ValueError ]) -> AnswerfileDict :
3540 assert isinstance (defn , dict ), f"{ defn !r} is not a dict"
3641 assert 'TAG' in defn , f"{ defn } has no TAG"
3742
3843 # type mutation through nearly-shallow copy
39- new_defn = {
44+ new_defn : AnswerfileDict = {
4045 'TAG' : defn ['TAG' ],
4146 'CONTENTS' : [],
4247 }
@@ -45,29 +50,37 @@ def _normalize_structure(defn):
4550 if isinstance (value , str ):
4651 new_defn ['CONTENTS' ] = value
4752 else :
53+ value_as_sequence : Sequence ["SimpleAnswerfileDict" ]
54+ if isinstance (value , Sequence ):
55+ value_as_sequence = value
56+ else :
57+ value_as_sequence = (
58+ cast (SimpleAnswerfileDict , value ),
59+ )
4860 new_defn ['CONTENTS' ] = [
4961 AnswerFile ._normalize_structure (item )
50- for item in value
62+ for item in value_as_sequence
5163 if item is not None
5264 ]
5365 elif key == 'TAG' :
5466 pass # already copied
5567 else :
56- new_defn [key ] = value
68+ new_defn [key ] = value # type: ignore[literal-required]
5769
5870 return new_defn
5971
6072 # convert to a ElementTree.Element tree suitable for further
6173 # modification before we serialize it to XML
6274 @staticmethod
63- def _defn_to_xml_et (defn , / , * , parent = None ):
75+ def _defn_to_xml_et (defn : AnswerfileDict , / , * , parent : Optional [ ET . Element ] = None ) -> ET . Element :
6476 assert isinstance (defn , dict )
65- defn = dict (defn )
66- name = defn .pop ('TAG' )
77+ defn_copy = dict (defn )
78+ name = defn_copy .pop ('TAG' )
6779 assert isinstance (name , str )
68- contents = defn .pop ('CONTENTS' , ( ))
80+ contents = cast ( Union [ str , "list[AnswerfileDict]" ], defn_copy .pop ('CONTENTS' , [] ))
6981 assert isinstance (contents , (str , list ))
70- element = ET .Element (name , {}, ** defn )
82+ defn_filtered = cast ("dict[str, str]" , defn_copy )
83+ element = ET .Element (name , {}, ** defn_filtered )
7184 if parent is not None :
7285 parent .append (element )
7386 if isinstance (contents , str ):
@@ -77,7 +90,7 @@ def _defn_to_xml_et(defn, /, *, parent=None):
7790 AnswerFile ._defn_to_xml_et (content , parent = element )
7891 return element
7992
80- def poweroff (ip ) :
93+ def poweroff (ip : str ) -> None :
8194 try :
8295 ssh (ip , ["poweroff" ])
8396 except SSHCommandFailed as e :
@@ -88,7 +101,7 @@ def poweroff(ip):
88101 else :
89102 raise
90103
91- def monitor_install (* , ip ) :
104+ def monitor_install (* , ip : str ) -> None :
92105 # wait for "yum install" phase to finish
93106 wait_for (lambda : ssh (ip , ["grep" ,
94107 "'DISPATCH: NEW PHASE: Completing installation'" ,
@@ -112,7 +125,7 @@ def monitor_install(*, ip):
112125 ).returncode == 1 ,
113126 "Wait for installer to terminate" )
114127
115- def monitor_upgrade (* , ip ) :
128+ def monitor_upgrade (* , ip : str ) -> None :
116129 # wait for "yum install" phase to start
117130 wait_for (lambda : ssh (ip , ["grep" ,
118131 "'DISPATCH: NEW PHASE: Reading package information'" ,
@@ -145,7 +158,7 @@ def monitor_upgrade(*, ip):
145158 ).returncode == 1 ,
146159 "Wait for installer to terminate" )
147160
148- def monitor_restore (* , ip ) :
161+ def monitor_restore (* , ip : str ) -> None :
149162 # wait for "yum install" phase to start
150163 wait_for (lambda : ssh (ip , ["grep" ,
151164 "'Restoring backup'" ,
0 commit comments