Coverage for /home/pi/Software/model-railway-signalling/model_railway_signals/library/file_interface.py: 41%

133 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-08 18:20 +0100

1# ---------------------------------------------------------------------------------------------- 

2# This library module enables layout schematics to be saved and loaded to/from file 

3# (this includes all schematic editor settings, schematic objects and object state) 

4# ---------------------------------------------------------------------------------------------- 

5#  

6# External API - classes and functions (used by the Schematic Editor): 

7# 

8# load_schematic - Opens a file picker dialog to select a 'sig' file to load and then loads the file, 

9# populating the global layout_state dictionary (for library object state) and returning 

10# the loaded file as a dictionary (containing the editor configuration and schematic objects) 

11# Returns the filename that was used to load the file (or None if the file was not loaded) 

12# Optional Parameters: 

13# requested_filename:str - A file load will be attempted without opening the file picker dialog 

14# 

15# purge_loaded_state_information() - called by the editor after the layout has been successfully created 

16# within the editor to stop any subsequently created objects erroneously inheriting state 

17#  

18# save_schematic - Saves the supplied 'settings' and 'objects' to file together with the current state of 

19# The library objects (which is queried directly from the library objects themselves 

20# Returns the filename that was used to save the file (or None if the file was not saved) 

21# Mandatory Parameters: 

22# settings:dict - dictionary holding the editor settings 

23# objects:dict - dictionary holding the editor objects 

24# requested_filename:str - to load/save - default = None (will default to 'main-python-script.sig') 

25# Optional Parameters: 

26# save_as:bool - Specify true to open the file picker 'save as' dialog(default=False) 

27# 

28# External API - classes and functions (used by the other library modules): 

29# 

30# get_initial_item_state(layout_element:str,item_id:int) - Called by other library modules on creation of 

31# library objects to query the initial state (e.g. layout_element="signal", item_id=1. 

32# Returns an object-specific dictionary containing key-value pairs of object state 

33# 

34#------------------------------------------------------------------------------------------------ 

35 

36import os 

37import json 

38import logging 

39import tkinter.messagebox 

40import tkinter.filedialog 

41from . import signals_common 

42from . import track_sections 

43from . import block_instruments 

44from . import points 

45from . import common 

46 

47#------------------------------------------------------------------------------------------------- 

48# Global variable to hold the last filename used for save/load 

49#------------------------------------------------------------------------------------------------- 

50 

51last_fully_qualified_file_name = None 

52 

53#------------------------------------------------------------------------------------------------- 

54# Global dictionary to hold the loaded layout state 

55#------------------------------------------------------------------------------------------------- 

56 

57layout_state ={} 

58 

59#------------------------------------------------------------------------------------------------- 

60# Define all the layout state elements that needs to be saved/loaded for each module. This 

61# makes the remainder of the code generic. If we ever need to add another "layout_element" or 

62# another "item_element" (to an existing layout element) then we just define that here 

63# Note that we also define the type for each element to enable a level of validation on load 

64# This is also important for "enum" types where we have to save the VALUE of the Enum 

65#------------------------------------------------------------------------------------------------- 

66 

67def get_sig_file_config(get_sig_file_data:bool = False): 

68 

69 signal_elements = ( ("sigclear","bool"),("subclear","bool"),("override","bool"), 

70 ("siglocked","bool") ,("sublocked","bool"),("routeset","enum"), 

71 ("releaseonred","bool"),("releaseonyel","bool"),("theatretext","str") ) 

72 point_elements = ( ("switched","bool"),("fpllock","bool"),("locked","bool") ) 

73 section_elements = ( ("occupied","bool"),("labeltext","str") ) 

74 instrument_elements = ( ("sectionstate","bool"),("repeaterstate","bool") ) 

75 

76 layout_elements = { "signals" : {"elements" : signal_elements}, 

77 "points" : {"elements" : point_elements}, 

78 "sections" : {"elements" : section_elements}, 

79 "instruments": {"elements" : instrument_elements} } 

80 

81 if get_sig_file_data: 81 ↛ 82line 81 didn't jump to line 82, because the condition on line 81 was never true

82 layout_elements["points"]["source"] = points.points 

83 layout_elements["signals"]["source"] = signals_common.signals 

84 layout_elements["sections"]["source"] = track_sections.sections 

85 layout_elements["instruments"]["source"] = block_instruments.instruments 

86 

87 return(layout_elements) 

88 

89#------------------------------------------------------------------------------------------------- 

90# API function purge the loaded layout_state information - this is called by the editor after the 

