codecov[bot]: # [Codecov](https://codecov.io/gh/pybel/pybel/pull/384?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`develop@7e5b3cc`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##             develop     #384   +/-   ##
==========================================
  Coverage           ?   87.14%           
==========================================
  Files              ?      133           
  Lines              ?     6401           
  Branches           ?      850           
==========================================
  Hits               ?     5578           
  Misses             ?      637           
  Partials           ?      186
```


| [Impacted Files](https://codecov.io/gh/pybel/pybel/pull/384?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/pybel/tokens.py](https://codecov.io/gh/pybel/pybel/pull/384/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3Rva2Vucy5weQ==) | `86.88% <ø> (ø)` | |
| [src/pybel/parser/parse\_identifier.py](https://codecov.io/gh/pybel/pybel/pull/384/diff?src=pr&el=tree#diff-c3JjL3B5YmVsL3BhcnNlci9wYXJzZV9pZGVudGlmaWVyLnB5) | `94.11% <100%> (ø)` | |

------

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

diff --git a/src/pybel/parser/parse_concept.py b/src/pybel/parser/parse_concept.py
index 0ea18dda..c8e94cad 100644
--- a/src/pybel/parser/parse_concept.py
+++ b/src/pybel/parser/parse_concept.py
@@ -13,7 +13,7 @@ from .exc import (
     UndefinedNamespaceWarning,
 )
 from .utils import quote, word
-from ..constants import DIRTY, NAME, NAMESPACE
+from ..constants import DIRTY, IDENTIFIER, NAME, NAMESPACE
 
 __all__ = [
     'ConceptParser',
@@ -47,9 +47,18 @@ class ConceptParser(BaseParser):
         self.default_namespace = set(default_namespace) if default_namespace is not None else None
         self.allow_naked_names = allow_naked_names
 
+        self.identifier_fqualified = (
+            word(NAMESPACE) +
+            Suppress(':') +
+            (word | quote)(IDENTIFIER) +
+            Suppress('!') +
+            (word | quote)(NAME)
+        )
+
         self.identifier_qualified = word(NAMESPACE) + Suppress(':') + (word | quote)(NAME)
 
         if self.namespace_to_terms:
+            self.identifier_fqualified.setParseAction(self.handle_identifier_qualified)
             self.identifier_qualified.setParseAction(self.handle_identifier_qualified)
 
         self.identifier_bare = (word | quote)(NAME)
@@ -60,7 +69,9 @@ class ConceptParser(BaseParser):
             self.handle_namespace_invalid
         )
 
-        super(ConceptParser, self).__init__(self.identifier_qualified | self.identifier_bare)
+        super().__init__(
+            self.identifier_fqualified | self.identifier_qualified | self.identifier_bare
+        )
 
     def has_enumerated_namespace(self, namespace: str) -> bool:
         """Check that the namespace has been defined by an enumeration."""

Add OBO-style name/identifier parsing
As a proof of concept, try parsing the identifier and name delimited by a `!` as in `p(HGNC:6893!MAPT)`. Quotes should be allowed on both the identifier and the name.

Should whitespace should be allowed?

`p(HGNC:6893 ! MAPT)`


diff --git a/tests/test_parse/test_parse_bel.py b/tests/test_parse/test_parse_bel.py
index 4a599f98..e76cc71d 100644
--- a/tests/test_parse/test_parse_bel.py
+++ b/tests/test_parse/test_parse_bel.py
@@ -9,7 +9,7 @@ from pybel import BELGraph
 from pybel.constants import (
     ABUNDANCE, ACTIVITY, BEL_DEFAULT_NAMESPACE, BIOPROCESS, COMPLEX, COMPOSITE, CONCEPT, DEGRADATION,
     DIRECTLY_INCREASES, DIRTY, EFFECT, FRAGMENT, FROM_LOC, FUNCTION, FUSION, FUSION_MISSING, FUSION_REFERENCE,
-    FUSION_START, FUSION_STOP, GENE, HAS_COMPONENT, HAS_VARIANT, HGVS, KIND, LOCATION, MEMBERS, MIRNA,
+    FUSION_START, FUSION_STOP, GENE, HAS_COMPONENT, HAS_VARIANT, HGVS, IDENTIFIER, KIND, LOCATION, MEMBERS, MIRNA,
     MODIFIER, NAME, NAMESPACE, OBJECT, PARTNER_3P, PARTNER_5P, PATHOLOGY, PRODUCTS, PROTEIN, RANGE_3P, RANGE_5P,
     REACTANTS, REACTION, RELATION, RNA, SUBJECT, TARGET, TO_LOC, TRANSLOCATION, VARIANTS,
 )
@@ -89,6 +89,71 @@ class TestAbundance(TestTokenParserBase):
         self._test_abundance_with_location_helper('abundance(CHEBI:"oxygen atom", location(GO:intracellular))')
 
 
+class TestAbundanceLabeled(TestTokenParserBase):
+    """2.1.1"""
+
+    def setUp(self):
+        self.parser.clear()
+        self.parser.general_abundance.setParseAction(self.parser.handle_term)
+
+        self.expected_node_data = abundance(namespace='chebi', name='oxygen atom', identifier='CHEBI:25805')
+        self.expected_canonical_bel = 'a(chebi:"oxygen atom")'
+
+    def _test_abundance_helper(self, statement):
+        result = self.parser.general_abundance.parseString(statement)
+        self.assertEqual(dict(self.expected_node_data), result.asDict())
+
+        self.assertIn(self.expected_node_data, self.graph)
+        self.assertEqual(self.expected_canonical_bel, self.graph.node_to_bel(self.expected_node_data))
+
+        self.assertEqual({}, modifier_po_to_dict(result), msg='The modifier dictionary should be empty')
+
+    def test_abundance(self):
+        """Test short/long abundance name."""
+        self._test_abundance_helper('a(chebi:"CHEBI:25805"!"oxygen atom")')
+        self._test_abundance_helper('abundance(chebi:"CHEBI:25805"!"oxygen atom")')
+        self._test_abundance_helper('abundance(chebi:"CHEBI:25805" ! "oxygen atom")')
+
+    def _test_abundance_with_location_helper(self, statement):
+        result = self.parser.general_abundance.parseString(statement)
+
+        expected_result = {
+            FUNCTION: ABUNDANCE,
+            CONCEPT: {
+                NAMESPACE: 'chebi',
+                NAME: 'oxygen atom',
+                IDENTIFIER: 'CHEBI:25805',
+            },
+            LOCATION: {
+                NAMESPACE: 'GO',
+                NAME: 'intracellular',
+            }
+        }
+
+        self.assertEqual(expected_result, result.asDict())
+
+        self.assertIn(self.expected_node_data, self.graph)
+        self.assertEqual(self.expected_canonical_bel, self.graph.node_to_bel(self.expected_node_data))
+
+        modifier = modifier_po_to_dict(result)
+        expected_modifier = {
+            LOCATION: {
+                NAMESPACE: 'GO',
+                NAME: 'intracellular',
+            }
+        }
+        self.assertEqual(expected_modifier, modifier)
+
+    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(
+            'abundance(chebi:"CHEBI:25805"!"oxygen atom", location(GO:intracellular))'
+        )
+
+
 class TestGene(TestTokenParserBase):
     """2.1.4 http://openbel.org/language/web/version_2.0/bel_specification_version_2.0.html#XgeneA"""
 

pip install -e .[neo4j,indra]
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
backcall==0.2.0
bel-resources==0.0.3
boto3==1.33.13
botocore==1.33.13
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
click==8.1.8
click-plugins==1.1.1
decorator==5.1.1
enum34==1.1.10
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
future==1.0.0
greenlet==3.1.1
idna==3.10
ijson==3.3.0
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
indra==1.22.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
interchange==2021.0.4
ipython==7.34.0
jedi==0.19.2
Jinja2==3.1.6
jmespath==1.0.1
lxml==5.3.1
MarkupSafe==2.0.1
matplotlib-inline==0.1.6
monotonic==1.6
mpmath==1.3.0
multisplitby==0.0.1
ndex2==2.0.1
networkx==2.6.3
numpy==1.21.6
objectpath==0.6.1
obonet==1.0.0
packaging @ file:///croot/packaging_1671697413597/work
pandas==1.2.5
pansi==2024.11.0
parso==0.8.4
pexpect==4.9.0
pickle5==0.0.12
pickleshare==0.7.5
Pillow==9.5.0
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
prompt_toolkit==3.0.48
protmapper==0.0.29
ptyprocess==0.7.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
py2neo==2021.2.4
-e git+https://github.com/pybel/pybel.git@11dd8c4d9008e3c0ea6c8b5fcb20e272215483b7#egg=pybel
pybiopax==0.1.5
Pygments==2.17.2
pyparsing==3.1.4
pysb==1.16.0
pysolr==3.10.0
pystow==0.5.6
pytest==7.1.2
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.31.0
requests-file==2.1.0
requests-toolbelt==1.0.0
s3transfer==0.8.2
scipy==1.7.3
six==1.17.0
SQLAlchemy==2.0.40
sympy==1.10.1
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tqdm==4.67.1
traitlets==5.9.0
typing_extensions==4.7.1
urllib3==1.26.20
wcwidth==0.2.13
zipp @ file:///croot/zipp_1672387121353/work

name: pybel
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - backcall==0.2.0
      - bel-resources==0.0.3
      - boto3==1.33.13
      - botocore==1.33.13
      - charset-normalizer==3.4.1
      - click==8.1.8
      - click-plugins==1.1.1
      - decorator==5.1.1
      - enum34==1.1.10
      - future==1.0.0
      - greenlet==3.1.1
      - idna==3.10
      - ijson==3.3.0
      - indra==1.22.0
      - interchange==2021.0.4
      - ipython==7.34.0
      - jedi==0.19.2
      - jinja2==3.1.6
      - jmespath==1.0.1
      - lxml==5.3.1
      - markupsafe==2.0.1
      - matplotlib-inline==0.1.6
      - monotonic==1.6
      - mpmath==1.3.0
      - multisplitby==0.0.1
      - ndex2==2.0.1
      - networkx==2.6.3
      - numpy==1.21.6
      - objectpath==0.6.1
      - obonet==1.0.0
      - pandas==1.2.5
      - pansi==2024.11.0
      - parso==0.8.4
      - pexpect==4.9.0
      - pickle5==0.0.12
      - pickleshare==0.7.5
      - pillow==9.5.0
      - prompt-toolkit==3.0.48
      - protmapper==0.0.29
      - ptyprocess==0.7.0
      - py2neo==2021.2.4
      - pybiopax==0.1.5
      - pygments==2.17.2
      - pyparsing==3.1.4
      - pysb==1.16.0
      - pysolr==3.10.0
      - pystow==0.5.6
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.31.0
      - requests-file==2.1.0
      - requests-toolbelt==1.0.0
      - s3transfer==0.8.2
      - scipy==1.7.3
      - six==1.17.0
      - sqlalchemy==2.0.40
      - sympy==1.10.1
      - tqdm==4.67.1
      - traitlets==5.9.0
      - typing-extensions==4.7.1
      - urllib3==1.26.20
      - wcwidth==0.2.13
prefix: /opt/conda/envs/pybel

tests/test_parse/test_parse_bel.py::TestAbundanceLabeled::test_abundance tests/test_parse/test_parse_bel.py::TestAbundanceLabeled::test_abundance_with_location
tests/test_parse/test_parse_bel.py::TestAbundance::test_abundance tests/test_parse/test_parse_bel.py::TestAbundance::test_abundance_with_location tests/test_parse/test_parse_bel.py::TestGene::test_gene_fusion_1 tests/test_parse/test_parse_bel.py::TestGene::test_gene_fusion_2 tests/test_parse/test_parse_bel.py::TestGene::test_gene_fusion_3 tests/test_parse/test_parse_bel.py::TestGene::test_gene_fusion_legacy_1 tests/test_parse/test_parse_bel.py::TestGene::test_gene_fusion_legacy_2 tests/test_parse/test_parse_bel.py::TestGene::test_gene_simple tests/test_parse/test_parse_bel.py::TestGene::test_gene_variant_chromosome tests/test_parse/test_parse_bel.py::TestGene::test_gene_variant_deletion tests/test_parse/test_parse_bel.py::TestGene::test_gene_variant_snp tests/test_parse/test_parse_bel.py::TestGene::test_gene_with_gmod tests/test_parse/test_parse_bel.py::TestGene::test_gene_with_hgvs tests/test_parse/test_parse_bel.py::TestGene::test_gene_with_location tests/test_parse/test_parse_bel.py::TestGene::test_gene_with_substitution tests/test_parse/test_parse_bel.py::TestGene::test_gene_with_substitution_and_location tests/test_parse/test_parse_bel.py::TestGene::test_multiple_variants tests/test_parse/test_parse_bel.py::TestMicroRna::test_mirna_location tests/test_parse/test_parse_bel.py::TestMicroRna::test_mirna_variant tests/test_parse/test_parse_bel.py::TestMicroRna::test_mirna_variant_location tests/test_parse/test_parse_bel.py::TestMicroRna::test_short tests/test_parse/test_parse_bel.py::TestProtein::test_ensure_no_dup_edges tests/test_parse/test_parse_bel.py::TestProtein::test_multiple_variants tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fragment_descriptor tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fragment_known tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fragment_unboundTerminal tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fragment_unbounded tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fragment_unknown tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fusion_1 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fusion_legacy_1 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_fusion_legacy_2 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_pmod_1 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_pmod_2 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_pmod_3 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_pmod_4 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_trunc_1 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_trunc_2 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_trunc_3 tests/test_parse/test_parse_bel.py::TestProtein::test_protein_variant_deletion tests/test_parse/test_parse_bel.py::TestProtein::test_protein_variant_reference tests/test_parse/test_parse_bel.py::TestProtein::test_protein_variant_substitution tests/test_parse/test_parse_bel.py::TestProtein::test_protein_variant_unspecified tests/test_parse/test_parse_bel.py::TestProtein::test_protein_with_location tests/test_parse/test_parse_bel.py::TestProtein::test_reference tests/test_parse/test_parse_bel.py::TestRna::test_multiple_variants tests/test_parse/test_parse_bel.py::TestRna::test_reference tests/test_parse/test_parse_bel.py::TestRna::test_rna_fusion_known_breakpoints tests/test_parse/test_parse_bel.py::TestRna::test_rna_fusion_legacy_1 tests/test_parse/test_parse_bel.py::TestRna::test_rna_fusion_unspecified_breakpoints tests/test_parse/test_parse_bel.py::TestRna::test_rna_variant_codingReference tests/test_parse/test_parse_bel.py::TestComplex::test_complex_list_long tests/test_parse/test_parse_bel.py::TestComplex::test_complex_list_short tests/test_parse/test_parse_bel.py::TestComplex::test_named_complex_singleton tests/test_parse/test_parse_bel.py::TestComposite::test_213a tests/test_parse/test_parse_bel.py::TestBiologicalProcess::test_231a tests/test_parse/test_parse_bel.py::TestPathology::test_232a tests/test_parse/test_parse_bel.py::TestActivity::test_activity_bare tests/test_parse/test_parse_bel.py::TestActivity::test_activity_legacy tests/test_parse/test_parse_bel.py::TestActivity::test_activity_on_listed_complex tests/test_parse/test_parse_bel.py::TestActivity::test_activity_on_named_complex tests/test_parse/test_parse_bel.py::TestActivity::test_activity_withMolecularActivityCustom tests/test_parse/test_parse_bel.py::TestActivity::test_activity_withMolecularActivityDefault tests/test_parse/test_parse_bel.py::TestActivity::test_activity_withMolecularActivityDefaultLong tests/test_parse/test_parse_bel.py::TestActivity::test_kinase_activity_on_listed_complex tests/test_parse/test_parse_bel.py::TestActivity::test_kinase_activity_on_named_complex tests/test_parse/test_parse_bel.py::TestTranslocationPermissive::test_unqualified_translocation_relation tests/test_parse/test_parse_bel.py::TestTranslocationPermissive::test_unqualified_translocation_single tests/test_parse/test_parse_bel.py::TestTransformation::test_clearance tests/test_parse/test_parse_bel.py::TestTransformation::test_degradation_long tests/test_parse/test_parse_bel.py::TestTransformation::test_degradation_short tests/test_parse/test_parse_bel.py::TestTransformation::test_reaction_1 tests/test_parse/test_parse_bel.py::TestTransformation::test_reaction_2 tests/test_parse/test_parse_bel.py::TestTransformation::test_translocation_bare tests/test_parse/test_parse_bel.py::TestTransformation::test_translocation_secretion tests/test_parse/test_parse_bel.py::TestTransformation::test_translocation_standard tests/test_parse/test_parse_bel.py::TestTransformation::test_translocation_surface tests/test_parse/test_parse_bel.py::TestTransformation::test_unqualified_translocation_strict tests/test_parse/test_parse_bel.py::TestSemantics::test_lenient_semantic_no_failure
src/pybel/parser/parse_concept.py
src/pybel/parser/parse_concept.py