Repository: xonsh/xonsh
Pull request: Fallback for `print_color` if shell is not yet initialized
Added a fallback for `xonsh.tools.print_color` (and `xonsh.tools.format_color`) if shell is not yet initialized, thus enabling their use in `.xonshrc`.

Fixes #3840 .


Problem statement:
print_color raises exception from RC file
Hi! Xonsh is super incredible! Thanks!

I've found that `print_color` not working when it used in rc-file:
```bash
# bash
cd /tmp
echo 'from xonsh.tools import print_color' > rc
echo 'print_color("{RED}Hello!")' >> rc
XONSH_DEBUG=1 XONSH_SHOW_TRACEBACK=1 xonsh --rc rc
```
Result:
```
Traceback (most recent call last):
  File "/opt/miniconda/lib/python3.8/site-packages/xonsh/environ.py", line 2245, in xonsh_script_run_control
    run_script_with_cache(filename, execer, ctx)
  File "/opt/miniconda/lib/python3.8/site-packages/xonsh/codecache.py", line 162, in run_script_with_cache
    run_compiled_code(ccode, glb, loc, mode)
  File "/opt/miniconda/lib/python3.8/site-packages/xonsh/codecache.py", line 67, in run_compiled_code
    func(code, glb, loc)
  File "rc", line 3, in <module>
    print_color('{RED}123')
  File "/opt/miniconda/lib/python3.8/site-packages/xonsh/tools.py", line 1783, in print_color
    builtins.__xonsh__.shell.shell.print_color(string, **kwargs)
AttributeError: 'NoneType' object has no attribute 'shell'
```

For example `aliases.py` contain `print_color` and could fail from rc.

It's on current master - `580b675f` (Oct 1 02:48:13 2020).

Please fix. Thanks!

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**

Hints from issue discussion:
That would be weird if it is related to THREAD_SUBPROCS.  In general, though I would not expect that `print_color()` to work until xonsh is fully started up. There may be some changes that could be made to use the ASCII printing prior to the shell being fully available as sort of a hack to get this working when xonshrc is run.

Commit message: no, the news file is not md, it's rst

Patch to review:
diff --git a/news/print_color_3840.rst b/news/print_color_3840.rst
new file mode 100644
index 0000000000..d6465f1b47
--- /dev/null
+++ b/news/print_color_3840.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Added ANSI fallback for ``xonsh.tools.print_color`` if shell is not yet initialized. Fixes #3840.
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/tools.py b/xonsh/tools.py
index bc18a34507..6efebcedca 100644
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -1837,7 +1837,14 @@ def format_color(string, **kwargs):
     shell instances method of the same name. The results of this function should
     be directly usable by print_color().
     """
-    return builtins.__xonsh__.shell.shell.format_color(string, **kwargs)
+    try:
+        return builtins.__xonsh__.shell.shell.format_color(string, **kwargs)
+    except AttributeError:
+        # fallback for ANSI if shell is not yet initialized
+        from xonsh.ansi_colors import ansi_partial_color_format
+
+        style = builtins.__xonsh__.env.get("XONSH_COLOR_STYLE")
+        return ansi_partial_color_format(string, style=style)
 
 
 def print_color(string, **kwargs):
@@ -1845,7 +1852,11 @@ def print_color(string, **kwargs):
     method of the same name. Colors will be formatted if they have not already
     been.
     """
-    builtins.__xonsh__.shell.shell.print_color(string, **kwargs)
+    try:
+        builtins.__xonsh__.shell.shell.print_color(string, **kwargs)
+    except AttributeError:
+        # fallback for ANSI if shell is not yet initialized
+        print(format_color(string, **kwargs))
 
 
 def color_style_names():


Code review comments:
diff_hunk: @@ -1837,15 +1837,26 @@ def format_color(string, **kwargs):
     shell instances method of the same name. The results of this function should
     be directly usable by print_color().
     """
-    return builtins.__xonsh__.shell.shell.format_color(string, **kwargs)
+    try:
+        return builtins.__xonsh__.shell.shell.format_color(string, **kwargs)
+    except AttributeError:
path: xonsh/tools.py
text: @user1:
Can you use `if hasattr()' instead please? I believe it is faster than try-except

@author:
I was curious about this one, so performed a quick test:
```python
class A():
    gotit = 1

a = A()

def t_try_happy():
    try:
        a.gotit
    except AttributeError:
        pass

def t_try_sad():
    try:
        a.notthere
    except AttributeError:
        pass

def t_hasattr_happy():
    if hasattr(a, "gotit"):
        a.gotit

def t_hasattr_sad():
    if hasattr(a, "notthere"):
        a.notthere
```

Results (cpython 3.8.5):
```
> timeit.timeit(t_try_happy, number=10000)
0.0008203879988286644
> timeit.timeit(t_try_sad, number=10000)
0.0037201289997028653
> timeit.timeit(t_hasattr_happy, number=10000)
0.0011529879993759096
> timeit.timeit(t_hasattr_sad, number=10000)
0.000985652000963455
```

So it seems that for the happy path, `try` is a little bit quicker. For the _unhappy_ it's much much slower indeed, but that should really be an edge case (using `print_color` without an initialized shell).

Of course, I can replace `try` with `hasattr` if you want me to, but I needed to check :)

@user1:
First off, thanks for the timing work! My concern here is that we will be meaning to hit the try-sad case (the slow case) in xonshrc (which is during startup). I am really trying to figure out how to keep startup times as low as possible, with the goal of getting to 100 ms.  Does this make sense?

@author:
Sure, now it's more clear, will do the change.