Repository: gtalarico/pyairtable (python)

Issue:
How to use field ids with fetch() or populate() on ORM model LinkField?
I'm struggling to understand how to use airtable field ids (as opposed to pretty names)  when calling populate() on a given instance of a LinkField. 

I have set the instance `meta.use_field_ids` to True, but the populate() method uses the `meta.api.use_field_ids` value which defaults to False. 

Is there a recommended way to override the api-level default when using the populate() method?

Many thanks for any insights!


Pull request description:
Model.from_ids should respect Meta.use_field_ids
`Model.from_ids` should reuse the existing logic in `Model.all` so that we retrieve record data from the API in the format that the model class will expect.

Fixes #421

Patch:
diff --git a/pyairtable/orm/model.py b/pyairtable/orm/model.py
index 5284a1c..188c9b6 100644
--- a/pyairtable/orm/model.py
+++ b/pyairtable/orm/model.py
@@ -449,10 +449,7 @@ class Model:
             # Only retrieve records that aren't already memoized
             formula = OR(EQ(RECORD_ID(), record_id) for record_id in sorted(remaining))
             by_id.update(
-                {
-                    record["id"]: cls.from_record(record, memoize=memoize)
-                    for record in cls.meta.table.all(formula=formula)
-                }
+                {obj.id: obj for obj in cls.all(formula=formula, memoize=memoize)}
             )
 
         # Ensure we return records in the same order, and raise KeyError if any are missing


Test patch:
diff --git a/tests/test_orm_fields.py b/tests/test_orm_fields.py
index 736a17f..4c505ec 100644
--- a/tests/test_orm_fields.py
+++ b/tests/test_orm_fields.py
@@ -877,7 +877,10 @@ def test_single_link_field__multiple_values():
     book = Book.from_record(fake_record(Author=[a1, a2, a3]))
     with mock.patch("pyairtable.Table.all", return_value=records) as m:
         book.author
-        m.assert_called_once_with(formula=OR(RECORD_ID().eq(records[0]["id"])))
+        m.assert_called_once_with(
+            **Book.meta.request_kwargs,
+            formula=OR(RECORD_ID().eq(records[0]["id"])),
+        )
 
     assert book.author.id == a1
     assert book.author.name == "Author 1"
@@ -956,6 +959,34 @@ def test_link_field__populate(field_type, requests_mock):
         T.link.populate(Linked())
 
 
+@pytest.mark.parametrize("field_type", (f.LinkField, f.SingleLinkField))
+def test_link_field__populate_with_field_ids(field_type, requests_mock):
+    """
+    Test that implementers can use Model.link_field.populate(instance)
+    when the linked model uses field IDs rather than field names.
+    """
+    field_id = fake_id("fld")
+    record_ids = [fake_id("rec", n) for n in range(3)]
+    records = [
+        fake_record(id=record_id, Name=f"link{n}")
+        for n, record_id in enumerate(record_ids)
+    ]
+
+    class Linked(Model):
+        Meta = fake_meta(use_field_ids=True)
+        name = f.TextField(field_id)
+
+    class T(Model):
+        Meta = fake_meta()
+        link = field_type("Link", Linked)
+
+    m = requests_mock.get(Linked.meta.table.urls.records, json={"records": records})
+    obj = T.from_record(fake_record(Link=record_ids))
+    obj.link
+    assert m.call_count == 1
+    assert m.last_request.qs.get("returnFieldsByFieldId") == ["1"]
+
+
 def test_lookup_field():
     class T:
         items = f.LookupField("Items")
diff --git a/tests/test_orm_model.py b/tests/test_orm_model.py
index ff77278..a5d7977 100644
--- a/tests/test_orm_model.py
+++ b/tests/test_orm_model.py
@@ -227,9 +227,10 @@ def test_from_ids(mock_api):
         url=FakeModel.meta.table.urls.records,
         fallback=("post", FakeModel.meta.table.urls.records_post),
         options={
+            **FakeModel.meta.request_kwargs,
             "formula": (
                 "OR(%s)" % ", ".join(f"RECORD_ID()='{id}'" for id in sorted(fake_ids))
-            )
+            ),
         },
     )
     assert len(contacts) == len(fake_records)
@@ -253,6 +254,15 @@ def test_from_ids__no_fetch(mock_all):
     assert set(contact.id for contact in contacts) == set(fake_ids)
 
 
+@mock.patch("pyairtable.Table.all")
+def test_from_ids__use_field_ids(mock_all):
+    fake_ids = [fake_id() for _ in range(10)]
+    mock_all.return_value = [fake_record(id=id) for id in fake_ids]
+    FakeModelByIds.from_ids(fake_ids)
+    assert mock_all.call_count == 1
+    assert mock_all.mock_calls[-1].kwargs["use_field_ids"] is True
+
+
 @pytest.mark.parametrize(
     "methodname,returns",
     (