Repository: xonsh/xonsh

Issue:
Using ** to append dictionaries not working
In python, you can use the ** operator to append dictionaries, but trying to use the same syntax in xonsh fails.

## xonfig

<details>

```
$ xonfig
+------------------+-----------------+
| xonsh            | 0.9.24          |
| Python           | 3.8.5           |
| PLY              | 3.11            |
| have readline    | True            |
| prompt toolkit   | 3.0.7           |
| shell type       | prompt_toolkit  |
| pygments         | 2.7.1           |
| on posix         | True            |
| on linux         | True            |
| distro           | pop             |
| 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 |
| on jupyter       | False           |
| jupyter kernel   | None            |
+------------------+-----------------+
```

</details>

## Expected Behaviour

Using either python or xonsh, the result should be the same.

```
$ python3 -c "print({**{'A': 1},**{'A': 2}})"
{'A': 2}

$ xonsh -c "print({**{'A': 1},**{'A': 2}})"
{'A': 2}
```

## Current Behaviour

Instead of running as expected, xonsh raises an exception:

```
$ xonsh -c "print({**{'A': 1},**{'A': 2}})"
# Failure, see traceback beneath
```

### Traceback

<details>

```
/usr/lib/python3/dist-packages/apport/report.py:13: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import fnmatch, glob, traceback, errno, sys, atexit, locale, imp
Traceback (most recent call last):
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/ptk_shell/shell.py", line 238, in _push
    code = self.execer.compile(src, mode="single", glbs=self.ctx, locs=None)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/execer.py", line 135, in compile
    tree = self.parse(input, ctx, mode=mode, filename=filename, transform=transform)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/execer.py", line 95, in parse
    tree, input = self._parse_ctx_free(input, mode=mode, filename=filename)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/execer.py", line 219, in _parse_ctx_free
    tree = self.parser.parse(
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/parsers/base.py", line 497, in parse
    tree = self.parser.parse(input=s, lexer=self.lexer, debug=debug_level)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/ply/ply/yacc.py", line 335, in parse
    return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/ply/ply/yacc.py", line 1122, in parseopt_notrack
    p.callable(pslice)
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/parsers/base.py", line 2760, in p_dictorsetmaker_i4
    lineno, col = lopen_loc(p1[0] or p2[0])
  File "/home/user/.local/lib/python3.8/site-packages/xonsh/parsers/base.py", line 190, in lopen_loc
    lineno = x._lopen_lineno if hasattr(x, "_lopen_lineno") else x.lineno
AttributeError: 'NoneType' object has no attribute 'lineno'
```

</details>

## Steps to Reproduce

Running this command is the easiest way I found to replicate the error:

```
$ xonsh -c "print({**{'A': 1},**{'A': 2}})"
```

Note that running using `xonsh -c` is not the only way to reproduce the issue, opening xonsh and then running `print({**{'A': 1},**{'A': 2}})` will fail all the same.

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
Thanks for reporting @kip93!

Resolving patch:
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
--- a/xonsh/parsers/base.py
+++ b/xonsh/parsers/base.py
@@ -2821,7 +2821,7 @@ def p_dictorsetmaker_i4(self, p):
         for k, v in zip(p2[::2], p2[1::2]):
             keys.append(k)
             vals.append(v)
-        lineno, col = lopen_loc(p1[0] or p2[0])
+        lineno, col = lopen_loc(p1[0] or p1[1])
         p[0] = ast.Dict(
             keys=keys, values=vals, ctx=ast.Load(), lineno=lineno, col_offset=col
         )
@@ -2835,6 +2835,16 @@ def p_dictorsetmaker_t4_dict(self, p):
             keys=keys, values=vals, ctx=ast.Load(), lineno=lineno, col_offset=col
         )
 
+    def p_dictorsetmaker_item_comma(self, p):
+        """dictorsetmaker : item comma_opt"""
+        p1 = p[1]
+        keys = [p1[0]]
+        vals = [p1[1]]
+        lineno, col = lopen_loc(p1[0] or p1[1])
+        p[0] = ast.Dict(
+            keys=keys, values=vals, ctx=ast.Load(), lineno=lineno, col_offset=col
+        )
+
     def p_dictorsetmaker_t4_set(self, p):
         """dictorsetmaker : test_or_star_expr comma_test_or_star_expr_list comma_opt"""
         p[0] = ast.Set(


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -730,6 +730,14 @@ def test_dict_three():
     check_ast("{42: 65, 6: 28, 1: 2}")
 
 
+def test_dict_from_dict_one():
+    check_ast('{**{"x": 2}}')
+
+
+def test_dict_from_dict_one_comma():
+    check_ast('{**{"x": 2},}')
+
+
 def test_dict_from_dict_two_xy():
     check_ast('{"x": 1, **{"y": 2}}')
 
@@ -742,6 +750,20 @@ def test_dict_from_dict_two_x_second():
     check_ast('{**{"x": 2}, "x": 1}')
 
 
+def test_dict_from_dict_two_x_none():
+    check_ast('{**{"x": 1}, **{"x": 2}}')
+
+
+@pytest.mark.parametrize("third", [True, False])
+@pytest.mark.parametrize("second", [True, False])
+@pytest.mark.parametrize("first", [True, False])
+def test_dict_from_dict_three_xyz(first, second, third):
+    val1 = '"x": 1' if first else '**{"x": 1}'
+    val2 = '"y": 2' if second else '**{"y": 2}'
+    val3 = '"z": 3' if third else '**{"z": 3}'
+    check_ast("{" + val1 + "," + val2 + "," + val3 + "}")
+
+
 def test_unpack_range_tuple():
     check_stmts("*range(4),")