LinguList: Thanks, this will greatly facilitate things. @chrzyki, you could test this on kitchensemitic. I'll also test it directly in the next dataset I work on.
chrzyki: Perfect, thank you very much! That looks incredibly helpful. Will test tomorrow with kitchensemitic!
diff --git a/src/pylexibank/cldf.py b/src/pylexibank/cldf.py
index deee9a2..cb38b21 100644
--- a/src/pylexibank/cldf.py
+++ b/src/pylexibank/cldf.py
@@ -1,5 +1,5 @@
 import re
-from collections import defaultdict
+from collections import defaultdict, OrderedDict
 from itertools import chain
 from pathlib import Path
 import logging
@@ -223,7 +223,7 @@ class LexibankWriter(CLDFWriter):
     def add_language(self, **kw):
         return self._add_object(self.dataset.language_class, **kw)
 
-    def add_languages(self, id_factory=lambda d: d['ID']):
+    def add_languages(self, id_factory='ID', lookup_factory=None):
         """
         Add languages as specified in a dataset's etc/languages.csv
 
@@ -231,14 +231,19 @@ class LexibankWriter(CLDFWriter):
         a value to be used as ID for the language.
         :return: The set of language IDs which have been added.
         """
-        ids = set()
-        for kw in self.dataset.languages:
+        assert callable(id_factory) or isinstance(id_factory, str)
+        ids = OrderedDict()
+        for i, kw in enumerate(self.dataset.languages):
             if (not kw.get('Glottocode')) and kw.get('ISO639P3code'):
                 kw['Glottocode'] = self.dataset.glottolog.glottocode_by_iso.get(kw['ISO639P3code'])
-            kw['ID'] = id_factory(kw)
-            ids.add(kw['ID'])
+            kw['ID'] = id_factory(kw) if callable(id_factory) else kw[id_factory]
+            if lookup_factory is None:
+                key = i
+            else:
+                key = lookup_factory(kw) if callable(lookup_factory) else kw[lookup_factory]
+            ids[key] = kw['ID']
             self.add_language(**kw)
-        return ids
+        return ids if lookup_factory else list(ids.values())
 
     def add_concept(self, **kw):
         if kw.get('Concepticon_ID'):
@@ -247,7 +252,7 @@ class LexibankWriter(CLDFWriter):
                 self.dataset.concepticon.cached_glosses[int(kw['Concepticon_ID'])])
         return self._add_object(self.dataset.concept_class, **kw)
 
-    def add_concepts(self, id_factory=lambda d: d.number):
+    def add_concepts(self, id_factory=lambda d: d.number, lookup_factory=None):
         """
         Add concepts as specified in a dataset's associated Concepticon concept list or in
         etc/concepts.csv
@@ -256,7 +261,8 @@ class LexibankWriter(CLDFWriter):
         returning a value to be used as ID for the concept.
         :return: The set of concept IDs which have been added.
         """
-        ids, concepts = set(), []
+        assert callable(id_factory) or isinstance(id_factory, str)
+        ids, concepts = OrderedDict(), []
         if self.dataset.conceptlist:
             concepts = self.dataset.conceptlist.concepts.values()
         else:
@@ -276,18 +282,22 @@ class LexibankWriter(CLDFWriter):
                 concepts.append(Concept(attributes=attrs, **kw))
 
         fieldnames = {f.lower(): f for f in self.dataset.concept_class.fieldnames()}
-        for c in concepts:
+        for i, c in enumerate(concepts):
             attrs = dict(
-                ID=id_factory(c),
+                ID=id_factory(c) if callable(id_factory) else getattr(c, id_factory),
                 Name=c.label,
                 Concepticon_ID=c.concepticon_id,
                 Concepticon_Gloss=c.concepticon_gloss)
             for fl, f in fieldnames.items():
                 if fl in c.attributes:
                     attrs[f] = c.attributes[fl]
-            ids.add(attrs['ID'])
+            if lookup_factory is None:
+                key = i
+            else:
+                key = lookup_factory(attrs) if callable(lookup_factory) else attrs[lookup_factory]
+            ids[key] = attrs['ID']
             self.add_concept(**attrs)
