From 0222ba8f7dc8d563cc137ea924d7763b0c043d43 Mon Sep 17 00:00:00 2001 From: Chris Crosley Date: Tue, 7 Jul 2026 00:42:18 -0400 Subject: [PATCH 1/2] flush buffered output at teardown and tag error entries per-write The throttled ScriptIO buffer could strand output that was written but not yet drained, and it applied error styling using a single stream-wide flag read at drain time. Both surfaced as visible output-window bugs: - Trailing prints, logger records, and uncaught tracebacks were dropped when the runtime was disposed before the flush timer could tick. CPython lost trailing prints entirely (sys.stdout is the raw stream with no teardown flush); IronPython/CPython lost uncaught traceback bodies. - Normal output buffered before an error rendered with the red traceback styling and a repeated engine header, because DrainOutput read the _errored latch at drain time rather than per entry (most visible on CPython, which buffers its whole run until teardown). Fixes: - ScriptExecutor: synchronously flush runtime.OutputStream before disposing the runtime, so buffered output renders to the still-live window for every engine. - ScriptIO: capture the error state (and engine) on each PendingEntry when it is enqueued, and render each entry by its own state, so normal output keeps normal styling regardless of a later traceback. WriteError finalizes any buffered normal output before switching into error mode. Adds a Log Output Tests dev pulldown (IronPython/CPython/C#) covering stdout flush, all logger levels, cross-path ordering, emoji/unicode, and uncaught tracebacks. --- .../ScriptExecutor.cs | 10 ++++ dev/pyRevitLabs.PyRevit.Runtime/ScriptIO.cs | 44 +++++++++++----- .../bundle.yaml | 6 +++ .../script.py | 47 ++++++++++++++++++ .../bundle.yaml | 6 +++ .../script.cs | 28 +++++++++++ .../bundle.yaml | 6 +++ .../script.py | 46 +++++++++++++++++ .../Log Output Tests.pulldown/bundle.yaml | 4 ++ .../Log Output Tests.pulldown/icon.dark.png | Bin 0 -> 1395 bytes .../Log Output Tests.pulldown/icon.png | Bin 0 -> 1134 bytes 11 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/bundle.yaml create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/script.py create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/bundle.yaml create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/script.cs create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/bundle.yaml create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/script.py create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/bundle.yaml create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.dark.png create mode 100644 extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.png diff --git a/dev/pyRevitLabs.PyRevit.Runtime/ScriptExecutor.cs b/dev/pyRevitLabs.PyRevit.Runtime/ScriptExecutor.cs index c9e0f7b9ce..cffd5af338 100644 --- a/dev/pyRevitLabs.PyRevit.Runtime/ScriptExecutor.cs +++ b/dev/pyRevitLabs.PyRevit.Runtime/ScriptExecutor.cs @@ -168,6 +168,16 @@ private static int ExecuteScriptNow(ScriptData scriptData, ScriptRuntimeConfigs if (scriptExecConfigs.SendTelemetry) ScriptTelemetry.LogScriptTelemetryRecord(ref runtime); + // Drain any output still buffered by the engine (trailing prints, + // logger records, uncaught tracebacks) to the live window before the + // runtime and its window references are torn down below. + try { + runtime.OutputStream.Flush(); + } + catch { + // output rendering must never break command teardown + } + // GC cleanups var re = runtime.ExecutionResult; runtime.Dispose(); diff --git a/dev/pyRevitLabs.PyRevit.Runtime/ScriptIO.cs b/dev/pyRevitLabs.PyRevit.Runtime/ScriptIO.cs index 0aba555aac..cfca1bc671 100644 --- a/dev/pyRevitLabs.PyRevit.Runtime/ScriptIO.cs +++ b/dev/pyRevitLabs.PyRevit.Runtime/ScriptIO.cs @@ -12,9 +12,24 @@ namespace PyRevitLabs.PyRevit.Runtime { /// surface used by the script engines is implemented. /// public class ScriptIO : Stream, IDisposable { + // A buffered output entry carries the error state captured when it was + // enqueued, so normal output drained after an error is not retroactively + // rendered as an error just because the stream later saw a traceback. + private struct PendingEntry { + public readonly string Text; + public readonly bool IsError; + public readonly ScriptEngineType Engine; + + public PendingEntry(string text, bool isError, ScriptEngineType engine) { + Text = text; + IsError = isError; + Engine = engine; + } + } + private WeakReference _runtime; private WeakReference _gui; - private readonly Queue _pending = new Queue(); + private readonly Queue _pending = new Queue(); private int _pendingChars; private readonly StringBuilder _partial = new StringBuilder(); private readonly object _logLock = new object(); @@ -183,7 +198,7 @@ public void WriteEntry(string content) { FinalizePendingEntry(splitLargeEntries: false); while (_pendingChars > MaxPendingChars && _pending.Count > 1) - _pendingChars -= _pending.Dequeue().Length; + _pendingChars -= _pending.Dequeue().Text.Length; pendingChars = _pendingChars; } @@ -192,6 +207,11 @@ public void WriteEntry(string content) { } public void WriteError(string error_msg, ScriptEngineType engineType) { + // Close out any buffered normal output first so it keeps its own + // (non-error) styling when it drains. + lock (this) { + FinalizePendingEntry(keepIncompleteShortcode: false); + } _errored = true; _erroredEngine = engineType; foreach (string message_part in error_msg.SplitIntoChunks(1024)) { @@ -238,7 +258,7 @@ public override void Write(byte[] buffer, int offset, int count) { FinalizePendingEntry(); while (_pendingChars > MaxPendingChars && _pending.Count > 1) - _pendingChars -= _pending.Dequeue().Length; + _pendingChars -= _pending.Dequeue().Text.Length; pendingChars = _pendingChars; } @@ -379,7 +399,7 @@ private void EnqueuePending(string entry) { if (entry.Length == 0) return; - _pending.Enqueue(entry); + _pending.Enqueue(new PendingEntry(entry, _errored, _erroredEngine)); _pendingChars += entry.Length; } @@ -466,7 +486,7 @@ private void FlushUpToBudget() { private bool FlushOneEntry() { ScriptConsole output; - string entry; + PendingEntry entry; bool morePending; lock (this) { @@ -484,8 +504,8 @@ private bool FlushOneEntry() { } entry = _pending.Dequeue(); - _pendingChars -= entry.Length; - _lastEntryChars = entry.Length; + _pendingChars -= entry.Text.Length; + _lastEntryChars = entry.Text.Length; morePending = _pending.Count > 0; } @@ -498,13 +518,13 @@ private bool FlushOneEntry() { return true; } - private void DrainOutput(ScriptConsole output, string pending) { - if (string.IsNullOrEmpty(pending)) + private void DrainOutput(ScriptConsole output, PendingEntry pending) { + if (string.IsNullOrEmpty(pending.Text)) return; - var prefixed = PrefixStartupOutput(pending); - if (_errored) - output.AppendError(prefixed, _erroredEngine); + var prefixed = PrefixStartupOutput(pending.Text); + if (pending.IsError) + output.AppendError(prefixed, pending.Engine); else output.AppendHtmlFragment(prefixed, ScriptConsoleConfigs.DefaultBlock); } diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/bundle.yaml b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/bundle.yaml new file mode 100644 index 0000000000..a4b0d8170c --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/bundle.yaml @@ -0,0 +1,6 @@ +title: Test Log Output - CPython +tooltip: >- + Output/log regression test for the CPython engine: rapid stdout, every logger + level, print/logger/print_html ordering, emoji and unicode, a trailing error + log, and the full traceback from an uncaught exception must all render. +context: zero-doc diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/script.py b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/script.py new file mode 100644 index 0000000000..533ff00335 --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CPython.pushbutton/script.py @@ -0,0 +1,47 @@ +#! python3 +# -*- coding: utf-8 -*- +"""Verify stdout, every logger level, output ordering, and tracebacks render. + +Exercises the buffered output path with a rapid stdout batch, each logger level, +interleaved print/logger/print_html to check ordering, emoji and wide unicode, a +trailing error-level log, and finally an uncaught exception. A correct run shows +every section in the order below, then the full CPython traceback. +""" +from pyrevit import script + +output = script.get_output() +logger = script.get_logger() +output.set_title('Log Output Test - CPython') + +# Rapid, un-delayed writes exercise the batched stdout path. +LINE_COUNT = 12 +for num in range(1, LINE_COUNT + 1): + print('flush-test line {} of {}'.format(num, LINE_COUNT)) + +# Each level renders with its own styling. error/critical are styled log records +# on the normal path, not the red traceback block; debug shows only in debug mode. +logger.debug('debug level (visible only in debug mode)') +logger.info('info level') +logger.success('success level') +logger.warning('warning level') +logger.error('error level (styled log, not a traceback)') +logger.critical('critical level') +logger.deprecate('deprecate level') + +# print, logger, and print_html travel different output paths; the rendered +# order must match the emission order below. +print('order 1 of 4: print') +logger.warning('order 2 of 4: logger') +output.print_html('order 3 of 4: print_html') +print('order 4 of 4: print') + +# Emoji shortcodes and wide unicode must survive the buffered path intact. +print('emoji :thumbs_up: :OK_hand: wide 结构结构 end') + +# A trailing error-level log must still reach the window at teardown. +logger.error('trailing error log must be visible') + +print('all sections above must be visible, then a full traceback below.') + +# Uncaught on purpose: exercises the error path, not print/logging. +raise RuntimeError('intentional test traceback - its full body must be visible') diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/bundle.yaml b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/bundle.yaml new file mode 100644 index 0000000000..f0b849c5fb --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/bundle.yaml @@ -0,0 +1,6 @@ +title: Test Log Output - C# +tooltip: >- + Output regression test for the CLR engine (also covers VB.NET). The window + must show every numbered line. CLR command exceptions surface through a Revit + dialog rather than the output window, so there is no traceback check here. +context: zero-doc diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/script.cs b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/script.cs new file mode 100644 index 0000000000..122d68af68 --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - CSharp.pushbutton/script.cs @@ -0,0 +1,28 @@ +using System; + +using Autodesk.Revit.UI; +using Autodesk.Revit.DB; + +using pyRevitLabs.PyRevit.Runtime.Shared; + +namespace LogOutputTest { + // Verifies buffered Console output reaches the output window before the + // command returns. CLR command exceptions surface through a Revit dialog + // rather than the output window, so this engine has no traceback check. + public class LogOutputTest : IExternalCommand { + public ExecParams execParams; + + public Result Execute(ExternalCommandData revit, ref string message, ElementSet elements) { + const int lineCount = 12; + + // Rapid, un-delayed writes exercise the batched output path. + for (int num = 1; num <= lineCount; num++) + Console.WriteLine(string.Format("flush-test line {0} of {1}", num, lineCount)); + + Console.WriteLine(string.Format( + "PASS if lines 1..{0} above are all visible.", lineCount)); + + return Result.Succeeded; + } + } +} diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/bundle.yaml b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/bundle.yaml new file mode 100644 index 0000000000..e528b5f344 --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/bundle.yaml @@ -0,0 +1,6 @@ +title: Test Log Output - IronPython +tooltip: >- + Output/log regression test for the IronPython engine: rapid stdout, every + logger level, print/logger/print_html ordering, emoji and unicode, a trailing + error log, and the full traceback from an uncaught exception must all render. +context: zero-doc diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/script.py b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/script.py new file mode 100644 index 0000000000..d06b2c3e84 --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/Test Log Output - IronPython.pushbutton/script.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""Verify stdout, every logger level, output ordering, and tracebacks render. + +Exercises the buffered output path with a rapid stdout batch, each logger level, +interleaved print/logger/print_html to check ordering, emoji and wide unicode, a +trailing error-level log, and finally an uncaught exception. A correct run shows +every section in the order below, then the full IronPython traceback. +""" +from pyrevit import script + +output = script.get_output() +logger = script.get_logger() +output.set_title('Log Output Test - IronPython') + +# Rapid, un-delayed writes exercise the batched stdout path. +LINE_COUNT = 12 +for num in range(1, LINE_COUNT + 1): + print('flush-test line {} of {}'.format(num, LINE_COUNT)) + +# Each level renders with its own styling. error/critical are styled log records +# on the normal path, not the red traceback block; debug shows only in debug mode. +logger.debug('debug level (visible only in debug mode)') +logger.info('info level') +logger.success('success level') +logger.warning('warning level') +logger.error('error level (styled log, not a traceback)') +logger.critical('critical level') +logger.deprecate('deprecate level') + +# print, logger, and print_html travel different output paths; the rendered +# order must match the emission order below. +print('order 1 of 4: print') +logger.warning('order 2 of 4: logger') +output.print_html('order 3 of 4: print_html') +print('order 4 of 4: print') + +# Emoji shortcodes and wide unicode must survive the buffered path intact. +print('emoji :thumbs_up: :OK_hand: wide 结构结构 end') + +# A trailing error-level log must still reach the window at teardown. +logger.error('trailing error log must be visible') + +print('all sections above must be visible, then a full traceback below.') + +# Uncaught on purpose: exercises the error path, not print/logging. +raise RuntimeError('intentional test traceback - its full body must be visible') diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/bundle.yaml b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/bundle.yaml new file mode 100644 index 0000000000..4a6ef6ab7e --- /dev/null +++ b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/bundle.yaml @@ -0,0 +1,4 @@ +layout: + - Test Log Output - IronPython + - Test Log Output - CPython + - Test Log Output - CSharp diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.dark.png b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.dark.png new file mode 100644 index 0000000000000000000000000000000000000000..30c63907802f0d8312195169060150b206ce4ea5 GIT binary patch literal 1395 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qt-46GcUE{-7;ac^gxJtgBR(Q5A#!j#7vyK=hP z&IXNjuB@>uW=y@;bd&R6&X=GsTu+!|e{R0~o#(uW?bYX zdgtfDR24Aed|UN0mApsKR0{3Z^?(^4Zx)=Na>Vz}9F=p&r%nVi?#v2W-8p;m5ui&Z z?dY5x9h_wfbb|%dG1*>gksOleyYecMBi3qeorQ3~PSd4fFv~waE2y3dHnO64$6hb6 ztM^#miBko;&zL>#H$!dG7vD|KanP zO>e%3dxO2ZzUjqo`#-h+7ysW}wc(iqM4h0^{KfTKT)t)fc)aiXf&R#%hRE%c!5&%f zSMpQok7dn~rA_Nv58wZ}HGbokgllgg_N^AOwUa9OrC8JIF|+RZf0wc|?+>^4yjk%{ z3G7(8hu8F`M#~Faw_ka^_19uIcKfFv_~UNO%krHFHX9g^(@(ACE_{2r>a2t8pJLg4 zE3S!xW8fpl_Og|Cs@(tlF}T8bYpbwL)sIC{(GW|O@7!8i^x7$=TA%P( zS+&R}@^`#0jz4tru=rIUWMOv~v#+W?RyBcVp_aUInI5d4_~~JH);14t*yuOCSUmkh z&JF%_pi`c8gJa=GQitDH?-{i}k__qr4NqY(d*!?mwkK z!SSnM$36Q~)==-imVesmzGJKR(`(j`p8pGs6M}_i?z`^~_Nwm}40#RlKt)UR?vT7% zhnV@lh16;ntW4{Iq%N?qt_x{}oD;-zRZK9vX06FRlVz?^^URZ7Rk8Vt;5BElvCj1N^kz|-TxUc{eSzw{;|hNU_r^?>FVdQ&MBb@0O0Gj5&!@I literal 0 HcmV?d00001 diff --git a/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.png b/extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Log Output Tests.pulldown/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c29a19509704db8062d6dcec9b379bcd52c558c1 GIT binary patch literal 1134 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf4nJ zXtsecqtcuEJwQRp64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<{OUwgVZ zhE&XXd&fURBwd2-!}C({$48E`wK%=ZRICyVIT7IDUu~GO$d+YJ^Mf_4=NVZ=GrfBi z{;k&4y|?E|m&rEg0Ku4Ceao<<0RDMfLT#>G`j&8N*Gje5&lT7C8t?Wr+id(k<=ao$ z^z_a8?|y#Rrfi?8m|nND@ZZLw zPT$Bt#v6Ux`6o@jCtlP`SowzWy|zOeV~V(iF86}}dwHXa z{N?Zcww!nBP^2K^jlcKe9Q<^?)-c-D)IYwazTbXFx%sJGQHmd@FYskGzn&SnU+TeK zwfn!;be%b7y)^%S*OColkxOF_BwdTR_Ak{$o?(~Kig($Sj=Ex5tT+5FZdo-!E!5@1 zNuIQI3=YRq*cdJO4qVcBR`&AHrr$d*@apdDsWFTDmG^G)x$Cxl(*;jRZk;tv6KGde z%G4efzI{gbwWkQ(>f%ZG^XsSmGx^y&DnG7t(@k7^g5gpNqek}=wLMdoH@W!-B+k=V z^zV+wyk9$*rWi7$@;FE`2OMLVqME1eR9NrH8s1>UwxEYmBbnipM1#@R6bRVOcI*wFIq{hs%~^_8`Y`a)O(G(eO8f4QkE-Id%e{Z57UG&UpVj^m^%D_w%|362k9Jo-&(%NeSzfJ+YhAIuTm-U%#crL_VXLq2Z>$nfbD=%rR_Ff`Vr8Z}DVcFfc64s}g$urmC$} zUVZ6Lhg1IqqR$+@bW{A6>6zFv|%+sq!M6)nP}6t r?Y4b9Z?iTu<$w}VLkAe#I-R|F_O=^JFV!yq%LWEdS3j3^P6 Date: Tue, 7 Jul 2026 00:42:57 -0400 Subject: [PATCH 2/2] fix(output): restore trailing/traceback output in the console window Settings.smartbutton did sorted(get_pyrevit_env_vars().items()), which under IronPython 3 raised "TypeError: '<' not supported between 'tuple' and 'str'" when the sort fell back to comparing the dict's heterogeneous values. Python 2 tolerated ordering unlike types; Python 3 does not. Sort by the env var name only (str-coerced) so values are never compared --- .../pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py index 48dd2d5958..90772f7ef5 100644 --- a/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py +++ b/extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Settings.smartbutton/script.py @@ -239,7 +239,10 @@ def _setup_user_extensions_list(self): def _setup_env_vars_list(self): """Reads the pyRevit environment variables and updates the list""" env_vars_list = [ - EnvVariable(k, v) for k, v in sorted(envvars.get_pyrevit_env_vars().items()) + EnvVariable(k, v) + for k, v in sorted( + envvars.get_pyrevit_env_vars().items(), key=lambda kv: str(kv[0]) + ) ] self.envvars_lb.ItemsSource = env_vars_list