Repository: pybel/pybel

Issue:
pybel.tokens.parse_result_to_dsl doesn't capture 'concept' information
### Current behavior
When passing a pybel Entity to `parse_result_to_dsl()`, the 'concept' entries don't get mapped to a corresponding 'entity' parameter in the returned node. For example, when passing `{'function': 'Complex', 'members': [{}, {}], 'concept': {'namespace': 'hgnc', 'name': 'ca'}}` to the function, the BEL complex returned has an `entity` value of `None`, as shown in the following image.

![image](https://user-images.githubusercontent.com/45149156/90273727-69e91900-de2d-11ea-9b09-fbd28a0f55ac.png)

### Expected behavior
Expected the function to return an `entity` value consistent with the node schema defined in the schema sub-module.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=h1) Report
> Merging [#465](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=desc) into [master](https://codecov.io/gh/pybel/pybel/commit/ddb346bbdbdb2f04960209d9dc3a4a972e0e29d3&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `95.23%`.

[![Impacted file tree graph](https://codecov.io/gh/pybel/pybel/pull/465/graphs/tree.svg?width=650&height=150&src=pr&token=J7joRTRygG)](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #465      +/-   ##
==========================================
+ Coverage   78.22%   78.30%   +0.07%     
==========================================
  Files         176      176              
  Lines        9404     9443      +39     
  Branches     1362     1364       +2     
==========================================
+ Hits         7356     7394      +38     
- Misses       1761     1762       +1     
  Partials      287      287              
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/dsl/node\_classes.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2RzbC9ub2RlX2NsYXNzZXMucHk=) | `92.12% <71.42%> (-0.51%)` | :arrow_down: |
| [src/pybel/io/triples/converters.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL3RyaXBsZXMvY29udmVydGVycy5weQ==) | `93.77% <84.61%> (+0.23%)` | :arrow_up: |
| [src/pybel/\_\_init\_\_.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pybel/io/\_\_init\_\_.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pybel/io/gpickle.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2dwaWNrbGUucHk=) | `95.65% <100.00%> (+3.65%)` | :arrow_up: |
| [src/pybel/io/indra.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2luZHJhLnB5) | `47.50% <100.00%> (+4.25%)` | :arrow_up: |
| [src/pybel/manager/models.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL21hbmFnZXIvbW9kZWxzLnB5) | `88.59% <100.00%> (+0.04%)` | :arrow_up: |
| [src/pybel/parser/parse\_concept.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9jb25jZXB0LnB5) | `96.05% <100.00%> (ø)` | |
| [src/pybel/parser/parse\_metadata.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9tZXRhZGF0YS5weQ==) | `94.11% <100.00%> (ø)` | |
| [src/pybel/parser/utils.py](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci91dGlscy5weQ==) | `100.00% <100.00%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/pybel/pybel/pull/465/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=footer). Last update [81f400f...b07f4cb](https://codecov.io/gh/pybel/pybel/pull/465?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/pybel/tokens.py b/src/pybel/tokens.py
index 2a43df6b..9c34502d 100644
--- a/src/pybel/tokens.py
+++ b/src/pybel/tokens.py
@@ -2,7 +2,9 @@
 
 """This module helps handle node data dictionaries."""
 
-from typing import List
+from typing import Any, List, Mapping, Union
+
+from pyparsing import ParseResults
 
 from .constants import (
     CONCEPT, FRAGMENT, FRAGMENT_DESCRIPTION, FRAGMENT_START, FRAGMENT_STOP, FUNCTION, FUSION, FUSION_MISSING,
@@ -34,6 +36,8 @@ def parse_result_to_dsl(tokens) -> BaseEntity:
         return _variant_po_to_dict(tokens)
 
     elif MEMBERS in tokens:
+        if CONCEPT in tokens:
+            return _list_po_with_concept_to_dict(tokens)
         return _list_po_to_dict(tokens)
 
     elif FUSION in tokens:
@@ -196,6 +200,25 @@ def _reaction_po_to_dict(tokens) -> Reaction:
     )
 
 
+def _list_po_with_concept_to_dict(tokens: Union[ParseResults, Mapping[str, Any]]) -> ListAbundance:
+    """Convert a list parse object to a node.
+
+    :type tokens: ParseResult
+    """
+    func = tokens[FUNCTION]
+    dsl = FUNC_TO_LIST_DSL[func]
+    members = _parse_tokens_list(tokens[MEMBERS])
+
+    concept = tokens[CONCEPT]
+    return dsl(
+        members=members,
+        namespace=concept[NAMESPACE],
+        name=concept.get(NAME),
+        identifier=concept.get(IDENTIFIER),
+        xrefs=tokens.get(XREFS),
+    )
+
+
 def _list_po_to_dict(tokens) -> ListAbundance:
     """Convert a list parse object to a node.
 


Test patch:
diff --git a/tests/test_dsl.py b/tests/test_dsl.py
index 76d8a9e9..bb177dd0 100644
--- a/tests/test_dsl.py
+++ b/tests/test_dsl.py
@@ -4,6 +4,7 @@
 
 import unittest
 
+import pybel.constants as pc
 from pybel import BELGraph
 from pybel.constants import NAME
 from pybel.dsl import (
@@ -12,6 +13,7 @@ from pybel.dsl import (
 )
 from pybel.language import Entity
 from pybel.testing.utils import n
+from pybel.tokens import parse_result_to_dsl
 from pybel.utils import ensure_quotes
 
 
@@ -210,5 +212,20 @@ class TestCentralDogma(unittest.TestCase):
             Reaction([], [])
 
 
+class TestParse(unittest.TestCase):
+    """Test that :func:`parse_result_to_dsl` works correctly."""
+
+    def test_named_complex(self):
+        x = ComplexAbundance(namespace='a', identifier='b', members=[
+            Protein(namespace='c', identifier='d'),
+            Protein(namespace='c', identifier='e'),
+        ])
+
+        y = parse_result_to_dsl(dict(x))
+        self.assertIsInstance(y, ComplexAbundance)
+        self.assertIn(pc.MEMBERS, y)
+        self.assertIn(pc.CONCEPT, y)
+
+
 if __name__ == '__main__':
     unittest.main()