-        return ids
+        return ids if lookup_factory else list(ids.values())
 
     def align_cognates(self,
                        alm=None,

Return lookup from LexibankWriter.add_* methods...
...if one is defined via a `lookup_factory` argument.

See https://github.com/lexibank/pylexibank/issues/132#issuecomment-545931006
diff --git a/tests/test_cldf.py b/tests/test_cldf.py
index c54c8e2..acd8657 100644
--- a/tests/test_cldf.py
+++ b/tests/test_cldf.py
@@ -8,6 +8,10 @@ def test_align_cognates(dataset):
         ds.add_language(ID='a-b')
         with pytest.raises(ValueError):
             ds.add_language(ID='a/b')
-        ds.add_languages()
-        ds.add_concepts()
+        lmap = ds.add_languages(lookup_factory='Name')
+        assert isinstance(lmap, dict)
+        assert lmap['abcd'] == '1'
+        cids = ds.add_concepts()
+        assert isinstance(cids, list)
+        assert '1' in cids
         ds.align_cognates()

pip install -e .[dev,test]
parse_log_pytest
requirements.txt
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
attrs==25.3.0
babel==2.17.0
backports.tarfile==1.2.0
beautifulsoup4==4.13.3
bibtexparser==2.0.0b8
bs4==0.0.2
cdstarcat==1.5.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
cldfbench==1.14.1
cldfcatalog==1.6.0
cldfzenodo==2.1.2
clldutils==3.24.2
colorama==0.4.6
colorlog==6.9.0
commonnexus==1.9.2
coverage==7.8.0
cryptography==44.0.2
csvw==3.5.1
docopt==0.6.2
docutils==0.21.2
et_xmlfile==2.0.0
exceptiongroup==1.2.2
flake8==7.2.0
gitdb==4.0.12
GitPython==3.1.44
greenlet==3.1.1
id==1.5.0
idna==3.10
importlib_metadata==8.6.1
iniconfig==2.1.0
isodate==0.7.2
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
jmespath==1.0.1
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
keyring==25.6.0
language-tags==1.2.0
latexcodec==3.0.0
linglit==1.7.1
lingpy==2.6.13
lxml==5.3.1
Markdown==3.7
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mccabe==0.7.0
mdurl==0.1.2
mock==5.2.0
more-itertools==10.6.0
nameparser==1.1.3
networkx==3.2.1
newick==1.10.0
nh3==0.2.21
numpy==2.0.2
openpyxl==3.1.5
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
prompt_toolkit==3.0.50
pybtex==0.24.0
pycdstar==1.1.0
pycldf==1.41.0
pyclts==3.2.0
pycodestyle==2.13.0
pyconcepticon==3.1.0
pycountry==24.6.1
pycparser==2.22
pyflakes==3.3.1
pyglottolog==3.14.0
Pygments==2.19.1
pyigt==2.2.0
pylatexenc==2.10
-e git+https://github.com/lexibank/pylexibank.git@0ddc78b6e280e9296bdd14f9bd884d1e0061cc53#egg=pylexibank
pyparsing==3.2.3
pytest==8.3.5
pytest-cov==6.0.0
pytest-mock==3.14.0
python-dateutil==2.9.0.post0
python-frontmatter==1.1.0
python-nexus==2.9.0
PyYAML==6.0.2
RapidFuzz==3.12.2
rdflib==7.1.4
readme_renderer==44.0
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==1.5.0
rich==14.0.0
rpds-py==0.24.0
SecretStorage==3.3.3
segments==2.3.0
six==1.17.0
smmap==5.0.2
soupsieve==2.6
SQLAlchemy==1.4.54
tabulate==0.9.0
termcolor==3.0.0
TexSoup==0.3.1
thefuzz==0.22.1
tomli==2.2.1
tqdm==4.67.1
twine==6.1.0
typing_extensions==4.13.0
Unidecode==1.3.8
uritemplate==4.1.1
urllib3==2.3.0
wcwidth==0.2.13
Whoosh==2.7.4
xlrd==2.0.1
zenodoclient==0.5.1
zipp==3.21.0

name: pylexibank
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
  - ca-certificates=2025.2.25=h06a4308_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=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - attrs==25.3.0
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - beautifulsoup4==4.13.3
      - bibtexparser==2.0.0b8
      - bs4==0.0.2
      - cdstarcat==1.5.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - cldfbench==1.14.1
      - cldfcatalog==1.6.0
      - cldfzenodo==2.1.2
      - clldutils==3.24.2
      - colorama==0.4.6
      - colorlog==6.9.0
      - commonnexus==1.9.2
      - coverage==7.8.0
      - cryptography==44.0.2
      - csvw==3.5.1
      - docopt==0.6.2
      - docutils==0.21.2
      - et-xmlfile==2.0.0
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - greenlet==3.1.1
      - id==1.5.0
      - idna==3.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isodate==0.7.2
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jmespath==1.0.1
      - jsonschema==4.23.0
      - jsonschema-specifications==2024.10.1
      - keyring==25.6.0
      - language-tags==1.2.0
      - latexcodec==3.0.0
      - linglit==1.7.1
      - lingpy==2.6.13
      - lxml==5.3.1
      - markdown==3.7
      - markdown-it-py==3.0.0
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mock==5.2.0
      - more-itertools==10.6.0
      - nameparser==1.1.3
      - networkx==3.2.1
      - newick==1.10.0
      - nh3==0.2.21
      - numpy==2.0.2
      - openpyxl==3.1.5
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - prompt-toolkit==3.0.50
      - pybtex==0.24.0
      - pycdstar==1.1.0
      - pycldf==1.41.0
      - pyclts==3.2.0
      - pycodestyle==2.13.0
      - pyconcepticon==3.1.0
      - pycountry==24.6.1
      - pycparser==2.22
      - pyflakes==3.3.1
      - pyglottolog==3.14.0
      - pygments==2.19.1
      - pyigt==2.2.0
      - pylatexenc==2.10
      - pyparsing==3.2.3
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - python-dateutil==2.9.0.post0
      - python-frontmatter==1.1.0
      - python-nexus==2.9.0
      - pyyaml==6.0.2
      - rapidfuzz==3.12.2
      - rdflib==7.1.4
      - readme-renderer==44.0
      - referencing==0.36.2
      - regex==2024.11.6
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==1.5.0
      - rich==14.0.0
      - rpds-py==0.24.0
      - secretstorage==3.3.3
      - segments==2.3.0
      - six==1.17.0
      - smmap==5.0.2
      - soupsieve==2.6
      - sqlalchemy==1.4.54
      - tabulate==0.9.0
      - termcolor==3.0.0
      - texsoup==0.3.1
      - thefuzz==0.22.1
      - tomli==2.2.1
      - tqdm==4.67.1
      - twine==6.1.0
      - typing-extensions==4.13.0
      - unidecode==1.3.8
      - uritemplate==4.1.1
      - urllib3==2.3.0
      - wcwidth==0.2.13
      - whoosh==2.7.4
      - xlrd==2.0.1
      - zenodoclient==0.5.1
      - zipp==3.21.0
prefix: /opt/conda/envs/pylexibank

tests/test_cldf.py::test_align_cognates
src/pylexibank/cldf.py
src/pylexibank/cldf.py