Repository: ipython/ipython

Issue:
Incorrect call to `open` function not handled correctly.
<!-- This is the repository for IPython command line, if you can try to make sure this question/bug/feature belong here and not on one of the Jupyter repositories. 

If it's a generic Python/Jupyter question, try other forums or discourse.jupyter.org.

If you are unsure, it's ok to post here, though, there are few maintainer so you might not get a fast response. 

-->

If you run this code:

```
def crash(file_name="test.txt"): 
     open(file_name)                                                                                                                                                                                                             

crash(True)  
```

ipython will crash with the following error:

```
Traceback (most recent call last):
  File "/home/yunoac/anaconda3/bin/ipython", line 11, in <module>
    sys.exit(start_ipython())
  File "/home/yunoac/anaconda3/lib/python3.7/site-packages/IPython/__init__.py", line 125, in start_ipython
    return launch_new_instance(argv=argv, **kwargs)
  File "/home/yunoac/anaconda3/lib/python3.7/site-packages/traitlets/config/application.py", line 664, in launch_instance
    app.start()
  File "/home/yunoac/anaconda3/lib/python3.7/site-packages/IPython/terminal/ipapp.py", line 356, in start
    self.shell.mainloop()
  File "/home/yunoac/anaconda3/lib/python3.7/site-packages/IPython/terminal/interactiveshell.py", line 498, in mainloop
    self.interact()
  File "/home/yunoac/anaconda3/lib/python3.7/site-packages/IPython/terminal/interactiveshell.py", line 478, in interact
    print(self.separate_in, end='')
OSError: [Errno 9] Bad file descriptor

If you suspect this is an IPython bug, please report it at:
    https://github.com/ipython/ipython/issues
or send an email to the mailing list at ipython-dev@python.org

You can print a more detailed traceback right now with "%tb", or use "%debug"
to interactively debug it.

Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
    %config Application.verbose_crash=True

Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
OSError: [Errno 9] Bad file descriptor
```


Discussion:
Note that `open(True)` doesn't crash. 
> Note that `open(True)` doesn't crash.

I'm in doubt, what could motivate you to `open(1)`, stealing IPython's `STDOUT` file descriptor? 🤔
I'm not sure that intent is really the core of the problem here. 

But if you're curious, this occurred in a situation where my students wrote a function like so:
```
def read_file(file_name="file.txt", read_lines=False):
  open(file_name)
  ...
```
And, by mistake, they called it using `read_file(True)`. Their intent was to use the default `file_name` and set `read_lines` to `True`. Beginners seem to make this mistake quite often. 
> Note that `open(True)` doesn't crash.

That is because the result get assigned to `_`, and is therefore not closed.

```
In[1]: open(True); # with semicolon to suppress output
```

Will crash.
It is reproducible in the vanilla-python as well:
```
% python
Python 3.9.13 (main, May 24 2022, 21:13:51) 
[Clang 13.1.6 (clang-1316.0.21.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> open(True)  # getting `_` bound to the result of open(True)
<_io.TextIOWrapper name=True mode='r' encoding='UTF-8'>
>>> 1  # losing the last reference to the file object, letting it to be garbage-collected
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 9] Bad file descriptor
>>> # readline remains broken from now on
``` 
So, we, probably, should not consider it a bug of IPython.

What do you think?
I guess we could inject this in user default namespace:

```
In [9]: def open(file, *args, **kwrags):
   ...:     if file in {0,1,2}:
   ...:         raise ValueError(f'IPython wont let you open fd={file} by default')
   ...:     return io.open(file, *args, **kwargs)
```

with a `@functools.wraps(io.open)` for the docstring to be correct.

I think we can patch user_ns in the `prepare_user_module` function. 

That's a rough outline, If someone want to make a PR that would be great. 


@meeseeksdev tag "help wanted"
I can try that fix, should be pretty straight-forward 

Resolving patch:
diff --git a/IPython/core/display.py b/IPython/core/display.py
--- a/IPython/core/display.py
+++ b/IPython/core/display.py
@@ -625,6 +625,7 @@ def _data_and_metadata(self):
     def _repr_json_(self):
         return self._data_and_metadata()
 
+
 _css_t = """var link = document.createElement("link");
 	link.ref = "stylesheet";
 	link.type = "text/css";
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -270,6 +270,16 @@ def __repr__(self):
         return '<%s object at %x, execution_count=%s error_before_exec=%s error_in_exec=%s info=%s result=%s>' %\
                 (name, id(self), self.execution_count, self.error_before_exec, self.error_in_exec, repr(self.info), repr(self.result))
 
+@functools.wraps(io_open)
+def _modified_open(file, *args, **kwargs):
+    if file in {0, 1, 2}:
+        raise ValueError(
+            f"IPython won't let you open fd={file} by default "
+            "as it is likely to crash IPython. If you know what you are doing, "
+            "you can use builtins' open."
+        )
+
+    return io_open(file, *args, **kwargs)
 
 class InteractiveShell(SingletonConfigurable):
     """An enhanced, interactive shell for Python."""
@@ -1323,6 +1333,7 @@ def init_user_ns(self):
 
         ns['exit'] = self.exiter
         ns['quit'] = self.exiter
+        ns["open"] = _modified_open
 
         # Sync what we've added so far to user_ns_hidden so these aren't seen
         # by %who


Test patch:
diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py
--- a/IPython/core/tests/test_interactiveshell.py
+++ b/IPython/core/tests/test_interactiveshell.py
@@ -103,6 +103,18 @@ def test_syntax_error(self):
         res = ip.run_cell("raise = 3")
         self.assertIsInstance(res.error_before_exec, SyntaxError)
 
+    def test_open_standard_input_stream(self):
+        res = ip.run_cell("open(0)")
+        self.assertIsInstance(res.error_in_exec, ValueError)
+
+    def test_open_standard_output_stream(self):
+        res = ip.run_cell("open(1)")
+        self.assertIsInstance(res.error_in_exec, ValueError)
+
+    def test_open_standard_error_stream(self):
+        res = ip.run_cell("open(2)")
+        self.assertIsInstance(res.error_in_exec, ValueError)
+
     def test_In_variable(self):
         "Verify that In variable grows with user input (GH-284)"
         oldlen = len(ip.user_ns['In'])