Repository: pyodide/pyodide

Issue:
AttributeError: '_WriteStream' object has no attribute 'isatty'
While trying to run pytest on a package in REPL (for https://github.com/pyodide/pyodide/issues/1791),
```
>>> import pytest
>>> import micropip
>>> await micropip.install('dit-cli')
>>> import dit_cli
>>> pytest.main(['--pyargs', 'dit_cli'])
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/lib/python3.9/site-packages/_pytest/main.py", line 265, in wrap_session
INTERNALERROR>     config._do_configure()
INTERNALERROR>   File "/lib/python3.9/site-packages/_pytest/config/__init__.py", line 982, in _do_configure
INTERNALERROR>     self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/hooks.py", line 308, in call_historic
INTERNALERROR>     res = self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/manager.py", line 84, in <lambda>
INTERNALERROR>     self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/lib/python3.9/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/lib/python3.9/site-packages/_pytest/terminal.py", line 228, in pytest_configure
INTERNALERROR>     reporter = TerminalReporter(config, sys.stdout)
INTERNALERROR>   File "/lib/python3.9/site-packages/_pytest/terminal.py", line 337, in __init__
INTERNALERROR>     self.isatty = file.isatty()
INTERNALERROR> AttributeError: '_WriteStream' object has no attribute 'isatty'
```

so we would need to implement that method. As far as I understand from https://docs.python.org/3/library/os.html#os.isatty it should return True for _WriteStream.


Discussion:

Resolving patch:
diff --git a/src/py/_pyodide/console.py b/src/py/_pyodide/console.py
--- a/src/py/_pyodide/console.py
+++ b/src/py/_pyodide/console.py
@@ -60,6 +60,9 @@ def write(self, text):
     def flush(self):
         pass
 
+    def isatty(self) -> bool:
+        return True
+
 
 class _ReadStream:
     """A utility class so we can specify our own handler for reading from stdin"""
@@ -74,6 +77,9 @@ def readline(self, n=-1):
     def flush(self):
         pass
 
+    def isatty(self) -> bool:
+        return True
+
 
 class _Compile(Compile):
     """Compile code with CodeRunner, and remember future imports
@@ -200,15 +206,15 @@ class Console:
     globals : ``dict``
         The global namespace in which to evaluate the code. Defaults to a new empty dictionary.
 
+    stdin_callback : ``Callable[[str], None]``
+        Function to call at each read from ``sys.stdin``. Defaults to ``None``.
+
     stdout_callback : ``Callable[[str], None]``
         Function to call at each write to ``sys.stdout``. Defaults to ``None``.
 
     stderr_callback : ``Callable[[str], None]``
         Function to call at each write to ``sys.stderr``. Defaults to ``None``.
 
-    stdin_callback : ``Callable[[str], None]``
-        Function to call at each read from ``sys.stdin``. Defaults to ``None``.
-
     persistent_stream_redirection : ``bool``
         Should redirection of standard streams be kept between calls to :any:`runcode <Console.runcode>`?
         Defaults to ``False``.
@@ -221,15 +227,15 @@ class Console:
         globals : ``Dict[str, Any]``
             The namespace used as the global
 
+        stdin_callback : ``Callback[[str], None]``
+            Function to call at each read from ``sys.stdin``.
+
         stdout_callback : ``Callback[[str], None]``
             Function to call at each write to ``sys.stdout``.
 
         stderr_callback : ``Callback[[str], None]``
             Function to call at each write to ``sys.stderr``.
 
-        stdin_callback : ``Callback[[str], None]``
-            Function to call at each read from ``sys.stdin``.
-
         buffer : ``List[str]``
             The list of strings that have been :any:`pushed <Console.push>` to the console.
 
@@ -241,9 +247,9 @@ def __init__(
         self,
         globals: Optional[dict] = None,
         *,
+        stdin_callback: Optional[Callable[[str], None]] = None,
         stdout_callback: Optional[Callable[[str], None]] = None,
         stderr_callback: Optional[Callable[[str], None]] = None,
-        stdin_callback: Optional[Callable[[str], None]] = None,
         persistent_stream_redirection: bool = False,
         filename: str = "<console>",
     ):
@@ -252,9 +258,9 @@ def __init__(
         self.globals = globals
         self._stdout = None
         self._stderr = None
+        self.stdin_callback = stdin_callback
         self.stdout_callback = stdout_callback
         self.stderr_callback = stderr_callback
-        self.stdin_callback = stdin_callback
         self.filename = filename
         self.buffer: List[str] = []
         self._lock = asyncio.Lock()
@@ -297,22 +303,18 @@ def _stdstreams_redirections_inner(self):
             yield
             return
         redirects = []
+        if self.stdin_callback:
+            stdin_name = getattr(sys.stdin, "name", "<stdin>")
+            stdin_stream = _ReadStream(self.stdin_callback, name=stdin_name)
+            redirects.append(redirect_stdin(stdin_stream))
         if self.stdout_callback:
-            redirects.append(
-                redirect_stdout(
-                    _WriteStream(self.stdout_callback, name=sys.stdout.name)
-                )
-            )
+            stdout_name = getattr(sys.stdout, "name", "<stdout>")
+            stdout_stream = _WriteStream(self.stdout_callback, name=stdout_name)
+            redirects.append(redirect_stdout(stdout_stream))
         if self.stderr_callback:
-            redirects.append(
-                redirect_stderr(
-                    _WriteStream(self.stderr_callback, name=sys.stderr.name)
-                )
-            )
-        if self.stdin_callback:
-            redirects.append(
-                redirect_stdin(_ReadStream(self.stdin_callback, name=sys.stdin.name))
-            )
+            stderr_name = getattr(sys.stderr, "name", "<stderr>")
+            stderr_stream = _WriteStream(self.stderr_callback, name=stderr_name)
+            redirects.append(redirect_stderr(stderr_stream))
         try:
             self._streams_redirected = True
             with ExitStack() as stack:


Test patch:
diff --git a/src/tests/test_console.py b/src/tests/test_console.py
--- a/src/tests/test_console.py
+++ b/src/tests/test_console.py
@@ -236,6 +236,9 @@ def test_nonpersistent_redirection(safe_sys_redirections):
     my_stdout = ""
     my_stderr = ""
 
+    def stdin_callback():
+        pass
+
     def stdout_callback(string):
         nonlocal my_stdout
         my_stdout += string
@@ -250,6 +253,7 @@ async def get_result(input):
         return await res
 
     shell = Console(
+        stdin_callback=stdin_callback,
         stdout_callback=stdout_callback,
         stderr_callback=stderr_callback,
         persistent_stream_redirection=False,
@@ -274,6 +278,10 @@ async def test():
 
         assert await get_result("1+1") == 2
 
+        assert await get_result("sys.stdin.isatty()")
+        assert await get_result("sys.stdout.isatty()")
+        assert await get_result("sys.stderr.isatty()")
+
     asyncio.get_event_loop().run_until_complete(test())