Repository: xonsh/xonsh

Issue:
remove dependency on 'random` library
I wonder what the use case was for a 'random' shell type feature,
nevertheless removing it will reduce start-up time and memory usage.
Apart from this obscure feature, there are no other uses of `import random`
in the main app.

This patch also includes a change in generation of choices for the `--shell-type`
argument. Prior to this patch the `--help` output looked like this:

```
  --shell-type {b,best,d,dumb,ptk,ptk1,ptk2,prompt-toolkit,prompt_toolkit,prompt-toolkit1,prompt-toolkit2,rand,random,rl,readline}
```

After this patch it looks like this:

```
  --shell-type {best,dumb,prompt_toolkit,prompt_toolkit1,prompt_toolkit2,readline}
```

I've changed it since it would still contain 'random' option otherwise,
but I think it also improves the readability of the `--help` output.

And for completeness sake, this feature was introduced in a9c606b8252c3b8ad50ac1735e32528940c9ed5e.

<!--- Thanks for opening a PR on xonsh! Please include a news entry with your PR
to help keep our changelog up to date! There are instructions available here:
https://xon.sh/devguide.html#changelog -->

<!--- If there is specific issue / feature request that this PR is addressing,
please link to the corresponding issue by using the `#issuenumber` syntax.
Thanks again! -->



Discussion:
For what it is worth, I use "random" I'll the time.  I'll explain more tomorrow.
Hi @rindeal - thanks for opening this issue.  

The purpose of the "random" shell type is mostly for development and testing. This is used to make sure that xonsh devs can ensure that all of the user-facing shell types are actually being used and aren't suffering from any weird issues or bit-rot. I use random so that I am testing out both readline and PTK on a daily basis, and I can't just avoid using one of them. 

Your concern that `import random` adds to unneeded startup time is totally valid.  However, this should mostly be mitigated by our use of [amalgamate](https://github.com/xonsh/amalgamate).  This generates lazy imports for normal xonsh use by placing a line that looks like:

```python
random = _LazyModule.load('random', 'random')
```

into the `__amalgam__.py` file.  That said, I hear you that this is a single use only import.  If you want to move the import into the function where & when it is used, that would be totally fine with me!

As for the shell type, I think we could do a better job with displaying the abbreviation. But since this is the only place where the abbreviations are currently documented, I think they should stay. Or if you'd like to write a better help message that includes a little table that maps the abreviations to their expanded values, that'd be awesome!

Thanks for understanding
> The purpose of the "random" shell type is mostly for development and testing. This is used to make sure that xonsh devs can ensure that all of the user-facing shell types are actually being used and aren't suffering from any weird issues or bit-rot. I use random so that I am testing out both readline and PTK on a daily basis, and I can't just avoid using one of them.

This is an unusual way though. The industry standard is unit testing with as much coverage as possible, since it's a much more solid way of testing, preventing bugs like for example your false feeling that "all of the user-facing shell types are actually being used", while in fact only 50% of them are at best, because xonsh has 4 shell types as of now: readline, jupyter, ptk1 and ptk2, but _random_ uses just _readline_ and _"currently available ptk version if any with readline fallback"_.

Anyway, since the number of users is not greater than the number of fingers on one hand of a typical human being, any such option should have been hidden from users and only exposed in dev docs at best, but more likely kept just as an Easter egg.

> Your concern that import random adds to unneeded startup time is totally valid. However, this should mostly be mitigated by our use of amalgamate. This generates lazy imports for normal xonsh use by placing a line that looks like:
> 
> random = _LazyModule.load('random', 'random')
> 
> into the __amalgam__.py file. That said, I hear you that this is a single use only import. If you want to move the import into the function where & when it is used, that would be totally fine with me!

Yes, the `_LazyModule` cls makes the performance penalty argument moot and the only issue that stays is the code clarity.

All in all, I suggest to deprecate or remove `random` shell type at least from all user-facing places.
@rindeal @scopatz In [xxh project](https://github.com/xxh/xxh) I omitted the `random` library importing by [simple reimplementing](https://github.com/xxh/xxh/blob/master/xxh_xxh/xxh.py#L384-L385) `randint` function as datatime microseconds. In xonsh case it would enogh too. You can save random option but reduce `random` dependency.
> @rindeal @scopatz In xxh project I omitted the random library importing by simple reimplementing randint function as datatime microseconds. In xonsh case it would enogh too. You can save random option but reduce random dependency.

Cool idea! That would be fine.  I like the `random` shell feature though, so do want to keep it

Resolving patch:
diff --git a/xonsh/shell.py b/xonsh/shell.py
--- a/xonsh/shell.py
+++ b/xonsh/shell.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 """The xonsh shell"""
 import sys
-import random
 import time
 import difflib
 import builtins
@@ -14,7 +13,7 @@
     has_prompt_toolkit,
     minimum_required_ptk_version,
 )
-from xonsh.tools import XonshError, print_exception
+from xonsh.tools import XonshError, print_exception, simple_random_choice
 from xonsh.events import events
 import xonsh.history.main as xhm
 
@@ -158,7 +157,7 @@ def choose_shell_type(init_shell_type=None, env=None):
         elif env and env.get("TERM", "") == "dumb":
             shell_type = "dumb"
         elif shell_type == "random":
-            shell_type = random.choice(("readline", "prompt_toolkit"))
+            shell_type = simple_random_choice(("readline", "prompt_toolkit"))
         if shell_type == "prompt_toolkit":
             if not has_prompt_toolkit():
                 use_vended_prompt_toolkit()
diff --git a/xonsh/tools.py b/xonsh/tools.py
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -127,6 +127,14 @@ def _expandpath(path):
     return expand_path(path, expand_user=expand_user)
 
 
+def simple_random_choice(lst):
+    """Returns random element from the list with length less than 1 million elements."""
+    l = len(lst)
+    if l > 1000000:  # microsecond maximum
+        raise ValueError("The list is too long.")
+    return lst[datetime.datetime.now().microsecond % l]
+
+
 def decode_bytes(b):
     """Tries to decode the bytes using XONSH_ENCODING if available,
     otherwise using sys.getdefaultencoding().


Test patch:
diff --git a/tests/test_tools.py b/tests/test_tools.py
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -79,6 +79,7 @@
     balanced_parens,
     iglobpath,
     all_permutations,
+    simple_random_choice,
 )
 from xonsh.environ import Env
 
@@ -94,6 +95,15 @@
 PATHEXT_ENV = {"PATHEXT": [".COM", ".EXE", ".BAT"]}
 
 
+def test_random_choice():
+    lst = [1, 2, 3]
+    r = simple_random_choice(lst)
+    assert r in lst
+
+    with pytest.raises(ValueError):
+        simple_random_choice(range(1010101))
+
+
 def test_subproc_toks_x():
     exp = "![x]"
     obs = subproc_toks("x", lexer=LEXER, returnline=True)