Repository: xonsh/xonsh

Issue:
`KeyError` in `${...}.pop('key', None)` for keys with default and no custom value
Doing a `${...}.pop('key', None)` gives a `KeyError` if `key` has a default value defined but isn't set to a custom value by the user.

## xonfig

<details>

```
$ xonfig
+------------------+----------------------+
| xonsh            | 0.8.12               |
| Git SHA          | 986bf28d             |
| Commit Date      | Mar 11 14:37:52 2019 |
| Python           | 3.6.7                |
| PLY              | 3.11                 |
| have readline    | True                 |
| prompt toolkit   | 2.0.9                |
| shell type       | prompt_toolkit2      |
| pygments         | 2.3.1                |
| on posix         | True                 |
| on linux         | True                 |
| distro           | ubuntu               |
| on darwin        | False                |
| on windows       | False                |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
+------------------+----------------------+
```
</details>

## Expected Behavior
`${...}.pop('key', None)` should return `None` (or the value of `${...}['key']`), even if `key` has a default.

## Current Behavior
A `KeyError` is raised.

### Traceback (if applicable)

<details>

```
Traceback (most recent call last):
  File "/home/user/.local/lib/python3.6/site-packages/xonsh/base_shell.py", line 360, in default
    run_compiled_code(code, self.ctx, None, "single")
  File "/home/user/.local/lib/python3.6/site-packages/xonsh/codecache.py", line 67, in run_compiled_code
    func(code, glb, loc)
  File "<xonsh-code>", line 1, in <module>
  File "/usr/lib/python3.6/_collections_abc.py", line 801, in pop
    del self[key]
  File "/home/user/.local/lib/python3.6/site-packages/xonsh/environ.py", line 1524, in __delitem__
    del self._d[key]
KeyError: 'XONSH_STDERR_PREFIX'
```
</details>

## Steps to Reproduce
```#!python
# This raises a `KeyError: 'XONSH_STDERR_PREFIX'`
${...}.pop('XONSH_STDERR_PREFIX', None)

# However, if the key exists, it works fine:
${...}['XONSH_STDERR_PREFIX'] = 'foo'
assert ${...}.pop('XONSH_STDERR_PREFIX', None) == 'foo'
```

This only seems to happen for keys that have a default set:

```#!python
# Undefined key
assert 'foo' not in ${...}
assert ${...}.pop('foo', None) is None

# Builtin key but no default
assert ${...}.pop('ANSICON', None) is None
```

## Possible cause
I took a quick look at the code and found a possible cause of the problem:

* Python's `MutableMapping.pop()` has this code:
```#!python
    try:
        value = self[key]
    except KeyError:
        ...
    else:
        del self[key]
        return value
```

* Xonsh's `Env.__getitem__()` falls back to `self._defaults` if the key is not in `self._d`, but `Env.__delitem__()` only operates on `self._d`.  So `MutableMapping.pop()` thinks the key always exists (and thus tries to delete it), regardless of if it's in `self._d` or not.



Discussion:

Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -1521,10 +1521,14 @@ def __setitem__(self, key, val):
             events.on_envvar_change.fire(name=key, oldvalue=old_value, newvalue=val)
 
     def __delitem__(self, key):
-        del self._d[key]
-        self._detyped = None
-        if self.get("UPDATE_OS_ENVIRON") and key in os_environ:
-            del os_environ[key]
+        if key in self._d:
+            del self._d[key]
+            self._detyped = None
+            if self.get("UPDATE_OS_ENVIRON") and key in os_environ:
+                del os_environ[key]
+        elif key not in self._defaults:
+            e = "Unknown environment variable: ${}"
+            raise KeyError(e.format(key))
 
     def get(self, key, default=None):
         """The environment will look up default values from its own defaults if a


Test patch:
diff --git a/tests/test_environ.py b/tests/test_environ.py
--- a/tests/test_environ.py
+++ b/tests/test_environ.py
@@ -273,3 +273,19 @@ def test_make_args_env():
         "ARG3": "3",
     }
     assert exp == obs
+
+def test_delitem():
+    env = Env(VAR="a value")
+    assert env['VAR'] == 'a value'
+    del env['VAR']
+    with pytest.raises(Exception):
+        a = env['VAR']
+
+def test_delitem_default():
+    env = Env()
+    a_key,a_value = next(env._defaults.items().__iter__())
+    del env[a_key]
+    assert env[a_key] == a_value
+    del env[a_key]
+    assert env[a_key] == a_value
+