Repository: pybel/pybel
Pull request #421 (2020-04-07 12:04:18)

Fix bug in BEP-0008 handling
- Also required explicitly enumerating namespaces used in tests with dummpy regex
- Removed bel: reference for default namespace from tests. This is no longer supported
- Upgraded some old namespaces from examples (no more SFAM, SCOMP, ADO)
- Make sure pop() works in translocations

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

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

```diff
@@            Coverage Diff             @@
##           master     #421      +/-   ##
==========================================
+ Coverage   83.85%   83.88%   +0.03%     
==========================================
  Files         160      160              
  Lines        7946     7944       -2     
  Branches     1118     1120       +2     
==========================================
+ Hits         6663     6664       +1     
+ Misses       1025     1022       -3     
  Partials      258      258              
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/421?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/parser/parse\_concept.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9jb25jZXB0LnB5) | `95.89% <81.25%> (+0.95%)` | :arrow_up: |
| [src/pybel/io/jgif.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2pnaWYucHk=) | `72.32% <100.00%> (+0.53%)` | :arrow_up: |
| [src/pybel/parser/parse\_bel.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9iZWwucHk=) | `94.92% <100.00%> (ø)` | |
| [src/pybel/testing/constants.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3Rlc3RpbmcvY29uc3RhbnRzLnB5) | `100.00% <100.00%> (ø)` | |
| [src/pybel/version.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3ZlcnNpb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [src/pybel/io/line\_utils.py](https://codecov.io/gh/pybel/pybel/pull/421/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL2lvL2xpbmVfdXRpbHMucHk=) | `70.37% <0.00%> (+1.85%)` | :arrow_up: |

------

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


Patch:
diff --git a/src/pybel/io/jgif.py b/src/pybel/io/jgif.py
index dd2aedc2..8aa26c96 100644
--- a/src/pybel/io/jgif.py
+++ b/src/pybel/io/jgif.py
@@ -10,9 +10,10 @@ JSON. Interchange with this format provides compatibilty with other software and
 import gzip
 import json
 import logging
+import re
 from collections import defaultdict
 from operator import methodcaller
-from typing import TextIO, Union
+from typing import Any, Mapping, Optional, TextIO, Union
 
 import requests
 from networkx.utils import open_file
@@ -126,6 +127,33 @@ def map_cbn(d):
     return d
 
 
+NAMESPACE_URLS = {
+    'HGNC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/hgnc-human-genes/hgnc-human-genes-20150601.belns',
+    'GOBP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-biological-process/go-biological-process-20150601.belns',
+    'SFAM': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-protein-families/selventa-protein-families-20150601.belns',
+    'GOCC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-cellular-component/go-cellular-component-20170511.belns',
+    'MESHPP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-processes/mesh-processes-20150601.belns',
+    'MGI': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mgi-mouse-genes/mgi-mouse-genes-20150601.belns',
+    'RGD': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/rgd-rat-genes/rgd-rat-genes-20150601.belns',
+    'CHEBI': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/chebi/chebi-20150601.belns',
+    'SCHEM': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-legacy-chemicals/selventa-legacy-chemicals-20150601.belns',
+    'EGID': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/entrez-gene-ids/entrez-gene-ids-20150601.belns',
+    'MESHD': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-diseases/mesh-diseases-20150601.belns',
+    'SDIS': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-legacy-diseases/selventa-legacy-diseases-20150601.belns',
+    'SCOMP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-named-complexes/selventa-named-complexes-20150601.belns',
+    'MESHC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-chemicals/mesh-chemicals-20170511.belns',
+    'GOBPID': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-biological-process-ids/go-biological-process-ids-20150601.belns',
+    'MESHCS': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-cell-structures/mesh-cell-structures-20150601.belns',
+}
+
+ANNOTATION_URLS = {
+    'Cell': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/cell-line/cell-line-20150601.belanno',
+    'Disease': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/disease/disease-20150601.belanno',
+    'Species': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/species-taxonomy-id/species-taxonomy-id-20170511.belanno',
+    'Tissue': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/mesh-anatomy/mesh-anatomy-20150601.belanno',
+}
+
+
 def from_cbn_jgif(graph_jgif_dict):
     """Build a BEL graph from CBN JGIF.
 
@@ -173,36 +201,13 @@ def from_cbn_jgif(graph_jgif_dict):
 
     graph = from_jgif(graph_jgif_dict)
 
-    graph.namespace_url.update({
-        'HGNC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/hgnc-human-genes/hgnc-human-genes-20150601.belns',
-        'GOBP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-biological-process/go-biological-process-20150601.belns',
-        'SFAM': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-protein-families/selventa-protein-families-20150601.belns',
-        'GOCC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-cellular-component/go-cellular-component-20170511.belns',
-        'MESHPP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-processes/mesh-processes-20150601.belns',
-        'MGI': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mgi-mouse-genes/mgi-mouse-genes-20150601.belns',
-        'RGD': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/rgd-rat-genes/rgd-rat-genes-20150601.belns',
-        'CHEBI': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/chebi/chebi-20150601.belns',
-        'SCHEM': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-legacy-chemicals/selventa-legacy-chemicals-20150601.belns',
-        'EGID': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/entrez-gene-ids/entrez-gene-ids-20150601.belns',
-        'MESHD': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-diseases/mesh-diseases-20150601.belns',
-        'SDIS': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-legacy-diseases/selventa-legacy-diseases-20150601.belns',
-        'SCOMP': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/selventa-named-complexes/selventa-named-complexes-20150601.belns',
-        'MESHC': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-chemicals/mesh-chemicals-20170511.belns',
-        'GOBPID': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/go-biological-process-ids/go-biological-process-ids-20150601.belns',
-        'MESHCS': 'https://arty.scai.fraunhofer.de/artifactory/bel/namespace/mesh-cell-structures/mesh-cell-structures-20150601.belns',
-    })
-
-    graph.annotation_url.update({
-        'Cell': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/cell-line/cell-line-20150601.belanno',
-        'Disease': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/disease/disease-20150601.belanno',
-        'Species': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/species-taxonomy-id/species-taxonomy-id-20170511.belanno',
-        'Tissue': 'https://arty.scai.fraunhofer.de/artifactory/bel/annotation/mesh-anatomy/mesh-anatomy-20150601.belanno',
-    })
+    graph.namespace_url.update(NAMESPACE_URLS)
+    graph.annotation_url.update(ANNOTATION_URLS)
 
     return graph
 
 
-def from_jgif(graph_jgif_dict):
+def from_jgif(graph_jgif_dict, parser_kwargs: Optional[Mapping[str, Any]] = None):
     """Build a BEL graph from a JGIF JSON object.
 
     :param dict graph_jgif_dict: The JSON object representing the graph in JGIF format
@@ -222,7 +227,10 @@ def from_jgif(graph_jgif_dict):
             if key in metadata:
                 graph.document[key] = metadata[key]
 
-    parser = BELParser(graph)
+    parser = BELParser(graph, namespace_to_pattern={
+        namespace: re.compile(r'.*')  # don't validate anything
+        for namespace in (set(NAMESPACE_URLS) | {'GO'})
+    })
     parser.bel_term.addParseAction(parser.handle_term)
 
     for node in root['nodes']:
diff --git a/src/pybel/parser/parse_bel.py b/src/pybel/parser/parse_bel.py
index 71631ea8..7dff7063 100644
--- a/src/pybel/parser/parse_bel.py
+++ b/src/pybel/parser/parse_bel.py
@@ -293,14 +293,16 @@ class BELParser(BaseParser):
                 annotation_to_term=annotation_to_term,
                 annotation_to_pattern=annotation_to_pattern,
                 annotation_to_local=annotation_to_local,
+                #
                 citation_clearing=citation_clearing,
                 required_annotations=required_annotations,
             )
 
             self.concept_parser = ConceptParser(
-                allow_naked_names=allow_naked_names,
                 namespace_to_term_to_encoding=namespace_to_term_to_encoding,
                 namespace_to_pattern=namespace_to_pattern,
+                #
+                allow_naked_names=allow_naked_names,
             )
 
         self.control_parser.get_line_number = self.get_line_number
@@ -385,12 +387,15 @@ class BELParser(BaseParser):
             ]) + opt_location,
         )
 
+        self.population = population_tag + nest(concept)
+
         self.single_abundance = MatchFirst([
             self.general_abundance,
             self.gene,
             self.mirna,
             self.protein,
             self.rna,
+            self.population,
         ])
 
         #: `2.1.2 <http://openbel.org/language/version_2.0/bel_specification_version_2.0.html#XcomplexA>`_
