pyxcp 0.23.3__cp312-cp312-win_arm64.whl → 0.25.6__cp312-cp312-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxcp/__init__.py +1 -1
- pyxcp/asamkeydll.exe +0 -0
- pyxcp/cmdline.py +15 -30
- pyxcp/config/__init__.py +73 -20
- pyxcp/cpp_ext/aligned_buffer.hpp +168 -0
- pyxcp/cpp_ext/bin.hpp +7 -6
- pyxcp/cpp_ext/cpp_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/cpp_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/cpp_ext/daqlist.hpp +241 -73
- pyxcp/cpp_ext/extension_wrapper.cpp +123 -15
- pyxcp/cpp_ext/framing.hpp +360 -0
- pyxcp/cpp_ext/mcobject.hpp +5 -3
- pyxcp/cpp_ext/sxi_framing.hpp +332 -0
- pyxcp/daq_stim/__init__.py +182 -45
- pyxcp/daq_stim/optimize/binpacking.py +2 -2
- pyxcp/daq_stim/scheduler.cpp +8 -8
- pyxcp/daq_stim/stim.cp310-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp311-win_arm64.pyd +0 -0
- pyxcp/daq_stim/stim.cp312-win_arm64.pyd +0 -0
- pyxcp/errormatrix.py +2 -2
- pyxcp/examples/run_daq.py +5 -3
- pyxcp/examples/xcp_policy.py +6 -6
- pyxcp/examples/xcp_read_benchmark.py +2 -2
- pyxcp/examples/xcp_skel.py +1 -2
- pyxcp/examples/xcp_unlock.py +10 -12
- pyxcp/examples/xcp_user_supplied_driver.py +1 -2
- pyxcp/examples/xcphello.py +2 -15
- pyxcp/examples/xcphello_recorder.py +2 -2
- pyxcp/master/__init__.py +1 -0
- pyxcp/master/errorhandler.py +248 -13
- pyxcp/master/master.py +838 -250
- pyxcp/recorder/.idea/.gitignore +8 -0
- pyxcp/recorder/.idea/misc.xml +4 -0
- pyxcp/recorder/.idea/modules.xml +8 -0
- pyxcp/recorder/.idea/recorder.iml +6 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +7 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/issuestore/index.pb +7 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/3/8/3808afc69ac1edb9d760000a2f137335b1b99728 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/9/a/9a2aa4db38d3115ed60da621e012c0efc0172aae +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/b/4/b49006702b459496a8e8c94ebe60947108361b91 +0 -0
- pyxcp/recorder/.idea/sonarlint/securityhotspotstore/index.pb +7 -0
- pyxcp/recorder/.idea/vcs.xml +10 -0
- pyxcp/recorder/__init__.py +5 -10
- pyxcp/recorder/converter/__init__.py +4 -10
- pyxcp/recorder/reader.hpp +0 -1
- pyxcp/recorder/reco.py +1 -0
- pyxcp/recorder/rekorder.cp310-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp311-win_arm64.pyd +0 -0
- pyxcp/recorder/rekorder.cp312-win_arm64.pyd +0 -0
- pyxcp/recorder/unfolder.hpp +129 -107
- pyxcp/recorder/wrap.cpp +3 -8
- pyxcp/scripts/xcp_fetch_a2l.py +2 -2
- pyxcp/scripts/xcp_id_scanner.py +1 -2
- pyxcp/scripts/xcp_info.py +66 -51
- pyxcp/scripts/xcp_profile.py +1 -2
- pyxcp/tests/test_daq.py +1 -1
- pyxcp/tests/test_framing.py +262 -0
- pyxcp/tests/test_master.py +210 -100
- pyxcp/tests/test_transport.py +138 -42
- pyxcp/timing.py +1 -1
- pyxcp/transport/__init__.py +8 -5
- pyxcp/transport/base.py +187 -143
- pyxcp/transport/can.py +117 -13
- pyxcp/transport/eth.py +55 -20
- pyxcp/transport/hdf5_policy.py +167 -0
- pyxcp/transport/sxi.py +126 -52
- pyxcp/transport/transport_ext.cp310-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp311-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.cp312-win_arm64.pyd +0 -0
- pyxcp/transport/transport_ext.hpp +214 -0
- pyxcp/transport/transport_wrapper.cpp +249 -0
- pyxcp/transport/usb_transport.py +47 -31
- pyxcp/types.py +0 -13
- pyxcp/{utils.py → utils/__init__.py} +3 -4
- pyxcp/utils/cli.py +78 -0
- pyxcp-0.25.6.dist-info/METADATA +341 -0
- pyxcp-0.25.6.dist-info/RECORD +153 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/WHEEL +1 -1
- pyxcp/examples/conf_sxi.json +0 -9
- pyxcp/examples/conf_sxi.toml +0 -7
- pyxcp-0.23.3.dist-info/METADATA +0 -219
- pyxcp-0.23.3.dist-info/RECORD +0 -131
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info}/entry_points.txt +0 -0
- {pyxcp-0.23.3.dist-info → pyxcp-0.25.6.dist-info/licenses}/LICENSE +0 -0
pyxcp/master/errorhandler.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
-
"""Implements error-handling according to XCP spec.
|
|
3
|
-
|
|
2
|
+
"""Implements error-handling according to XCP spec."""
|
|
3
|
+
|
|
4
4
|
import functools
|
|
5
5
|
import logging
|
|
6
6
|
import threading
|
|
@@ -17,6 +17,25 @@ from pyxcp.types import COMMAND_CATEGORIES, XcpError, XcpResponseError, XcpTimeo
|
|
|
17
17
|
|
|
18
18
|
handle_errors = True # enable/disable XCP error-handling.
|
|
19
19
|
|
|
20
|
+
# Thread-local flag to suppress logging for expected XCP negative responses
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_thread_flags = threading.local()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_suppress_xcp_error_log(value: bool) -> None:
|
|
27
|
+
try:
|
|
28
|
+
_thread_flags.suppress_xcp_error_log = bool(value)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_suppress_xcp_error_log() -> bool:
|
|
34
|
+
try:
|
|
35
|
+
return bool(getattr(_thread_flags, "suppress_xcp_error_log", False))
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
38
|
+
|
|
20
39
|
|
|
21
40
|
class SingletonBase:
|
|
22
41
|
_lock = threading.Lock()
|
|
@@ -196,6 +215,166 @@ class Handler:
|
|
|
196
215
|
self._repeater = None
|
|
197
216
|
self.logger = logging.getLogger("PyXCP")
|
|
198
217
|
|
|
218
|
+
def _diagnostics_enabled(self) -> bool:
|
|
219
|
+
try:
|
|
220
|
+
app = getattr(self.instance, "config", None)
|
|
221
|
+
if app is None:
|
|
222
|
+
return True
|
|
223
|
+
general = getattr(app, "general", None)
|
|
224
|
+
if general is None:
|
|
225
|
+
return True
|
|
226
|
+
return bool(getattr(general, "diagnostics_on_failure", True))
|
|
227
|
+
except Exception:
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
def _build_transport_diagnostics(self) -> str:
|
|
231
|
+
try:
|
|
232
|
+
transport = getattr(self.instance, "transport", None)
|
|
233
|
+
if transport is None:
|
|
234
|
+
return ""
|
|
235
|
+
if hasattr(transport, "_build_diagnostics_dump"):
|
|
236
|
+
return transport._build_diagnostics_dump() # type: ignore[attr-defined]
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
def _append_diag(self, msg: str) -> str:
|
|
242
|
+
# Suppress diagnostics entirely when XCP error logging is suppressed (e.g., try_command probing)
|
|
243
|
+
if is_suppress_xcp_error_log():
|
|
244
|
+
return msg
|
|
245
|
+
if not self._diagnostics_enabled():
|
|
246
|
+
return msg
|
|
247
|
+
diag = self._build_transport_diagnostics()
|
|
248
|
+
if not diag:
|
|
249
|
+
return msg
|
|
250
|
+
# Prefer a Rich-formatted table for compact, readable diagnostics.
|
|
251
|
+
try:
|
|
252
|
+
header = "--- Diagnostics (for troubleshooting) ---"
|
|
253
|
+
body = diag
|
|
254
|
+
if "\n" in diag and diag.startswith("--- Diagnostics"):
|
|
255
|
+
header, body = diag.split("\n", 1)
|
|
256
|
+
|
|
257
|
+
# Try to parse the structured JSON body produced by transports
|
|
258
|
+
import json as _json # Local import to avoid hard dependency at module import time
|
|
259
|
+
|
|
260
|
+
payload = _json.loads(body)
|
|
261
|
+
transport_params = payload.get("transport_params") or {}
|
|
262
|
+
last_pdus = payload.get("last_pdus") or []
|
|
263
|
+
|
|
264
|
+
# Try to use rich if available
|
|
265
|
+
try:
|
|
266
|
+
from rich.console import Console
|
|
267
|
+
from rich.table import Table
|
|
268
|
+
from rich.panel import Panel
|
|
269
|
+
from textwrap import shorten
|
|
270
|
+
|
|
271
|
+
console = Console(file=None, force_terminal=False, width=120, record=True, markup=False)
|
|
272
|
+
|
|
273
|
+
# Transport parameters table
|
|
274
|
+
tp_table = Table(title="Transport Parameters", title_style="bold", show_header=True, header_style="bold magenta")
|
|
275
|
+
tp_table.add_column("Key", style="cyan", no_wrap=True)
|
|
276
|
+
tp_table.add_column("Value", style="white")
|
|
277
|
+
from rich.markup import escape as _escape
|
|
278
|
+
|
|
279
|
+
for k, v in (transport_params or {}).items():
|
|
280
|
+
# Convert complex values to compact repr
|
|
281
|
+
sv = repr(v)
|
|
282
|
+
sv = shorten(sv, width=80, placeholder="…")
|
|
283
|
+
tp_table.add_row(_escape(str(k)), _escape(sv))
|
|
284
|
+
|
|
285
|
+
# Last PDUs table (most recent last)
|
|
286
|
+
pdu_table = Table(
|
|
287
|
+
title="Last PDUs (most recent last)", title_style="bold", show_header=True, header_style="bold magenta"
|
|
288
|
+
)
|
|
289
|
+
for col in ("dir", "cat", "ctr", "ts", "len", "data"):
|
|
290
|
+
pdu_table.add_column(col, no_wrap=(col in {"dir", "cat", "ctr", "len"}), style="white")
|
|
291
|
+
for pdu in last_pdus:
|
|
292
|
+
try:
|
|
293
|
+
dir_ = str(pdu.get("dir", ""))
|
|
294
|
+
cat = str(pdu.get("cat", ""))
|
|
295
|
+
ctr = str(pdu.get("ctr", ""))
|
|
296
|
+
# Format timestamp: convert ns -> s with 5 decimals if numeric
|
|
297
|
+
ts_val = pdu.get("ts", "")
|
|
298
|
+
try:
|
|
299
|
+
ts_num = int(ts_val)
|
|
300
|
+
ts = f"{ts_num / 1_000_000_000:.5f}"
|
|
301
|
+
except Exception:
|
|
302
|
+
ts = str(ts_val)
|
|
303
|
+
ln = str(pdu.get("len", ""))
|
|
304
|
+
# Prefer showing actual data content; avoid repr quotes
|
|
305
|
+
data_val = pdu.get("data", "")
|
|
306
|
+
try:
|
|
307
|
+
if isinstance(data_val, (bytes, bytearray, list, tuple)):
|
|
308
|
+
# Lazily import to avoid hard dependency
|
|
309
|
+
from pyxcp.utils import hexDump as _hexDump
|
|
310
|
+
|
|
311
|
+
data_str = _hexDump(data_val)
|
|
312
|
+
else:
|
|
313
|
+
data_str = str(data_val)
|
|
314
|
+
except Exception:
|
|
315
|
+
data_str = str(data_val)
|
|
316
|
+
# Shorten potentially huge values to keep table compact
|
|
317
|
+
from textwrap import shorten as _shorten
|
|
318
|
+
|
|
319
|
+
ts = _shorten(ts, width=20, placeholder="…")
|
|
320
|
+
data = _shorten(data_str, width=40, placeholder="…")
|
|
321
|
+
# Escape strings to avoid Rich markup interpretation (e.g., '[' ']' in hex dumps)
|
|
322
|
+
dir_e = _escape(dir_)
|
|
323
|
+
cat_e = _escape(cat)
|
|
324
|
+
ctr_e = _escape(ctr)
|
|
325
|
+
ts_e = _escape(ts)
|
|
326
|
+
ln_e = _escape(ln)
|
|
327
|
+
data_e = _escape(data)
|
|
328
|
+
pdu_table.add_row(dir_e, cat_e, ctr_e, ts_e, ln_e, data_e)
|
|
329
|
+
except Exception:
|
|
330
|
+
# If anything odd in structure, add a single-cell row with repr
|
|
331
|
+
from textwrap import shorten as _shorten
|
|
332
|
+
|
|
333
|
+
pdu_table.add_row(_shorten(repr(pdu), width=80, placeholder="…"), "", "", "", "", "")
|
|
334
|
+
|
|
335
|
+
# Combine into a single panel and capture as text
|
|
336
|
+
console.print(Panel.fit(tp_table, title=header))
|
|
337
|
+
if last_pdus:
|
|
338
|
+
console.print(pdu_table)
|
|
339
|
+
rendered = console.export_text(clear=False)
|
|
340
|
+
|
|
341
|
+
except Exception:
|
|
342
|
+
# Rich not available or rendering failed; fallback to compact logger lines
|
|
343
|
+
self.logger.error(header)
|
|
344
|
+
if transport_params:
|
|
345
|
+
self.logger.error("transport_params: %s", transport_params)
|
|
346
|
+
if last_pdus:
|
|
347
|
+
self.logger.error("last_pdus (most recent last):")
|
|
348
|
+
for pdu in last_pdus:
|
|
349
|
+
try:
|
|
350
|
+
ts_val = pdu.get("ts", "")
|
|
351
|
+
try:
|
|
352
|
+
ts_num = int(ts_val)
|
|
353
|
+
ts_fmt = f"{ts_num / 1_000_000_000:.5f}"
|
|
354
|
+
except Exception:
|
|
355
|
+
ts_fmt = str(ts_val)
|
|
356
|
+
data_val = pdu.get("data", "")
|
|
357
|
+
if isinstance(data_val, (bytes, bytearray, list, tuple)):
|
|
358
|
+
from pyxcp.utils import hexDump as _hexDump
|
|
359
|
+
|
|
360
|
+
data_str = _hexDump(data_val)
|
|
361
|
+
else:
|
|
362
|
+
data_str = str(data_val)
|
|
363
|
+
pdu_copy = dict(pdu)
|
|
364
|
+
pdu_copy["ts"] = ts_fmt
|
|
365
|
+
pdu_copy["data"] = data_str
|
|
366
|
+
self.logger.error("%s", pdu_copy)
|
|
367
|
+
except Exception:
|
|
368
|
+
self.logger.error("%s", pdu)
|
|
369
|
+
except Exception:
|
|
370
|
+
# As a last resort, emit the whole diagnostics blob verbatim
|
|
371
|
+
try:
|
|
372
|
+
for line in diag.splitlines():
|
|
373
|
+
self.logger.error(line)
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
return msg
|
|
377
|
+
|
|
199
378
|
def __str__(self):
|
|
200
379
|
return f"Handler(func = {func_name(self.func)} -- {self.arguments} service = {self.service} error_code = {self.error_code})"
|
|
201
380
|
|
|
@@ -268,16 +447,16 @@ class Handler:
|
|
|
268
447
|
if item == Action.NONE:
|
|
269
448
|
pass
|
|
270
449
|
elif item == Action.DISPLAY_ERROR:
|
|
271
|
-
raise SystemExit("Could not proceed due to unhandled error (DISPLAY_ERROR).", self.error_code)
|
|
450
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (DISPLAY_ERROR)."), self.error_code)
|
|
272
451
|
elif item == Action.RETRY_SYNTAX:
|
|
273
|
-
raise SystemExit("Could not proceed due to unhandled error (RETRY_SYNTAX).", self.error_code)
|
|
452
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_SYNTAX)."), self.error_code)
|
|
274
453
|
elif item == Action.RETRY_PARAM:
|
|
275
|
-
raise SystemExit("Could not proceed due to unhandled error (RETRY_PARAM).", self.error_code)
|
|
454
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RETRY_PARAM)."), self.error_code)
|
|
276
455
|
elif item == Action.USE_A2L:
|
|
277
|
-
raise SystemExit("Could not proceed due to unhandled error (USE_A2L).", self.error_code)
|
|
456
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (USE_A2L)."), self.error_code)
|
|
278
457
|
elif item == Action.USE_ALTERATIVE:
|
|
279
458
|
raise SystemExit(
|
|
280
|
-
"Could not proceed due to unhandled error (USE_ALTERATIVE).", self.error_code
|
|
459
|
+
self._append_diag("Could not proceed due to unhandled error (USE_ALTERATIVE)."), self.error_code
|
|
281
460
|
) # TODO: check alternatives.
|
|
282
461
|
elif item == Action.REPEAT:
|
|
283
462
|
repetitionCount = Repeater.REPEAT
|
|
@@ -286,13 +465,15 @@ class Handler:
|
|
|
286
465
|
elif item == Action.REPEAT_INF_TIMES:
|
|
287
466
|
repetitionCount = Repeater.INFINITE
|
|
288
467
|
elif item == Action.RESTART_SESSION:
|
|
289
|
-
raise SystemExit("Could not proceed due to unhandled error (RESTART_SESSION).", self.error_code)
|
|
468
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (RESTART_SESSION)."), self.error_code)
|
|
290
469
|
elif item == Action.TERMINATE_SESSION:
|
|
291
|
-
raise SystemExit(
|
|
470
|
+
raise SystemExit(
|
|
471
|
+
self._append_diag("Could not proceed due to unhandled error (TERMINATE_SESSION)."), self.error_code
|
|
472
|
+
)
|
|
292
473
|
elif item == Action.SKIP:
|
|
293
474
|
pass
|
|
294
475
|
elif item == Action.NEW_FLASH_WARE:
|
|
295
|
-
raise SystemExit("Could not proceed due to unhandled error (NEW_FLASH_WARE)", self.error_code)
|
|
476
|
+
raise SystemExit(self._append_diag("Could not proceed due to unhandled error (NEW_FLASH_WARE)"), self.error_code)
|
|
296
477
|
return result_pre_actions, result_actions, Repeater(repetitionCount)
|
|
297
478
|
|
|
298
479
|
|
|
@@ -366,6 +547,48 @@ class Executor(SingletonBase):
|
|
|
366
547
|
# self.logger.critical(f"XcpResponseError [{e.get_error_code()}]")
|
|
367
548
|
self.error_code = e.get_error_code()
|
|
368
549
|
handler.error_code = self.error_code
|
|
550
|
+
try:
|
|
551
|
+
svc = getattr(inst.service, "name", None)
|
|
552
|
+
# Derive a human-friendly error name if available
|
|
553
|
+
try:
|
|
554
|
+
err_name = (
|
|
555
|
+
getattr(XcpError, int(self.error_code)).name
|
|
556
|
+
if hasattr(XcpError, "__members__")
|
|
557
|
+
else str(self.error_code)
|
|
558
|
+
)
|
|
559
|
+
except Exception:
|
|
560
|
+
# Fallbacks: try enum-style .name or string conversion
|
|
561
|
+
err_name = getattr(self.error_code, "name", None) or str(self.error_code)
|
|
562
|
+
try:
|
|
563
|
+
err_code_int = int(self.error_code)
|
|
564
|
+
except Exception:
|
|
565
|
+
err_code_int = self.error_code # best effort
|
|
566
|
+
msg = f"XCP negative response: {err_name} (0x{err_code_int:02X})"
|
|
567
|
+
if svc:
|
|
568
|
+
msg += f" on service {svc}"
|
|
569
|
+
# Suppress noisy ERROR log if requested by caller context
|
|
570
|
+
if is_suppress_xcp_error_log():
|
|
571
|
+
self.logger.debug(
|
|
572
|
+
msg,
|
|
573
|
+
extra={
|
|
574
|
+
"event": "xcp_error_suppressed",
|
|
575
|
+
"service": svc,
|
|
576
|
+
"error_code": err_code_int,
|
|
577
|
+
"error_name": err_name,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
self.logger.error(
|
|
582
|
+
msg,
|
|
583
|
+
extra={
|
|
584
|
+
"event": "xcp_error",
|
|
585
|
+
"service": svc,
|
|
586
|
+
"error_code": err_code_int,
|
|
587
|
+
"error_name": err_name,
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
except Exception:
|
|
591
|
+
pass
|
|
369
592
|
except XcpTimeoutError:
|
|
370
593
|
is_connect = func.__name__ == "connect"
|
|
371
594
|
self.logger.warning(f"XcpTimeoutError -- Service: {func.__name__!r}")
|
|
@@ -405,9 +628,21 @@ class Executor(SingletonBase):
|
|
|
405
628
|
if handler.repeater.repeat():
|
|
406
629
|
continue
|
|
407
630
|
else:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
631
|
+
msg = f"Max. repetition count reached while trying to execute service {handler.func.__name__!r}."
|
|
632
|
+
# Try to append diagnostics from the transport
|
|
633
|
+
try:
|
|
634
|
+
if hasattr(handler, "_append_diag"):
|
|
635
|
+
msg = handler._append_diag(msg)
|
|
636
|
+
except Exception:
|
|
637
|
+
pass
|
|
638
|
+
try:
|
|
639
|
+
self.logger.error(
|
|
640
|
+
"XCP unrecoverable",
|
|
641
|
+
extra={"event": "xcp_unrecoverable", "service": getattr(inst.service, "name", None)},
|
|
642
|
+
)
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
645
|
+
raise UnrecoverableError(msg)
|
|
411
646
|
finally:
|
|
412
647
|
# cleanup of class variables
|
|
413
648
|
self.previous_error_code = None
|