Repository: ucfopen/canvasapi

Issue:
Custom Gradebook Columns
**What resource needs additional coverage?**

Custom Gradebook Columns

**What endpoints need to be covered?**

https://canvas.instructure.com/doc/api/custom_gradebook_columns.html

Custom Gradebook Columns:
- [ ] List custom gradebook columns
- [ ] Create a custom gradebook column
- [ ] Update a custom gradebook column
- [ ] Delete a custom gradebook column
- [ ] Reorder custom columns

Custom Gradebook Column Data:
- [ ] List entries for a column
- [ ] Update column data
- [ ] Bulk update column data


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/32286165/badge)](https://coveralls.io/builds/32286165)

Coverage remained the same at 100.0% when pulling **b29f9d4d61ee2d5c825f6b0a4483758830de2f54 on aileenpongnon:issue/295-custom-gradebook-columns** into **7872028cc364fd2aa437e6f037ee1186cbd920b4 on ucfopen:develop**.

Thetwam: Looks good 👍 

Great work, @aileenpongnon!

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 4f8a40b..9573d47 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -11,6 +11,7 @@ Patches and Suggestions
 -----------------------
 
 - Adrian Goetz [@a-goetz](https://github.com/a-goetz)
+- Aileen Pongnon [@aileenpongnon](https://github.com/aileenpongnon)
 - Alyssa Davis [@allygator](https://github.com/allygator)
 - [@amorqiu](https://github.com/amorqiu)
 - Andrew Gardener [@andrew-gardener](https://github.com/andrew-gardener)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e007b2f..0260a03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### New Endpoint Coverage
+
+- Custom Gradebook Columns (Thanks,[@aileenpongnon](https://github.com/aileenpongnon))
+
 ### Bugfixes
 
 - Fixed an issue where `Quiz.get_quiz_group` incorrectly set `course_id` to the quiz ID.  (Thanks,[@hcolclou](https://github.com/hcolclou))
diff --git a/canvasapi/course.py b/canvasapi/course.py
index 29c06a1..79786ef 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -3,6 +3,7 @@ from canvasapi.blueprint import BlueprintSubscription
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.collaboration import Collaboration
 from canvasapi.course_epub_export import CourseEpubExport
+from canvasapi.custom_gradebook_columns import CustomGradebookColumn
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.gradebook_history import (
     Day,
@@ -50,7 +51,7 @@ class Course(CanvasObject):
         :param title: The title for the Grading Standard
         :type title: str
         :param grading_scheme: A list of dictionaries containing keys for "name" and "value"
-        :type grading_scheme: list[dict]
+        :type grading_scheme: list of dict
         :rtype: :class:`canvasapi.grading_standards.GradingStandard`
         """
         if not isinstance(grading_scheme_entry, list) or len(grading_scheme_entry) <= 0:
@@ -73,6 +74,28 @@ class Course(CanvasObject):
         )
         return GradingStandard(self._requester, response.json())
 
+    def column_data_bulk_update(self, column_data, **kwargs):
+        """
+        Set the content of custom columns.
+
+        :calls: `PUT /api/v1/courses/:course_id/custom_gradebook_column_data \
+        <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_column_data_api.bulk_update>`_
+
+        :param column_data: Content to put into the column
+        :type column_data: list
+        :rtype: :class:`canvasapi.progress.Progress`
+        """
+
+        kwargs["column_data"] = column_data
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/custom_gradebook_column_data".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return Progress(self._requester, response.json())
+
     def conclude(self):
         """
         Mark this course as concluded.
@@ -216,6 +239,33 @@ class Course(CanvasObject):
 
         return Section(self._requester, response.json())
 
+    def create_custom_column(self, column, **kwargs):
+        """
+        Create a custom gradebook column.
+
+        :calls: `POST /api/v1/courses/:course_id/custom_gradebook_columns \
+        <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_columns_api.create>`_
+
+        :param column: A dictionary representing the Custom Gradebook Column to create
+        :type column: dict
+
+        :rtype: :class:`canvasapi.custom_gradebook_columns.CustomGradebookColumn`
+        """
+        if isinstance(column, dict) and "title" in column:
+            kwargs["column"] = column
+        else:
+            raise RequiredFieldMissing("Dictionary with key 'title' is required.")
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/custom_gradebook_columns".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+        column_json = response.json()
+        column_json.update({"course_id": self.id})
+
+        return CustomGradebookColumn(self._requester, column_json)
+
     def create_discussion_topic(self, **kwargs):
         """
         Creates a new discussion topic for the course or group.
@@ -245,7 +295,6 @@ class Course(CanvasObject):
 
         :rtype: :class:`canvasapi.course_epub_export.CourseEpubExport`
         """
-
         response = self._requester.request(
             "POST",
             "courses/{}/epub_exports/".format(self.id),
@@ -985,6 +1034,25 @@ class Course(CanvasObject):
 
         return response.json()
 
+    def get_custom_columns(self, **kwargs):
+        """
+        List of all the custom gradebook columns for a course.
+
+        :calls: `GET /api/v1/courses/:course_id/custom_gradebook_columns \
+        <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_columns_api.index>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.custom_gradebook_columns.CustomGradebookColumn`
+        """
+        return PaginatedList(
+            CustomGradebookColumn,
+            self._requester,
+            "GET",
+            "courses/{}/custom_gradebook_columns".format(self.id),
+            {"course_id": self.id},
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_discussion_topic(self, topic):
         """
         Return data on an individual discussion topic.
diff --git a/canvasapi/custom_gradebook_columns.py b/canvasapi/custom_gradebook_columns.py
new file mode 100644
index 0000000..4791173
--- /dev/null
+++ b/canvasapi/custom_gradebook_columns.py
@@ -0,0 +1,132 @@
+from canvasapi.canvas_object import CanvasObject
+from canvasapi.paginated_list import PaginatedList
+from canvasapi.util import (
+    combine_kwargs,
+    is_multivalued,
+)
+
+
+class CustomGradebookColumn(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.title, self.id)
+
+    def delete(self, **kwargs):
+        """
+        Permanently delete a custom column.
+
+        :calls: `DELETE /api/v1/courses/:course_id/custom_gradebook_columns/:id \
+            <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_columns_api.destroy>`_
+        :rtype: :class:`canvasapi.custom_gradebook_columns.CustomGradebookColumn`
+        """
+        response = self._requester.request(
+            "DELETE",
+            "courses/{}/custom_gradebook_columns/{}".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return CustomGradebookColumn(self._requester, response.json())
+
+    def get_column_entries(self, **kwargs):
+        """
+        Returns a list of ColumnData objects.
+
+        :calls: `GET /api/v1/courses/:course_id/custom_gradebook_columns/:id/data \
+            <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_column_data_api.index>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.custom_gradebook_columns.ColumnData`
+        """
+        return PaginatedList(
+            ColumnData,
+            self._requester,
+            "GET",
+            "courses/{}/custom_gradebook_columns/{}/data".format(
+                self.course_id, self.id
+            ),
+            {"course_id": self.course_id, "gradebook_column_id": self.id},
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+    def reorder_custom_columns(self, order, **kwargs):
+        """
+        Put the given columns in a specific order based on given parameter.
+
+        :calls: `POST /api/v1/courses/:course_id/custom_gradebook_columns/reorder \
+            <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_columns_api.reorder>`_
+
+        :param order: The order the columns are supposed to be in.
+        :type order: list of int
+
+        :returns: True if successful (status code of 200)
+        :rtype: bool
+        """
+        # Convert iterable sequence to comma-separated string
+        if is_multivalued(order):
+            order = ",".join([str(topic_id) for topic_id in order])
+
+        # Check if is a string with commas
+        if not isinstance(order, str) or "," not in order:
+            raise ValueError("Param `order` must be a list, tuple, or string.")
+
+        response = self._requester.request(
+            "POST",
+            "courses/{}/custom_gradebook_columns/reorder".format(self.course_id),
+            _kwargs=combine_kwargs(**kwargs),
+            order=order,
+        )
+
+        return response.json().get("reorder")
+
+    def update_custom_column(self, **kwargs):
+        """
+        Update a CustomColumn object.
+
+        :calls: `PUT /api/v1/courses/:course_id/custom_gradebook_columns/:id \
+            <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_columns_api.update>`_
+
+        :rtype: :class:`canvasapi.custom_gradebook_columns.CustomGradebookColumn`
+        """
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/custom_gradebook_columns/{}".format(self.course_id, self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        if response.json().get("title"):
+            super(CustomGradebookColumn, self).set_attributes(response.json())
+
+        return self
+
+
+class ColumnData(CanvasObject):
+    def __str__(self):
+        return "{} ({})".format(self.user_id, self.content)
+
+    def update_column_data(self, column_data, **kwargs):
+        """
+        Sets the content of a custom column.
+
+        :calls: `PUT /api/v1/courses/:course_id/custom_gradebook_columns/:id/data/:user_id \
+            <https://canvas.instructure.com/doc/api/custom_gradebook_columns.html#method.custom_gradebook_column_data_api.update>`_
+
+        :param column_data: The content in the column.
+        :type column_data: str
+
+        :rtype: :class:`canvasapi.custom_gradebook_columns.ColumnData`
+        """
+
+        kwargs["column_data"] = column_data
+
+        response = self._requester.request(
+            "PUT",
+            "courses/{}/custom_gradebook_columns/{}/data/{}".format(
+                self.course_id, self.gradebook_column_id, self.user_id
+            ),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        if response.json().get("content"):
+            super(ColumnData, self).set_attributes(response.json())
+
+        return self
diff --git a/canvasapi/quiz_group.py b/canvasapi/quiz_group.py
index 304f490..bc8e4a7 100644
--- a/canvasapi/quiz_group.py
+++ b/canvasapi/quiz_group.py
@@ -37,7 +37,7 @@ class QuizGroup(CanvasObject):
         :type id: int
         :param order: A list of dictionaries containing the key 'id' of
             the question to be placed at order's index.
-        :type id: list[dict]
+        :type order: list[dict]
 
         :returns: True if the result was successful (Status code of 204)
         :rtype: bool
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index 4da3343..e95326d 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -22,6 +22,7 @@ Class Reference
     course-ref
     course-epub-export-ref
     current-user-ref
+    custom-gradebook-columns-ref
     discussion-entry-ref
     discussion-topic-ref
     enrollment-ref
diff --git a/docs/custom-gradebook-columns-ref.rst b/docs/custom-gradebook-columns-ref.rst
new file mode 100644
index 0000000..faade27
--- /dev/null
+++ b/docs/custom-gradebook-columns-ref.rst
@@ -0,0 +1,13 @@
+=====================
+CustomGradebookColumn
+=====================
+
+.. autoclass:: canvasapi.custom_gradebook_columns.CustomGradebookColumn
+    :members:
+
+==========
+ColumnData
+==========
+
+.. autoclass:: canvasapi.custom_gradebook_columns.ColumnData
+    :members:
\ No newline at end of file


Test patch:
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index 45b03aa..9cfe312 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -1,4 +1,54 @@
 {
+	"get_custom_columns": {
+		"method": "GET",
+		"endpoint": "courses/1/custom_gradebook_columns",
+		"data": [
+			{
+				"id": 1,
+				"title": "Column One",
+				"position": 1,
+				"hidden": false,
+				"read_only": true
+			},
+			{
+				"id": 2,
+				"title": "Column Two",
+				"position": 2,
+				"hidden": false,
+				"read_only": true
+			}
+		],
+		"status_code": 200
+	},
+	"column_data_bulk_update": {
+		"method": "PUT",
+		"endpoint": "courses/1/custom_gradebook_column_data",
+		"data": {
+			"id": 3,
+			"context_id": 1,
+			"context_type": "Course",
+			"user_id": null,
+			"tag": "columns_update",
+			"completion": null,
+			"workflow_state": "queued",
+			"updated_at": "2013-01-15T15:04:00Z",
+			"message": null,
+			"url": "https://canvas.example.edu/api/v1/progress/3"
+		},
+		"status_code": 200
+	},
+	"create_custom_column": {
+		"method": "POST",
+		"endpoint": "courses/1/custom_gradebook_columns",
+		"data": {
+			"id": 1,
+			"title": "Test Title",
+			"position": 1,
+			"hidden": false,
+			"read_only": true
+		},
+		"status_code": 200
+	},
 	"conclude": {
 		"method": "DELETE",
 		"endpoint": "courses/1",
diff --git a/tests/fixtures/custom_gradebook_columns.json b/tests/fixtures/custom_gradebook_columns.json
new file mode 100644
index 0000000..98dcd41
--- /dev/null
+++ b/tests/fixtures/custom_gradebook_columns.json
@@ -0,0 +1,61 @@
+{
+	"delete": {
+		"method": "DELETE",
+		"endpoint": "courses/1/custom_gradebook_columns/2",
+		"data": {
+			"id": 2,
+			"teacher_notes": false,
+			"title": "Deleted Column",
+			"position": 1,
+			"hidden": false,
+			"read_only": true
+		},
+		"status_code": 200
+	},
+	"get_column_entries": {
+		"method": "GET",
+		"endpoint": "courses/1/custom_gradebook_columns/2/data",
+		"data": [
+			{
+				"content": "Example content",
+				"user_id": 1
+			},
+			{
+				"content": "More example content",
+				"user_id": 1
+			}
+		],
+		"status_code": 200
+	},
+	"reorder_custom_columns": {
+		"method": "POST",
+		"endpoint": "courses/1/custom_gradebook_columns/reorder",
+		"data": {
+			"reorder": true,
+			"order": [1, 2, 3]
+		},
+		"status_code": 200
+	},
+	"update_custom_column": {
+		"method": "PUT",
+		"endpoint": "courses/1/custom_gradebook_columns/2",
+		"data": {
+			"id": 2,
+			"teacher_notes": false,
+			"title": "Example title",
+			"position": 1,
+			"hidden": false,
+			"read_only": true
+		},
+		"status_code": 200
+	}, 
+	"update_column_data": {
+		"method": "PUT",
+		"endpoint": "courses/1/custom_gradebook_columns/2/data/1",
+		"data": {
+			"content": "Updated content",
+			"user_id": 1
+		},
+		"status_code": 200
+	}
+}
diff --git a/tests/test_course.py b/tests/test_course.py
index a45d3a6..3cdc9a5 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -43,6 +43,7 @@ from canvasapi.user import User
 from canvasapi.usage_rights import UsageRights
 from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.content_export import ContentExport
+from canvasapi.custom_gradebook_columns import CustomGradebookColumn
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
@@ -71,6 +72,22 @@ class TestCourse(unittest.TestCase):
         string = str(self.course)
         self.assertIsInstance(string, str)
 
+    # column_data_bulk_update()
+    def test_column_data_bulk_update(self, m):
+        register_uris(
+            {"course": ["column_data_bulk_update"], "progress": ["course_progress"]}, m,
+        )
+        progress = self.course.column_data_bulk_update(
+            column_data=[
+                {"column_id": 1, "user_id": 1, "content": "Test Content One"},
+                {"column_id": 2, "user_id": 2, "content": "Test Content Two"},
+            ]
+        )
+        self.assertIsInstance(progress, Progress)
+        self.assertTrue(progress.context_type == "Course")
+        progress = progress.query()
+        self.assertTrue(progress.context_type == "Course")
+
     # conclude()
     def test_conclude(self, m):
         register_uris({"course": ["conclude"]}, m)
@@ -677,6 +694,15 @@ class TestCourse(unittest.TestCase):
         category_list = [category for category in response]
         self.assertIsInstance(category_list[0], GroupCategory)
 
+    # get_custom_columns()
+    def test_get_custom_columns(self, m):
+        register_uris({"course": ["get_custom_columns"]}, m)
+
+        custom_columns = self.course.get_custom_columns()
+
+        self.assertIsInstance(custom_columns, PaginatedList)
+        self.assertIsInstance(custom_columns[0], CustomGradebookColumn)
+
     # get_discussion_topic()
     def test_get_discussion_topic(self, m):
         register_uris({"course": ["get_discussion_topic"]}, m)
@@ -738,6 +764,23 @@ class TestCourse(unittest.TestCase):
         self.assertTrue(hasattr(discussion_list[0], "course_id"))
         self.assertEqual(2, len(discussion_list))
 
+    # create_custom_column()
+    def test_create_column(self, m):
+        register_uris({"course": ["create_custom_column"]}, m)
+
+        title_str = "Test Title"
+        new_column = self.course.create_custom_column(column={"title": title_str})
+
+        self.assertIsInstance(new_column, CustomGradebookColumn)
+        self.assertTrue(hasattr(new_column, "title"))
+        self.assertEqual(new_column.title, title_str)
+        self.assertTrue(hasattr(new_column, "course_id"))
+        self.assertEqual(new_column.course_id, self.course.id)
+
+    def test_create_column_fail(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.course.create_custom_column(column={})
+
     # create_discussion_topic()
     def test_create_discussion_topic(self, m):
         register_uris({"course": ["create_discussion_topic"]}, m)
diff --git a/tests/test_custom_gradebook_columns.py b/tests/test_custom_gradebook_columns.py
new file mode 100644
index 0000000..6692056
--- /dev/null
+++ b/tests/test_custom_gradebook_columns.py
@@ -0,0 +1,121 @@
+import unittest
+from urllib.parse import quote
+
+import requests
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.custom_gradebook_columns import ColumnData
+from canvasapi.paginated_list import PaginatedList
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestCustomGradebookColumn(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            register_uris({"course": ["get_by_id", "get_custom_columns"]}, m)
+
+            self.course = self.canvas.get_course(1)
+            self.gradebook_column = self.course.get_custom_columns()[1]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.gradebook_column)
+        self.assertIsInstance(string, str)
+
+    # delete()
+    def test_delete(self, m):
+        register_uris({"custom_gradebook_columns": ["delete"]}, m)
+
+        success = self.gradebook_column.delete()
+        self.assertTrue(success)
+
+    # get_column_entries()
+    def test_get_column_entries(self, m):
+        register_uris({"custom_gradebook_columns": ["get_column_entries"]}, m)
+
+        column_entries = self.gradebook_column.get_column_entries()
+
+        self.assertIsInstance(column_entries, PaginatedList)
+        self.assertIsInstance(column_entries[0], ColumnData)
+        self.assertTrue(hasattr(column_entries[0], "gradebook_column_id"))
+        self.assertEqual(
+            column_entries[0].gradebook_column_id, self.gradebook_column.id
+        )
+
+    # reorder_custom_columns()
+    def test_reorder_custom_columns(self, m):
+        def custom_matcher(request):
+            match_text = "1,2,3"
+            if request.text == "order={}".format(quote(match_text)):
+                resp = requests.Response()
+                resp._content = b'{"reorder": true, "order": [1, 2, 3]}'
+                resp.status_code = 200
+                return resp
+
+        m.add_matcher(custom_matcher)
+
+        order = [1, 2, 3]
+        columns = self.gradebook_column.reorder_custom_columns(order=order)
+        self.assertTrue(columns)
+
+    def test_reorder_custom_columns_tuple(self, m):
+        register_uris({"custom_gradebook_columns": ["reorder_custom_columns"]}, m)
+
+        order = (1, 2, 3)
+        columns = self.gradebook_column.reorder_custom_columns(order=order)
+        self.assertTrue(columns)
+
+    def test_reorder_custom_columns_comma_separated_string(self, m):
+        register_uris({"custom_gradebook_columns": ["reorder_custom_columns"]}, m)
+
+        order = "1,2,3"
+        gradebook = self.gradebook_column.reorder_custom_columns(order=order)
+        self.assertTrue(gradebook)
+
+    def test_reorder_custom_columns_invalid_input(self, m):
+        order = "invalid string"
+        with self.assertRaises(ValueError):
+            self.gradebook_column.reorder_custom_columns(order=order)
+
+    # update_custom_column()
+    def test_update_custom_column(self, m):
+        register_uris({"custom_gradebook_columns": ["update_custom_column"]}, m)
+
+        new_title = "Example title"
+        self.gradebook_column.update_custom_column(column={"title": new_title})
+        self.assertEqual(self.gradebook_column.title, new_title)
+
+
+@requests_mock.Mocker()
+class TestColumnData(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {
+                "course": ["get_by_id", "get_custom_columns"],
+                "custom_gradebook_columns": ["get_column_entries"],
+            }
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+            self.gradebook_column = self.course.get_custom_columns()[1]
+            self.data = self.gradebook_column.get_column_entries()[1]
+
+    # __str__()
+    def test__str__(self, m):
+        string = str(self.data)
+        self.assertIsInstance(string, str)
+
+    # update_column_data()
+    def test_update_column_data(self, m):
+        register_uris({"custom_gradebook_columns": ["update_column_data"]}, m)
+
+        new_content = "Updated content"
+        self.data.update_column_data(column_data={"content": new_content})
+        self.assertEqual(self.data.content, new_content)
diff --git a/tests/util.py b/tests/util.py
index c72d009..32280a1 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -21,7 +21,7 @@ def register_uris(requirements, requests_mocker, base_url=None):
         try:
             with open("tests/fixtures/{}.json".format(fixture)) as file:
                 data = json.loads(file.read())
-        except IOError:
+        except (IOError, ValueError):
             raise ValueError("Fixture {}.json contains invalid JSON.".format(fixture))
 
         if not isinstance(objects, list):