class ParserTestV0_9_5(unittest.TestCase): @classmethod def setUpClass(cls): cls.test_filters = [] for fn in sorted(glob(os.path.join(testfile_dir, "*.inp"))): with open(fn) as f: cls.test_filters.append(f.read().strip()) def setUp(self): self.parser = LarkParser(version=(0, 9, 5)) def test_inputs(self): for tf in self.test_filters: if tf == "filter=number=0.0.1": self.assertRaises(ParserError, self.parser.parse, tf) else: tree = self.parser.parse(tf) self.assertTrue(tree, Tree) def test_parser_version(self): v = (0, 9, 5) p = LarkParser(version=v) self.assertIsInstance(p.parse(self.test_filters[0]), Tree) self.assertEqual(p.version, v) def test_repr(self): self.assertIsNotNone(repr(self.parser)) self.parser.parse(self.test_filters[0]) self.assertIsNotNone(repr(self.parser))
class TestParserV0_9_5: @pytest.fixture(autouse=True) def set_up(self): self.test_filters = [] for fn in sorted(glob(os.path.join(testfile_dir, "*.inp"))): with open(fn) as f: self.test_filters.append(f.read().strip()) self.parser = LarkParser(version=(0, 9, 5)) def test_inputs(self): for tf in self.test_filters: if tf == "filter=number=0.0.1": with pytest.raises(ParserError): self.parser.parse(tf) else: tree = self.parser.parse(tf) assert isinstance(tree, Tree) def test_parser_version(self): v = (0, 9, 5) p = LarkParser(version=v) assert isinstance(p.parse(self.test_filters[0]), Tree) assert p.version == v def test_repr(self): assert repr(self.parser) is not None self.parser.parse(self.test_filters[0]) assert repr(self.parser) is not None
def test_mongo_special_id(self, mapper): from optimade.filtertransformers.mongo import MongoTransformer from bson import ObjectId class MyMapper(mapper("StructureMapper")): ALIASES = (("immutable_id", "_id"), ) transformer = MongoTransformer(mapper=MyMapper()) parser = LarkParser(version=self.version, variant=self.variant) assert transformer.transform( parser.parse('immutable_id = "5cfb441f053b174410700d02"')) == { "_id": { "$eq": ObjectId("5cfb441f053b174410700d02") } } assert transformer.transform( parser.parse('immutable_id != "5cfb441f053b174410700d02"')) == { "_id": { "$ne": ObjectId("5cfb441f053b174410700d02") } } for op in ("CONTAINS", "STARTS WITH", "ENDS WITH", "HAS"): with pytest.raises( BadRequest, match= r".*not supported for query on field 'immutable_id', can only test for equality.*", ): transformer.transform( parser.parse(f'immutable_id {op} "abcdef"'))
def __init__( self, resource_cls: EntryResource, resource_mapper: BaseResourceMapper, transformer: Transformer, ): """Initialize the collection for the given parameters. Parameters: resource_cls (EntryResource): The `EntryResource` model that is stored by the collection. resource_mapper (BaseResourceMapper): A resource mapper object that handles aliases and format changes between deserialization and response. transformer (Transformer): The Lark `Transformer` used to interpret the filter. """ self.parser = LarkParser() self.resource_cls = resource_cls self.resource_mapper = resource_mapper self.transformer = transformer self.provider_prefix = CONFIG.provider.prefix self.provider_fields = [ field if isinstance(field, str) else field["name"] for field in CONFIG.provider_fields.get(resource_mapper.ENDPOINT, []) ] self._all_fields: Set[str] = None
def set_up(self): from optimade.filtertransformers.elasticsearch import Transformer, Quantity self.parser = LarkParser(version=(0, 10, 0), variant="elastic") nelements = Quantity(name="nelements") elements_only = Quantity(name="elements_only") elements_ratios = Quantity(name="elements_ratios") elements_ratios.nested_quantity = elements_ratios elements = Quantity( name="elements", length_quantity=nelements, has_only_quantity=elements_only, nested_quantity=elements_ratios, ) dimension_types = Quantity(name="dimension_types") dimension_types.length_quantity = dimension_types quantities = [ nelements, elements_only, elements_ratios, elements, dimension_types, Quantity(name="chemical_formula_reduced"), ] self.transformer = Transformer(quantities=quantities)
def __init__( self, collection, resource_cls: EntryResource, resource_mapper: BaseResourceMapper, transformer: Transformer, ): """Initialize the collection for the given parameters. Parameters: collection: The backend-specific collection. resource_cls (EntryResource): The `EntryResource` model that is stored by the collection. resource_mapper (BaseResourceMapper): A resource mapper object that handles aliases and format changes between deserialization and response. transformer (Transformer): The Lark `Transformer` used to interpret the filter. """ self.collection = collection self.parser = LarkParser() self.resource_cls = resource_cls self.resource_mapper = resource_mapper self.transformer = transformer self.provider_prefix = CONFIG.provider.prefix self.provider_fields = CONFIG.provider_fields.get( resource_mapper.ENDPOINT, [])
def test_list_length_aliases(self, mapper): from optimade.filtertransformers.mongo import MongoTransformer transformer = MongoTransformer(mapper=mapper("StructureMapper")()) parser = LarkParser(version=self.version, variant=self.variant) assert transformer.transform(parser.parse("elements LENGTH 3")) == { "nelements": 3 } assert transformer.transform( parser.parse('elements HAS "Li" AND elements LENGTH = 3') ) == {"$and": [{"elements": {"$in": ["Li"]}}, {"nelements": 3}]} assert transformer.transform(parser.parse("elements LENGTH > 3")) == { "nelements": {"$gt": 3} } assert transformer.transform(parser.parse("elements LENGTH < 3")) == { "nelements": {"$lt": 3} } assert transformer.transform(parser.parse("elements LENGTH = 3")) == { "nelements": 3 } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH <= 3") ) == {"nsites": {"$lte": 3}} assert transformer.transform( parser.parse("cartesian_site_positions LENGTH >= 3") ) == {"nsites": {"$gte": 3}}
def __init__(self): self.opers = { "=": self.eq, ">": self.gt, ">=": self.ge, "<": self.lt, "<=": self.le, "!=": self.ne, "OR": self.or_, "AND": self.and_, "NOT": self.not_, } self.parser = LarkParser(version=(0, 9, 7))
def test_other_provider_fields(self, mapper): """Test that fields from other providers generate queries that treat the value of the field as `null`. """ from optimade.filtertransformers.mongo import MongoTransformer t = MongoTransformer(mapper=mapper("StructureMapper")) p = LarkParser(version=self.version, variant=self.variant) assert t.transform(p.parse("_other_provider_field > 1")) == { "_other_provider_field": { "$gt": 1 } }
def __init__(self): p = LarkParser(version=(1, 0, 0), variant="default") t = MongoTransformer() self.transform = lambda inp: t.transform(p.parse(inp)) client = MongoClient('mongodb://{}:{}@{}:{}/?authSource={}'.format( "admin", "admin", "localhost", "27017", "admin")) db = client["MaterialsDB"] self.cl = db["Data.Calculation.StaticCalculation"] self.lu = Lower2Upper() self.data = info1 self.info = info2
def __init__( self, name: str, resource_cls: EntryResource, resource_mapper: BaseResourceMapper, database: str = CONFIG.mongo_database, ): """Initialize the MongoCollection for the given parameters. Parameters: name: The name of the collection. resource_cls: The type of entry resource that is stored by the collection. resource_mapper: A resource mapper object that handles aliases and format changes between deserialization and response. database: The name of the underlying MongoDB database to connect to. """ super().__init__( resource_cls, resource_mapper, MongoTransformer(mapper=resource_mapper), ) self.parser = LarkParser(version=(1, 0, 0), variant="default") self.collection = CLIENT[database][name] # check aliases do not clash with mongo operators self._check_aliases(self.resource_mapper.all_aliases()) self._check_aliases(self.resource_mapper.all_length_aliases())
def __init__( self, name: str, resource_cls: "EntryResource", resource_mapper: "BaseResourceMapper", ): """Initialize the AsyncMongoCollection for the given parameters. Parameters: name: The name of the collection. resource_cls: The `EntryResource` model that is stored by the collection. resource_mapper: A resource mapper object that handles aliases and format changes between deserialization and response. """ from optimade_gateway.mongo.database import ( # pylint: disable=import-outside-toplevel MONGO_DB, ) super().__init__( resource_cls=resource_cls, resource_mapper=resource_mapper, transformer=MongoTransformer(mapper=resource_mapper), ) self.parser = LarkParser(version=(1, 0, 0), variant="default") self.collection: MongoCollection = MONGO_DB[name] # Check aliases do not clash with mongo operators self._check_aliases(self.resource_mapper.all_aliases()) self._check_aliases(self.resource_mapper.all_length_aliases())
def __init__( self, collection: Union[pymongo.collection.Collection, mongomock.collection.Collection], resource_cls: EntryResource, resource_mapper: BaseResourceMapper, ): """Initialize the MongoCollection for the given parameters. Parameters: collection (Union[pymongo.collection.Collection, mongomock.collection.Collection]): The backend-specific collection. resource_cls (EntryResource): The `EntryResource` model that is stored by the collection. resource_mapper (BaseResourceMapper): A resource mapper object that handles aliases and format changes between deserialization and response. """ super().__init__( collection, resource_cls, resource_mapper, MongoTransformer(mapper=resource_mapper), ) self.parser = LarkParser( version=(0, 10, 1), variant="default" ) # The MongoTransformer only supports v0.10.1 as the latest grammar # check aliases do not clash with mongo operators self._check_aliases(self.resource_mapper.all_aliases()) self._check_aliases(self.resource_mapper.all_length_aliases())
def __init__( self, collection: Union[pymongo.collection.Collection, mongomock.collection.Collection], resource_cls: EntryResource, resource_mapper: ResourceMapper, ): super().__init__(collection, resource_cls, resource_mapper) self.transformer = MongoTransformer() self.provider = CONFIG.provider["prefix"] self.provider_fields = CONFIG.provider_fields.get( resource_mapper.ENDPOINT, []) self.parser = LarkParser( version=(0, 10, 1), variant="default" ) # The MongoTransformer only supports v0.10.1 as the latest grammar
def __init__( self, collection, resource_cls: EntryResource, resource_mapper: ResourceMapper ): self.collection = collection self.parser = LarkParser() self.resource_cls = resource_cls self.resource_mapper = resource_mapper
def test_list_length_aliases(self): from optimade.server.mappers import StructureMapper transformer = MongoTransformer(mapper=StructureMapper()) parser = LarkParser(version=self.version, variant=self.variant) self.assertEqual( transformer.transform(parser.parse("elements LENGTH 3")), {"nelements": 3}) self.assertEqual( transformer.transform( parser.parse('elements HAS "Li" AND elements LENGTH = 3')), {"$and": [{ "elements": { "$in": ["Li"] } }, { "nelements": 3 }]}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH > 3")), {"nelements": { "$gt": 3 }}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH < 3")), {"nelements": { "$lt": 3 }}, ) self.assertEqual( transformer.transform(parser.parse("elements LENGTH = 3")), {"nelements": 3}) self.assertEqual( transformer.transform( parser.parse("cartesian_site_positions LENGTH <= 3")), {"nsites": { "$lte": 3 }}, ) self.assertEqual( transformer.transform( parser.parse("cartesian_site_positions LENGTH >= 3")), {"nsites": { "$gte": 3 }}, )
def test_list_length_aliases(): """Check LENGTH aliases for lists""" from optimade.server.mappers import StructureMapper transformer = AiidaTransformer(mapper=StructureMapper()) parser = LarkParser(version=VERSION, variant=VARIANT) assert transformer.transform(parser.parse("elements LENGTH 3")) == { "nelements": 3 } assert transformer.transform( parser.parse('elements HAS "Li" AND elements LENGTH = 3')) == { "and": [{ "elements": { "contains": ["Li"] } }, { "nelements": 3 }] } assert transformer.transform(parser.parse("elements LENGTH > 3")) == ({ "nelements": { ">": 3 } }) assert transformer.transform(parser.parse("elements LENGTH < 3")) == ({ "nelements": { "<": 3 } }) assert transformer.transform(parser.parse("elements LENGTH = 3")) == { "nelements": 3 } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH <= 3")) == { "nsites": { "<=": 3 } } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH >= 3")) == { "nsites": { ">=": 3 } }
def __init__( self, collection: orm.entities.Collection, resource_cls: EntryResource, resource_mapper: ResourceMapper, ): super().__init__(collection, resource_cls, resource_mapper) self.transformer = AiidaTransformerV0_10_1() self.provider = CONFIG.provider["prefix"] self.provider_fields = CONFIG.provider_fields[resource_mapper.ENDPOINT] self.page_limit = CONFIG.page_limit self.db_page_limit = CONFIG.db_page_limit self.parser = LarkParser(version=(0, 10, 0)) # "Cache" self._data_available: int = None self._data_returned: int = None self._filter_fields: set = None self._latest_filter: dict = None
def __init__( self, collection: Union[pymongo.collection.Collection, mongomock.collection.Collection], resource_cls: EntryResource, resource_mapper: BaseResourceMapper, ): super().__init__(collection, resource_cls, resource_mapper) self.transformer = MongoTransformer(mapper=resource_mapper) self.provider_prefix = CONFIG.provider.prefix self.provider_fields = CONFIG.provider_fields.get( resource_mapper.ENDPOINT, []) self.parser = LarkParser( version=(0, 10, 1), variant="default" ) # The MongoTransformer only supports v0.10.1 as the latest grammar # check aliases do not clash with mongo operators self._check_aliases(self.resource_mapper.all_aliases()) self._check_aliases(self.resource_mapper.all_length_aliases())
def __init__( self, entity: Entity, resource_cls: EntryResource, resource_mapper: ResourceMapper, ): self.entity = entity self.parser = LarkParser() self.resource_cls = resource_cls self.resource_mapper = resource_mapper self.transformer = AiidaTransformer() self.provider = CONFIG.provider.prefix self.provider_fields = CONFIG.provider_fields[resource_mapper.ENDPOINT] self.parser = LarkParser() # "Cache" self._data_available: int = None self._data_returned: int = None self._filter_fields: set = None self._latest_filter: dict = None
def setUpClass(cls): parser = LarkParser(version=(0, 9, 7)) transformer = MongoTransformer() def convert(_, q): parsed = parser.parse(q) return transformer.transform(parsed) cls.convert = convert cls.client = MongoClient() cls.db = cls.client[f"test_db_{uuid.uuid4()}"] cls.coll = cls.db.data cls.coll.insert_many([{ "a": a, "b": b } for a, b in itertools.product(range(10), range(10))])
class BaseTestFilterParser(abc.ABC): """Base class for parsing different versions of the grammar using `LarkParser`.""" version: Tuple[int, int, int] variant: str = "default" @pytest.fixture(autouse=True) def set_up(self): self.parser = LarkParser(version=self.version, variant=self.variant) def test_repr(self): assert repr(self.parser) is not None self.parse("band_gap = 1") assert repr(self.parser) is not None def parse(self, inp): return self.parser.parse(inp) def test_parser_version(self): assert self.parser.version == self.version assert self.parser.variant == self.variant
return "string" def number(self, arg): # number: SIGNED_INT | SIGNED_FLOAT print("Node: ", "number", arg) return "number" def __default__(self, data, children, meta): print("Node: ", data, children) return data if __name__ == "__main__": # pragma: no cover from optimade.filterparser import LarkParser p = LarkParser(version=(0, 10, 0)) # t = DebugTransformer() t = TransformerSkeleton() # f = 'a.a = "text" OR a<a AND NOT b>=8' # single list f = "list HAS < 3" f = "list HAS < 3, > 4" # -> error f = "list HAS ALL < 3, > 4" # multiple lists f = "list1:list2 HAS < 3 : > 4"
def test_filtering_on_relationships(self, mapper): """Test the nested properties with special names like "structures", "references" etc. are applied to the relationships field. """ from optimade.filtertransformers.mongo import MongoTransformer t = MongoTransformer(mapper=mapper("StructureMapper")) p = LarkParser(version=self.version, variant=self.variant) assert t.transform(p.parse('references.id HAS "dummy/2019"')) == { "relationships.references.data.id": { "$in": ["dummy/2019"] } } assert t.transform( p.parse('structures.id HAS ANY "dummy/2019", "dijkstra1968"')) == { "relationships.structures.data.id": { "$in": ["dummy/2019", "dijkstra1968"] } } assert t.transform( p.parse('structures.id HAS ALL "dummy/2019", "dijkstra1968"')) == { "relationships.structures.data.id": { "$all": ["dummy/2019", "dijkstra1968"] } } assert t.transform(p.parse('structures.id HAS ONLY "dummy/2019"')) == { "$and": [ { "relationships.structures.data": { "$size": 1 } }, { "relationships.structures.data.id": { "$all": ["dummy/2019"] } }, ] } assert t.transform( p.parse( 'structures.id HAS ONLY "dummy/2019" AND structures.id HAS "dummy/2019"' )) == { "$and": [ { "$and": [ { "relationships.structures.data": { "$size": 1, } }, { "relationships.structures.data.id": { "$all": ["dummy/2019"] } }, ] }, { "relationships.structures.data.id": { "$in": ["dummy/2019"] } }, ], }
def set_up(self): from optimade.filtertransformers.mongo import MongoTransformer p = LarkParser(version=self.version, variant=self.variant) t = MongoTransformer() self.transform = lambda inp: t.transform(p.parse(inp))
class DjangoTransformer: """Filter transformer for implementations using Django. !!! warning "Warning" This transformer is deprecated as it only supports the 0.9.7 grammar version, and works different to other filter transformers in this package. """ def __init__(self): self.opers = { "=": self.eq, ">": self.gt, ">=": self.ge, "<": self.lt, "<=": self.le, "!=": self.ne, "OR": self.or_, "AND": self.and_, "NOT": self.not_, } self.parser = LarkParser(version=(0, 9, 7)) def parse_raw_q(self, raw_query): return self.parser.parse(raw_query) def eq(self, a, b): return Q(**{a: b}) def gt(self, a, b): return Q(**{a + "__gt": b}) def ge(self, a, b): return Q(**{a + "__gte": b}) def lt(self, a, b): return Q(**{a + "__lt": b}) def le(self, a, b): return Q(**{a + "__lte": b}) def ne(self, a, b): return ~Q(**{a: b}) def not_(self, a): return ~a def and_(self, a, b): return operator.and_(a, b) def or_(self, a, b): return operator.or_(a, b) def evaluate(self, parse_Tree): if isinstance(parse_Tree, Tree): children = parse_Tree.children if len(children) == 1: return self.evaluate(children[0]) elif len(children) == 2: op_fn = self.evaluate(children[0]) return op_fn(self.evaluate(children[1])) elif len(children) == 3: if parse_Tree.data == "comparison": db_prop = self.evaluate(children[0]) op_fn = self.evaluate(children[1]) if db_prop in django_db_keys.keys(): return op_fn(django_db_keys[db_prop], self.evaluate(children[2])) else: raise DjangoQueryError( "Unknown property is queried : " + (db_prop)) else: op_fn = self.evaluate(children[1]) return op_fn(self.evaluate(children[0]), self.evaluate(children[2])) else: raise DjangoQueryError( "Not compatible format. Tree has >3 children") elif isinstance(parse_Tree, Token): if parse_Tree.type == "VALUE": return parse_Tree.value elif parse_Tree.type in ["NOT", "CONJUNCTION", "OPERATOR"]: return self.opers[parse_Tree.value] else: raise DjangoQueryError("Not a Lark Tree or Token")
def test_suspected_timestamp_fields(self, mapper): import datetime import bson.tz_util from optimade.filtertransformers.mongo import MongoTransformer from optimade.server.warnings import TimestampNotRFCCompliant example_RFC3339_date = "2019-06-08T04:13:37Z" example_RFC3339_date_2 = "2019-06-08T04:13:37" example_non_RFC3339_date = "2019-06-08T04:13:37.123Z" expected_datetime = datetime.datetime( year=2019, month=6, day=8, hour=4, minute=13, second=37, microsecond=0, tzinfo=bson.tz_util.utc, ) assert self.transform(f'last_modified > "{example_RFC3339_date}"') == { "last_modified": { "$gt": expected_datetime } } assert self.transform( f'last_modified > "{example_RFC3339_date_2}"') == { "last_modified": { "$gt": expected_datetime } } non_rfc_datetime = expected_datetime.replace(microsecond=123000) with pytest.warns(TimestampNotRFCCompliant): assert self.transform( f'last_modified > "{example_non_RFC3339_date}"') == { "last_modified": { "$gt": non_rfc_datetime } } class MyMapper(mapper("StructureMapper")): ALIASES = (("last_modified", "ctime"), ) transformer = MongoTransformer(mapper=MyMapper) parser = LarkParser(version=self.version, variant=self.variant) assert transformer.transform( parser.parse(f'last_modified > "{example_RFC3339_date}"')) == { "ctime": { "$gt": expected_datetime } } assert transformer.transform( parser.parse(f'last_modified > "{example_RFC3339_date_2}"')) == { "ctime": { "$gt": expected_datetime } }
def parser(): return LarkParser()
def test_aliases(self, mapper): """Test that valid aliases are allowed, but do not affect r-values.""" from optimade.filtertransformers.mongo import MongoTransformer class MyStructureMapper(mapper("StructureMapper")): ALIASES = ( ("elements", "my_elements"), ("A", "D"), ("property_a", "D"), ("B", "E"), ("C", "F"), ("_exmpl_nested_field", "nested_field"), ) PROVIDER_FIELDS = ("D", "E", "F", "nested_field") mapper = MyStructureMapper t = MongoTransformer(mapper=mapper) p = LarkParser(version=self.version, variant=self.variant) assert mapper.get_backend_field("elements") == "my_elements" test_filter = 'elements HAS "A"' assert t.transform(p.parse(test_filter)) == { "my_elements": { "$in": ["A"] } } test_filter = 'elements HAS ANY "A","B","C" AND elements HAS "D"' assert t.transform(p.parse(test_filter)) == { "$and": [ { "my_elements": { "$in": ["A", "B", "C"] } }, { "my_elements": { "$in": ["D"] } }, ] } test_filter = 'elements = "A"' assert t.transform(p.parse(test_filter)) == { "my_elements": { "$eq": "A" } } test_filter = 'property_a HAS "B"' assert t.transform(p.parse(test_filter)) == {"D": {"$in": ["B"]}} test_filter = "_exmpl_nested_field.sub_property > 1234.5" assert t.transform(p.parse(test_filter)) == { "nested_field.sub_property": { "$gt": 1234.5 } } test_filter = "_exmpl_nested_field.sub_property.x IS UNKNOWN" assert t.transform(p.parse(test_filter)) == { "$or": [ { "nested_field.sub_property.x": { "$exists": False } }, { "nested_field.sub_property.x": { "$eq": None } }, ] }
def test_aliased_length_operator(self, mapper): from optimade.filtertransformers.mongo import MongoTransformer class MyMapper(mapper("StructureMapper")): ALIASES = ( ("elements", "my_elements"), ("nelements", "nelem"), ("elements_ratios", "ratios"), ) LENGTH_ALIASES = ( ("chemsys", "nelements"), ("cartesian_site_positions", "nsites"), ("elements", "nelements"), ) PROVIDER_FIELDS = ("chemsys", ) m = MyMapper transformer = MongoTransformer(mapper=m) parser = LarkParser(version=self.version, variant=self.variant) assert transformer.transform( parser.parse("cartesian_site_positions LENGTH <= 3")) == { "nsites": { "$lte": 3 } } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH < 3")) == { "nsites": { "$lt": 3 } } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH = 3")) == { "nsites": 3 } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH 3")) == { "nsites": 3 } assert transformer.transform( parser.parse("elements_ratios LENGTH 3")) == { "ratios": { "$size": 3 } } assert transformer.transform( parser.parse("cartesian_site_positions LENGTH >= 10")) == { "nsites": { "$gte": 10 } } assert transformer.transform( parser.parse("structure_features LENGTH > 10")) == { "structure_features.11": { "$exists": True } } assert transformer.transform(parser.parse("nsites LENGTH > 10")) == { "nsites.11": { "$exists": True } } assert transformer.transform(parser.parse("elements LENGTH 3")) == { "nelem": 3 } assert transformer.transform(parser.parse('elements HAS "Ag"')) == { "my_elements": { "$in": ["Ag"] } } assert transformer.transform( parser.parse("_exmpl_chemsys LENGTH 3")) == { "nelem": 3 }