@@ -433,9 +438,7 @@ class BELParser(BaseParser):
         #: `2.3.2 <http://openbel.org/language/version_2.0/bel_specification_version_2.0.html#_pathology_path>`_
         self.pathology = pathology_tag + nest(concept)
 
-        self.population = population_tag + nest(concept)
-
-        self.bp_path = self.biological_process | self.pathology | self.population
+        self.bp_path = self.biological_process | self.pathology
         self.bp_path.setParseAction(self.check_function_semantics)
 
         self.activity_standard = activity_tag + nest(
diff --git a/src/pybel/parser/parse_concept.py b/src/pybel/parser/parse_concept.py
index 4cf422eb..072ac400 100644
--- a/src/pybel/parser/parse_concept.py
+++ b/src/pybel/parser/parse_concept.py
@@ -63,13 +63,13 @@ class ConceptParser(BaseParser):
 
             self.namespace_to_name_to_encoding = dict(self.namespace_to_name_to_encoding)
             self.namespace_to_identifier_to_encoding = dict(self.namespace_to_identifier_to_encoding)
-
-            self.identifier_fqualified.setParseAction(self.handle_identifier_qualified)
-            self.identifier_qualified.setParseAction(self.handle_identifier_qualified)
         else:
             self.namespace_to_name_to_encoding = {}
             self.namespace_to_identifier_to_encoding = {}
 
+        self.identifier_fqualified.setParseAction(self.handle_identifier_fqualified)
+        self.identifier_qualified.setParseAction(self.handle_identifier_qualified)
+
         self.namespace_to_pattern = namespace_to_pattern or {}
         self.default_namespace = set(default_namespace) if default_namespace is not None else None
         self.allow_naked_names = allow_naked_names
@@ -93,49 +93,32 @@ class ConceptParser(BaseParser):
         """Check that the namespace has been defined by a regular expression."""
         return namespace in self.namespace_to_pattern
 
-    def has_namespace(self, namespace: str) -> bool:
-        """Check that the namespace has either been defined by an enumeration or a regular expression."""
-        return self.has_enumerated_namespace(namespace) or self.has_regex_namespace(namespace)
-
-    def has_enumerated_namespace_name(self, namespace: str, name: str) -> bool:
-        """Check that the namespace is defined by an enumeration and that the name is a member."""
-        return self.has_enumerated_namespace(namespace) and name in self.namespace_to_name_to_encoding[namespace]
-
-    def has_regex_namespace_name(self, namespace: str, name: str) -> bool:
-        """Check that the namespace is defined as a regular expression and the name matches it."""
-        return self.has_regex_namespace(namespace) and self.namespace_to_pattern[namespace].match(name)
-
-    def has_namespace_name(self, line: str, position: int, namespace: str, name: str) -> bool:
-        """Check that the namespace is defined and has the given name."""
-        self.raise_for_missing_namespace(line, position, namespace, name)
-        return self.has_enumerated_namespace_name(namespace, name) or self.has_regex_namespace_name(namespace, name)
-
     def raise_for_missing_namespace(self, line: str, position: int, namespace: str, name: str) -> None:
         """Raise an exception if the namespace is not defined."""
-        if not self.has_namespace(namespace):
+        if not self.has_enumerated_namespace(namespace) and not self.has_regex_namespace(namespace):
             raise UndefinedNamespaceWarning(self.get_line_number(), line, position, namespace, name)
 
     def raise_for_missing_name(self, line: str, position: int, namespace: str, name: str) -> None:
         """Raise an exception if the namespace is not defined or if it does not validate the given name."""
         self.raise_for_missing_namespace(line, position, namespace, name)
 
-        if self.has_enumerated_namespace(namespace) and not self.has_enumerated_namespace_name(namespace, name):
+        if self.has_enumerated_namespace(namespace) and name not in self.namespace_to_name_to_encoding[namespace]:
             raise MissingNamespaceNameWarning(self.get_line_number(), line, position, namespace, name)
 
-        if self.has_regex_namespace(namespace) and not self.has_regex_namespace_name(namespace, name):
+        if self.has_regex_namespace(namespace) and not self.namespace_to_pattern[namespace].match(name):
             raise MissingNamespaceRegexWarning(self.get_line_number(), line, position, namespace, name)
 
-    def raise_for_missing_default(self, line: str, position: int, name: str) -> None:
-        """Raise an exception if the name does not belong to the default namespace."""
-        if not self.default_namespace:
-            raise ValueError('Default namespace is not set')
-
-        if name not in self.default_namespace:
-            raise MissingDefaultNameWarning(self.get_line_number(), line, position, name)
+    def handle_identifier_fqualified(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
+        """Handle parsing a qualified OBO-style identifier."""
+        return self._handle_identifier(line, position, tokens, key=IDENTIFIER)
 
     def handle_identifier_qualified(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
         """Handle parsing a qualified identifier."""
-        namespace, name = tokens[NAMESPACE], tokens[NAME]
+        return self._handle_identifier(line, position, tokens, key=NAME)
+
+    def _handle_identifier(self, line: str, position: int, tokens: ParseResults, key) -> ParseResults:
+        """Handle parsing a qualified identifier."""
+        namespace, name = tokens[NAMESPACE], tokens[key]
 
         self.raise_for_missing_namespace(line, position, namespace, name)
         self.raise_for_missing_name(line, position, namespace, name)
@@ -145,7 +128,10 @@ class ConceptParser(BaseParser):
     def handle_namespace_default(self, line: str, position: int, tokens: ParseResults) -> ParseResults:
         """Handle parsing an identifier for the default namespace."""
         name = tokens[NAME]
-        self.raise_for_missing_default(line, position, name)
+        if not self.default_namespace:
+            raise ValueError('Default namespace is not set')
+        if name not in self.default_namespace:
+            raise MissingDefaultNameWarning(self.get_line_number(), line, position, name)
         return tokens
 
     @staticmethod


Test patch:
diff --git a/src/pybel/testing/constants.py b/src/pybel/testing/constants.py
index 718a6280..ffc183d9 100644
--- a/src/pybel/testing/constants.py
+++ b/src/pybel/testing/constants.py
@@ -48,6 +48,7 @@ test_bel_thorough = os.path.join(bel_dir_path, 'thorough.bel')
 test_bel_isolated = os.path.join(bel_dir_path, 'isolated.bel')
 test_bel_misordered = os.path.join(bel_dir_path, 'misordered.bel')
 test_bel_no_identifier_valiation = os.path.join(bel_dir_path, 'no_identifier_validation_test.bel')
+test_bel_with_obo = os.path.join(bel_dir_path, 'obo.bel')
 
 # JSON Files
 
diff --git a/src/pybel/testing/resources/bel/obo.bel b/src/pybel/testing/resources/bel/obo.bel
new file mode 100644
index 00000000..a7dabb04
--- /dev/null
+++ b/src/pybel/testing/resources/bel/obo.bel
@@ -0,0 +1,17 @@
+SET DOCUMENT Name = "PyBEL Test Simple"
+SET DOCUMENT Description = "Made for testing PyBEL parsing"
+SET DOCUMENT Version = "1.6.0"
+SET DOCUMENT Copyright = "Copyright (c) Charles Tapley Hoyt. All Rights Reserved."
+SET DOCUMENT Authors = "Charles Tapley Hoyt"
+SET DOCUMENT Licenses = "WTF License"
+SET DOCUMENT ContactInfo = "cthoyt@gmail.com"
+SET DOCUMENT Project = "PyBEL Testing"
+
+# Definition
+DEFINE NAMESPACE hgnc AS PATTERN "\d+"
+
+# Statement
+SET Citation = {"PubMed", "123456"}
+SET Evidence = "Test Evidence"
+
+p(hgnc:391 ! AKT1) -> p(hgnc:3236 ! EGFR)
diff --git a/tests/constants.py b/tests/constants.py
index a7a390ba..7d76778d 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -3,6 +3,7 @@
 """Constants for PyBEL tests."""
 
 import logging
+import re
 import unittest
 from json import dumps
 
@@ -169,6 +170,20 @@ class TestTokenParserBase(unittest.TestCase):
             cls.graph,
             autostreamline=False,
             disallow_unqualified_translocations=True,
+            namespace_to_pattern={
+                'HGNC': re.compile(r'.*'),
+                'SNP': re.compile(r'.*'),
+                'CHEBI': re.compile(r'.*'),
+                'REF': re.compile(r'.*'),
+                'MOD': re.compile(r'.*'),
+                'EFO': re.compile(r'.*'),
+                'GO': re.compile(r'.*'),
+                'UBERON': re.compile(r'.*'),
+                'FPLX': re.compile(r'.*'),
+                'MESH': re.compile(r'.*'),
+                'MGI': re.compile(r'.*'),
+                'dbSNP': re.compile(r'.*'),
+            }
         )
 
     def setUp(self):
diff --git a/tests/test_io/test_import.py b/tests/test_io/test_import.py
index 1431e33f..ff2d45d6 100644
--- a/tests/test_io/test_import.py
+++ b/tests/test_io/test_import.py
@@ -14,6 +14,7 @@ from pybel import (
     BELGraph, from_bel_script, from_bel_script_url, from_bytes, from_nodelink, from_nodelink_file, from_pickle,
     to_bel_script_lines, to_bytes, to_csv, to_graphml, to_gsea, to_nodelink, to_nodelink_file, to_pickle, to_sif,
 )
+from pybel.io.line_utils import parse_definitions
 from pybel.config import PYBEL_MINIMUM_IMPORT_VERSION
 from pybel.constants import (
     ANNOTATIONS, CITATION, DECREASES, DIRECTLY_DECREASES, EVIDENCE, GRAPH_PYBEL_VERSION, INCREASES, RELATION,
@@ -30,7 +31,7 @@ from pybel.parser.exc import (
 from pybel.struct.summary import get_syntax_errors
 from pybel.testing.cases import TemporaryCacheClsMixin
 from pybel.testing.constants import (
-    test_bel_isolated, test_bel_misordered, test_bel_simple, test_bel_slushy, test_bel_thorough,
+    test_bel_isolated, test_bel_misordered, test_bel_simple, test_bel_slushy, test_bel_thorough, test_bel_with_obo,
 )
 from pybel.testing.mocks import mock_bel_resources
 from tests.constants import (
@@ -432,5 +433,21 @@ class TestRandom(unittest.TestCase):
             )
 
 
+class TestNomenclature(unittest.TestCase):
+    """Test that `BEP-0008 nomenclature <http://bep.bel.bio/published/BEP-0008.html>`_ gets validated properly."""
+
+    def test_bep_0008(self):
+        """Test parsing works right"""
+        graph = from_bel_script(test_bel_with_obo)
+        self.assertIn('hgnc', graph.namespace_pattern)
+        self.assertEqual(r'\d+', graph.namespace_pattern['hgnc'])
+
+        self.assertEqual(0, graph.number_of_warnings(), msg=',\n'.join(map(str, graph.warnings)))
+
+        self.assertEqual(2, graph.number_of_nodes())
+        self.assertIn(Protein(namespace='hgnc', identifier='391', name='AKT1'), graph)
+        self.assertIn(Protein(namespace='hgnc', identifier='3236', name='EGFR'), graph)
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_parse/test_parse_bel.py b/tests/test_parse/test_parse_bel.py
index aefb4a42..3af364dd 100644
--- a/tests/test_parse/test_parse_bel.py
+++ b/tests/test_parse/test_parse_bel.py
@@ -3,6 +3,7 @@
 """Tests for the BEL parser."""
 
 import logging
+import re
 import unittest
 
 from pybel import BELGraph
@@ -96,8 +97,8 @@ class TestAbundanceLabeled(TestTokenParserBase):
         self.parser.clear()
         self.parser.general_abundance.setParseAction(self.parser.handle_term)
 
-        self.expected_node = abundance(namespace='chebi', name='oxygen atom', identifier='CHEBI:25805')
-        self.expected_canonical_bel = 'a(chebi:"CHEBI:25805" ! "oxygen atom")'
+        self.expected_node = abundance(namespace='CHEBI', name='oxygen atom', identifier='CHEBI:25805')
+        self.expected_canonical_bel = 'a(CHEBI:"CHEBI:25805" ! "oxygen atom")'
 
     def _test_abundance_helper(self, statement):
         result = self.parser.general_abundance.parseString(statement)
@@ -111,8 +112,8 @@ class TestAbundanceLabeled(TestTokenParserBase):
 
     def test_abundance(self):
         """Test short/long abundance name."""
-        for s in ('a(chebi:"CHEBI:25805"!"oxygen atom")', 'abundance(chebi:"CHEBI:25805"!"oxygen atom")',
-                  'abundance(chebi:"CHEBI:25805" ! "oxygen atom")'):
+        for s in ('a(CHEBI:"CHEBI:25805"!"oxygen atom")', 'abundance(CHEBI:"CHEBI:25805"!"oxygen atom")',
+                  'abundance(CHEBI:"CHEBI:25805" ! "oxygen atom")'):
             with self.subTest(s=s):
                 self._test_abundance_helper(s)
 
@@ -122,7 +123,7 @@ class TestAbundanceLabeled(TestTokenParserBase):
         expected_result = {
             FUNCTION: ABUNDANCE,
             CONCEPT: {
-                NAMESPACE: 'chebi',
+                NAMESPACE: 'CHEBI',
                 NAME: 'oxygen atom',
                 IDENTIFIER: 'CHEBI:25805',
             },
@@ -148,11 +149,11 @@ class TestAbundanceLabeled(TestTokenParserBase):
 
     def test_abundance_with_location(self):
         """Test short/long abundance name and short/long location name."""
-        self._test_abundance_with_location_helper('a(chebi:"CHEBI:25805"!"oxygen atom", loc(GO:intracellular))')
-        self._test_abundance_with_location_helper('abundance(chebi:"CHEBI:25805"!"oxygen atom", loc(GO:intracellular))')
-        self._test_abundance_with_location_helper('a(chebi:"CHEBI:25805"!"oxygen atom", location(GO:intracellular))')
+        self._test_abundance_with_location_helper('a(CHEBI:"CHEBI:25805"!"oxygen atom", loc(GO:intracellular))')
+        self._test_abundance_with_location_helper('abundance(CHEBI:"CHEBI:25805"!"oxygen atom", loc(GO:intracellular))')
+        self._test_abundance_with_location_helper('a(CHEBI:"CHEBI:25805"!"oxygen atom", location(GO:intracellular))')
         self._test_abundance_with_location_helper(
-            'abundance(chebi:"CHEBI:25805"!"oxygen atom", location(GO:intracellular))'
+            'abundance(CHEBI:"CHEBI:25805"!"oxygen atom", location(GO:intracellular))'
         )
 
 
@@ -1326,19 +1327,19 @@ class TestComplex(TestTokenParserBase):
         self.parser.complex_abundances.setParseAction(self.parser.handle_term)
 
     def test_named_complex_singleton(self):
-        statement = 'complex(SCOMP:"AP-1 Complex")'
+        statement = 'complex(FPLX:AP1)'
         result = self.parser.complex_abundances.parseString(statement)
 
         expected_dict = {
             FUNCTION: COMPLEX,
             CONCEPT: {
-                NAMESPACE: 'SCOMP',
-                NAME: 'AP-1 Complex',
+                NAMESPACE: 'FPLX',
+                NAME: 'AP1',
             },
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        expected_node = named_complex_abundance('SCOMP', 'AP-1 Complex')
+        expected_node = named_complex_abundance('FPLX', 'AP1')
         self.assert_has_node(expected_node)
 
     def test_complex_list_short(self):
@@ -1507,22 +1508,22 @@ class TestPopulation(TestTokenParserBase):
         self.parser.population.setParseAction(self.parser.handle_term)
 
     def test_parse_population(self):
-        statement = 'pop(uberon:blood)'
+        statement = 'pop(UBERON:blood)'
         result = self.parser.population.parseString(statement)
 
         expected_dict = {
             FUNCTION: POPULATION,
             CONCEPT: {
-                NAMESPACE: 'uberon',
+                NAMESPACE: 'UBERON',
                 NAME: 'blood',
             },
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        expected_node = Population('uberon', 'blood')
+        expected_node = Population('UBERON', 'blood')
         self.assert_has_node(expected_node)
 
-        self.assertEqual('pop(uberon:blood)',
+        self.assertEqual('pop(UBERON:blood)',
                          self.graph.node_to_bel(expected_node),
                          msg='Nodes: {}'.format(list(self.graph)))
 
@@ -1612,13 +1613,13 @@ class TestActivity(TestTokenParserBase):
 
     def test_activity_withMolecularActivityCustom(self):
         """Tests activity modifier with molecular activity from custom namespaced"""
-        statement = 'act(p(HGNC:AKT1), ma(GOMF:"catalytic activity"))'
+        statement = 'act(p(HGNC:AKT1), ma(GO:"catalytic activity"))'
         result = self.parser.activity.parseString(statement)
 
         expected_dict = {
             MODIFIER: ACTIVITY,
             EFFECT: {
-                NAMESPACE: 'GOMF',
+                NAMESPACE: 'GO',
                 NAME: 'catalytic activity'
             },
             TARGET: {
@@ -1635,7 +1636,7 @@ class TestActivity(TestTokenParserBase):
         expected_mod = {
             MODIFIER: ACTIVITY,
             EFFECT: {
-                NAMESPACE: 'GOMF',
+                NAMESPACE: 'GO',
                 NAME: 'catalytic activity'
             },
         }
@@ -1699,6 +1700,10 @@ class TestTranslocationPermissive(unittest.TestCase):
         cls.parser = BELParser(
             cls.graph,
             disallow_unqualified_translocations=False,
+            namespace_to_pattern={
+                'HGNC': re.compile(r'.*'),
+                'CHEBI': re.compile(r'.*'),
+            }
         )
 
     def setUp(self):
@@ -1745,14 +1750,14 @@ class TestTranslocationPermissive(unittest.TestCase):
         """
         update_provenance(self.parser.control_parser)
 
-        statement = 'a(ADO:"Abeta_42") => tloc(a(CHEBI:"calcium(2+)"))'
+        statement = 'a(CHEBI:"Abeta_42") => tloc(a(CHEBI:"calcium(2+)"))'
         result = self.parser.relation.parseString(statement)
 
         expected_dict = {
             SUBJECT: {
                 FUNCTION: ABUNDANCE,
                 CONCEPT: {
-                    NAMESPACE: 'ADO',
+                    NAMESPACE: 'CHEBI',
                     NAME: 'Abeta_42',
                 },
             },
@@ -1770,7 +1775,7 @@ class TestTranslocationPermissive(unittest.TestCase):
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        sub = abundance('ADO', 'Abeta_42')
+        sub = abundance('CHEBI', 'Abeta_42')
         self.assert_has_node(sub)
 
         obj = abundance('CHEBI', 'calcium(2+)')
@@ -1912,7 +1917,7 @@ class TestTransformation(TestTokenParserBase):
 
     def test_unqualified_translocation_strict(self):
         """Fail on an improperly written single argument translocation"""
-        statement = 'tloc(a(NS:"T-Lymphocytes"))'
+        statement = 'tloc(pop(EFO:"CD8-Positive T-Lymphocytes"))'
         with self.assertRaises(MalformedTranslocationWarning):
             self.parser.translocation.parseString(statement)
 
diff --git a/tests/test_parse/test_parse_bel_relations.py b/tests/test_parse/test_parse_bel_relations.py
index 6af80500..bc31fe65 100644
--- a/tests/test_parse/test_parse_bel_relations.py
+++ b/tests/test_parse/test_parse_bel_relations.py
@@ -65,14 +65,14 @@ class TestRelations(TestTokenParserBase):
 
     def test_singleton(self):
         """Test singleton composite in subject."""
-        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(ADO:"Abeta_42"))'
+        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(CHEBI:"Abeta_42"))'
         result = self.parser.statement.parseString(statement)
 
         expected = [
             COMPOSITE,
             [PROTEIN, ['HGNC', 'CASP8']],
             [PROTEIN, ['HGNC', 'FADD']],
-            [ABUNDANCE, ['ADO', 'Abeta_42']]
+            [ABUNDANCE, ['CHEBI', 'Abeta_42']]
         ]
         self.assertEqual(expected, result.asList())
 
@@ -82,7 +82,7 @@ class TestRelations(TestTokenParserBase):
         sub_member_2 = protein('HGNC', 'FADD')
         self.assert_has_node(sub_member_2)
 
-        sub_member_3 = abundance('ADO', 'Abeta_42')
+        sub_member_3 = abundance('CHEBI', 'Abeta_42')
         self.assert_has_node(sub_member_3)
 
         sub = composite_abundance([sub_member_1, sub_member_2, sub_member_3])
@@ -93,7 +93,7 @@ class TestRelations(TestTokenParserBase):
 
     def test_predicate_failure(self):
         """Checks that if there's a problem with the relation/object, that an error gets thrown"""
-        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(ADO:"Abeta_42")) -> nope(GO:"neuron apoptotic process")'
+        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(CHEBI:"Abeta_42")) -> nope(GO:"neuron apoptotic process")'
 
         with self.assertRaises(ParseException):
             self.parser.relation.parseString(statement)
@@ -102,12 +102,12 @@ class TestRelations(TestTokenParserBase):
         """Test composite in subject. See BEL 2.0 specification
         `3.1.1 <http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#Xincreases>`_
         """
-        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(ADO:"Abeta_42")) -> bp(GO:"neuron apoptotic process")'
+        statement = 'composite(p(HGNC:CASP8),p(HGNC:FADD),a(CHEBI:"Abeta_42")) -> bp(GO:"neuron apoptotic process")'
         result = self.parser.relation.parseString(statement)
 
         expected = [
             [COMPOSITE, [PROTEIN, ['HGNC', 'CASP8']], [PROTEIN, ['HGNC', 'FADD']],
-             [ABUNDANCE, ['ADO', 'Abeta_42']]],
+             [ABUNDANCE, ['CHEBI', 'Abeta_42']]],
             INCREASES,
             [BIOPROCESS, ['GO', 'neuron apoptotic process']]
         ]
@@ -119,7 +119,7 @@ class TestRelations(TestTokenParserBase):
         sub_member_2 = protein('HGNC', 'FADD')
         self.assert_has_node(sub_member_2)
 
-        sub_member_3 = abundance('ADO', 'Abeta_42')
+        sub_member_3 = abundance('CHEBI', 'Abeta_42')
         self.assert_has_node(sub_member_3)
 
         sub = composite_abundance([sub_member_1, sub_member_2, sub_member_3])
@@ -175,7 +175,7 @@ class TestRelations(TestTokenParserBase):
         """Test translocation in object. See BEL 2.0 specification
         `3.1.2 <http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#XdIncreases>`_
         """
-        statement = 'a(ADO:"Abeta_42") => tloc(a(CHEBI:"calcium(2+)"),fromLoc(MESH:"Cell Membrane"),' \
+        statement = 'a(CHEBI:"Abeta_42") => tloc(a(CHEBI:"calcium(2+)"),fromLoc(MESH:"Cell Membrane"),' \
                     'toLoc(MESH:"Intracellular Space"))'
         result = self.parser.relation.parseString(statement)
 
@@ -183,7 +183,7 @@ class TestRelations(TestTokenParserBase):
             SUBJECT: {
                 FUNCTION: ABUNDANCE,
                 CONCEPT: {
-                    NAMESPACE: 'ADO',
+                    NAMESPACE: 'CHEBI',
                     NAME: 'Abeta_42',
                 },
             },
@@ -205,7 +205,7 @@ class TestRelations(TestTokenParserBase):
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        sub = abundance('ADO', 'Abeta_42')
+        sub = abundance('CHEBI', 'Abeta_42')
         self.assert_has_node(sub)
 
         obj = abundance('CHEBI', 'calcium(2+)')
@@ -229,7 +229,7 @@ class TestRelations(TestTokenParserBase):
 
         3.1.3 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#Xdecreases
         """
-        statement = 'pep(p(SFAM:"CAPN Family", location(GO:intracellular))) -| reaction(reactants(p(HGNC:CDK5R1)),products(p(HGNC:CDK5)))'
+        statement = 'pep(p(FPLX:CAPN, location(GO:intracellular))) -| reaction(reactants(p(HGNC:CDK5R1)),products(p(HGNC:CDK5)))'
         result = self.parser.relation.parseString(statement)
 
         expected_dict = {
@@ -238,8 +238,8 @@ class TestRelations(TestTokenParserBase):
                 TARGET: {
                     FUNCTION: PROTEIN,
                     CONCEPT: {
-                        NAMESPACE: 'SFAM',
-                        NAME: 'CAPN Family',
+                        NAMESPACE: 'FPLX',
+                        NAME: 'CAPN',
                     },
                     LOCATION: {NAMESPACE: 'GO', NAME: 'intracellular'}
                 },
@@ -273,7 +273,7 @@ class TestRelations(TestTokenParserBase):
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        sub = protein('SFAM', 'CAPN Family')
+        sub = protein('FPLX', 'CAPN')
         self.assert_has_node(sub)
 
         obj_member_1 = protein('HGNC', 'CDK5R1')
@@ -454,7 +454,7 @@ class TestRelations(TestTokenParserBase):
 
         See also: 3.1.6 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#Xcnc
         """
-        statement = 'g(HGNC:APP,sub(G,275341,C)) cnc path(MESHD:"Alzheimer Disease")'
+        statement = 'g(HGNC:APP,sub(G,275341,C)) cnc path(MESH:"Alzheimer Disease")'
         result = self.parser.relation.parseString(statement)
 
         expected_dict = {
@@ -475,7 +475,7 @@ class TestRelations(TestTokenParserBase):
             OBJECT: {
                 FUNCTION: PATHOLOGY,
                 CONCEPT: {
-                    NAMESPACE: 'MESHD',
+                    NAMESPACE: 'MESH',
                     NAME: 'Alzheimer Disease',
                 },
             },
@@ -487,7 +487,7 @@ class TestRelations(TestTokenParserBase):
         sub = app_gene.with_variants(hgvs('c.275341G>C'))
         self.assert_has_node(sub)
 
-        obj = Pathology('MESHD', 'Alzheimer Disease')
+        obj = Pathology('MESH', 'Alzheimer Disease')
         self.assert_has_node(obj)
 
         self.assert_has_edge(sub, obj, relation=expected_dict[RELATION])
@@ -581,7 +581,7 @@ class TestRelations(TestTokenParserBase):
         """
         3.2.1 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#XnegCor
         Test phosphoralation tag"""
-        statement = 'kin(p(SFAM:"GSK3 Family")) neg p(HGNC:MAPT,pmod(P))'
+        statement = 'kin(p(FPLX:GSK3)) neg p(HGNC:MAPT,pmod(P))'
         result = self.parser.relation.parseString(statement)
 
         expected_dict = {
@@ -594,8 +594,8 @@ class TestRelations(TestTokenParserBase):
                 TARGET: {
                     FUNCTION: PROTEIN,
                     CONCEPT: {
-                        NAMESPACE: 'SFAM',
-                        NAME: 'GSK3 Family',
+                        NAMESPACE: 'FPLX',
+                        NAME: 'GSK3',
                     },
                 },
             },
@@ -611,7 +611,7 @@ class TestRelations(TestTokenParserBase):
         }
         self.assertEqual(expected_dict, result.asDict())
 
-        sub = protein('SFAM', 'GSK3 Family')
+        sub = protein('FPLX', 'GSK3')
         self.assert_has_node(sub)
 
         obj = protein('HGNC', 'MAPT', variants=pmod('Ph'))
@@ -758,11 +758,11 @@ class TestRelations(TestTokenParserBase):
         self.assertEqual('r(HGNC:AKT1, loc(GO:intracellular)) translatedTo p(HGNC:AKT1)', calculated_edge_bel)
 
     def test_component_list(self):
-        s = 'complex(SCOMP:"C1 Complex") hasComponents list(p(HGNC:C1QB), p(HGNC:C1S))'
+        s = 'complex(FPLX:C1) hasComponents list(p(HGNC:C1QB), p(HGNC:C1S))'
         result = self.parser.relation.parseString(s)
 
         expected_result_list = [
-            [COMPLEX, ['SCOMP', 'C1 Complex']],
+            [COMPLEX, ['FPLX', 'C1']],
             'hasComponents',
             [
                 [PROTEIN, ['HGNC', 'C1QB']],
@@ -771,7 +771,7 @@ class TestRelations(TestTokenParserBase):
         ]
         self.assertEqual(expected_result_list, result.asList())
 
-        sub = named_complex_abundance('SCOMP', 'C1 Complex')
+        sub = named_complex_abundance('FPLX', 'C1')
         self.assert_has_node(sub)
         child_1 = hgnc('C1QB')
         self.assert_has_node(child_1)
@@ -784,10 +784,10 @@ class TestRelations(TestTokenParserBase):
         """
         3.4.2 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#_hasmembers
         """
-        statement = 'p(PKC:a) hasMembers list(p(HGNC:PRKCA), p(HGNC:PRKCB), p(HGNC:PRKCD), p(HGNC:PRKCE))'
+        statement = 'p(FPLX:PKC) hasMembers list(p(HGNC:PRKCA), p(HGNC:PRKCB), p(HGNC:PRKCD), p(HGNC:PRKCE))'
         result = self.parser.relation.parseString(statement)
         expected_result = [
-            [PROTEIN, ['PKC', 'a']],
+            [PROTEIN, ['FPLX', 'PKC']],
             'hasMembers',
             [
                 [PROTEIN, ['HGNC', 'PRKCA']],
@@ -798,7 +798,7 @@ class TestRelations(TestTokenParserBase):
         ]
         self.assertEqual(expected_result, result.asList())
 
-        sub = protein('PKC', 'a')
+        sub = protein('FPLX', 'PKC')
         obj_1 = protein('HGNC', 'PRKCA')
         obj_2 = protein('HGNC', 'PRKCB')
         obj_3 = protein('HGNC', 'PRKCD')
diff --git a/tests/test_parse/test_parse_bel_variants.py b/tests/test_parse/test_parse_bel_variants.py
index 4b0630e0..eedcf2f9 100644
--- a/tests/test_parse/test_parse_bel_variants.py
+++ b/tests/test_parse/test_parse_bel_variants.py
@@ -3,6 +3,7 @@
 """Test parsing variants."""
 
 import logging
+import re
 import unittest
 
 from pybel.constants import (
@@ -95,7 +96,10 @@ class TestHGVSParser(unittest.TestCase):
 
 class TestPmod(unittest.TestCase):
     def setUp(self):
-        identifier_parser = ConceptParser()
+        identifier_parser = ConceptParser(namespace_to_pattern={
+            'MOD': re.compile('.*'),
+            'HGNC': re.compile('.*'),
+        })
         identifier_qualified = identifier_parser.identifier_qualified
         self.parser = get_protein_modification_language(identifier_qualified)
 
@@ -117,15 +121,11 @@ class TestPmod(unittest.TestCase):
         self._help_test_pmod_simple('proteinModification(P)')
         # long function, new modification
         self._help_test_pmod_simple('proteinModification(Ph)')
-        # long function, qualified modification
-        self._help_test_pmod_simple('proteinModification(bel:Ph)')
 
         # short function, legacy modification
         self._help_test_pmod_simple('pmod(P)')
         # short function, new modification
         self._help_test_pmod_simple('pmod(Ph)')
-        # short function, qualified modification
-        self._help_test_pmod_simple('pmod(bel:Ph)')
 
     def _help_test_pmod_with_residue(self, statement):
         result = self.parser.parseString(statement)
@@ -168,14 +168,10 @@ class TestPmod(unittest.TestCase):
         self._help_test_pmod_full('proteinModification(P, S, 473)')
         self._help_test_pmod_full('proteinModification(Ph, Ser, 473)')
         self._help_test_pmod_full('proteinModification(Ph, S, 473)')
-        self._help_test_pmod_full('proteinModification(bel:Ph, Ser, 473)')
-        self._help_test_pmod_full('proteinModification(bel:Ph, S, 473)')
         self._help_test_pmod_full('pmod(P, Ser, 473)')
         self._help_test_pmod_full('pmod(P, S, 473)')
         self._help_test_pmod_full('pmod(Ph, Ser, 473)')
         self._help_test_pmod_full('pmod(Ph, S, 473)')
-        self._help_test_pmod_full('pmod(bel:Ph, Ser, 473)')
-        self._help_test_pmod_full('pmod(bel:Ph, S, 473)')
 
     def _help_test_non_standard_namespace(self, statement):
         result = self.parser.parseString(statement)
@@ -372,7 +368,7 @@ class TestTruncationParser(unittest.TestCase):
 
 class TestFusionParser(unittest.TestCase):
     def setUp(self):
-        identifier_parser = ConceptParser()
+        identifier_parser = ConceptParser(namespace_to_pattern={'HGNC': re.compile('.*')})
         identifier_qualified = identifier_parser.identifier_qualified
         self.parser = get_fusion_language(identifier_qualified)
 
@@ -505,7 +501,7 @@ class TestFusionParser(unittest.TestCase):
 
 class TestLocation(unittest.TestCase):
     def setUp(self):
-        identifier_parser = ConceptParser()
+        identifier_parser = ConceptParser(namespace_to_pattern={'GO': re.compile('.*')})
         identifier_qualified = identifier_parser.identifier_qualified
         self.parser = get_location_language(identifier_qualified)
 
diff --git a/tests/test_parse/test_parse_identifier.py b/tests/test_parse/test_parse_identifier.py
index 569072a3..e8553923 100644
--- a/tests/test_parse/test_parse_identifier.py
+++ b/tests/test_parse/test_parse_identifier.py
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 
+import re
 import unittest
 
 from pybel.constants import DIRTY
 from pybel.parser import ConceptParser
-from pybel.parser.exc import NakedNameWarning
+from pybel.parser.exc import MissingNamespaceRegexWarning, NakedNameWarning
 
 
 class _ParserMixin(unittest.TestCase):
@@ -23,7 +24,7 @@ class _ParserMixin(unittest.TestCase):
         }
 
 
-class TestIdentifierParser(_ParserMixin):
+class TestConceptEnumerated(_ParserMixin):
     def setUp(self):
         super().setUp()
         self.parser = ConceptParser(namespace_to_term_to_encoding=self.namespace_to_term)
@@ -67,7 +68,9 @@ class TestIdentifierParser(_ParserMixin):
             self.parser.parseString(s)
 
 
-class TestNamespaceParserDefault(_ParserMixin):
+class TestConceptParserDefault(_ParserMixin):
+    """Tests where the concept parser allows an enumerated list of unqualified entities."""
+
     def setUp(self):
         super().setUp()
         default_namespace = {'X', 'Y', 'W Z'}
@@ -82,6 +85,7 @@ class TestNamespaceParserDefault(_ParserMixin):
 
         self.assertIn('namespace', result)
         self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual('A', result['namespace'])
         self.assertEqual('3', result['name'])
 
@@ -97,6 +101,7 @@ class TestNamespaceParserDefault(_ParserMixin):
         result = self.parser.parseString(s)
 
         self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual('W Z', result['name'])
 
     def test_not_in_defaultNs(self):
@@ -105,7 +110,9 @@ class TestNamespaceParserDefault(_ParserMixin):
             self.parser.parseString(s)
 
 
-class TestNamespaceParserLenient(_ParserMixin):
+class TestConceptParserLenient(_ParserMixin):
+    """Tests where naked names are allowed."""
+
     def setUp(self):
         super().setUp()
         self.parser = ConceptParser(
@@ -119,6 +126,7 @@ class TestNamespaceParserLenient(_ParserMixin):
 
         self.assertIn('namespace', result)
         self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual('A', result['namespace'])
         self.assertEqual('3', result['name'])
 
@@ -128,6 +136,7 @@ class TestNamespaceParserLenient(_ParserMixin):
 
         self.assertIn('namespace', result)
         self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual('A', result['namespace'])
         self.assertEqual('3', result['name'])
 
@@ -145,11 +154,63 @@ class TestNamespaceParserLenient(_ParserMixin):
         s = 'bare'
         result = self.parser.parseString(s)
 
+        self.assertIn('namespace', result)
+        self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual(DIRTY, result['namespace'])
         self.assertEqual('bare', result['name'])
 
     def test_not_invalid_4(self):
         s = '"quoted"'
         result = self.parser.parseString(s)
+
+        self.assertIn('namespace', result)
+        self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
         self.assertEqual(DIRTY, result['namespace'])
         self.assertEqual('quoted', result['name'])
+
+
+class TestConceptParserRegex(unittest.TestCase):
+    """Tests for regular expression parsing"""
+
+    def setUp(self) -> None:
+        self.parser = ConceptParser(namespace_to_pattern={'hgnc': re.compile(r'\d+')})
+        self.assertEqual({}, self.parser.namespace_to_identifier_to_encoding)
+        self.assertEqual({}, self.parser.namespace_to_name_to_encoding)
+
+    def test_valid(self):
+        s = 'hgnc:391'
+        result = self.parser.parseString(s)
+
+        self.assertIn('namespace', result)
+        self.assertIn('name', result)
+        self.assertNotIn('identifier', result)
+        self.assertEqual('hgnc', result['namespace'])
+        self.assertEqual('391', result['name'])
+
+    def test_invalid(self):
+        """Test invalid BEL term."""
+        s = 'hgnc:AKT1'
+        with self.assertRaises(MissingNamespaceRegexWarning):
+            result = self.parser.parseString(s)
+            print(result.asDict())
+
+    def test_valid_obo(self):
+        """Test parsing an identifier that has a name."""
+        s = 'hgnc:391 ! AKT1'
+        result = self.parser.parseString(s)
+
+        self.assertIn('namespace', result)
+        self.assertIn('name', result)
+        self.assertIn('identifier', result)
+        self.assertEqual('hgnc', result['namespace'])
+        self.assertEqual('AKT1', result['name'])
+        self.assertEqual('391', result['identifier'])
+
+    def test_invalid_obo(self):
+        """Test parsing an OBO-style identifier where the identifier and name are switched."""
+        s = 'hgnc:AKT1 ! 391'
+        with self.assertRaises(MissingNamespaceRegexWarning):
+            result = self.parser.parseString(s)
+            print(result.asDict())