From 8fa41a3bb9259e3337df9540097e1d762aee8e60 Mon Sep 17 00:00:00 2001 From: dbhart Date: Sat, 16 Sep 2023 12:46:45 -0600 Subject: [PATCH] Update to have cleaner exceptions during IO --- wntr/epanet/exceptions.py | 33 ++++---- wntr/epanet/io.py | 169 +++++++++++++++++++------------------- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 5a2bdf353..6672cc95e 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -21,24 +21,24 @@ 110: "cannot solve network hydraulic equations", 120: "cannot solve water quality transport equations", # Apply only to an input file - 200: "one or more errors in input file", - 201: "syntax error", + 200: "one or more errors in input file %s", + 201: "syntax error (%s)", # Apply to both IO file and API functions - 202: "illegal numeric value", - 203: "undefined node %s", - 204: "undefined link %s", - 205: "undefined time pattern %s", - 206: "undefined curve %s", + 202: "illegal numeric value, %s", + 203: "undefined node, %s", + 204: "undefined link, %s", + 205: "undefined time pattern, %s", + 206: "undefined curve, %s", 207: "attempt to control a CV/GPV link", 208: "illegal PDA pressure limits", 209: "illegal node property value", 211: "illegal link property value", 212: "undefined trace node", - 213: "invalid option value", + 213: "invalid option value %s", 214: "too many characters in input line", 215: "duplicate ID label", 216: "reference to undefined pump", - 217: "invalid pump energy data", + 217: "pump has no head curve or power defined", 219: "illegal valve connection to tank node", 220: "illegal valve connection to another valve", 221: "misplaced rule clause in rule-based control", @@ -176,14 +176,13 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> super().__init__(msg) class ENSyntaxError(EpanetException, SyntaxError): - def __init__(self, code, *args) -> None: - super().__init__(code, *args) + def __init__(self, code, *args, line_num=None, line=None) -> None: + super().__init__(code, *args, line_num=line_num, line=line) -class ENNameError(EpanetException, NameError): - def __init__(self, code, name, *args) -> None: - super().__init__(code, name, *args) +class ENKeyError(EpanetException, KeyError): + def __init__(self, code, name, *args, line_num=None, line=None) -> None: + super().__init__(code, name, *args, line_num=line_num, line=line) class ENValueError(EpanetException, ValueError): - def __init__(self, code, value, *args) -> None: - super().__init__(code, value, *args) - + def __init__(self, code, value, *args, line_num=None, line=None) -> None: + super().__init__(code, value, *args, line_num=line_num, line=line) diff --git a/wntr/epanet/io.py b/wntr/epanet/io.py index 27936ba3c..8372faf76 100644 --- a/wntr/epanet/io.py +++ b/wntr/epanet/io.py @@ -25,6 +25,7 @@ import pandas as pd import six import wntr +from wntr.epanet.exceptions import ENKeyError, ENSyntaxError, ENValueError, EpanetException import wntr.network from wntr.network.base import Link from wntr.network.controls import (AndCondition, Comparison, Control, @@ -147,8 +148,7 @@ def _str_time_to_sec(s): if bool(time_tuple): return int(time_tuple.groups()[0])*60*60 else: - raise RuntimeError("Time format in " - "INP file not recognized. ") + raise ENValueError('invalid-option-value', s) def _clock_time_to_sec(s, am_pm): @@ -176,7 +176,7 @@ def _clock_time_to_sec(s, am_pm): elif am_pm.upper() == 'PM': am = False else: - raise RuntimeError('am_pm option not recognized; options are AM or PM') + raise ENValueError('invalid-option-value', s, 'Ambiguous time of day') pattern1 = re.compile(r'^(\d+):(\d+):(\d+)$') time_tuple = pattern1.search(s) @@ -188,7 +188,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600*12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -201,7 +201,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -213,12 +213,11 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: - raise RuntimeError("Time format in " - "INP file not recognized. ") + raise ENValueError('invalid-option-value', s, 'Cannot parse time') def _sec_to_string(sec): @@ -313,99 +312,102 @@ def read(self, inp_files, wn=None): section = None break else: - raise RuntimeError('%(fname)s:%(lnum)d: Invalid section "%(sec)s"' % edata) + raise ENSyntaxError(201, line_num=lnum, line = line) elif section is None and line.startswith(';'): self.top_comments.append(line[1:]) continue elif section is None: logger.debug('Found confusing line: %s', repr(line)) - raise RuntimeError('%(fname)s:%(lnum)d: Non-comment outside of valid section!' % edata) + raise ENSyntaxError(201, line_num=lnum, line=line) # We have text, and we are in a section self.sections[section].append((lnum, line)) # Parse each of the sections # The order of operations is important as certain things require prior knowledge + try: - ### OPTIONS - self._read_options() + ### OPTIONS + self._read_options() - ### TIMES - self._read_times() + ### TIMES + self._read_times() - ### CURVES - self._read_curves() + ### CURVES + self._read_curves() - ### PATTERNS - self._read_patterns() + ### PATTERNS + self._read_patterns() - ### JUNCTIONS - self._read_junctions() + ### JUNCTIONS + self._read_junctions() - ### RESERVOIRS - self._read_reservoirs() + ### RESERVOIRS + self._read_reservoirs() - ### TANKS - self._read_tanks() + ### TANKS + self._read_tanks() - ### PIPES - self._read_pipes() + ### PIPES + self._read_pipes() - ### PUMPS - self._read_pumps() + ### PUMPS + self._read_pumps() - ### VALVES - self._read_valves() + ### VALVES + self._read_valves() - ### COORDINATES - self._read_coordinates() + ### COORDINATES + self._read_coordinates() - ### SOURCES - self._read_sources() + ### SOURCES + self._read_sources() - ### STATUS - self._read_status() + ### STATUS + self._read_status() - ### CONTROLS - self._read_controls() + ### CONTROLS + self._read_controls() - ### RULES - self._read_rules() + ### RULES + self._read_rules() - ### REACTIONS - self._read_reactions() + ### REACTIONS + self._read_reactions() - ### TITLE - self._read_title() + ### TITLE + self._read_title() - ### ENERGY - self._read_energy() + ### ENERGY + self._read_energy() - ### DEMANDS - self._read_demands() + ### DEMANDS + self._read_demands() - ### EMITTERS - self._read_emitters() - - ### QUALITY - self._read_quality() + ### EMITTERS + self._read_emitters() + + ### QUALITY + self._read_quality() - self._read_mixing() - self._read_report() - self._read_vertices() - self._read_labels() + self._read_mixing() + self._read_report() + self._read_vertices() + self._read_labels() - ### Parse Backdrop - self._read_backdrop() + ### Parse Backdrop + self._read_backdrop() - ### TAGS - self._read_tags() + ### TAGS + self._read_tags() + + # Set the _inpfile io data inside the water network, so it is saved somewhere + wn._inpfile = self + + ### Finish tags + self._read_end() + except EpanetException as e: + raise EpanetException(200, filename) from e - # Set the _inpfile io data inside the water network, so it is saved somewhere - wn._inpfile = self - - ### Finish tags - self._read_end() - return self.wn def write(self, filename, wn, units=None, version=2.2, force_coordinates=False): @@ -635,7 +637,7 @@ def _read_tanks(self): overflow = False volume = 0.0 else: - raise RuntimeError('Tank entry format not recognized.') + raise ENSyntaxError(201, 'Tank entry format not recognized.', line_num=lnum, line=line) self.wn.add_tank(current[0], to_si(self.flow_units, float(current[1]), HydParam.Elevation), to_si(self.flow_units, float(current[2]), HydParam.Length), @@ -772,13 +774,12 @@ def create_curve(curve_name): # assert pattern is None, 'In [PUMPS] entry, PATTERN may only be specified once.' pattern = self.wn.get_pattern(current[i+1]).name else: - raise RuntimeError('Pump keyword in inp file not recognized.') - + raise ENSyntaxError(201, 'Pump keyword not recognized: {}'.format(current[i].upper()), line_num=lnum, line=line) if speed is None: speed = 1.0 if pump_type is None: - raise RuntimeError('Either head curve id or pump power must be specified for all pumps.') + raise ENSyntaxError(217, line_num=lnum, line=line) self.wn.add_pump(current[0], current[1], current[2], pump_type, value, speed, pattern) def _write_pumps(self, f, wn): @@ -826,7 +827,7 @@ def _read_valves(self): current.append(0.0) else: if len(current) != 7: - raise RuntimeError('The [VALVES] section of an INP file must have 6 or 7 entries.') + raise ENSyntaxError(201, 'valve definitions must have 6 or 7 values', line_num=lnum, line=line) valve_type = current[4].upper() if valve_type in ['PRV', 'PSV', 'PBV']: valve_set = to_si(self.flow_units, float(current[5]), HydParam.Pressure) @@ -844,7 +845,7 @@ def _read_valves(self): self.wn.add_curve(curve_name, 'HEADLOSS', curve_points) valve_set = curve_name else: - raise RuntimeError('VALVE type "%s" unrecognized' % valve_type) + raise ENSyntaxError(213, 'valve type unrecognized', line_num=lnum, line=line) self.wn.add_valve(current[0], current[1], current[2], @@ -994,7 +995,7 @@ def _read_patterns(self): # If default is '1' but it does not exist, then it is constant # Any other default that does not exist is an error if self.wn.options.hydraulic.pattern is not None and self.wn.options.hydraulic.pattern != '1': - raise KeyError('Default pattern {} is undefined'.format(self.wn.options.hydraulic.pattern)) + raise ENKeyError(205, self.wn.options.hydraulic.pattern) self.wn.options.hydraulic.pattern = None def _write_patterns(self, f, wn): @@ -1402,7 +1403,7 @@ def _read_reactions(self): elif key1 == 'ROUGHNESS': self.wn.options.reaction.roughness_correl = float(current[2]) else: - raise RuntimeError('Reaction option not recognized: %s'%key1) + raise ENValueError(213, key1, line_num=lnum, line=line) def _write_reactions(self, f, wn): f.write( '[REACTIONS]\n'.encode(sys_default_enc)) @@ -1512,7 +1513,7 @@ def _read_mixing(self): tank.mixing_model = MixType.Mix2 tank.mixing_fraction = float(current[2]) elif key == '2COMP' and len(current) < 3: - raise RuntimeError('Mixing model 2COMP requires fraction on tank %s'%tank_name) + raise ENSyntaxError(201, 'missing fraction for mixing', line_num=lnum, line=line) elif key == 'FIFO': tank.mixing_model = MixType.FIFO elif key == 'LIFO': @@ -1555,7 +1556,7 @@ def _read_options(self): if words is not None and len(words) > 0: if len(words) < 2: edata['key'] = words[0] - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENValueError(213, 'NULL', line_num=lnum, line=line) key = words[0].upper() if key == 'UNITS': self.flow_units = FlowUnits[words[1].upper()] @@ -1583,7 +1584,7 @@ def _read_options(self): self.mass_units = MassUnits.ug opts.quality.inpfile_units = words[2] else: - raise ValueError('Invalid chemical units in OPTIONS section') + raise ENValueError(213, 'for chemical units', line_num=lnum, line=line) else: self.mass_units = MassUnits.mg opts.quality.inpfile_units = 'mg/L' @@ -1617,7 +1618,7 @@ def _read_options(self): opts.hydraulic.pressure_exponent = float(words[2]) else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s unknown option %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) else: opts.hydraulic.inpfile_pressure_units = words[1] elif key == 'PATTERN': @@ -1630,16 +1631,16 @@ def _read_options(self): opts.hydraulic.demand_model = words[2] else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s unknown option %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) elif key == 'EMITTER': if len(words) > 2: opts.hydraulic.emitter_exponent = float(words[2]) else: edata['key'] = 'EMITTER EXPONENT' - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) elif key == 'TOLERANCE': opts.quality.tolerance = float(words[1]) elif key == 'CHECKFREQ': @@ -1661,9 +1662,9 @@ def _read_options(self): logger.warn('%(lnum)-6d %(sec)13s option "%(key)s" is undocumented; adding, but please verify syntax', edata) if isinstance(opts.time.report_timestep, (float, int)): if opts.time.report_timestep < opts.time.hydraulic_timestep: - raise RuntimeError('opts.report_timestep must be greater than or equal to opts.hydraulic_timestep.') + raise ENValueError(202, 'report timestep less than hydraulic timestep') if opts.time.report_timestep % opts.time.hydraulic_timestep != 0: - raise RuntimeError('opts.report_timestep must be a multiple of opts.hydraulic_timestep') + raise ENValueError(202, 'report timestep must be integer multiple of hydraulic timestep') def _write_options(self, f, wn, version=2.2): f.write('[OPTIONS]\n'.encode(sys_default_enc))