Repository: pybel/pybel
Pull request #461 (2020-07-29 12:48:32)

Update annotation storage
References #458

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

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

```diff
@@            Coverage Diff             @@
##           master     #461      +/-   ##
==========================================
+ Coverage   78.22%   78.39%   +0.16%     
==========================================
  Files         176      176              
  Lines        9404     9575     +171     
  Branches     1362     1413      +51     
==========================================
+ Hits         7356     7506     +150     
- Misses       1761     1771      +10     
- Partials      287      298      +11     
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/461?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/struct/graph.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3N0cnVjdC9ncmFwaC5weQ==) | `83.69% <59.42%> (-3.83%)` | :arrow_down: |
| [src/pybel/dsl/node\_classes.py](https://codecov.io/gh/pybel/pybel/pull/461/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/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL3RyaXBsZXMvY29udmVydGVycy5weQ==) | `93.77% <84.61%> (+0.23%)` | :arrow_up: |
| [src/pybel/manager/cache\_manager.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL21hbmFnZXIvY2FjaGVfbWFuYWdlci5weQ==) | `80.32% <85.71%> (+0.04%)` | :arrow_up: |
| [src/pybel/io/jgif.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2pnaWYucHk=) | `71.59% <87.50%> (+0.08%)` | :arrow_up: |
| [src/pybel/struct/summary/edge\_summary.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3N0cnVjdC9zdW1tYXJ5L2VkZ2Vfc3VtbWFyeS5weQ==) | `77.77% <87.50%> (+1.85%)` | :arrow_up: |
| [src/pybel/testing/utils.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3Rlc3RpbmcvdXRpbHMucHk=) | `97.29% <90.00%> (-2.71%)` | :arrow_down: |
| [src/pybel/\_\_init\_\_.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pybel/canonicalize.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2Nhbm9uaWNhbGl6ZS5weQ==) | `94.65% <100.00%> (+0.04%)` | :arrow_up: |
| [src/pybel/constants.py](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2NvbnN0YW50cy5weQ==) | `100.00% <100.00%> (ø)` | |
| ... and [20 more](https://codecov.io/gh/pybel/pybel/pull/461/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pybel/pybel/pull/461?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/461?src=pr&el=footer). Last update [81f400f...6d9aaa4](https://codecov.io/gh/pybel/pybel/pull/461?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/pybel/canonicalize.py b/src/pybel/canonicalize.py
index a01a3209..7408e23b 100644
--- a/src/pybel/canonicalize.py
+++ b/src/pybel/canonicalize.py
@@ -208,16 +208,19 @@ def _evidence_sort_key(t: EdgeTuple) -> str:
     return t[3][EVIDENCE]
 
 
-def _set_annotation_to_str(annotation_data: Mapping[str, Mapping[str, bool]], key: str) -> str:
+def _set_annotation_to_str(annotation_data: Mapping[str, List[Entity]], key: str, use_curie: bool = False) -> str:
     """Return a set annotation string."""
     value = annotation_data[key]
 
     if len(value) == 1:
-        return 'SET {} = "{}"'.format(key, list(value)[0])
+        value = list(value)[0]
+        return f'SET {key} = "{value if use_curie else value.identifier}"'
 
-    x = ('"{}"'.format(v) for v in sorted(value))
-
-    return 'SET {} = {{{}}}'.format(key, ', '.join(x))
+    value_strings = ', '.join(
+        f'"{v if use_curie else v.identifier}"'
+        for v in sorted(value, key=lambda e: (e.namespace, e.identifier, e.name))
+    )
+    return f'SET {key} = {{{value_strings}}}'
 
 
 def _unset_annotation_to_str(keys: List[str]) -> str:
@@ -275,7 +278,7 @@ def _to_bel_lines_body(graph, use_identifiers: bool = False) -> Iterable[str]:
 
                 keys = sorted(annotations_data) if annotations_data is not None else tuple()
                 for key in keys:
-                    yield _set_annotation_to_str(annotations_data, key)
+                    yield _set_annotation_to_str(annotations_data, key, use_curie=key in graph.annotation_curie)
 
                 yield graph.edge_to_bel(u, v, data, use_identifiers=use_identifiers)
 
diff --git a/src/pybel/constants.py b/src/pybel/constants.py
index b670d074..dca4afe1 100644
--- a/src/pybel/constants.py
+++ b/src/pybel/constants.py
@@ -247,8 +247,6 @@ CITATION_IDENTIFIER = IDENTIFIER  # for backwards compatibility
 EVIDENCE = 'evidence'
 #: The key for an internal edge data dictionary for the annotations dictionary
 ANNOTATIONS = 'annotations'
-#: The key for free annotations
-FREE_ANNOTATIONS = 'free_annotations'
 SOURCE = 'source'
 SUBJECT = SOURCE  # for backwards compatibility
 TARGET = 'target'
@@ -425,6 +423,8 @@ GRAPH_METADATA = 'document_metadata'
 GRAPH_NAMESPACE_URL = 'namespace_url'
 GRAPH_NAMESPACE_PATTERN = 'namespace_pattern'
 GRAPH_ANNOTATION_URL = 'annotation_url'
+GRAPH_ANNOTATION_MIRIAM = 'annotation_miriam'
+GRAPH_ANNOTATION_CURIE = 'annotation_curie'
 GRAPH_ANNOTATION_PATTERN = 'annotation_pattern'
 GRAPH_ANNOTATION_LIST = 'annotation_list'
 GRAPH_WARNINGS = 'warnings'
diff --git a/src/pybel/grounding.py b/src/pybel/grounding.py
index 473f08cb..0cdd48fd 100644
--- a/src/pybel/grounding.py
+++ b/src/pybel/grounding.py
@@ -51,7 +51,6 @@ After installation with ``pip install git+https://github.com/cthoyt/selventa-kno
 
 import logging
 import unittest
-from collections import defaultdict
 from typing import Any, Mapping, Tuple, Union
 
 import pyobo
@@ -63,13 +62,15 @@ from pyobo.xrefdb.sources.famplex import get_remapping
 from tqdm import tqdm
 
 from pybel.constants import (
-    ACTIVITY, ANNOTATIONS, CONCEPT, EFFECT, FREE_ANNOTATIONS, FROM_LOC, FUSION, GMOD, IDENTIFIER, KIND, LOCATION,
+    ACTIVITY, ANNOTATIONS, CONCEPT, EFFECT, FROM_LOC, FUSION, GMOD, IDENTIFIER, KIND, LOCATION,
     MEMBERS, MODIFIER, NAME, NAMESPACE, PARTNER_3P, PARTNER_5P, PMOD, PRODUCTS, REACTANTS, SOURCE_MODIFIER,
     TARGET_MODIFIER, TO_LOC, TRANSLOCATION, VARIANTS,
 )
 from pybel.dsl import BaseConcept
 from pybel.io import from_nodelink, to_nodelink
-from pybel.language import Entity, activity_mapping, compartment_mapping, gmod_mappings, pmod_mappings
+from pybel.language import (
+    Entity, activity_mapping, compartment_mapping, gmod_mappings, pmod_mappings, text_location_labels,
+)
 from pybel.struct import BELGraph, get_annotations, get_namespaces, get_ungrounded_nodes
 
 __all__ = [
@@ -166,9 +167,11 @@ def ground_nodelink(graph_nodelink_dict) -> None:
 _BEL_ANNOTATION_PREFIX_MAP = {
     'MeSHDisease': 'mesh',
     'MeSHAnatomy': 'mesh',
+    'CellStructure': 'mesh',
     'Species': 'ncbitaxon',
     'Disease': 'doid',
     'Cell': 'cl',
+    'Anatomy': 'uberon',
 }
 _BEL_ANNOTATION_PREFIX_CATEGORY_MAP = {
     'MeSHDisease': 'Disease',
@@ -176,72 +179,59 @@ _BEL_ANNOTATION_PREFIX_CATEGORY_MAP = {
 }
 
 _UNHANDLED_ANNOTATION = set()
+CATEGORY_BLACKLIST = {
+    'TextLocation',
+}
 
 
 def _process_annotations(data, remove_ungrounded: bool = False) -> None:
     """Process the annotations in a PyBEL edge data dictionary."""
-    grounded_category_curie_polarity = []
-    ungrounded_category_name_polarity = []
-
-    for category, names in data[ANNOTATIONS].items():
-        if category == 'CellLine':
-            _namespaces = [
-                'efo',
-                # 'clo',  # FIXME implement CLO import and add here
-            ]
-            for name, polarity in names.items():
-                g_prefix, g_identifier, g_name = pyobo.ground(_namespaces, name)
+    cell_line_entities = data[ANNOTATIONS].get('CellLine')
+    if cell_line_entities:
+        ne = []
+        for entity in cell_line_entities:
+            if entity[NAMESPACE] == 'CellLine':
+                _namespaces = [
+                    'efo',
+                    # 'clo',  # FIXME implement CLO in PyOBO then uncomment
+                ]
+                g_prefix, g_identifier, g_name = pyobo.ground(_namespaces, entity[IDENTIFIER])
                 if g_prefix and g_identifier:
-                    grounded_category_curie_polarity.append((
-                        category, Entity(namespace=g_prefix, identifier=g_identifier, name=g_name), polarity,
-                    ))
-                else:
-                    ungrounded_category_name_polarity.append((category, name, polarity))
-
-        elif category in _BEL_ANNOTATION_PREFIX_MAP:
-            norm_prefix = _BEL_ANNOTATION_PREFIX_MAP[category]
-            norm_category = _BEL_ANNOTATION_PREFIX_CATEGORY_MAP.get(category, category)
-            for name, polarity in names.items():
-                _, identifier, _ = pyobo.ground(norm_prefix, name)
-                if identifier:
-                    grounded_category_curie_polarity.append((
-                        norm_category, Entity(namespace=norm_prefix, identifier=identifier, name=name), polarity,
-                    ))
-                else:
-                    ungrounded_category_name_polarity.append((norm_category, name, polarity))
-
-        elif normalize_prefix(category):
-            norm_prefix = normalize_prefix(category)
-            for name, polarity in names.items():
-                _, identifier, _ = pyobo.ground(norm_prefix, name)
-                if identifier:
-                    grounded_category_curie_polarity.append((
-                        category, Entity(namespace=norm_prefix, identifier=identifier, name=name), polarity,
-                    ))
-                else:
-                    ungrounded_category_name_polarity.append((category, name, polarity))
-
-        else:
-            if category not in _UNHANDLED_ANNOTATION:
-                logger.warning('unhandled annotation: %s', category)
-                _UNHANDLED_ANNOTATION.add(category)
-
-            if isinstance(names, dict):
-                for name, polarity in names.items():
-                    ungrounded_category_name_polarity.append((category, name, polarity))
-            else:
-                ungrounded_category_name_polarity.append((category, names, True))
-
-    data[ANNOTATIONS] = defaultdict(dict)
-    for category, entity, polarity in grounded_category_curie_polarity:
-        data[ANNOTATIONS][category][entity.curie] = polarity
-    data[ANNOTATIONS] = dict(data[ANNOTATIONS])
+                    ne.append(Entity(namespace=g_prefix, identifier=g_identifier, name=g_name))
+                elif not remove_ungrounded:
+                    logger.warning('could not ground CellLine: "%s"', entity[IDENTIFIER])
+                    ne.append(entity)
+        data[ANNOTATIONS]['CellLine'] = ne
+
+    # fix text locations
+    text_location = data[ANNOTATIONS].get('TextLocation')
+    if text_location:
+        data[ANNOTATIONS]['TextLocation'] = [
+            text_location_labels.get(entity.identifier, entity)
+            for entity in text_location
+        ]
 
-    if not remove_ungrounded and ungrounded_category_name_polarity:
-        data[FREE_ANNOTATIONS] = defaultdict(dict)
-        for category, name, polarity in ungrounded_category_name_polarity:
-            data[FREE_ANNOTATIONS][category][name] = polarity
-        data[FREE_ANNOTATIONS] = dict(data[FREE_ANNOTATIONS])
+    # remap category names
+    data[ANNOTATIONS] = {
+        _BEL_ANNOTATION_PREFIX_CATEGORY_MAP.get(category, category): entities
+        for category, entities in data[ANNOTATIONS].items()
+    }
+    # fix namespaces that were categories before
+    for category, entities in data[ANNOTATIONS].items():
+        if category in CATEGORY_BLACKLIST:
+            continue
+
+        ne = []
+        for entity in entities:
+            if not isinstance(entity, dict):
+                raise TypeError(f'entity should be a dict. got: {entity}')
+            nn = _BEL_ANNOTATION_PREFIX_MAP.get(entity[NAMESPACE])
+            if nn is not None:
+                entity[NAMESPACE] = nn
+
+            _process_concept(concept=entity)
+            ne.append(entity)
+        data[ANNOTATIONS][category] = ne
 
 
 def _process_edge_side(side_data) -> bool:
@@ -295,7 +285,7 @@ def _process_concept(*, concept, node=None) -> bool:
 
     prefix = normalize_prefix(namespace)
     if prefix is None:
-        logger.warning('could not normalize namespace %s in concept %s', namespace, concept)
+        logger.warning('could not normalize namespace "%s" in concept "%s"', namespace, concept)
         return False
 
     concept[NAMESPACE] = prefix
@@ -304,6 +294,8 @@ def _process_concept(*, concept, node=None) -> bool:
     name = concept.get(NAME)
     if identifier:  # don't trust whatever was put for the name, even if it's available
         map_success = _handle_identifier_not_name(concept=concept, prefix=prefix, identifier=identifier)
+        if not map_success:  # just in case the name gets put in the identifier
+            map_success = _handle_name_and_not_identifier(concept=concept, prefix=prefix, name=identifier, node=node)
     else:
         map_success = _handle_name_and_not_identifier(concept=concept, prefix=prefix, name=name, node=node)
 
@@ -349,11 +341,12 @@ def _handle_identifier_not_name(*, concept, prefix, identifier) -> bool:
         return False
 
     if id_name_mapping is None:
-        logger.warning('could not get names for prefix %s', prefix)
+        logger.warning('could not get names for prefix "%s"', prefix)
         return False
+
     name = id_name_mapping.get(identifier)
     if name is None:
-        logger.warning('could not get name for %s:%s', prefix, identifier)
+        logger.warning('could not get name for curie %s:%s', prefix, identifier)
         return False
     concept[NAME] = name
 
@@ -403,7 +396,7 @@ def _handle_name_and_not_identifier(*, concept, prefix, name, node=None) -> bool
         concept[NAME] = _mapped[NAME]
         return True
     elif prefix == 'bel':
-        logger.warning('could not figure out how to map bel ! %s', name)
+        logger.warning('could not figure out how to map bel ! "%s"', name)
         return False
 
     if prefix == 'uniprot':
@@ -419,7 +412,7 @@ def _handle_name_and_not_identifier(*, concept, prefix, name, node=None) -> bool
             concept[NAME] = mnemomic
             return True
 
-        logger.warning('could not interpret uniprot name: %s', name)
+        logger.warning('could not interpret uniprot name: "%s"', name)
         return False
 
     try:
@@ -434,7 +427,7 @@ def _handle_name_and_not_identifier(*, concept, prefix, name, node=None) -> bool
 
     identifier = id_name_mapping.get(name)
     if identifier is None:
-        logger.warning('could not find name %s in namespace %s', name, prefix)
+        logger.warning('could not find name "%s" in namespace "%s"', name, prefix)
         return False
 
     concept[IDENTIFIER] = identifier
@@ -651,59 +644,89 @@ class TestGround(unittest.TestCase):
                 'Test Upgrade MeSH disease, MeSH anatomy, and species',
                 {  # Expected
                     ANNOTATIONS: {
-                        'Disease': {'mesh:D010300': True},
-                        'Anatomy': {'mesh:D013378': True},
-                        'Species': {'ncbitaxon:9606': True},
+                        'Disease': [Entity(namespace='mesh', identifier='D010300', name='Parkinson Disease')],
+                        'Anatomy': [Entity(namespace='mesh', identifier='D013378', name='Substantia Nigra')],
+                        'Species': [Entity(namespace='ncbitaxon', identifier='9606', name='Homo sapiens')],
                     },
                 },
                 {  # Original
                     ANNOTATIONS: {
-                        'MeSHDisease': {'Parkinson Disease': True},
-                        'MeSHAnatomy': {'Substantia Nigra': True},
-                        'Species': {'Homo sapiens': True},
+                        'MeSHDisease': [Entity(namespace='MeSHDisease', identifier='Parkinson Disease')],
+                        'MeSHAnatomy': [Entity(namespace='MeSHAnatomy', identifier='Substantia Nigra')],
+                        'Species': [Entity(namespace='Species', identifier='Homo sapiens')],
                     },
                 },
             ),
             (
-                'When the name of the annotation does not need to be mapped',
+                'Test Upgrade MeSH disease, MeSH anatomy, and species but with names',
+                {  # Expected
+                    ANNOTATIONS: {
+                        'Disease': [Entity(namespace='mesh', identifier='D010300', name='Parkinson Disease')],
+                        'Anatomy': [Entity(namespace='mesh', identifier='D013378', name='Substantia Nigra')],
+                        'Species': [Entity(namespace='ncbitaxon', identifier='9606', name='Homo sapiens')],
+                    },
+                },
+                {  # Original
+                    ANNOTATIONS: {
+                        'MeSHDisease': [Entity(namespace='MeSHDisease', name='Parkinson Disease')],
+                        'MeSHAnatomy': [Entity(namespace='MeSHAnatomy', name='Substantia Nigra')],
+                        'Species': [Entity(namespace='Species', name='Homo sapiens')],
+                    },
+                },
+            ),
+            (
+                'When the name of the annotation does not need to be mapped via identifiers',
+                {  # Expected
+                    ANNOTATIONS: {
+                        'Disease': [Entity(namespace='doid', identifier='14330', name="Parkinson's disease")],
+                        'Cell': [Entity(namespace='cl', identifier='0000030', name='glioblast')],
+                    },
+                },
                 {
                     ANNOTATIONS: {
-                        'Disease': {'doid:14330': True},
-                        'Cell': {'cl:0000030': True},
+                        'Disease': [Entity(namespace='doid', identifier="Parkinson's disease")],
+                        'Cell': [Entity(namespace='Cell', identifier='glioblast')],
+                    },
+                },
+            ),
+            (
+                'When the name of the annotation does not need to be mapped via names',
+                {  # Expected
+                    ANNOTATIONS: {
+                        'Disease': [Entity(namespace='doid', identifier='14330', name="Parkinson's disease")],
+                        'Cell': [Entity(namespace='cl', identifier='0000030', name='glioblast')],
                     },
                 },
                 {
                     ANNOTATIONS: {
-                        'Disease': {"Parkinson's disease": True},
-                        'Cell': {'glioblast': True},
+                        'Disease': [Entity(namespace='doid', name="Parkinson's disease")],
+                        'Cell': [Entity(namespace='cl', name='glioblast')],
                     },
                 },
             ),
             (
                 'Check unmappable disease',
-                {
-                    ANNOTATIONS: {},
-                    FREE_ANNOTATIONS: {
-                        'Disease': {"Failure": True},
-                    }
+                {  # Expected
+                    ANNOTATIONS: {
+                        'Disease': [Entity(namespace='doid', identifier='Failure')],
+                    },
                 },
                 {
                     ANNOTATIONS: {
-                        'Disease': {"Failure": True},
+                        'Disease': [Entity(namespace='Disease', identifier='Failure')],
                     },
                 },
             ),
             (
                 'Check unhandled annotation',
-                {
-                    ANNOTATIONS: {},
-                    FREE_ANNOTATIONS: {
-                        'Custom Annotation': {"Custom Value": True},
+                {  # Expected
+                    ANNOTATIONS: {
+                        'Custom Annotation': [Entity(namespace='Custom Annotation', identifier="Custom Value")],
                     }
                 },
                 {
                     ANNOTATIONS: {
-                        'Custom Annotation': {"Custom Value": True},
+                        'Custom Annotation': [Entity(namespace='Custom Annotation', identifier="Custom Value")],
                     },
                 },
             ),
@@ -712,3 +735,7 @@ class TestGround(unittest.TestCase):
             with self.subTest(name=name):
                 _process_annotations(data)
                 self.assertEqual(expected_data, data)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/pybel/io/cx.py b/src/pybel/io/cx.py
index cf8d2b14..c74507c1 100644
--- a/src/pybel/io/cx.py
+++ b/src/pybel/io/cx.py
@@ -30,6 +30,7 @@ from ..constants import (
     RANGE_3P, RANGE_5P, REACTANTS, RELATION, SOURCE_MODIFIER, TARGET_MODIFIER, UNQUALIFIED_EDGES, VARIANTS,
 )
 from ..dsl import BaseAbundance, BaseEntity
+from ..language import Entity
 from ..struct import BELGraph
 from ..tokens import parse_result_to_dsl
 from ..utils import expand_dict, flatten_dict
@@ -203,7 +204,7 @@ def to_cx(graph: BELGraph) -> List[Dict]:  # noqa: C901
                 edge_attributes_entry.append({
                     'po': edge_index,
                     'n': annotation,
-                    'v': sorted(values),
+                    'v': sorted(values, key=lambda e: (e.namespace, e.identifier, e.name)),
                     'd': 'list_of_string',
                 })
 
@@ -540,7 +541,7 @@ def from_cx(cx: List[Dict]) -> BELGraph:  # noqa: C901
     for eid in edge_relation:
         if eid in edge_annotations:  # FIXME stick this in edge_data.items() iteration
             edge_data_pp[eid][ANNOTATIONS] = {
-                key: {v: True for v in values}
+                key: [Entity(**v) for v in values]
                 for key, values in edge_annotations[eid].items()
             }
 
diff --git a/src/pybel/io/jgif.py b/src/pybel/io/jgif.py
index 757c1a56..443d0ce1 100644
--- a/src/pybel/io/jgif.py
+++ b/src/pybel/io/jgif.py
@@ -20,7 +20,7 @@ from networkx.utils import open_file
 from pyparsing import ParseException
 
 from ..constants import (
-    ANNOTATIONS, CITATION, EVIDENCE, METADATA_AUTHORS, METADATA_CONTACT,
+    ANNOTATIONS, CITATION, EVIDENCE, GRAPH_ANNOTATION_URL, GRAPH_NAMESPACE_URL, METADATA_AUTHORS, METADATA_CONTACT,
     METADATA_INSERT_KEYS, METADATA_LICENSES, RELATION, UNQUALIFIED_EDGES,
 )
 from ..exceptions import NakedNameWarning, UndefinedNamespaceWarning
@@ -198,6 +198,8 @@ def from_cbn_jgif(graph_jgif_dict):
     """
     graph_jgif_dict = map_cbn(graph_jgif_dict)
 
+    graph_jgif_dict['graph'][GRAPH_NAMESPACE_URL] = NAMESPACE_URLS
+    graph_jgif_dict['graph'][GRAPH_ANNOTATION_URL] = ANNOTATION_URLS
     graph_jgif_dict['graph']['metadata'].update({
         METADATA_AUTHORS: 'Causal Biological Networks Database',
         METADATA_LICENSES: """
@@ -219,14 +221,10 @@ def from_cbn_jgif(graph_jgif_dict):
     })
 
     graph = from_jgif(graph_jgif_dict)
-
-    graph.namespace_url.update(NAMESPACE_URLS)
-    graph.annotation_url.update(ANNOTATION_URLS)
-
     return graph
 
 
-def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None):
+def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None):  # noqa:C901
     """Build a BEL graph from a JGIF JSON object.
 
     :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
@@ -246,6 +244,10 @@ def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None
             if key in metadata:
                 graph.document[key] = metadata[key]
 
+    for k in (GRAPH_ANNOTATION_URL, GRAPH_NAMESPACE_URL):
+        if k in root:
+            graph.graph[k] = root[k]
+
     parser = BELParser(graph, namespace_to_pattern=NAMESPACE_TO_PATTERN)
     parser.bel_term.addParseAction(parser.handle_term)
 
@@ -310,7 +312,8 @@ def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None
                 parser.control_parser.citation_db = citation['type'].strip()
                 parser.control_parser.citation_db_id = citation['id'].strip()
                 parser.control_parser.evidence = summary_text
-                parser.control_parser.annotations.update(evidence[EXPERIMENT_CONTEXT])
+                annotations = parser.graph._clean_annotations(evidence[EXPERIMENT_CONTEXT])
+                parser.control_parser.annotations.update(annotations)
 
                 try:
                     parser.parseString(bel_statement, line_number=i)
diff --git a/src/pybel/io/nodelink.py b/src/pybel/io/nodelink.py
index 1189cc44..563b0aed 100644
--- a/src/pybel/io/nodelink.py
+++ b/src/pybel/io/nodelink.py
@@ -13,8 +13,8 @@ from networkx.utils import open_file
 
 from .utils import ensure_version
 from ..constants import (
-    CITATION, FUSION, GRAPH_ANNOTATION_LIST, MEMBERS, PARTNER_3P, PARTNER_5P, PRODUCTS, REACTANTS, SOURCE_MODIFIER,
-    TARGET_MODIFIER,
+    CITATION, FUSION, GRAPH_ANNOTATION_CURIE, GRAPH_ANNOTATION_LIST, GRAPH_ANNOTATION_MIRIAM, MEMBERS, PARTNER_3P,
+    PARTNER_5P, PRODUCTS, REACTANTS, SOURCE_MODIFIER, TARGET_MODIFIER,
 )
 from ..dsl import BaseEntity
 from ..language import CitationDict
@@ -44,13 +44,20 @@ def to_nodelink(graph: BELGraph) -> Mapping[str, Any]:
     """
     graph_json_dict = _to_nodelink_json_helper(graph)
 
+    _prepare_graph_dict(graph_json_dict['graph'])
+
+    return graph_json_dict
+
+
+def _prepare_graph_dict(g):
     # Convert annotation list definitions (which are sets) to canonicalized/sorted lists
-    graph_json_dict['graph'][GRAPH_ANNOTATION_LIST] = {
+    g[GRAPH_ANNOTATION_LIST] = {
         keyword: list(sorted(values))
-        for keyword, values in graph_json_dict['graph'].get(GRAPH_ANNOTATION_LIST, {}).items()
+        for keyword, values in g.get(GRAPH_ANNOTATION_LIST, {}).items()
     }
 
-    return graph_json_dict
+    g[GRAPH_ANNOTATION_CURIE] = list(sorted(g[GRAPH_ANNOTATION_CURIE]))
+    g[GRAPH_ANNOTATION_MIRIAM] = list(sorted(g[GRAPH_ANNOTATION_MIRIAM]))
 
 
 @open_file(1, mode='w')
@@ -148,11 +155,13 @@ def _augment_node(node: BaseEntity) -> BaseEntity:
     return rv
 
 
-def _fix_annotation_list(graph: BELGraph):
+def _recover_graph_dict(graph: BELGraph):
     graph.graph[GRAPH_ANNOTATION_LIST] = {
         keyword: set(values)
         for keyword, values in graph.graph.get(GRAPH_ANNOTATION_LIST, {}).items()
     }
+    graph.graph[GRAPH_ANNOTATION_CURIE] = set(graph.graph.get(GRAPH_ANNOTATION_CURIE, []))
+    graph.graph[GRAPH_ANNOTATION_MIRIAM] = set(graph.graph.get(GRAPH_ANNOTATION_MIRIAM, []))
 
 
 def _from_nodelink_json_helper(data: Mapping[str, Any]) -> BELGraph:
@@ -162,7 +171,7 @@ def _from_nodelink_json_helper(data: Mapping[str, Any]) -> BELGraph:
     """
     graph = BELGraph()
     graph.graph = data.get('graph', {})
-    _fix_annotation_list(graph)
+    _recover_graph_dict(graph)
 
     mapping = []
 
diff --git a/src/pybel/io/sbel.py b/src/pybel/io/sbel.py
index 6b3b7a5f..44b3a086 100644
--- a/src/pybel/io/sbel.py
+++ b/src/pybel/io/sbel.py
@@ -8,7 +8,7 @@ from typing import Any, Iterable, List, TextIO, Union
 
 from networkx.utils import open_file
 
-from .nodelink import _augment_node, _fix_annotation_list
+from .nodelink import _augment_node, _prepare_graph_dict, _recover_graph_dict
 from ..constants import CITATION, SOURCE_MODIFIER, TARGET_MODIFIER
 from ..language import CitationDict
 from ..struct.graph import BELGraph, _handle_modifier
@@ -57,7 +57,9 @@ def to_sbel(graph: BELGraph) -> List[SBEL]:
 
 def iterate_sbel(graph: BELGraph) -> Iterable[SBEL]:
     """Iterate over JSON dictionaries corresponding to lines in BEL JSONL."""
-    yield graph.graph.copy()
+    g = graph.graph.copy()
+    _prepare_graph_dict(g)
+    yield g
     for u, v, k, d in graph.edges(data=True, keys=True):
         yield {
             'source': _augment_node(u),
@@ -79,7 +81,7 @@ def from_sbel(it: Iterable[SBEL], includes_metadata: bool = True) -> BELGraph:
     rv = BELGraph()
     if includes_metadata:
         rv.graph.update(next(it))
-        _fix_annotation_list(rv)
+        _recover_graph_dict(rv)
     add_sbel(rv, it)
     return rv
 
diff --git a/src/pybel/language.py b/src/pybel/language.py
index 79c1b15e..d61938cd 100644
--- a/src/pybel/language.py
+++ b/src/pybel/language.py
@@ -8,7 +8,8 @@ This module contains mappings between PyBEL's internal constants and BEL languag
 from typing import Optional
 
 from .constants import (
-    ABUNDANCE, BIOPROCESS, CELL_SURFACE, COMPLEX, COMPOSITE, EXTRACELLULAR, GENE, IDENTIFIER, INTRACELLULAR, MIRNA, NAME, NAMESPACE, PATHOLOGY, PROTEIN, RNA, TRANSCRIBED_TO, TRANSLATED_TO,
+    ABUNDANCE, BIOPROCESS, CELL_SURFACE, COMPLEX, COMPOSITE, EXTRACELLULAR, GENE, IDENTIFIER, INTRACELLULAR, MIRNA,
+    NAME, NAMESPACE, PATHOLOGY, PROTEIN, RNA, TRANSCRIBED_TO, TRANSLATED_TO,
 )
 from .utils import ensure_quotes
 
@@ -49,9 +50,9 @@ class Entity(dict):
 
         if identifier is not None:
             if not isinstance(identifier, str):
-                raise TypeError('identifier should be a string {}'.format(identifier))
+                raise TypeError(f'identifier should be a string. Got {type(identifier)} {identifier}')
             if not identifier:
-                raise ValueError('identifier shold be non-empty')
+                raise ValueError('identifier should be non-empty')
             self[IDENTIFIER] = identifier
 
     @property
@@ -89,6 +90,16 @@ class Entity(dict):
     def __str__(self):  # noqa: D105
         return self.obo if self.identifier and self.name else self.curie
 
+    def __hash__(self) -> int:
+        return hash((self.namespace, self.identifier, self.name))
+
+
+text_location_labels = {
+    'Abstract': Entity(namespace='iao', identifier='0000315', name='abstract'),
+    'Review': Entity(namespace='iao', identifier='0000311', name='publication'),  # sue me
+    'Results': Entity(namespace='iao', identifier='0000318', name='results section'),
+    'Legend': Entity(namespace='sio', identifier='000468 ', name='legend'),
+}
 
 #: A dictionary of activity labels used in the ma() function in activity(p(X), ma(Y))
 activity_labels = {
diff --git a/src/pybel/manager/cache_manager.py b/src/pybel/manager/cache_manager.py
index 7a0e516d..8128b05e 100644
--- a/src/pybel/manager/cache_manager.py
+++ b/src/pybel/manager/cache_manager.py
@@ -325,12 +325,13 @@ class NamespaceManager(BaseManager):
         """
         return self._ensure_namespace_urls([url], is_annotation=True)[0]
 
-    def get_annotation_entries_by_names(self, url: str, names: Iterable[str]) -> List[NamespaceEntry]:
+    def get_annotation_entries_by_names(self, url: str, entities: Iterable[Entity]) -> List[NamespaceEntry]:
         """Get annotation entries by URL and names.
 
         :param url: The url of the annotation source
-        :param names: The names of the annotation entries from the given url's document
+        :param entities: The names of the annotation entries from the given url's document
         """
+        names = [e.identifier if isinstance(e, Entity) else e for e in entities]
         annotation_filter = and_(Namespace.url == url, NamespaceEntry.name.in_(names))
         return self.session.query(NamespaceEntry).join(Namespace).filter(annotation_filter).all()
 
@@ -678,7 +679,7 @@ class InsertManager(NamespaceManager, LookupManager):
                 except Exception as e:
                     self.session.rollback()
                     logger.exception('error storing edge in database. edge data: %s', data)
-                    raise EdgeAddError(e, u, v, key, data) from e
+                    raise EdgeAddError(e, u, v, key, data)
                 else:
                     yield edge
 
@@ -686,9 +687,9 @@ class InsertManager(NamespaceManager, LookupManager):
     def _iter_from_annotations_dict(
         graph: BELGraph,
         annotations_dict: AnnotationsDict,
-    ) -> Iterable[Tuple[str, Set[str]]]:
+    ) -> Iterable[Tuple[str, Set[Entity]]]:
         """Iterate over the key/value pairs in this edge data dictionary normalized to their source URLs."""
-        for key, names in annotations_dict.items():
+        for key, entities in annotations_dict.items():
             if key in graph.annotation_url:
                 url = graph.annotation_url[key]
             elif key in graph.annotation_list:
@@ -699,7 +700,7 @@ class InsertManager(NamespaceManager, LookupManager):
             else:
                 raise ValueError('Graph resources does not contain keyword: {}'.format(key))
 
-            yield url, set(names)
+            yield url, set(entities)
 
     def _get_annotation_entries_from_data(self, graph: BELGraph, data: EdgeData) -> Optional[List[NamespaceEntry]]:
         """Get the annotation entries from an edge data dictionary."""
@@ -707,8 +708,8 @@ class InsertManager(NamespaceManager, LookupManager):
         if annotations_dict is None:
             return
         rv = []
-        for url, names in self._iter_from_annotations_dict(graph, annotations_dict=annotations_dict):
-            for entry in self.get_annotation_entries_by_names(url, names):
+        for url, entities in self._iter_from_annotations_dict(graph, annotations_dict=annotations_dict):
+            for entry in self.get_annotation_entries_by_names(url, entities):
                 rv.append(entry)
         return rv
 
diff --git a/src/pybel/manager/models.py b/src/pybel/manager/models.py
index 5dcbeef2..2cd58613 100644
--- a/src/pybel/manager/models.py
+++ b/src/pybel/manager/models.py
@@ -155,7 +155,7 @@ class NamespaceEntry(Base):
     id = Column(Integer, primary_key=True)
 
     name = Column(
-        String(1023), index=True, nullable=False,
+        String(1023), index=True, nullable=True,
         doc='Name that is defined in the corresponding namespace definition file',
     )
     identifier = Column(String(255), index=True, nullable=True, doc='The database accession number')
diff --git a/src/pybel/manager/query_manager.py b/src/pybel/manager/query_manager.py
index 002ba298..911cfb04 100644
--- a/src/pybel/manager/query_manager.py
+++ b/src/pybel/manager/query_manager.py
@@ -23,10 +23,13 @@ __all__ = [
 def graph_from_edges(edges: Iterable[Edge], **kwargs) -> BELGraph:
     """Build a BEL graph from edges."""
     graph = BELGraph(**kwargs)
+    graph.raise_on_missing_annotations = False
 
     for edge in edges:
         edge.insert_into_graph(graph)
 
+    graph.raise_on_missing_annotations = True
+
     return graph
 
 
diff --git a/src/pybel/parser/parse_bel.py b/src/pybel/parser/parse_bel.py
index 5461ef44..05c4e492 100644
--- a/src/pybel/parser/parse_bel.py
+++ b/src/pybel/parser/parse_bel.py
@@ -801,11 +801,11 @@ class BELParser(BaseParser):
         else:
             return self.graph.add_qualified_edge(source=source, target=target, relation=relation, **d)
 
-    def _add_qualified_edge(self, *, source, source_modifier, relation, target, target_modifier, annotations) -> str:
+    def _add_qualified_edge(self, *, source, source_modifier, relation, target, target_modifier) -> str:
         """Add an edge, then adds the opposite direction edge if it should."""
         d = dict(
             relation=relation,
-            annotations=annotations,
+            annotations=self.control_parser.annotations,
         )
         if relation in TWO_WAY_RELATIONS:
             self._add_qualified_edge_helper(
@@ -823,30 +823,12 @@ class BELParser(BaseParser):
         target = self.ensure_node(tokens[TARGET])
         target_modifier = modifier_po_to_dict(tokens[TARGET])
 
-        annotations = self._get_prepared_annotations()
-
         return self._add_qualified_edge(
             source=source, source_modifier=source_modifier,
             relation=relation,
             target=target, target_modifier=target_modifier,
-            annotations=annotations,
         )
 
-    def _get_prepared_annotations(self):
-        return {
-            annotation_name: (
-                {
-                    ae: True
-                    for ae in annotation_entry
-                }
-                if isinstance(annotation_entry, (set, list)) else
-                {
-                    annotation_entry: True,
-                }
-            )
-            for annotation_name, annotation_entry in self.control_parser.annotations.items()
-        }
-
     def _handle_relation_harness(self, line: str, position: int, tokens: Union[ParseResults, Dict]) -> ParseResults:
         """Handle BEL relations based on the policy specified on instantiation.
 
diff --git a/src/pybel/parser/parse_control.py b/src/pybel/parser/parse_control.py
index 223dd502..0a489632 100644
--- a/src/pybel/parser/parse_control.py
+++ b/src/pybel/parser/parse_control.py
@@ -26,7 +26,7 @@ from ..exceptions import (
     InvalidPubMedIdentifierWarning, MissingAnnotationKeyWarning, MissingAnnotationRegexWarning,
     MissingCitationException, UndefinedAnnotationWarning,
 )
-from ..language import CitationDict
+from ..language import CitationDict, Entity
 
 __all__ = ['ControlParser']
 
@@ -256,7 +256,7 @@ class ControlParser(BaseParser):
         """Handle a ``SET X = "Y"`` statement."""
         key, value = tokens['key'], tokens['value']
         self.raise_for_invalid_annotation_value(line, position, key, value)
-        self.annotations[key] = value
+        self.annotations[key] = [Entity(namespace=key, identifier=value)]
         return tokens
 
     def handle_set_command_list(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
@@ -264,7 +264,7 @@ class ControlParser(BaseParser):
         key, values = tokens['key'], tokens['values']
         for value in values:
             self.raise_for_invalid_annotation_value(line, position, key, value)
-        self.annotations[key] = set(values)
+        self.annotations[key] = [Entity(namespace=key, identifier=value) for value in values]
         return tokens
 
     def handle_unset_statement_group(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
diff --git a/src/pybel/struct/graph.py b/src/pybel/struct/graph.py
index e26f22aa..799139ba 100644
--- a/src/pybel/struct/graph.py
+++ b/src/pybel/struct/graph.py
@@ -19,12 +19,12 @@ from ..canonicalize import edge_to_bel
 from ..constants import (
     ACTIVITY, ANNOTATIONS, ASSOCIATION, CAUSES_NO_CHANGE, CITATION, CITATION_AUTHORS, CITATION_TYPE_PUBMED, CORRELATION,
     DECREASES, DEGRADATION, DIRECTLY_DECREASES, DIRECTLY_INCREASES, EFFECT, EQUIVALENT_TO, EVIDENCE, FROM_LOC,
-    GRAPH_ANNOTATION_LIST, GRAPH_ANNOTATION_PATTERN, GRAPH_ANNOTATION_URL, GRAPH_METADATA, GRAPH_NAMESPACE_PATTERN,
-    GRAPH_NAMESPACE_URL, GRAPH_PATH, GRAPH_PYBEL_VERSION, HAS_PRODUCT, HAS_REACTANT, HAS_VARIANT, IDENTIFIER, INCREASES,
-    IS_A, LOCATION, METADATA_AUTHORS, METADATA_CONTACT, METADATA_COPYRIGHT, METADATA_DESCRIPTION, METADATA_DISCLAIMER,
-    METADATA_LICENSES, METADATA_NAME, METADATA_VERSION, MODIFIER, NAMESPACE, NEGATIVE_CORRELATION, NO_CORRELATION,
-    ORTHOLOGOUS, PART_OF, POSITIVE_CORRELATION, REGULATES, RELATION, SOURCE_MODIFIER, TARGET_MODIFIER, TO_LOC,
-    TRANSCRIBED_TO, TRANSLATED_TO, TRANSLOCATION,
+    GRAPH_ANNOTATION_CURIE, GRAPH_ANNOTATION_LIST, GRAPH_ANNOTATION_MIRIAM, GRAPH_ANNOTATION_PATTERN,
+    GRAPH_ANNOTATION_URL, GRAPH_METADATA, GRAPH_NAMESPACE_PATTERN, GRAPH_NAMESPACE_URL, GRAPH_PATH, GRAPH_PYBEL_VERSION,
+    HAS_PRODUCT, HAS_REACTANT, HAS_VARIANT, IDENTIFIER, INCREASES, IS_A, LOCATION, METADATA_AUTHORS, METADATA_CONTACT,
+    METADATA_COPYRIGHT, METADATA_DESCRIPTION, METADATA_DISCLAIMER, METADATA_LICENSES, METADATA_NAME, METADATA_VERSION,
+    MODIFIER, NAMESPACE, NEGATIVE_CORRELATION, NO_CORRELATION, ORTHOLOGOUS, PART_OF, POSITIVE_CORRELATION, REGULATES,
+    RELATION, SOURCE_MODIFIER, TARGET_MODIFIER, TO_LOC, TRANSCRIBED_TO, TRANSLATED_TO, TRANSLOCATION,
 )
 from ..dsl import (
     BaseAbundance, BaseConcept, BaseEntity, CentralDogma, ComplexAbundance, Gene, ListAbundance, MicroRna, Protein,
@@ -42,7 +42,7 @@ __all__ = [
 
 logger = logging.getLogger(__name__)
 
-AnnotationsDict = Mapping[str, Mapping[str, bool]]
+AnnotationsDict = Mapping[str, List[Entity]]
 AnnotationsHint = Union[Mapping[str, str], Mapping[str, Set[str]], AnnotationsDict]
 WarningTuple = Tuple[Optional[str], BELParserWarning, EdgeData]
 
@@ -78,15 +78,17 @@ class BELGraph(nx.MultiDiGraph):
 
         self._warnings = []
 
-        self.graph[GRAPH_PYBEL_VERSION] = get_version()
-        self.graph[GRAPH_METADATA] = {}
-
-        self.graph[GRAPH_NAMESPACE_URL] = {}
-        self.graph[GRAPH_NAMESPACE_PATTERN] = {}
-
-        self.graph[GRAPH_ANNOTATION_URL] = {}
-        self.graph[GRAPH_ANNOTATION_PATTERN] = {}
-        self.graph[GRAPH_ANNOTATION_LIST] = defaultdict(set)
+        self.graph.update({
+            GRAPH_PYBEL_VERSION: get_version(),
+            GRAPH_METADATA: {},
+            GRAPH_NAMESPACE_URL: {},
+            GRAPH_NAMESPACE_PATTERN: {},
+            GRAPH_ANNOTATION_URL: {},
+            GRAPH_ANNOTATION_PATTERN: {},
+            GRAPH_ANNOTATION_LIST: defaultdict(set),
+            GRAPH_ANNOTATION_CURIE: set(),
+            GRAPH_ANNOTATION_MIRIAM: set(),
+        })
 
         if name:
             self.name = name
@@ -115,7 +117,7 @@ class BELGraph(nx.MultiDiGraph):
         if path:
             self.path = path
 
-        #: A refernce to the parent graph
+        #: A reference to the parent graph
         self.parent = None
         self._count = CountDispatch(self)
         self._expand = ExpandDispatch(self)
@@ -123,6 +125,8 @@ class BELGraph(nx.MultiDiGraph):
         self._plot = PlotDispatch(self)
         self._summary = SummarizeDispatch(self)
 
+        self.raise_on_missing_annotations = True
+
     def child(self) -> 'BELGraph':
         """Create an empty graph with a "parent" reference back to this one."""
         rv = BELGraph()
@@ -316,6 +320,20 @@ class BELGraph(nx.MultiDiGraph):
         """
         return self.graph[GRAPH_ANNOTATION_URL]
 
+    @property
+    def annotation_miriam(self) -> Set[str]:  # noqa: D401
+        """The set of annotations defined by MIRIAM."""
+        if GRAPH_ANNOTATION_MIRIAM not in self.graph:
+            self.graph[GRAPH_ANNOTATION_MIRIAM] = set()
+        return self.graph[GRAPH_ANNOTATION_MIRIAM]
+
+    @property
+    def annotation_curie(self) -> Set[str]:  # noqa: D401
+        """The set of annotations defined by CURIE."""
+        if GRAPH_ANNOTATION_CURIE not in self.graph:
+            self.graph[GRAPH_ANNOTATION_CURIE] = set()
+        return self.graph[GRAPH_ANNOTATION_CURIE]
+
     @property
     def annotation_pattern(self) -> Dict[str, str]:  # noqa: D401
         """The mapping from the annotation keywords used to create this graph to their regex patterns as strings.
@@ -501,8 +519,8 @@ class BELGraph(nx.MultiDiGraph):
         )
         return self._help_add_edge(source=source, target=target, attr=attr)
 
-    @staticmethod
     def _build_attr(
+        self,
         relation: str,
         evidence: str,
         citation: Union[str, Tuple[str, str], CitationDict],
@@ -518,7 +536,7 @@ class BELGraph(nx.MultiDiGraph):
         })
 
         if annotations:  # clean up annotations
-            attr[ANNOTATIONS] = _clean_annotations(annotations)
+            attr[ANNOTATIONS] = self._clean_annotations(annotations)
 
         if source_modifier:
             attr[SOURCE_MODIFIER] = _handle_modifier(source_modifier)
@@ -868,17 +886,98 @@ class BELGraph(nx.MultiDiGraph):
         from ..grounding import ground
         return ground(self, **kwargs)
 
+    def _clean_annotations(self, annotations_dict: AnnotationsHint) -> AnnotationsDict:
+        """Fix the formatting of annotation dict.
 
-def _clean_annotations(annotations_dict: AnnotationsHint) -> AnnotationsDict:
-    """Fix the formatting of annotation dict."""
-    return {
-        key: (
-            values if isinstance(values, dict) else
-            {v: True for v in values} if isinstance(values, set) else
-            {values: True}
-        )
-        for key, values in annotations_dict.items()
-    }
+        .. seealso:: https://github.com/vtoure/bep/blob/master/docs/published/BEP-0013.md
+
+        Scenarios:
+
+        1. ``DEFINE ANNOTATION CellLine AS URL "..."``
+           ``SET CellLine = "NIH-3T3 cell"``
+           ``{'CellLine': dict(namespace='CellLine', identifier=None, name='NIH-3T3 cell')}``
+        2. ``DEFINE ANNOTATION CellLine AS CURIE``
+           ``SET CellLine = "bto:0000944 ! NIH-3T3 cell"``
+           ``{'CellLine': dict(namespace='bto', identifier='0000944', name='NIH-3T3 cell')}``
+        3. ``DEFINE ANNOTATION ECO AS MIRIAM``
+           ``SET ECO = "0007682 ! reporter gene assay evidence used in manual assertion"``
+           ``{'ECO': dict(namespace='ECO', identifier='0007682', name='reporter gene assay...')}``
+        """
+        return {
+            key: sorted(self._clean_value(key, values), key=lambda e: (e.namespace, e.identifier, e.name))
+            for key, values in annotations_dict.items()
+        }
+
+    def _clean_value(
+        self,
+        key,
+        values: Union[str, Entity, List[str], List[Mapping[str, str]], List[Entity]],
+    ) -> List[Entity]:
+        if key in self.annotation_miriam:  # this annotation was given by a lookup
+            return self._clean_value_helper(key=key, namespace=key, values=values)
+        if key in self.annotation_curie:
+            if isinstance(values, Entity):
+                return [values]
+            if all(isinstance(v, dict) for v in values):
+                return [Entity(**v) for v in values]
+            if not all(isinstance(v, Entity) for v in values):
+                raise ValueError('if annotation_curie, all must be given as Entity instances')
+            return values
+        if key in self.annotation_miriam:
+            raise NotImplementedError('parsing of annotation as MIRIAM not yet implemented')
+        if key in self.annotation_list:
+            if isinstance(values, str):
+                return [Entity(namespace=key, identifier=values)]
+            if all(isinstance(v, Entity) for v in values):
+                return values
+            if all(isinstance(v, dict) for v in values):
+                return [Entity(**v) for v in values]
+            if all(isinstance(v, str) for v in values):
+                return [Entity(namespace=key, identifier=v) for v in values]
+            raise TypeError(f'Mixed values: {values}')
+        if key in self.annotation_pattern:
+            # TODO pattern checking?
+            if isinstance(values, str):
+                return [Entity(namespace=key, identifier=values)]
+            if all(isinstance(v, Entity) for v in values):
+                return values
+            if all(isinstance(v, str) for v in values):
+                return [Entity(namespace=key, identifier=v) for v in values]
+            raise TypeError(f'Mixed values: {values}')
+        if key in self.annotation_url:  # this is a name given
+            return self._clean_value_helper(key=key, namespace=key, values=values)
+
+        if self.raise_on_missing_annotations:
+            raise NotImplementedError(f'where is key {key}?')
+
+        return self._clean_value_helper(key=key, namespace=key, values=values)
+
+    @staticmethod
+    def _clean_value_helper(key, namespace, values):
+        if isinstance(values, str):
+            return [
+                Entity(namespace=namespace, identifier=values),
+            ]
+        if isinstance(values, (list, set)):
+            if all(isinstance(v, str) for v in values):
+                return [
+                    Entity(namespace=namespace, identifier=identifier)
+                    for identifier in sorted(values)
+                ]
+            elif all(isinstance(v, Entity) for v in values):
+                return values
+            elif all(isinstance(v, dict) for v in values):
+                return [Entity(**v) for v in values]
+            else:
+                raise TypeError(f'list of wrong format for key {key}: {values}')
+        if isinstance(values, dict):
+            if all(isinstance(v, bool) for v in values.values()):
+                return [
+                    Entity(namespace=namespace, identifier=identifier)
+                    for identifier in sorted(values)
+                ]
+            raise TypeError(f'dictionary of wrong format for key {key}: {values}')
+        raise TypeError(f'values of wrong data type for key {key}: {values}')
 
 
 def _handle_modifier(side_data: Dict[str, Any]) -> Mapping[str, Any]:
diff --git a/src/pybel/struct/grouping/annotations.py b/src/pybel/struct/grouping/annotations.py
index 7bfbdf77..37c07259 100644
--- a/src/pybel/struct/grouping/annotations.py
+++ b/src/pybel/struct/grouping/annotations.py
@@ -8,6 +8,7 @@ from typing import Mapping, Optional
 
 from ..graph import BELGraph
 from ...constants import ANNOTATIONS
+from ...language import Entity
 
 __all__ = [
     'get_subgraphs_by_annotation',
@@ -16,7 +17,7 @@ __all__ = [
 logger = logging.getLogger(__name__)
 
 
-def _get_subgraphs_by_annotation_disregard_undefined(graph: BELGraph, annotation: str) -> Mapping[str, BELGraph]:
+def _get_subgraphs_by_annotation_disregard_undefined(graph: BELGraph, annotation: str) -> Mapping[Entity, BELGraph]:
     result = defaultdict(graph.child)
 
     for source, target, key, data in graph.edges(keys=True, data=True):
@@ -28,8 +29,8 @@ def _get_subgraphs_by_annotation_disregard_undefined(graph: BELGraph, annotation
         if annotation not in annotation_dict:
             continue
 
-        for value in annotation_dict[annotation]:
-            result[value].add_edge(source, target, key=key, **data)
+        for entity in annotation_dict[annotation]:
+            result[entity].add_edge(source, target, key=key, **data)
 
     return dict(result)
 
@@ -38,7 +39,7 @@ def _get_subgraphs_by_annotation_keep_undefined(
     graph: BELGraph,
     annotation: str,
     sentinel: Optional[str],
-) -> Mapping[str, BELGraph]:
+) -> Mapping[Entity, BELGraph]:
     result = defaultdict(graph.child)
 
     for source, target, key, data in graph.edges(keys=True, data=True):
@@ -47,8 +48,8 @@ def _get_subgraphs_by_annotation_keep_undefined(
         if annotation_dict is None or annotation not in annotation_dict:
             result[sentinel].add_edge(source, target, key=key, **data)
         else:
-            for value in annotation_dict[annotation]:
-                result[value].add_edge(source, target, key=key, **data)
+            for entity in annotation_dict[annotation]:
+                result[entity].add_edge(source, target, key=key, **data)
 
     return dict(result)
 
@@ -57,7 +58,7 @@ def get_subgraphs_by_annotation(
     graph: BELGraph,
     annotation: str,
     sentinel: Optional[str] = None,
-) -> Mapping[str, BELGraph]:
+) -> Mapping[Entity, BELGraph]:
     """Stratify the given graph into sub-graphs based on the values for edges' annotations.
 
     :param graph: A BEL graph
diff --git a/src/pybel/struct/mutation/induction/annotations.py b/src/pybel/struct/mutation/induction/annotations.py
index a53d28ce..8fda8f74 100644
--- a/src/pybel/struct/mutation/induction/annotations.py
+++ b/src/pybel/struct/mutation/induction/annotations.py
@@ -3,11 +3,11 @@
 """Functions for inducing graphs based on edge annotations."""
 
 import logging
-from typing import Iterable, Mapping, Optional, Union
+from typing import Iterable, Optional, Union
 
 from .utils import get_subgraph_by_edge_filter
 from ...filters.edge_predicate_builders import build_annotation_dict_all_filter, build_annotation_dict_any_filter
-from ...graph import BELGraph
+from ...graph import AnnotationsHint, BELGraph
 from ...pipeline import transformation
 
 __all__ = [
@@ -21,13 +21,13 @@ logger = logging.getLogger(__name__)
 @transformation
 def get_subgraph_by_annotations(
     graph: BELGraph,
-    annotations: Mapping[str, Iterable[str]],
+    annotations: AnnotationsHint,
     or_: Optional[bool] = None,
 ) -> BELGraph:
     """Induce a sub-graph given an annotations filter.
 
-    :param graph: pybel.BELGraph graph: A BEL graph
-    :param dict[str,iter[str]] annotations: Annotation filters (match all with :func:`pybel.utils.subdict_matches`)
+    :param graph: A BEL graph
+    :param annotations: Annotation filters (match all with :func:`pybel.utils.subdict_matches`)
     :param or_: if True any annotation should be present, if False all annotations should be present in the
      edge. Defaults to True.
     :return: A subgraph of the original BEL graph
@@ -38,6 +38,7 @@ def get_subgraph_by_annotations(
         build_annotation_dict_all_filter
     )
 
+    annotations = graph._clean_annotations(annotations)
     return get_subgraph_by_edge_filter(graph, edge_filter_builder(annotations))
 
 
diff --git a/src/pybel/struct/summary/edge_summary.py b/src/pybel/struct/summary/edge_summary.py
index 5a66f85c..1e6ac644 100644
--- a/src/pybel/struct/summary/edge_summary.py
+++ b/src/pybel/struct/summary/edge_summary.py
@@ -11,6 +11,7 @@ from ..graph import BELGraph
 from ...canonicalize import edge_to_bel
 from ...constants import ANNOTATIONS, RELATION
 from ...dsl import BaseEntity
+from ...language import Entity
 from ...utils import CanonicalEdge, canonicalize_edge
 
 __all__ = [
@@ -28,31 +29,30 @@ __all__ = [
 ]
 
 
-def iter_annotation_value_pairs(graph: BELGraph) -> Iterable[Tuple[str, str]]:
+def iter_annotation_value_pairs(graph: BELGraph) -> Iterable[Tuple[str, Entity]]:
     """Iterate over the key/value pairs, with duplicates, for each annotation used in a BEL graph.
 
     :param graph: A BEL graph
     """
     return (
-        (key, value)
+        (key, entity)
         for _, _, data in graph.edges(data=True)
-        if ANNOTATIONS in data
-        for key, values in data[ANNOTATIONS].items()
-        for value in values
+        for key, entities in data.get(ANNOTATIONS, {}).items()
+        for entity in entities
     )
 
 
-def iter_annotation_values(graph: BELGraph, annotation: str) -> Iterable[str]:
+def iter_annotation_values(graph: BELGraph, annotation: str) -> Iterable[Entity]:
     """Iterate over all of the values for an annotation used in the graph.
 
     :param graph: A BEL graph
     :param annotation: The annotation to grab
     """
     return (
-        value
+        entity
         for _, _, data in graph.edges(data=True)
         if edge_has_annotation(data, annotation)
-        for value in data[ANNOTATIONS][annotation]
+        for entity in data[ANNOTATIONS][annotation]
     )
 
 
@@ -68,7 +68,7 @@ def _group_dict_set(iterator):
     return dict(d)
 
 
-def get_annotation_values_by_annotation(graph: BELGraph) -> Mapping[str, Set[str]]:
+def get_annotation_values_by_annotation(graph: BELGraph) -> Mapping[str, Set[Entity]]:
     """Get the set of values for each annotation used in a BEL graph.
 
     :param graph: A BEL graph
@@ -77,7 +77,7 @@ def get_annotation_values_by_annotation(graph: BELGraph) -> Mapping[str, Set[str
     return _group_dict_set(iter_annotation_value_pairs(graph))
 
 
-def get_annotation_values(graph: BELGraph, annotation: str) -> Set[str]:
+def get_annotation_values(graph: BELGraph, annotation: str) -> Set[Entity]:
     """Get all values for the given annotation.
 
     :param graph: A BEL graph
@@ -147,10 +147,9 @@ def get_unused_list_annotation_values(graph: BELGraph) -> Mapping[str, Set[str]]
     """
     result = {}
     for annotation, values in graph.annotation_list.items():
-        used_values = get_annotation_values(graph, annotation)
-        if len(used_values) == len(values):  # all values have been used
-            continue
-        result[annotation] = set(values) - used_values
+        unused = values - {e.identifier for e in get_annotation_values(graph, annotation)}
+        if unused:
+            result[annotation] = unused
     return result
 
 
diff --git a/src/pybel/utils.py b/src/pybel/utils.py
index eeb72fe8..ba07f14d 100644
--- a/src/pybel/utils.py
+++ b/src/pybel/utils.py
@@ -5,6 +5,7 @@
 import hashlib
 import json
 import logging
+import re
 import typing
 from collections import defaultdict
 from collections.abc import Iterable, MutableMapping
@@ -96,9 +97,13 @@ def tokenize_version(version_string: str) -> Tuple[int, int, int]:
     return int(major), int(minor), int(patch)
 
 
+_re = re.compile(r'^[a-zA-Z0-9-\.]*$')
+
+
 def ensure_quotes(s: str) -> str:
     """Quote a string that isn't solely alphanumeric."""
-    return '"{}"'.format(s) if not s.isalnum() else s
+    s.isalnum()
+    return s if _re.match(s) else f'"{s}"'
 
 
 CREATION_DATE_FMT = '%Y-%m-%dT%H:%M:%S'


Test patch:
diff --git a/src/pybel/testing/utils.py b/src/pybel/testing/utils.py
index 511b726c..8076ec17 100644
--- a/src/pybel/testing/utils.py
+++ b/src/pybel/testing/utils.py
@@ -9,7 +9,7 @@ from requests.compat import urlparse
 from ..manager import Manager
 from ..manager.models import Namespace, NamespaceEntry
 from ..struct import BELGraph
-from ..struct.summary import get_annotation_values_by_annotation
+from ..struct.summary import get_annotation_values_by_annotation, iter_annotation_value_pairs
 from ..struct.summary.node_summary import get_names
 
 _FRAUNHOFER_RESOURCES = 'https://owncloud.scai.fraunhofer.de/index.php/s/JsfpQvkdx3Y5EMx/download?path='
@@ -47,16 +47,15 @@ def make_dummy_namespaces(manager: Manager, graph: BELGraph) -> None:
 
 def make_dummy_annotations(manager: Manager, graph: BELGraph):
     """Make dummy annotations for the test."""
-    annotation_names = get_annotation_values_by_annotation(graph)
-
-    for keyword, names in annotation_names.items():
-        graph.annotation_url[keyword] = url = n()
-
-        namespace = Namespace(keyword=keyword, url=url, is_annotation=True)
-        manager.session.add(namespace)
-
-        for name in names:
-            entry = NamespaceEntry(name=name, namespace=namespace)
-            manager.session.add(entry)
+    namespaces = {}
+    for keyword, entity in iter_annotation_value_pairs(graph):
+        namespace = namespaces.get(keyword)
+        if namespace is None:
+            graph.annotation_url[keyword] = url = n()
+            namespace = Namespace(keyword=keyword, url=url, is_annotation=True)
+            manager.session.add(namespace)
+
+        entry = NamespaceEntry(name=entity.name, identifier=entity.identifier, namespace=namespace)
+        manager.session.add(entry)
 
     manager.session.commit()
diff --git a/tests/constant_helper.py b/tests/constant_helper.py
index df04cec2..c97d0dc2 100644
--- a/tests/constant_helper.py
+++ b/tests/constant_helper.py
@@ -22,7 +22,7 @@ expected_test_simple_metadata = {
     METADATA_COPYRIGHT: "Copyright (c) Charles Tapley Hoyt. All Rights Reserved.",
     METADATA_AUTHORS: "Charles Tapley Hoyt",
     METADATA_LICENSES: "WTF License",
-    METADATA_CONTACT: "charles.hoyt@scai.fraunhofer.de",
+    METADATA_CONTACT: "cthoyt@gmail.com",
     METADATA_PROJECT: 'PyBEL Testing',
 }
 
@@ -33,7 +33,7 @@ expected_test_thorough_metadata = {
     METADATA_COPYRIGHT: "Copyright (c) Charles Tapley Hoyt. All Rights Reserved.",
     METADATA_AUTHORS: "Charles Tapley Hoyt",
     METADATA_LICENSES: "WTF License",
-    METADATA_CONTACT: "charles.hoyt@scai.fraunhofer.de",
+    METADATA_CONTACT: "cthoyt@gmail.com",
 }
 
 citation_1 = citation_dict(namespace=CITATION_TYPE_PUBMED, identifier='123455')
diff --git a/tests/constants.py b/tests/constants.py
index 71e9326c..d98d6759 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -11,8 +11,8 @@ from pybel import BELGraph
 from pybel.canonicalize import edge_to_bel
 from pybel.constants import (
     ANNOTATIONS, ASSOCIATION, CITATION, CITATION_TYPE_PUBMED, DECREASES, DIRECTLY_DECREASES, EVIDENCE, IDENTIFIER,
-    INCREASES, METADATA_AUTHORS, METADATA_DESCRIPTION, METADATA_LICENSES, METADATA_NAME, METADATA_VERSION, NAMESPACE,
-    PART_OF, RELATION,
+    INCREASES, LINE, METADATA_AUTHORS, METADATA_DESCRIPTION, METADATA_LICENSES, METADATA_NAME, METADATA_VERSION,
+    NAMESPACE, PART_OF, RELATION,
 )
 from pybel.dsl import BaseEntity, ComplexAbundance, Pathology, Protein
 from pybel.dsl.namespaces import hgnc
@@ -24,11 +24,11 @@ from pybel.exceptions import (
     NestedRelationWarning, PlaceholderAminoAcidWarning, UndefinedAnnotationWarning, UndefinedNamespaceWarning,
     VersionFormatWarning,
 )
+from pybel.language import Entity, citation_dict
 from pybel.parser.parse_bel import BELParser
 from pybel.parser.parse_control import ControlParser
 from pybel.testing.constants import test_bel_thorough
 from pybel.utils import subdict_matches
-from pybel.language import citation_dict
 from tests.constant_helper import (
     BEL_THOROUGH_EDGES, BEL_THOROUGH_NODES, citation_1, evidence_1, expected_test_simple_metadata,
     expected_test_thorough_metadata,
@@ -110,14 +110,20 @@ def any_subdict_matches(dict_of_dicts, query_dict) -> bool:
     )
 
 
+def _remove_line(d):
+    return {k: v for k, v in d.items() if k != LINE}
+
+
 def assert_has_edge(
     self: unittest.TestCase,
     u: BaseEntity,
     v: BaseEntity,
     graph: BELGraph,
+    *,
+    only: bool = False,
     permissive: bool = True,
     use_identifiers: bool = False,
-    **kwargs
+    **expected_edge_data
 ):
     """A helper function for checking if an edge with the given properties is contained within a graph."""
     self.assertIsInstance(u, BaseEntity)
@@ -131,21 +137,32 @@ def assert_has_edge(
         ))
     )
 
-    if not kwargs:
+    if not expected_edge_data:
         return
 
-    if permissive:
-        matches = any_subdict_matches(graph[u][v], kwargs)
-    else:
-        matches = any_dict_matches(graph[u][v], kwargs)
+    if ANNOTATIONS in expected_edge_data:
+        expected_edge_data[ANNOTATIONS] = graph._clean_annotations(expected_edge_data[ANNOTATIONS])
 
-    msg = 'No edge ({}, {}) with correct properties. expected:\n {}\nbut got:\n{}'.format(
-        u,
-        v,
-        dumps(kwargs, indent=2, sort_keys=True),
-        str(graph[u][v])
-    )
-    self.assertTrue(matches, msg=msg)
+    if only:
+        _key, actual_edge_data = list(graph[u][v].items())[0]
+        self.assertEqual(_remove_line(expected_edge_data), _remove_line(actual_edge_data), msg='Only entry not equal')
+    else:
+        actual_dicts = {
+            k: _remove_line(v)
+            for k, v in graph[u][v].items()
+        }
+        if permissive:
+            matches = any_subdict_matches(actual_dicts, _remove_line(expected_edge_data))
+        else:
+            matches = any_dict_matches(actual_dicts, _remove_line(expected_edge_data))
+
+        msg = 'No edge ({}, {}) with correct properties. expected:\n {}\nbut got:\n{}'.format(
+            u,
+            v,
+            dumps(expected_edge_data, indent=2, sort_keys=True),
+            dumps(actual_dicts, indent=2, sort_keys=True),
+        )
+        self.assertTrue(matches, msg=msg)
 
 
 class TestGraphMixin(unittest.TestCase):
@@ -155,9 +172,9 @@ class TestGraphMixin(unittest.TestCase):
         """Help assert node membership."""
         assert_has_node(self, n, g, **kwargs)
 
-    def assert_has_edge(self, g: BELGraph, u: BaseEntity, v: BaseEntity, **kwargs):
+    def assert_has_edge(self, g: BELGraph, u: BaseEntity, v: BaseEntity, only=False, permissive=True, **kwargs):
         """Help assert edge membership."""
-        assert_has_edge(self, u, v, g, **kwargs)
+        assert_has_edge(self, u, v, g, only=only, permissive=permissive, **kwargs)
 
 
 class TestTokenParserBase(unittest.TestCase):
@@ -166,9 +183,8 @@ class TestTokenParserBase(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
         """Build a BEL graph and BEL parser that persist through the class."""
-        cls.graph = BELGraph()
         cls.parser = BELParser(
-            cls.graph,
+            BELGraph(),  # gets overwritten in each test
             autostreamline=False,
             disallow_unqualified_translocations=True,
             namespace_to_pattern={
@@ -184,20 +200,25 @@ class TestTokenParserBase(unittest.TestCase):
                 'MESH': re.compile(r'.*'),
                 'MGI': re.compile(r'.*'),
                 'dbSNP': re.compile(r'.*'),
-            }
+            },
         )
 
+    @property
+    def graph(self):
+        return self.parser.graph
+
     def setUp(self):
         """Clear the parser at the beginning of each test."""
         self.parser.clear()
+        self.parser.graph = BELGraph()  # because all the good stuff in the graph is cleared
 
     def assert_has_node(self, member: BaseEntity, **kwargs):
         """Assert that this test case's graph has the given node."""
         assert_has_node(self, member, self.graph, **kwargs)
 
-    def assert_has_edge(self, u: BaseEntity, v: BaseEntity, **kwargs):
+    def assert_has_edge(self, u: BaseEntity, v: BaseEntity, only: bool = False, **kwargs):
         """Assert that this test case's graph has the given edge."""
-        assert_has_edge(self, u, v, self.graph, **kwargs)
+        assert_has_edge(self, u, v, self.graph, only=only, **kwargs)
 
     def add_default_provenance(self):
         """Add a default citation and evidence to the parser."""
@@ -248,66 +269,59 @@ class BelReconstitutionMixin(TestGraphMixin):
         self.assertIn(fadd, graph)
         self.assertIn(casp8, graph)
 
-        bel_simple_citation_1 = {
-            IDENTIFIER: "123455",
-            NAMESPACE: CITATION_TYPE_PUBMED,
-        }
-
-        bel_simple_citation_2 = {
-            IDENTIFIER: "123456",
-            NAMESPACE: CITATION_TYPE_PUBMED,
-        }
+        bel_simple_citation_1 = citation_dict(namespace=CITATION_TYPE_PUBMED, identifier="123455")
+        bel_simple_citation_2 = citation_dict(namespace=CITATION_TYPE_PUBMED, identifier="123456")
 
         evidence_1_extra = "Evidence 1 w extra notes"
         evidence_2 = 'Evidence 2'
         evidence_3 = 'Evidence 3'
 
-        assert_has_edge(self, akt1, egfr, graph, **{
+        assert_has_edge(self, akt1, egfr, graph, only=True, **{
             RELATION: INCREASES,
             CITATION: bel_simple_citation_1,
             EVIDENCE: evidence_1_extra,
             ANNOTATIONS: {
-                'Species': {'9606': True}
+                'Species': [Entity(namespace='Species', identifier='9606')],
             }
         })
-        assert_has_edge(self, egfr, fadd, graph, **{
+        assert_has_edge(self, egfr, fadd, graph, only=True, **{
             RELATION: DECREASES,
             ANNOTATIONS: {
-                'Species': {'9606': True},
-                'CellLine': {'10B9 cell': True}
+                'Species': [Entity(namespace='Species', identifier='9606')],
+                'CellLine': [Entity(namespace='CellLine', identifier='10B9 cell')],
             },
             CITATION: bel_simple_citation_1,
             EVIDENCE: evidence_2
         })
-        assert_has_edge(self, egfr, casp8, graph, **{
+        assert_has_edge(self, egfr, casp8, graph, only=True, **{
             RELATION: DIRECTLY_DECREASES,
             ANNOTATIONS: {
-                'Species': {'9606': True},
-                'CellLine': {'10B9 cell': True}
+                'Species': [Entity(namespace='Species', identifier='9606')],
+                'CellLine': [Entity(namespace='CellLine', identifier='10B9 cell')],
             },
             CITATION: bel_simple_citation_1,
             EVIDENCE: evidence_2,
         })
-        assert_has_edge(self, fadd, casp8, graph, **{
+        assert_has_edge(self, fadd, casp8, graph, only=True, **{
             RELATION: INCREASES,
             ANNOTATIONS: {
-                'Species': {'10116': True}
+                'Species': [Entity(namespace='Species', identifier='10116')],
             },
             CITATION: bel_simple_citation_2,
             EVIDENCE: evidence_3,
         })
-        assert_has_edge(self, akt1, casp8, graph, **{
+        assert_has_edge(self, akt1, casp8, graph, only=True, **{
             RELATION: ASSOCIATION,
             ANNOTATIONS: {
-                'Species': {'10116': True}
+                'Species': [Entity(namespace='Species', identifier='10116')],
             },
             CITATION: bel_simple_citation_2,
             EVIDENCE: evidence_3,
         })
-        assert_has_edge(self, casp8, akt1, graph, **{
+        assert_has_edge(self, casp8, akt1, graph, only=True, **{
             RELATION: ASSOCIATION,
             ANNOTATIONS: {
-                'Species': {'10116': True}
+                'Species': [Entity(namespace='Species', identifier='10116')],
             },
             CITATION: bel_simple_citation_2,
             EVIDENCE: evidence_3,
@@ -441,7 +455,7 @@ class BelReconstitutionMixin(TestGraphMixin):
         self.assertEqual(2, graph.number_of_nodes(), msg='Wrong number of nodes retrieved from slushy.bel')
         self.assertEqual(1, graph.number_of_edges(), msg='Wrong nunber of edges retrieved from slushy.bel')
 
-        assert_has_edge(self, akt1, egfr, graph, **{
+        assert_has_edge(self, akt1, egfr, graph, only=True, **{
             RELATION: INCREASES,
             CITATION: citation_1,
             EVIDENCE: evidence_1,
@@ -465,5 +479,5 @@ class BelReconstitutionMixin(TestGraphMixin):
         self.assertIn(adgrb_complex, graph)
         self.assertIn(achlorhydria, graph)
 
-        assert_has_edge(self, adgrb1, adgrb_complex, graph, relation=PART_OF)
-        assert_has_edge(self, adgrb2, adgrb_complex, graph, relation=PART_OF)
+        assert_has_edge(self, adgrb1, adgrb_complex, graph, only=True, relation=PART_OF)
+        assert_has_edge(self, adgrb2, adgrb_complex, graph, only=True, relation=PART_OF)
diff --git a/tests/test_canonicalization.py b/tests/test_canonicalization.py
index b5fba0a0..3980a8b9 100644
--- a/tests/test_canonicalization.py
+++ b/tests/test_canonicalization.py
@@ -142,6 +142,7 @@ class TestCanonicalizeEdge(unittest.TestCase):
 
     def setUp(self):
         self.g = BELGraph()
+        self.g.annotation_pattern['Species'] = r'\d+'
         self.u = Protein(name='u', namespace='TEST')
         self.v = Protein(name='v', namespace='TEST')
         self.g.add_node_from_data(self.u)
@@ -245,7 +246,7 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertEqual(lines, list(_to_bel_lines_body(self.graph)))
 
     def test_simple(self):
-        """Tests a scenario with a qualified edge, but no annotaitons"""
+        """Test a scenario with a qualified edge, but no annotations."""
         self.graph.add_increases(
             Protein(namespace='HGNC', name='YFG1'),
             Protein(namespace='HGNC', name='YFG'),
@@ -267,29 +268,62 @@ class TestSerializeBEL(unittest.TestCase):
 
         self._help_check_lines(expected_lines)
 
+    def test_different_key_and_namespace(self):
+        key, namespace, value = map(lambda _: n(), range(3))
+
+        self.graph.annotation_curie.add(key)
+        self.graph.add_increases(
+            Protein(namespace='HGNC', name='YFG1'),
+            Protein(namespace='HGNC', name='YFG'),
+            citation=self.citation,
+            evidence=self.evidence,
+            annotations={
+                key: Entity(namespace=namespace, identifier=value),
+            }
+        )
+
+        self.assertEqual(2, self.graph.number_of_nodes())
+        self.assertEqual(1, self.graph.number_of_edges())
+
+        expected_lines = [
+            f'SET Citation = {{"PubMed", "{self.citation}"}}\n',
+            f'SET SupportingText = "{self.evidence}"',
+            f'SET {key} = "{namespace}:{value}"',
+            'p(HGNC:YFG1) increases p(HGNC:YFG)',
+            f'UNSET {key}',
+            'UNSET SupportingText',
+            'UNSET Citation\n',
+            ('#' * 80),
+        ]
+
+        self._help_check_lines(expected_lines)
+
     def test_single_annotation(self):
-        """Tests a scenario with a qualified edge, but no annotaitons"""
+        """Test a scenario with a qualified edge, but no annotations."""
         a1, v1 = map(lambda _: n(), range(2))
-
+        self.graph.annotation_list[a1] = {v1}
         self.graph.add_increases(
             Protein(namespace='HGNC', name='YFG1'),
             Protein(namespace='HGNC', name='YFG'),
             citation=self.citation,
             evidence=self.evidence,
             annotations={
-                a1: {v1}
+                a1: {v1},
             }
         )
 
         self.assertEqual(2, self.graph.number_of_nodes())
         self.assertEqual(1, self.graph.number_of_edges())
 
+        # Means that only the identifier needs to be written out
+        self.assertNotIn(a1, self.graph.annotation_curie)
+
         expected_lines = [
             'SET Citation = {{"PubMed", "{}"}}\n'.format(self.citation),
-            'SET SupportingText = "{}"'.format(self.evidence),
-            'SET {} = "{}"'.format(a1, v1),
+            f'SET SupportingText = "{self.evidence}"',
+            f'SET {a1} = "{v1}"',
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
-            'UNSET {}'.format(a1),
+            f'UNSET {a1}',
             'UNSET SupportingText',
             'UNSET Citation\n',
             '#' * 80,
@@ -301,13 +335,14 @@ class TestSerializeBEL(unittest.TestCase):
         a1, v1, v2 = map(lambda _: n(), range(3))
         v1, v2 = sorted([v1, v2])
 
+        self.graph.annotation_list[a1] = {v1, v2}
         self.graph.add_increases(
             Protein(namespace='HGNC', name='YFG1'),
             Protein(namespace='HGNC', name='YFG'),
             citation=self.citation,
             evidence=self.evidence,
             annotations={
-                a1: {v1, v2}
+                a1: {v1, v2},
             }
         )
 
@@ -315,11 +350,11 @@ class TestSerializeBEL(unittest.TestCase):
         self.assertEqual(1, self.graph.number_of_edges())
 
         expected_lines = [
-            'SET Citation = {{"PubMed", "{}"}}\n'.format(self.citation),
-            'SET SupportingText = "{}"'.format(self.evidence),
-            'SET {} = {{"{}", "{}"}}'.format(a1, v1, v2),
+            f'SET Citation = {{"PubMed", "{self.citation}"}}\n',
+            f'SET SupportingText = "{self.evidence}"',
+            f'SET {a1} = {{"{v1}", "{v2}"}}',
             'p(HGNC:YFG1) increases p(HGNC:YFG)',
-            'UNSET {}'.format(a1),
+            f'UNSET {a1}',
             'UNSET SupportingText',
             'UNSET Citation\n',
             ('#' * 80),
diff --git a/tests/test_dsl.py b/tests/test_dsl.py
index bb177dd0..a03ee89a 100644
--- a/tests/test_dsl.py
+++ b/tests/test_dsl.py
@@ -77,11 +77,23 @@ class TestDSL(unittest.TestCase):
         with self.assertRaises(ValueError):
             Protein(namespace='uniprot', identifier='', name='123')
 
-    def test_abundance_as_bel_quoted(self):
+    def test_abundance_as_bel_dash_unquoted(self):
         """Test converting an abundance to BEL with a name that needs quotation."""
         namespace, name = 'HGNC', 'YFG-1'
         node = Abundance(namespace=namespace, name=name)
-        self.assertEqual('a(HGNC:"YFG-1")', node.as_bel())
+        self.assertEqual('a(HGNC:YFG-1)', node.as_bel())
+
+    def test_abundance_as_no_quotes(self):
+        """Test converting an abundance that doesn't need quotes, but looks crazy."""
+        namespace, name = 'a-c', 'd.e.f'
+        node = Abundance(namespace=namespace, name=name)
+        self.assertEqual('a(a-c:d.e.f)', node.as_bel())
+
+    def test_abundance_as_bel_quoted(self):
+        """Test converting an abundance to BEL with a name that needs quotation."""
+        namespace, name = 'HGNC', 'YFG~1'
+        node = Abundance(namespace=namespace, name=name)
+        self.assertEqual('a(HGNC:"YFG~1")', node.as_bel())
 
     def test_abundance_as_bel(self):
         """Test converting an abundance to BEL with a name that does not need quotation."""
diff --git a/tests/test_io/test_cx/examples.py b/tests/test_io/test_cx/examples.py
index 2b1d1e27..e38c89af 100644
--- a/tests/test_io/test_cx/examples.py
+++ b/tests/test_io/test_cx/examples.py
@@ -11,6 +11,9 @@ from pybel.dsl import (
 from pybel.language import citation_dict
 
 example_graph = BELGraph()
+example_graph.annotation_list['Confidence'] = {'Low', 'Medium', 'High', 'Very High'}
+example_graph.annotation_pattern['Number'] = r'\d+'
+example_graph.annotation_pattern['Species'] = r'\d+'
 
 ptk2 = Protein(namespace='hgnc', name='PTK2', variants=pmod('Ph', 'Tyr', 925))
 mapk1 = Protein(namespace='hgnc', name='MAPK1')
diff --git a/tests/test_io/test_import.py b/tests/test_io/test_import.py
index 57e9e462..cd16aa43 100644
--- a/tests/test_io/test_import.py
+++ b/tests/test_io/test_import.py
@@ -17,7 +17,8 @@ from pybel import (
 )
 from pybel.config import PYBEL_MINIMUM_IMPORT_VERSION
 from pybel.constants import (
-    ANNOTATIONS, CITATION, DECREASES, DIRECTLY_DECREASES, EVIDENCE, GRAPH_PYBEL_VERSION, INCREASES, RELATION,
+    ANNOTATIONS, CITATION, DECREASES, DIRECTLY_DECREASES, EVIDENCE, GRAPH_ANNOTATION_MIRIAM, GRAPH_PYBEL_VERSION,
+    INCREASES, RELATION,
 )
 from pybel.dsl import BaseEntity, Gene, Protein
 from pybel.examples.sialic_acid_example import sialic_acid_graph
@@ -27,6 +28,7 @@ from pybel.exceptions import (
 from pybel.io.exc import ImportVersionWarning, import_version_message_fmt
 from pybel.io.line_utils import parse_lines
 from pybel.io.nodelink import from_nodelink_jsons, to_nodelink_jsons
+from pybel.language import Entity
 from pybel.parser import BELParser
 from pybel.struct.summary import get_syntax_errors
 from pybel.testing.cases import TemporaryCacheClsMixin, TemporaryCacheMixin
@@ -281,7 +283,7 @@ class TestInterchange(TemporaryCacheClsMixin, BelReconstitutionMixin):
                 'TESTAN1': {testan1: True}
             }
         }
-        self.assert_has_edge(self.misordered_graph, akt1, egfr, **e1)
+        self.assert_has_edge(self.misordered_graph, akt1, egfr, only=True, **e1)
 
         e2 = {
             RELATION: DECREASES,
@@ -291,7 +293,7 @@ class TestInterchange(TemporaryCacheClsMixin, BelReconstitutionMixin):
                 'TESTAN1': {testan1: True}
             }
         }
-        self.assert_has_edge(self.misordered_graph, egfr, fadd, **e2)
+        self.assert_has_edge(self.misordered_graph, egfr, fadd, only=True, **e2)
 
         e3 = {
             RELATION: DIRECTLY_DECREASES,
@@ -301,7 +303,7 @@ class TestInterchange(TemporaryCacheClsMixin, BelReconstitutionMixin):
                 'TESTAN1': {testan1: True},
             },
         }
-        self.assert_has_edge(self.misordered_graph, egfr, casp8, **e3)
+        self.assert_has_edge(self.misordered_graph, egfr, casp8, only=True, **e3)
 
 
 namespace_to_term = {
@@ -321,14 +323,24 @@ annotation_to_term = {
 class TestFull(TestTokenParserBase):
     @classmethod
     def setUpClass(cls):
-        cls.graph = BELGraph()
         cls.parser = BELParser(
-            cls.graph,
+            graph=BELGraph(),  # gets overwritten in each test
             namespace_to_term_to_encoding=namespace_to_term,
             annotation_to_term=annotation_to_term,
             namespace_to_pattern={'dbSNP': re.compile('rs[0-9]*')},
         )
 
+    def setUp(self):
+        self.parser.clear()
+        self.parser.graph = BELGraph()
+        self.graph.annotation_list.update({
+            'TestAnnotation1': {'A', 'B', 'C'},
+            'TestAnnotation2': {'X', 'Y', 'Z'},
+            'TestAnnotation3': {'D', 'E', 'F'},
+        })
+        self.parser.graph = self.graph
+        self.assertIn(GRAPH_ANNOTATION_MIRIAM, self.graph.graph, msg=f'Graph metadata: {self.graph.graph}')
+
     def test_regex_match(self):
         line = 'g(dbSNP:rs10234) -- g(dbSNP:rs10235)'
         self.add_default_provenance()
@@ -367,11 +379,15 @@ class TestFull(TestTokenParserBase):
         statements = [
             'SET TestAnnotation1 = "A"',
             'SET TestAnnotation2 = "X"',
-            'g(TESTNS:1) -> g(TESTNS:2)'
+            'g(TESTNS:1) -> g(TESTNS:2)',
         ]
 
         self.parser.parse_lines(statements)
 
+        self.assertEqual(2, len(self.parser.control_parser.annotations))
+        self.assertIn('TestAnnotation1', self.parser.control_parser.annotations)
+        self.assertIn('TestAnnotation2', self.parser.control_parser.annotations)
+
         test_node_1 = Gene(namespace='TESTNS', name='1')
         test_node_2 = Gene(namespace='TESTNS', name='2')
 
@@ -382,16 +398,19 @@ class TestFull(TestTokenParserBase):
         self.assertEqual(1, self.parser.graph.number_of_edges())
 
         kwargs = {
+            RELATION: INCREASES,
             ANNOTATIONS: {
                 'TestAnnotation1': {'A': True},
                 'TestAnnotation2': {'X': True},
             },
             EVIDENCE: test_evidence_text,
-            CITATION: test_citation_dict
+            CITATION: test_citation_dict,
         }
-        self.assert_has_edge(test_node_1, test_node_2, **kwargs)
+
+        self.assert_has_edge(test_node_1, test_node_2, only=True, **kwargs)
 
     def test_annotations_with_list(self):
+        self.assertIsNotNone(self.parser.graph)
         self.add_default_provenance()
 
         statements = [
@@ -399,8 +418,28 @@ class TestFull(TestTokenParserBase):
             'SET TestAnnotation2 = "X"',
             'g(TESTNS:1) -> g(TESTNS:2)'
         ]
+
         self.parser.parse_lines(statements)
 
+        self.assertEqual(2, len(self.parser.control_parser.annotations))
+        self.assertIn('TestAnnotation1', self.parser.control_parser.annotations)
+        self.assertIn('TestAnnotation2', self.parser.control_parser.annotations)
+        self.assertEqual(2, len(self.parser.control_parser.annotations['TestAnnotation1']))
+        self.assertEqual(
+            [
+                Entity(namespace='TestAnnotation1', identifier='A'),
+                Entity(namespace='TestAnnotation1', identifier='B'),
+            ],
+            self.parser.control_parser.annotations['TestAnnotation1'],
+        )
+        self.assertEqual(1, len(self.parser.control_parser.annotations['TestAnnotation2']))
+        self.assertEqual(
+            [
+                Entity(namespace='TestAnnotation2', identifier='X'),
+            ],
+            self.parser.control_parser.annotations['TestAnnotation2'],
+        )
+
         test_node_1_dict = Gene(namespace='TESTNS', name='1')
         test_node_2_dict = Gene(namespace='TESTNS', name='2')
 
@@ -411,13 +450,15 @@ class TestFull(TestTokenParserBase):
         self.assertEqual(1, self.parser.graph.number_of_edges())
 
         kwargs = {
+            RELATION: INCREASES,
+            EVIDENCE: test_evidence_text,
             ANNOTATIONS: {
                 'TestAnnotation1': {'A': True, 'B': True},
-                'TestAnnotation2': {'X': True}
+                'TestAnnotation2': {'X': True},
             },
-            CITATION: test_citation_dict
+            CITATION: test_citation_dict,
         }
-        self.assert_has_edge(test_node_1_dict, test_node_2_dict, **kwargs)
+        self.assert_has_edge(test_node_1_dict, test_node_2_dict, only=True, **kwargs)
 
     def test_annotations_with_multilist(self):
         self.add_default_provenance()
@@ -426,28 +467,35 @@ class TestFull(TestTokenParserBase):
             'SET TestAnnotation1 = {"A","B"}',
             'SET TestAnnotation2 = "X"',
             'SET TestAnnotation3 = {"D","E"}',
-            'g(TESTNS:1) -> g(TESTNS:2)'
+            'g(TESTNS:1) -> g(TESTNS:2)',
         ]
         self.parser.parse_lines(statements)
 
-        test_node_1_dict = Gene(namespace='TESTNS', name='1')
-        test_node_2_dict = Gene(namespace='TESTNS', name='2')
+        self.assertEqual(3, len(self.parser.control_parser.annotations))
+        self.assertIn('TestAnnotation1', self.parser.control_parser.annotations)
+        self.assertIn('TestAnnotation2', self.parser.control_parser.annotations)
+        self.assertIn('TestAnnotation3', self.parser.control_parser.annotations)
+
+        test_node_1 = Gene(namespace='TESTNS', name='1')
+        test_node_2 = Gene(namespace='TESTNS', name='2')
 
         self.assertEqual(2, self.parser.graph.number_of_nodes())
-        self.assertIn(test_node_1_dict, self.graph)
-        self.assertIn(test_node_2_dict, self.graph)
+        self.assertIn(test_node_1, self.graph)
+        self.assertIn(test_node_2, self.graph)
 
         self.assertEqual(1, self.parser.graph.number_of_edges())
 
         kwargs = {
+            RELATION: INCREASES,
+            EVIDENCE: test_evidence_text,
             ANNOTATIONS: {
                 'TestAnnotation1': {'A': True, 'B': True},
                 'TestAnnotation2': {'X': True},
-                'TestAnnotation3': {'D': True, 'E': True}
+                'TestAnnotation3': {'D': True, 'E': True},
             },
-            CITATION: test_citation_dict
+            CITATION: test_citation_dict,
         }
-        self.assert_has_edge(test_node_1_dict, test_node_2_dict, **kwargs)
+        self.assert_has_edge(test_node_1, test_node_2, only=True, **kwargs)
 
 
 class TestRandom(unittest.TestCase):
diff --git a/tests/test_io/test_jgif.py b/tests/test_io/test_jgif.py
index 0c81e848..827e30ed 100644
--- a/tests/test_io/test_jgif.py
+++ b/tests/test_io/test_jgif.py
@@ -114,7 +114,8 @@ jgif_expected_edges = [
         },
         TARGET_MODIFIER: {MODIFIER: ACTIVITY, EFFECT: activity_mapping['cat']},
         ANNOTATIONS: {
-            'Tissue': {'lung': True}
+            'Tissue': {'lung': True},
+            'Species': {'9606': True},
         }
     })
 ]
@@ -134,7 +135,7 @@ class TestJgif(TestGraphMixin):
         self.assertEqual(jgif_expected_nodes, set(graph))
 
         for u, v, d in jgif_expected_edges:
-            self.assert_has_edge(graph, u, v, **d)
+            self.assert_has_edge(graph, u, v, permissive=False, **d)
 
         # TODO test more thoroughly?
         export_jgif = to_jgif(graph)
diff --git a/tests/test_manager/test_manager_drop.py b/tests/test_manager/test_manager_drop.py
index 0186c22e..62d658c0 100644
--- a/tests/test_manager/test_manager_drop.py
+++ b/tests/test_manager/test_manager_drop.py
@@ -30,6 +30,8 @@ class TestReconstituteNodeTuples(TemporaryCacheMixin):
     def test_simple(self, mock):
         """This test checks that the network can be added and dropped"""
         graph = BELGraph(name='test', version='0.0.0')
+        graph.annotation_pattern['Disease'] = '.*'
+        graph.annotation_pattern['Cell'] = '.*'
         graph.add_increases(
             yfg1,
             yfg2,
@@ -37,8 +39,8 @@ class TestReconstituteNodeTuples(TemporaryCacheMixin):
             citation=test_citation_dict,
             annotations={
                 'Disease': {'Disease1': True},
-                'Cell': {'Cell1': True}
-            }
+                'Cell': {'Cell1': True},
+            },
         )
 
         make_dummy_namespaces(self.manager, graph)
diff --git a/tests/test_manager/test_manager_graph.py b/tests/test_manager/test_manager_graph.py
index 716597f1..8b592410 100644
--- a/tests/test_manager/test_manager_graph.py
+++ b/tests/test_manager/test_manager_graph.py
@@ -785,6 +785,7 @@ class TestReconstituteEdges(TemporaryCacheMixin):
         """Creates a unit test with a manager and graph"""
         super().setUp()
         self.graph = BELGraph(name=n(), version=n())
+        self.graph.annotation_pattern['Species'] = r'\d+'
 
     @mock_bel_resources
     def test_translocation_default(self, mock):
diff --git a/tests/test_manager/test_manager_model.py b/tests/test_manager/test_manager_model.py
index a0696bc7..f2f8d50d 100644
--- a/tests/test_manager/test_manager_model.py
+++ b/tests/test_manager/test_manager_model.py
@@ -84,7 +84,7 @@ class TestModels(unittest.TestCase):
             species='9606',
             version='1.0.0',
             author='Charles Tapley Hoyt',
-            contact='charles.hoyt@scai.fraunhofer.de',
+            contact='cthoyt@gmail.com',
             uploaded=uploaded,
         )
 
@@ -112,7 +112,7 @@ class TestModels(unittest.TestCase):
             species='9606',
             version='1.0.0',
             author='Charles Tapley Hoyt',
-            contact='charles.hoyt@scai.fraunhofer.de',
+            contact='cthoyt@gmail.com',
             uploaded=uploaded,
         )
 
diff --git a/tests/test_parse/test_parse_bel_relations.py b/tests/test_parse/test_parse_bel_relations.py
index 92792ed5..ca45f56e 100644
--- a/tests/test_parse/test_parse_bel_relations.py
+++ b/tests/test_parse/test_parse_bel_relations.py
@@ -12,7 +12,8 @@ from pybel.canonicalize import edge_to_bel
 from pybel.constants import (
     ABUNDANCE, ACTIVITY, ANNOTATIONS, BIOPROCESS, CAUSES_NO_CHANGE, CITATION, COMPLEX, COMPOSITE, CONCEPT, CORRELATION,
     DECREASES, DIRECTLY_DECREASES, DIRECTLY_INCREASES, EFFECT, EQUIVALENT_TO, EVIDENCE, FROM_LOC, FUNCTION, GENE, GMOD,
-    HAS_PRODUCT, HAS_REACTANT, HAS_VARIANT, HGVS, IDENTIFIER, INCREASES, IS_A, KIND, LOCATION, MEMBERS, MODIFIER, NAME,
+    GRAPH_ANNOTATION_LIST, HAS_PRODUCT, HAS_REACTANT, HAS_VARIANT, HGVS, IDENTIFIER, INCREASES, IS_A, KIND, LOCATION,
+    MEMBERS, MODIFIER, NAME,
     NAMESPACE, NEGATIVE_CORRELATION, NO_CORRELATION, ORTHOLOGOUS, PART_OF, PATHOLOGY, POSITIVE_CORRELATION, PRODUCTS,
     PROTEIN, RATE_LIMITING_STEP_OF, REACTANTS, REACTION, REGULATES, RELATION, RNA, SOURCE, SOURCE_MODIFIER,
     SUBPROCESS_OF, TARGET, TARGET_MODIFIER, TO_LOC, TRANSCRIBED_TO, TRANSLATED_TO, TRANSLOCATION, VARIANTS,
@@ -353,10 +354,14 @@ class TestRelations(TestTokenParserBase):
         Tests simple triple"""
         statement = 'g(HGNC:CAT, location(GO:intracellular)) directlyDecreases abundance(CHEBI:"hydrogen peroxide")'
 
-        annotations = {
+        self.graph.annotation_list.update({
+            'ListAnnotation': set('abef'),
+            'ScalarAnnotation': set('cghi'),
+        })
+        annotations = self.parser.graph._clean_annotations({
             'ListAnnotation': {'a', 'b'},
-            'ScalarAnnotation': {'c'}
-        }
+            'ScalarAnnotation': {'c'},
+        })
 
         self.parser.control_parser.annotations.update(annotations)
 
@@ -402,11 +407,16 @@ class TestRelations(TestTokenParserBase):
             CITATION: test_citation_dict,
             EVIDENCE: test_evidence_text,
             ANNOTATIONS: {
-                'ListAnnotation': {'a': True, 'b': True},
-                'ScalarAnnotation': {'c': True},
-            }
+                'ListAnnotation': [
+                    Entity(namespace='ListAnnotation', identifier='a'),
+                    Entity(namespace='ListAnnotation', identifier='b'),
+                ],
+                'ScalarAnnotation': [
+                    Entity(namespace='ScalarAnnotation', identifier='c'),
+                ],
+            },
         }
-        self.assert_has_edge(sub, obj, **expected_attrs)
+        self.assert_has_edge(sub, obj, only=True, **expected_attrs)
 
     def test_rateLimitingStepOf_subjectActivity(self):
         """3.1.5 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#_ratelimitingstepof"""
diff --git a/tests/test_parse/test_parse_control.py b/tests/test_parse/test_parse_control.py
index 3dd7398b..aac83736 100644
--- a/tests/test_parse/test_parse_control.py
+++ b/tests/test_parse/test_parse_control.py
@@ -11,6 +11,7 @@ from pybel.exceptions import (
     InvalidPubMedIdentifierWarning, MissingAnnotationKeyWarning, MissingAnnotationRegexWarning,
     UndefinedAnnotationWarning,
 )
+from pybel.language import Entity
 from pybel.parser import ControlParser
 from pybel.parser.parse_control import set_citation_stub
 from pybel.testing.utils import n
@@ -271,7 +272,7 @@ class TestParseControl2(TestParseControl):
         self.parser.parse_lines(s)
 
         expected_annotation = {
-            'Custom1': 'Custom1_A'
+            'Custom1': [Entity(namespace='Custom1', identifier='Custom1_A')],
         }
 
         self.assertEqual(expected_annotation, self.parser.annotations)
@@ -284,7 +285,10 @@ class TestParseControl2(TestParseControl):
         self.parser.parse_lines(s)
 
         expected_annotation = {
-            'Custom1': {'Custom1_A', 'Custom1_B'}
+            'Custom1': [
+                Entity(namespace='Custom1', identifier='Custom1_A'),
+                Entity(namespace='Custom1', identifier='Custom1_B'),
+            ],
         }
 
         self.assertEqual(expected_annotation, self.parser.annotations)
@@ -292,7 +296,7 @@ class TestParseControl2(TestParseControl):
         expected_dict = {
             ANNOTATIONS: expected_annotation,
             CITATION: test_citation_dict,
-            EVIDENCE: None
+            EVIDENCE: None,
         }
 
         self.assertEqual(expected_dict, self.parser.get_annotations())
@@ -360,8 +364,13 @@ class TestParseControl2(TestParseControl):
         v = str(randint(0, 1e5))
         s = [
             SET_CITATION_TEST,
-            'SET CustomRegex = "{}"'.format(v)
+            f'SET CustomRegex = "{v}"'
         ]
         self.parser.parse_lines(s)
 
-        self.assertEqual(v, self.parser.annotations['CustomRegex'])
+        self.assertEqual(
+            [
+                Entity(namespace='CustomRegex', identifier=v),
+            ],
+            self.parser.annotations['CustomRegex'],
+        )
diff --git a/tests/test_struct/test_filters/test_node_predicates.py b/tests/test_struct/test_filters/test_node_predicates.py
index 94e478f6..dfadb4e0 100644
--- a/tests/test_struct/test_filters/test_node_predicates.py
+++ b/tests/test_struct/test_filters/test_node_predicates.py
@@ -141,6 +141,7 @@ class TestNodePredicates(unittest.TestCase):
     def test_p1_active(self):
         """cat(p(HGNC:HSD11B1)) increases deg(a(CHEBI:cortisol))"""
         g = BELGraph()
+        g.annotation_pattern['Species'] = r'\d+'
 
         u = protein(name='HSD11B1', namespace='HGNC')
         v = abundance(name='cortisol', namespace='CHEBI', identifier='17650')
@@ -173,6 +174,7 @@ class TestNodePredicates(unittest.TestCase):
     def test_object_has_translocation(self):
         """p(HGNC: EGF) increases tloc(p(HGNC: VCP), GO:0005634, GO:0005737)"""
         g = BELGraph()
+        g.annotation_pattern['Species'] = r'\d+'
         u = protein(name='EFG', namespace='HGNC')
         v = protein(name='VCP', namespace='HGNC')
 
@@ -204,6 +206,8 @@ class TestNodePredicates(unittest.TestCase):
     def test_object_has_secretion(self):
         """p(MGI:Il4) increases sec(p(MGI:Cxcl1))"""
         g = BELGraph()
+        g.annotation_pattern['Species'] = r'\d+'
+        g.annotation_pattern['MeSH'] = '.*'
         u = protein(name='Il4', namespace='MGI')
         v = protein(name='Cxcl1', namespace='MGI')
 
@@ -235,6 +239,8 @@ class TestNodePredicates(unittest.TestCase):
     def test_subject_has_secretion(self):
         """sec(p(MGI:S100b)) increases a(CHEBI:"nitric oxide")"""
         g = BELGraph()
+        g.annotation_pattern['Species'] = r'\d+'
+        g.annotation_pattern['Cell'] = r'.*'
         u = protein(name='S100b', namespace='MGI')
         v = abundance(name='nitric oxide', namespace='CHEBI')
 
diff --git a/tests/test_struct/test_filters/test_struct_filters.py b/tests/test_struct/test_filters/test_struct_filters.py
index 8132bb6a..032d9e7c 100644
--- a/tests/test_struct/test_filters/test_struct_filters.py
+++ b/tests/test_struct/test_filters/test_struct_filters.py
@@ -158,13 +158,14 @@ class TestEdgeFilters(unittest.TestCase):
 
     def test_any_filter(self):
         graph = BELGraph()
+        graph.annotation_list['A'] = set('12345')
         graph.add_increases(Protein(n(), n()), Protein(n(), n()), citation=n(), evidence=n(), annotations={
             'A': {'1', '2', '3'}
         })
 
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter({'A': {'1'}})))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter({'A': {'1', '2'}})))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter({'A': {'1', '2', '3'}})))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter(graph._clean_annotations({'A': {'1'}}))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter(graph._clean_annotations({'A': {'1', '2'}}))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_any_filter(graph._clean_annotations({'A': {'1', '2', '3'}}))))
 
     def test_b(self):
         self.assertTrue(_annotation_dict_all_filter(
@@ -225,37 +226,36 @@ class TestEdgeFilters(unittest.TestCase):
 
     def test_all_filter(self):
         graph = BELGraph()
+        graph.annotation_list['A'] = set('12345')
         graph.add_increases(Protein(n(), n()), Protein(n(), n()), citation=n(), evidence=n(), annotations={
-            'A': {'1', '2', '3'}
+            'A': {'1', '2', '3'},
         })
 
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'1'}})))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'1', '2'}})))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'1', '2', '3'}})))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'1'}}))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'1', '2'}}))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'1', '2', '3'}}))))
         self.assertEqual(0,
-                         count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'1', '2', '3', '4'}})))
-        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'4'}})))
+                         count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'1', '2', '3', '4'}}))))
+        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'4'}}))))
 
     def test_all_filter_dict(self):
         graph = BELGraph()
-        graph.add_edge(1, 2, annotations={
-            'A': {'1', '2', '3'}
+        graph.annotation_list['A'] = set('12345')
+        a, b = Protein(namespace='hgnc', identifier='1', name='A'), Protein(namespace='hgnc', identifier='2', name='B')
+        graph.add_increases(a, b, citation=n(), evidence=n(), annotations={
+            'A': {'1', '2', '3'},
         })
 
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({'A': {'1': True}})))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({'A': {'1': True}}))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({
             'A': {'1': True, '2': True}
-        })))
-        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter({
+        }))))
+        self.assertEqual(1, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({
             'A': {'1': True, '2': True, '3': True}
-        })))
-        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter({
+        }))))
+        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({
             'A': {'1': True, '2': True, '3': True, '4': True}
-        })))
-        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter({
+        }))))
+        self.assertEqual(0, count_passed_edge_filter(graph, build_annotation_dict_all_filter(graph._clean_annotations({
             'A': {'4': True}
-        })))
-
-
-if __name__ == '__main__':
-    unittest.main()
+        }))))
diff --git a/tests/test_struct/test_grouping.py b/tests/test_struct/test_grouping.py
index 6e78be11..600e3f57 100644
--- a/tests/test_struct/test_grouping.py
+++ b/tests/test_struct/test_grouping.py
@@ -7,6 +7,7 @@ import unittest
 from pybel import BELGraph
 from pybel.constants import CITATION_TYPE_PUBMED
 from pybel.dsl import protein
+from pybel.language import Entity
 from pybel.struct.grouping import get_subgraphs_by_annotation, get_subgraphs_by_citation
 from pybel.testing.utils import n
 
@@ -36,10 +37,10 @@ class TestAnnotation(unittest.TestCase):
         subgraphs = get_subgraphs_by_annotation(self.graph, annotation='subgraph')
 
         self.assertEqual(2, len(subgraphs))
-        self.assertIn('1', subgraphs)
-        self.assertIn('2', subgraphs)
+        self.assertIn(Entity(namespace='subgraph', identifier='1'), subgraphs)
+        self.assertIn(Entity(namespace='subgraph', identifier='2'), subgraphs)
 
-        subgraph_1 = subgraphs['1']
+        subgraph_1 = subgraphs[Entity(namespace='subgraph', identifier='1')]
         self.assertIsInstance(subgraph_1, BELGraph)
 
         self.assertIn('test', subgraph_1.namespace_url)
@@ -56,7 +57,7 @@ class TestAnnotation(unittest.TestCase):
         self.assertNotIn(d, subgraph_1[a])
         self.assertNotIn(d, subgraph_1[c])
 
-        subgraph_2 = subgraphs['2']
+        subgraph_2 = subgraphs[Entity(namespace='subgraph', identifier='2')]
         self.assertIsInstance(subgraph_2, BELGraph)
 
         self.assertIn('test', subgraph_2.namespace_url)
@@ -77,8 +78,8 @@ class TestAnnotation(unittest.TestCase):
         subgraphs = get_subgraphs_by_annotation(self.graph, annotation='subgraph', sentinel=sentinel)
 
         self.assertEqual(3, len(subgraphs))
-        self.assertIn('1', subgraphs)
-        self.assertIn('2', subgraphs)
+        self.assertIn(Entity(namespace='subgraph', identifier='1'), subgraphs)
+        self.assertIn(Entity(namespace='subgraph', identifier='2'), subgraphs)
         self.assertIn(sentinel, subgraphs)
 
 
diff --git a/tests/test_struct/test_struct_graph.py b/tests/test_struct/test_struct_graph.py
index 1b57a176..fbe66f46 100644
--- a/tests/test_struct/test_struct_graph.py
+++ b/tests/test_struct/test_struct_graph.py
@@ -14,6 +14,7 @@ from pybel import BELGraph
 from pybel.constants import CITATION_TYPE_PUBMED, IDENTIFIER, NAMESPACE
 from pybel.dsl import hgvs, protein
 from pybel.io.api import InvalidExtensionError
+from pybel.language import Entity
 from pybel.testing.utils import n
 
 
@@ -154,6 +155,8 @@ class TestGetGraphProperties(unittest.TestCase):
     def setUp(self):
         """Set up the test case with a fresh BEL graph."""
         self.graph = BELGraph()
+        self.graph.annotation_pattern['Species'] = r'\d+'
+        self.graph.annotation_list['Confidence'] = {'Very Low', 'Low', 'Medium', 'High', 'Very High'}
 
     def test_get_qualified_edge(self):
         """Test adding an edge to a graph."""
@@ -196,11 +199,9 @@ class TestGetGraphProperties(unittest.TestCase):
         self.assertIsNotNone(annotations)
         self.assertIsInstance(annotations, dict)
         self.assertIn('Species', annotations)
-        self.assertIn('9606', annotations['Species'])
-        self.assertTrue(annotations['Species']['9606'])
+        self.assertIn(Entity(namespace='Species', identifier='9606'), annotations['Species'])
         self.assertIn('Confidence', annotations)
-        self.assertIn('Very High', annotations['Confidence'])
-        self.assertTrue(annotations['Confidence']['Very High'])
+        self.assertIn(Entity(namespace='Confidence', identifier='Very High'), annotations['Confidence'])
 
     def test_get_unqualified_edge(self):
         """Test adding an unqualified edge."""
diff --git a/tests/test_struct/test_summary/test_struct_summary_edges.py b/tests/test_struct/test_summary/test_struct_summary_edges.py
index ba2e0712..72cf5cfe 100644
--- a/tests/test_struct/test_summary/test_struct_summary_edges.py
+++ b/tests/test_struct/test_summary/test_struct_summary_edges.py
@@ -8,6 +8,7 @@ from collections import Counter
 from pybel import BELGraph
 from pybel.dsl import protein
 from pybel.examples import sialic_acid_graph
+from pybel.language import Entity
 from pybel.struct.summary.edge_summary import (
     count_annotations, get_annotation_values, get_annotation_values_by_annotation, get_annotations,
     get_unused_annotations, get_unused_list_annotation_values, iter_annotation_value_pairs, iter_annotation_values,
@@ -21,6 +22,11 @@ class TestEdgeSummary(unittest.TestCase):
     def test_1(self):
         """Test iterating over annotation/value pairs."""
         graph = BELGraph()
+        graph.annotation_list.update({
+            'A': set('1234'),
+            'B': set('XYZ'),
+            'C': set('abcde'),
+        })
         u = protein('HGNC', name='U')
         v = protein('HGNC', name='V')
         w = protein('HGNC', name='W')
@@ -43,8 +49,8 @@ class TestEdgeSummary(unittest.TestCase):
             citation=n(),
             annotations={
                 'A': {'1', '3'},
-                'C': {'a'}
-            }
+                'C': {'a'},
+            },
         )
 
         graph.add_increases(
@@ -54,7 +60,7 @@ class TestEdgeSummary(unittest.TestCase):
             citation=n(),
         )
 
-        x = dict(Counter(iter_annotation_value_pairs(graph)))
+        x = dict(Counter((key, entity.identifier) for key, entity in iter_annotation_value_pairs(graph)))
 
         self.assertEqual({
             ('A', '1'): 2,
@@ -76,8 +82,13 @@ class TestEdgeSummary(unittest.TestCase):
     def test_get_annotation_values(self):
         """Test getting annotation values."""
         expected = {
-            'Confidence': {'High', 'Low'},
-            'Species': {'9606'}
+            'Confidence': {
+                Entity(namespace='Confidence', identifier='High'),
+                Entity(namespace='Confidence', identifier='Low'),
+            },
+            'Species': {
+                Entity(namespace='Species', identifier='9606'),
+            },
         }
 
         self.assertEqual({'Confidence', 'Species'}, get_annotations(sialic_acid_graph))
@@ -110,8 +121,16 @@ class TestEdgeSummary(unittest.TestCase):
     def test_get_unused_annotation_list_values(self):
         """Test getting unused annotation list values."""
         graph = BELGraph()
-        name = 'test'
-        a, b, c = 'abc'
-        graph.annotation_list[name] = {a, b, c}
-        graph.add_increases(protein(n(), n()), protein(n(), n()), citation=n(), evidence=n(), annotations={name: {a}})
-        self.assertEqual({name: {b, c}}, get_unused_list_annotation_values(graph))
+        annotation_key = 'test'
+        graph.annotation_list[annotation_key] = set('abc')
+        graph.add_increases(
+            protein(n(), n()), protein(n(), n()),
+            citation=n(),
+            evidence=n(),
+            annotations={annotation_key: {'a'}},
+        )
+
+        rv = get_unused_list_annotation_values(graph)
+        self.assertIsInstance(rv, dict)
+
+        self.assertEqual({annotation_key: set('bc')}, rv)
diff --git a/tests/test_struct/test_transformations/test_induction.py b/tests/test_struct/test_transformations/test_induction.py
index ef6dbef0..ce4e6565 100644
--- a/tests/test_struct/test_transformations/test_induction.py
+++ b/tests/test_struct/test_transformations/test_induction.py
@@ -343,6 +343,7 @@ class TestEdgeInduction(unittest.TestCase):
     def test_get_subgraph_by_annotation_value(self):
         """Test getting a subgraph by a single annotation value."""
         graph = BELGraph()
+        graph.annotation_url['Subgraph'] = n()
         a, b, c, d = [protein(namespace='test', name=n()) for _ in range(4)]
 
         k1 = graph.add_increases(a, b, citation=n(), evidence=n(), annotations={
@@ -370,6 +371,8 @@ class TestEdgeInduction(unittest.TestCase):
     def test_get_subgraph_by_annotation_values(self):
         """Test getting a subgraph by multiple annotation value."""
         graph = BELGraph()
+        graph.annotation_list['Subgraph'] = set('ABCDE')
+
         a, b, c, d = [protein(namespace='test', name=n()) for _ in range(4)]
 
         k1 = graph.add_increases(a, b, citation=n(), evidence=n(), annotations={
diff --git a/tests/test_struct/test_transformations/test_metadata.py b/tests/test_struct/test_transformations/test_metadata.py
index 31dfa8a5..aaaf1ab6 100644
--- a/tests/test_struct/test_transformations/test_metadata.py
+++ b/tests/test_struct/test_transformations/test_metadata.py
@@ -25,6 +25,7 @@ class TestMetadata(unittest.TestCase):
         y = protein(namespace='HGNC', name='X')
 
         graph = BELGraph()
+        graph.annotation_list['A'] = set('ABC')
         key = graph.add_increases(
             x,
             y,
@@ -85,6 +86,7 @@ class TestMetadata(unittest.TestCase):
         y = protein(namespace='HGNC', name='X')
 
         graph = BELGraph()
+        graph.annotation_list['A'] = set('ABC')
 
         k0 = graph.add_part_of(x, y)
         k1 = graph.add_increases(