91# layout has been successfully created within the editor to stop any subsequently created objects 

92# (with the same Item ID) erroneously inheriting state. 

93#------------------------------------------------------------------------------------------------- 

94 

95def purge_loaded_state_information(): 

96 global layout_state 

97 layout_state ={} 

98 return() 

99 

100#------------------------------------------------------------------------------------------------- 

101# API function to handle the loading of a schematic file by the the schematic editor. 

102# Returns the name of the loaded file if successful (otherwise None) and the loaded 

103# 'layout_state' (dict containing the schematic settings, objects and object state). 

104# Also populates the global 'layout_state' dictonary with the loaded data as this is 

105# queried when library objects are created in order to set the initial state. 

106#------------------------------------------------------------------------------------------------- 

107 

108def load_schematic(requested_filename:str=None): 

109 global last_fully_qualified_file_name ## Set by 'load_state' and 'save_state' ## 

110 global layout_state ## populated on successful file load ## 

111 # If the requested filename is 'None' then we always open a file picker dialog. This 

112 # is pre-populated with the 'last_fully_qualified_file_name' if it exists as a file 

113 # Otherwise, we will attempt to load the requested filename (without a dialog) 

114 if requested_filename is None: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true

115 if last_fully_qualified_file_name is not None and os.path.isfile(last_fully_qualified_file_name): 

116 path, name = os.path.split(last_fully_qualified_file_name) 

117 else: 

118 path, name = ".", "" 

119 filename_to_load = tkinter.filedialog.askopenfilename(title='Load Layout State', 

120 filetypes=(('sig files','*.sig'),('all files','*.*')), 

121 initialdir=path, initialfile=name) 

122 # If dialogue is cancelled then Filename will remain as 'None' as nothing will be loaded 

123 if filename_to_load == () or filename_to_load == "": 

124 filename_to_load = None 

125 elif not os.path.isfile(requested_filename): 125 ↛ 126line 125 didn't jump to line 126, because the condition on line 125 was never true

126 filename_to_load = None 

127 else: 

128 filename_to_load = requested_filename 

129 # We should now have a valid filename (and the file exists) unless the user has cancelled 

130 # the file load dialog or the specified file does not exist (in this case it will be None) 

131 if filename_to_load is None: 131 ↛ 132line 131 didn't jump to line 132, because the condition on line 131 was never true

132 logging.info("Load File - No file selected - Layout will remain in its default state") 

133 else: 

134 logging.info("Load File - Loading layout configuration from '"+filename_to_load+"'") 

135 try: 

136 with open (filename_to_load,'r') as file: 

137 file_contents=file.read() 

138 file.close 

139 except Exception as exception: 

140 logging.error("Load File - Error opening file - Layout will remain in its default state") 

141 logging.error("Load File - Reported Exception: "+str(exception)) 

142 tkinter.messagebox.showerror(parent=common.root_window, 

143 title="File Load Error", message=str(exception)) 

144 filename_to_load = None 

145 else: 

146 # The file has been successfuly opened and loaded - Now convert it from the json format back 

147 # into the dictionary of signals, points and sections - with exception handling in case it fails 

148 try: 

149 loaded_state = json.loads(file_contents) 

150 except Exception as exception: 

151 logging.error("Load File - Couldn't read file - Layout will be created in its default state") 

152 logging.error("Load File - Reported exception: "+str(exception)) 

153 tkinter.messagebox.showerror(parent=common.root_window, 

154 title="File Parse Error", message=str(exception)) 

155 filename_to_load = None 

156 else: 

157 # File parsing was successful - we can populate the global dictionary and 

158 # update the global 'last_fully_qualified_file_name' for the next save/load 

159 layout_state = loaded_state 

160 last_fully_qualified_file_name = filename_to_load 

161 # Return the filename that was actually loaded (which will be None if the load failed) 

162 # And the dictionary containing the layout state (configuration, objects, state etc) 

163 return(filename_to_load, layout_state) 

164 

165#------------------------------------------------------------------------------------------------- 

166# API function to handle the saving of a schematic file by the the schematic editor. 

167# (the schematic settings, objects - passed in to the function by the editor and and 

168# the current state of all libray objects - which are populated by this function)) 

169# Returns the name of the saved file if successful (otherwise None) 

170#------------------------------------------------------------------------------------------------- 

171 

172def save_schematic(settings:dict, objects:dict, requested_filename:str, save_as:bool=False): 

173 global last_fully_qualified_file_name 

174 dictionary_to_save={} 

175 dictionary_to_save["settings"] = settings 

