Repository: hylang/hy

Issue:
curses and SIGWINCH do not work properly with Hy.
After importing curses under hy, curses is unable to detect the size of the terminal as it is resized.
This manifests as curses.LINES and curses.COLS not being updated, stdscr.getmaxyx not working and so on.

However, the workaround of launching the hy program from python with:
```
import hy
import curses
from main import main_event_loop

if __name__ == "__main__":
    curses.wrapper(main_event_loop, ...)
```
allows curses to dynamically detect the size of the terminal.

I conclude therefore the problem is with the hy binary. My (limited) understanding acquired during tracking down the source of this problem is that curses uses the SIGWINCH signal, so perhaps that is a place to look.

Void linux x86 64bit, python 3.9.0
Freebsd 12.2, python 3.8.6
hy 0.19.0 (reported by pip) installed from git master branch via pip


Discussion:
Can you provide a reproducible example? I can't try that program without `main`.
Sorry, here are the two files required for a minimal example.

main.py:
```
import hy
import curses
from main import main

if __name__ == "__main__":
    curses.wrapper(main)
```

main.hy:
```
(import curses)
(import [time [sleep]])

(defn main [stdscr]
 (setv window (.newwin curses curses.LINES curses.COLS))
 (.clear stdscr)
 (while True
  (.update_lines_cols curses)
  (.resize window curses.LINES curses.COLS)
  (.addnstr window 1 1 f"{curses.LINES} {curses.COLS}" 30)
  (.addnstr window 2 1 f"{(.getmaxyx stdscr)}" 30)
  (.refresh window)
  (sleep 0.1)))
   
(if (= __name__ "__main__")
 (.wrapper curses main))
```
Thanks, I can confirm this.

While unrelated to this bug, I should warn you against making one file named `main.py` and another named `main.hy` in the same directory: there's no guarantee that `import main` will get the one you're thinking of.
Thank you. Yes, I wouldn't normally have such a name collision, it was just quickly knocked up for the purposes of the example.
Hilariously, this turns out to be [a bug that's been in CPython for 13 years](https://bugs.python.org/issue2675#msg265564). Hy triggers it with `import readline` in `hy.completer`, which in turn is imported by `hy.cmdline`. Let's see if I can work around this for the typical case by just delaying the import of Readline until it's really necessary.

Resolving patch:
diff --git a/hy/completer.py b/hy/completer.py
--- a/hy/completer.py
+++ b/hy/completer.py
@@ -8,24 +8,19 @@
 import hy.compiler
 
 
-docomplete = True
-
-try:
-    import readline
-except AttributeError as e:
-    # https://github.com/pyreadline/pyreadline/issues/65
-    if "module 'collections' has no attribute 'Callable'" in str(e):
-        docomplete = False
-    else:
-        raise
-except ImportError:
-    docomplete = False
-
-if docomplete:
-    if sys.platform == 'darwin' and 'libedit' in readline.__doc__:
-        readline_bind = "bind ^I rl_complete"
-    else:
-        readline_bind = "tab: complete"
+# Lazily import `readline` to work around
+# https://bugs.python.org/issue2675#msg265564
+readline = None
+def init_readline():
+    global readline
+    try:
+        import readline
+    except AttributeError as e:
+        # https://github.com/pyreadline/pyreadline/issues/65
+        if "module 'collections' has no attribute 'Callable'" not in str(e):
+            raise
+    except ImportError:
+        pass
 
 
 class Completer:
@@ -86,33 +81,42 @@ def complete(self, text, state):
         except IndexError:
             return None
 
-
 @contextlib.contextmanager
 def completion(completer=None):
     delims = "()[]{} "
+
+    init_readline()
+    if not readline:
+        # We have nothing to do. Act like a null context manager.
+        yield
+        return
+
     if not completer:
         completer = Completer()
 
-    if docomplete:
-        readline.set_completer(completer.complete)
-        readline.set_completer_delims(delims)
+    if sys.platform == 'darwin' and 'libedit' in readline.__doc__:
+        readline_bind = "bind ^I rl_complete"
+    else:
+        readline_bind = "tab: complete"
 
-        history = os.environ.get(
-            "HY_HISTORY", os.path.expanduser("~/.hy-history"))
-        readline.parse_and_bind("set blink-matching-paren on")
+    readline.set_completer(completer.complete)
+    readline.set_completer_delims(delims)
 
-        try:
-            readline.read_history_file(history)
-        except OSError:
-            pass
+    history = os.environ.get(
+        "HY_HISTORY", os.path.expanduser("~/.hy-history"))
+    readline.parse_and_bind("set blink-matching-paren on")
 
-        readline.parse_and_bind(readline_bind)
+    try:
+        readline.read_history_file(history)
+    except OSError:
+        pass
+
+    readline.parse_and_bind(readline_bind)
 
     try:
         yield
     finally:
-        if docomplete:
-            try:
-                readline.write_history_file(history)
-            except OSError:
-                pass
+        try:
+            readline.write_history_file(history)
+        except OSError:
+            pass


Test patch:
diff --git a/tests/test_completer.py b/tests/test_completer.py
--- a/tests/test_completer.py
+++ b/tests/test_completer.py
@@ -1,11 +1,12 @@
 import os
 import sys
-from hy.completer import completion
 import pytest
+import hy.completer
 
+hy.completer.init_readline()
 
 @pytest.mark.skipif(
-    "readline" not in sys.modules,
+    not hy.completer.readline,
     reason="Module 'readline' is not available.")
 def test_history_custom_location(tmp_path):
     import readline
@@ -15,7 +16,7 @@ def test_history_custom_location(tmp_path):
     history_location = tmp_path / ".hy-custom-history"
     os.environ["HY_HISTORY"] = str(history_location)
 
-    with completion():
+    with hy.completer.completion():
         readline.clear_history()
         readline.add_history(expected_entry)