Repository: burnash/gspread

Issue:
add `get_records` changes to README migration guide
The result of issue #1367 should be summarised and added to the [README v6 migration guide](https://github.com/burnash/gspread#v512-to-v60-migration-guide)

After that, we can make the v6.0.0 release

Discussion:

Patch:
diff --git a/README.md b/README.md
index c84e660..caf3b7a 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,20 @@ file.sheet1.update_tab_color(tab_color)
 + age = spreadsheet.get_lastUpdateTime()
 ```
 
+### Replace method `Worksheet.get_records`
+
+In v6 you can now only get *all* sheet records, using `Worksheet.get_all_records()`. The method `Worksheet.get_records()` has been removed. You can get some records using your own fetches and combine them with `gspread.utils.to_records()`.
+
+```diff
++ from gspread import utils
+  all_records = spreadsheet.get_all_records(head=1)
+- some_records = spreadsheet.get_all_records(head=1, first_index=6, last_index=9)
+- some_records = spreadsheet.get_records(head=1, first_index=6, last_index=9)
++ header = spreadsheet.get("1:1")[0]
++ cells = spreadsheet.get("6:9")
++ some_records = utils.to_records(header, cells)
+```
+
 ### Silence warnings
 
 In version 5 there are many warnings to mark deprecated feature/functions/methods.
diff --git a/gspread/utils.py b/gspread/utils.py
index 880991c..a22b232 100644
--- a/gspread/utils.py
+++ b/gspread/utils.py
@@ -878,6 +878,35 @@ def get_a1_from_absolute_range(range_name: str) -> str:
     return range_name
 
 
+def to_records(
+    headers: Iterable[Any] = [], values: Iterable[Iterable[Any]] = [[]]
+) -> List[Dict[str, Union[str, int, float]]]:
+    """Builds the list of dictionaries, all of them have the headers sequence as keys set,
+    each key is associated to the corresponding value for the same index in each list from
+    the matrix ``values``.
+    There are as many dictionaries as they are entry in the list of given values.
+
+    :param list: headers the key set for all dictionaries
+    :param list: values a matrix of values
+
+    Examples::
+
+        >>> to_records(["name", "City"], [["Spiderman", "NY"], ["Batman", "Gotham"]])
+        [
+            {
+                "Name": "Spiderman",
+                "City": "NY",
+            },
+            {
+                "Name": "Batman",
+                "City": "Gotham",
+            },
+        ]
+    """
+
+    return [dict(zip(headers, row)) for row in values]
+
+
 # SHOULD NOT BE NEEDED UNTIL NEXT MAJOR VERSION
 # def deprecation_warning(version: str, msg: str) -> None:
 #     """Emit a deprecation warning.
diff --git a/gspread/worksheet.py b/gspread/worksheet.py
index 8b4f551..a6d6bc1 100644
--- a/gspread/worksheet.py
+++ b/gspread/worksheet.py
@@ -55,6 +55,7 @@ from .utils import (
     is_full_a1_notation,
     numericise_all,
     rowcol_to_a1,
+    to_records,
 )
 
 CellFormat = TypedDict(
@@ -462,6 +463,15 @@ class Worksheet:
         dictionaries holding the contents of subsequent rows of cells as
         values.
 
+        This method uses the function :func:`gspread.utils.to_records` to build the resulting
+        records. It mainly wraps around the function and handle the simplest use case
+        using a header row (default = 1) and the the reste of the entire sheet.
+
+        .. note::
+
+           for any particular use-case, please get your dataset, your headers
+           then use the function :func:`gspread.utils.to_records` to build the records.
+
         Cell values are numericised (strings that can be read as ints or floats
         are converted), unless specified in numericise_ignore
 
@@ -498,10 +508,10 @@ class Worksheet:
 
             # Read all rows from the sheet
             >>> worksheet.get_all_records()
-            {
+            [
                 {"A1": "A6", "B2": "B7", "C3": "C8"},
                 {"A1": "A11", "B2": "B12", "C3": "C13"}
-            }
+            ]
         """
         entire_sheet = self.get(
             value_render_option=value_render_option,
@@ -551,9 +561,7 @@ class Worksheet:
                 for row in values
             ]
 
-        formatted_records = [dict(zip(keys, row)) for row in values]
-
-        return formatted_records
+        return to_records(keys, values)
 
     def get_all_cells(self) -> List[Cell]:
         """Returns a list of all `Cell` of the current sheet."""


Test patch:
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 14144fa..187e74e 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -465,3 +465,41 @@ class UtilsTest(unittest.TestCase):
         self.assertEqual(utils.get_a1_from_absolute_range("A1:B2"), "A1:B2")
         self.assertEqual(utils.get_a1_from_absolute_range("A1:B"), "A1:B")
         self.assertEqual(utils.get_a1_from_absolute_range("2"), "2")
+
+    def test_to_records_empty_args(self):
+        """Test to_records with empty args"""
+
+        self.assertListEqual(utils.to_records([], []), [])
+        self.assertListEqual(utils.to_records([], [[]]), [{}])
+        self.assertListEqual(utils.to_records(["a1", "b2"], []), [])
+        self.assertListEqual(utils.to_records(["a1", "b2"], [[]]), [{}])
+        self.assertListEqual(utils.to_records([], [["a1"]]), [{}])
+        self.assertListEqual(utils.to_records([], [["a1"], ["a2"]]), [{}, {}])
+        self.assertListEqual(utils.to_records([], [[], ["a2"]]), [{}, {}])
+
+    def test_to_records(self):
+        """Test to_records with values"""
+
+        headers = ["HA", "HB", "HC"]
+        values = [["A2", "B2", "C2"], ["A3", "B3"], ["", "B4", "C4"]]
+
+        records = utils.to_records(headers, values)
+
+        self.assertEqual(len(values), len(records))
+
+        for i in range(len(records)):
+            record = records[i]
+            keys = record.keys()
+
+            # Some rows have shorter values ("A3", "B3")
+            # so the list of keys is smaller
+            # but never bigger than the list of headers
+            self.assertLessEqual(len(keys), len(headers))
+
+            # Each resulting key must be part of the given header
+            for key in keys:
+                self.assertIn(key, headers)
+
+                # given key are unordered
+                # but they must match a value from the given input values
+                self.assertIn(record[key], values[i])