176 dictionary_to_save["objects"] = objects 

177 # If the 'save_as' option has been specified then we want to provide a default file 

178 # to the user in the dialog. This will be the requested_filename (if this is a valid file) 

179 # or the last loaded / saved file (if the last_fully_qualified_file_name is valid) 

180 # If the 'save_as' option has not been specified ('save' rather than 'save-as') then 

181 # we will just try to save the specified requested_filename (if it fails, it fails) 

182 if save_as: 

183 if requested_filename is not None and os.path.isfile(requested_filename): 

184 path, name = os.path.split(requested_filename) 

185 elif last_fully_qualified_file_name is not None and os.path.isfile(last_fully_qualified_file_name): 

186 path, name = os.path.split(last_fully_qualified_file_name) 

187 else: 

188 path, name = ".", "" 

189 filename_to_save = tkinter.filedialog.asksaveasfilename(title='Save Layout State', 

190 filetypes=(('sig files','*.sig'),('all files','*.*')), 

191 initialfile=name, initialdir=path) 

192 # If dialogue is cancelled then Filename will remain as 'None' as nothing will be saved 

193 if filename_to_save == () or filename_to_save == "": filename_to_save = None 

194 elif not os.path.isfile(requested_filename): 

195 filename_to_save = None 

196 else: 

197 filename_to_save = requested_filename 

198 # We should now have a valid filename (and the file exists) unless the user has cancelled 

199 # the file save dialog or the specified file does not exist (in this case it will be None) 

200 if filename_to_save is None: 

201 logging.info("Save File - No file selected") 

202 else: 

203 # We have a valid filename - Force the ".sig" extension 

204 if not filename_to_save.endswith(".sig"): filename_to_save = filename_to_save+".sig" 

205 # Note that the asksaveasfilename dialog returns the fully qualified filename 

206 # (including the path) - we only need the name so strip out the path element 

207 logging.info("Save File - Saving Layout Configuration as '"+filename_to_save+"'") 

208 dictionary_to_save["information"] = "Model Railway Signalling Configuration File" 

209 # Retrieve the DEFINITION of all the data items we need to save to maintain state 

210 # These are defined in a single function at the top of this source file. We also 

211 # retrieve the source DATA we need to save from the various source dictionaries 

212 layout_elements = get_sig_file_config(get_sig_file_data=True) 

213 # Iterate through the main LAYOUT-ELEMENTS (e.g. signals, points, sections etc) 

214 for layout_element in layout_elements: 

215 # For each LAYOUT ELEMENT create a sub-dictionary to hold the individual ITEMS 

216 # The individual ITEMS will be the individual points, signals, sections etc 

217 dictionary_to_save[layout_element] = {} 

218 # Get the dictionary containing the source data for this LAYOUT ELEMENT 

219 source_data_dictionary = layout_elements[layout_element]["source"] 

220 # Get the list of the ITEM ELEMENTS we need to save for this LAYOUT ELEMENT 

221 item_elements_to_save = layout_elements[layout_element]["elements"] 

222 # Iterate through the ITEMS that exist for this LAYOUT ELEMENT 

223 # Each ITEM represents a specific signal, point, section etc 

224 for item in source_data_dictionary: 

225 # For each ITEM, create a sub-dictionary to hold the individual ITEM ELEMENTS 

226 # Each ITEM ELEMENT represents a specific parameter for an ITEM (e.g."sigclear")  

227 dictionary_to_save[layout_element][item] = {} 

228 # Iterate through the ITEM ELEMENTS to save for the specific ITEM 

229 for item_element in item_elements_to_save: 

230 # Value [0] is the element name, Value [1] is the element type 

231 if item_element[0] not in source_data_dictionary[item].keys(): 

232 # if the element isn't present in the source dict then we save a NULL value 

233 dictionary_to_save[layout_element][item][item_element[0]] = None 

234 elif item_element[1]=="enum": 

235 # Enumeration values cannot be converted to json as is - we need to use the value 

236 parameter = source_data_dictionary[item][item_element[0]] 

237 dictionary_to_save[layout_element][item][item_element[0]] = parameter.value 

238 else: 

239 # The Json conversion should support all standard python types 

240 parameter = source_data_dictionary[item][item_element[0]] 

241 dictionary_to_save[layout_element][item][item_element[0]] = parameter 

242 # convert the file to a human readable json format and save the file 

243 file_contents = json.dumps(dictionary_to_save,indent=4,sort_keys=True) 

244 try: 

245 with open (filename_to_save,'w') as file: 

246 file.write(file_contents) 

