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:
codecov-commenter: # [Codecov](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) Report
> Merging [#4624](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) (f8e3cc7) into [main](https://codecov.io/gh/xonsh/xonsh/commit/c01fc6efe37bbd314779e62f80f9de598b9680bc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) (c01fc6e) will **decrease** coverage by `2.47%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/xonsh/xonsh/pull/4624/graphs/tree.svg?width=650&height=150&src=pr&token=GXLt8EOk8Z&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh)](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh)

```diff
@@            Coverage Diff             @@
##             main    #4624      +/-   ##
==========================================
- Coverage   62.93%   60.46%   -2.48%     
==========================================
  Files         133      133              
  Lines       22610    22616       +6     
  Branches     4412     4412              
==========================================
- Hits        14230    13674     -556     
- Misses       7263     7810     +547     
- Partials     1117     1132      +15     
```

| Flag | Coverage Δ | |
|---|---|---|
| linux | `60.46% <100.00%> (-0.05%)` | :arrow_down: |
| macos | `?` | |
| windows | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) | Coverage Δ | |
|---|---|---|
| [xonsh/parsers/base.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcGFyc2Vycy9iYXNlLnB5) | `85.08% <100.00%> (+0.05%)` | :arrow_up: |
| [xonsh/macutils.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvbWFjdXRpbHMucHk=) | `0.00% <0.00%> (-80.00%)` | :arrow_down: |
| [xonsh/prompt/job.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcHJvbXB0L2pvYi5weQ==) | `20.00% <0.00%> (-50.00%)` | :arrow_down: |
| [xonsh/winutils.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvd2ludXRpbHMucHk=) | `0.00% <0.00%> (-47.81%)` | :arrow_down: |
| [xonsh/platform.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcGxhdGZvcm0ucHk=) | `44.00% <0.00%> (-17.54%)` | :arrow_down: |
| [xonsh/completers/path.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvY29tcGxldGVycy9wYXRoLnB5) | `58.24% <0.00%> (-15.39%)` | :arrow_down: |
| [xonsh/lazyimps.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvbGF6eWltcHMucHk=) | `55.73% <0.00%> (-14.76%)` | :arrow_down: |
| [xonsh/procs/readers.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvcHJvY3MvcmVhZGVycy5weQ==) | `45.32% <0.00%> (-10.29%)` | :arrow_down: |
| [xonsh/jobs.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvam9icy5weQ==) | `36.67% <0.00%> (-10.04%)` | :arrow_down: |
| [xonsh/history/sqlite.py](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh#diff-eG9uc2gvaGlzdG9yeS9zcWxpdGUucHk=) | `69.36% <0.00%> (-9.46%)` | :arrow_down: |
| ... and [28 more](https://codecov.io/gh/xonsh/xonsh/pull/4624/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh). Last update [c01fc6e...f8e3cc7](https://codecov.io/gh/xonsh/xonsh/pull/4624?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=xonsh).

jnoortheen: Thanks @yaxollum for putting this in. 

Patch:
diff --git a/news/fix-dict-unpacking-into-dict.rst b/news/fix-dict-unpacking-into-dict.rst
new file mode 100644
index 00000000..24b49923
--- /dev/null
+++ b/news/fix-dict-unpacking-into-dict.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Fixed unpacking of dictionaries inside a dictionary
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
index aa593ffe..94a78bbe 100644
--- a/xonsh/parsers/base.py
+++ b/xonsh/parsers/base.py
@@ -2821,7 +2821,7 @@ class BaseParser:
         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 @@ class BaseParser:
             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
index e1b9f718..33e1d656 100644
--- 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),")