247 file.close 

248 except Exception as exception: 

249 logging.error("Save File - Error saving file - Reported exception: "+str(exception)) 

250 tkinter.messagebox.showerror(parent=common.root_window, 

251 title="File Save Error",message=str(exception)) 

252 filename_to_save = None 

253 else: 

254 # File parsing was successful - update the global 'last_fully_qualified_file_name' 

255 last_fully_qualified_file_name = filename_to_save 

256 return(filename_to_save) 

257 

258#------------------------------------------------------------------------------------------------- 

259# Library Function called on creation of a Library Object to return the initial state of the object 

260# from the loaded data. If no layout state has been loaded or the loaded data doesn't include an 

261# entry for the Object then we return 'None' and the Object will retain its "as created" default state 

262#------------------------------------------------------------------------------------------------- 

263 

264def get_initial_item_state(layout_element:str,item_id:int): 

265 # Retrieve the DEFINITION of all the data items that are available 

266 sig_file_config = get_sig_file_config() 

267 # Check if the requested LAYOUT ELEMENT is a supported 

268 if layout_element not in sig_file_config.keys(): 268 ↛ 269line 268 didn't jump to line 269, because the condition on line 268 was never true

269 logging.error("File Interface - Item type not supported : "+layout_element) 

270 state_to_return = None 

271 else: 

272 # Create a dictionary to hold the state information we want to return 

273 state_to_return = {} 

274 # Iterate through the ITEM ELEMENTS we are interested in for the LAYOUT ELEMENT and 

275 # set an initial value of NONE (to be returned if we fail to validate the loaded data) 

276 for item_element in sig_file_config[layout_element]["elements"]: 

277 # retrieve the required ITEM ELEMENT Name 

278 item_element_name = item_element[0] 

279 state_to_return[item_element_name] = None 

280 # See if the specified LAYOUT ELEMENT exists in the loaded file  

281 if not layout_element in layout_state.keys(): 

282 # This could be a valid condition if no file has been loaded - fail silently 

283 pass 

284 # See if the specified ITEM (for the LAYOUT ELEMENT) exists in the loaded file 

285 elif str(item_id) not in layout_state[layout_element].keys(): 285 ↛ 287line 285 didn't jump to line 287, because the condition on line 285 was never true

286 # We know a file is loaded - therefore this is a valid error to report 

287 logging.warning("File Interface - Data missing for '"+layout_element+"-" 

288 +str(item_id)+"' - Default values will be set") 

289 else: 

290 # Iterate through the ITEM ELEMENTS we are interested in for the LAYOUT ELEMENT 

291 for item_element in sig_file_config[layout_element]["elements"]: 

292 # retrieve the required ITEM ELEMENT Name and expected ITEM ELEMENT Type 

293 element_name = item_element[0] 

294 element_type = item_element[1] 

295 # Test to see if the required ITEM ELEMENT is present for the ITEM 

296 if element_name not in layout_state[layout_element][str(item_id)]: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true

297 logging.warning("File Interface - Data missing for '"+layout_element +"-" 

298 +str(item_id)+"-"+element_name+"' - Default value will be set") 

299 else: 

300 # Retrieve the ITEM ELEMENT Value from the loaded data 

301 element_value = layout_state[layout_element][str(item_id)][element_name] 

302 # We can do some basic validation on the loaded data to check the expected type  

303 if element_type == "bool" and not isinstance(element_value,bool) and element_value is not None: 303 ↛ 304line 303 didn't jump to line 304, because the condition on line 303 was never true

304 logging.warning("File Interface - Data corrupted for '"+layout_element 

305 +"-"+str(item_id)+"-"+element_name+"' - Default value will be set") 

306 elif element_type == "str" and not isinstance(element_value,str) and element_value is not None: 306 ↛ 307line 306 didn't jump to line 307, because the condition on line 306 was never true

307 logging.warning("File Interface - Data corrupted for '"+layout_element 

308 +"-"+str(item_id)+"-"+element_name+"' - Default value will be set") 

309 elif element_type == "enum" and not isinstance(element_value,int) and element_value is not None: 309 ↛ 310line 309 didn't jump to line 310, because the condition on line 309 was never true

310 logging.warning("File Interface - Data corrupted for '"+layout_element 

311 +"-"+str(item_id)+"-"+element_name+"' - Default value will be set") 

312 else: 

313 # Add the ITEM ELEMENT (and the loaded ITEM VALUE) to the dictionary 

314 state_to_return[element_name] = element_value 

315 return(state_to_return) 

316 

317############################################################################################################