Repository: ucfopen/canvasapi
Pull request #448 (2020-12-04 16:09:36)

Release v2.1.0
🎉


Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index dc84e02..f4d34ce 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -6,28 +6,31 @@ Below you'll find guidelines for contributing that will keep our codebase clean
 
 ## Table of Contents
 
-* [How can I contribute?](#how-can-i-contribute)
-    * [Bug reports](#bug-reports)
-    * [Resolving issues](#resolving-issues)
-    * [Making your first contribution](#making-your-first-contribution)
-        * [Setting up the environment](#setting-up-the-environment)
-        * [Writing tests](#writing-tests)
-            * [API Coverage Tests](#api-coverage-tests)
-            * [Engine tests](#engine-tests)
-        * [Running tests / coverage reports](#running-tests--coverage-reports)
-        * [Making a Pull Request](#making-a-pull-request)
-* [Code style guidelines](#code-style-guidelines)
-    * [Foolish consistency](#foolish-consistency)
-    * [Method docstrings](#method-docstrings)
-        * [Descriptions](#descriptions)
-        * [Links to related API endpoints](#links-to-related-api-endpoints)
-        * [Parameters](#parameters)
-        * [Returns](#returns)
-        * [Docstring examples](#docstring-examples)
+* [Contributing to CanvasAPI](#contributing-to-canvasapi)
+    * [Table of Contents](#table-of-contents)
+    * [How can I contribute?](#how-can-i-contribute)
+        * [Bug reports](#bug-reports)
+        * [Resolving issues](#resolving-issues)
+        * [Making your first contribution](#making-your-first-contribution)
+            * [Setting up the environment](#setting-up-the-environment)
+            * [Writing tests](#writing-tests)
+                * [API coverage tests](#api-coverage-tests)
+                * [Engine tests](#engine-tests)
+            * [Running tests / coverage reports](#running-tests--coverage-reports)
+            * [Making a pull request](#making-a-pull-request)
+    * [Code style guidelines](#code-style-guidelines)
+        * [Running code style checks](#running-code-style-checks)
+        * [Foolish consistency](#foolish-consistency)
+        * [Method docstrings](#method-docstrings)
+            * [Descriptions](#descriptions)
+            * [Links to related API endpoints](#links-to-related-api-endpoints)
+            * [Parameters](#parameters)
+            * [Returns](#returns)
+            * [Docstring Examples](#docstring-examples)
 
 ## How can I contribute?
 
-### Bug Reports
+### Bug reports
 
 Bug reports are awesome. Writing quality bug reports helps us identify issues and solve them even faster. You can submit bug reports directly to our [issue tracker](https://github.com/ucfopen/canvasapi/issues).
 
@@ -42,15 +45,16 @@ Here are a few things worth mentioning when making a report:
 
 We welcome pull requests for bug fixes and new features! Feel free to browse our open, unassigned issues and assign yourself to them. You can also filter by labels:
 
-* [simple](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Asimple) -- easier issues to start working on; great for getting familiar with the codebase.
-* [api coverage](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aapi-coverage) -- covering new endpoints or updating existing ones.
-* [enhancement](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aenhancement) -- updates to the engine to improve performance or add new functionality.
-* [major](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Amajor) -- difficult or major changes or additions that require familiarity with the library.
-* [bug](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Abug) -- happy little code accidents.
-* [fixed-in-develop](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Afixed-in-develop) -- issues that have been resolved but the changes are not in the latest release yet.
-* [canvas-bug](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Acanvas-bug) -- confirmed to be an issue with the Canvas LMS rather than the CanvasAPI library.
-* [help wanted](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3A"help%20wanted") -- we need *your* help to figure these out!
-* [documentation](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Adocumentation) -- issues relating to Documentation. Specifically, any of the `.md` files or our [class reference docs](http://canvasapi.readthedocs.io/en/latest/).
+* [api coverage](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aapi-coverage): covering new endpoints or updating existing ones.
+* [backstage](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Abackstage): issues affecting the repository or project internals rather than user-facing features.
+* [bug](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Abug): happy little code accidents.
+* [canvas](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Acanvas-bug): confirmed to be an issue with the Canvas LMS rather than the CanvasAPI library.
+* [documentation](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Adocumentation): issues relating to Documentation. Specifically, any of the `.md` files or our [class reference docs](http://canvasapi.readthedocs.io/en/latest/).
+* [enhancement](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aenhancement): updates to the engine to improve performance or add new functionality.
+* [help wanted](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3A"help%20wanted"): we need *your* help to figure these out!
+* [major](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Amajor): difficult or major changes or additions that require familiarity with the library.
+* [question](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Aquestion): issues that aren't reporting functionality or requesting improvement but requesting clarification on existing behavior
+* [simple](https://github.com/ucfopen/canvasapi/issues?q=sort%3Aid_desc-desc+is%3Aopen+label%3Asimple): easier issues to start working on; great for getting familiar with the codebase.
 
 Once you've found an issue you're interested in tackling, take a look at our [first contribution tutorial](#making-your-first-contribution) for information on our pull request policy.
 
@@ -58,12 +62,12 @@ Once you've found an issue you're interested in tackling, take a look at our [fi
 
 #### Setting up the environment
 
-Now that you've selected an issue to work on, you'll need to set up an environment for writing code. We'll assume you already have pip, virtualenv, and git installed and are using a terminal. If not, please set those up before continuing.
+Now that you've selected an issue to work on, you'll need to set up an environment for writing code. We'll assume you already have Python 3 (pip / venv) and git installed and are using a terminal. If not, please set those up before continuing.
 
-1. Clone our repository by executing `git clone git@github.com:ucfopen/canvasapi.git`
-2. Checkout (`git checkout develop`) and then pull the latest commit from the develop branch: `git pull origin develop`
+1. Fork CanvasAPI on GitHub ([see the docs here](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo))
+2. Checkout (`git checkout develop`) and then pull the latest commit from the develop branch: `git pull upstream develop`
 3. Create a new branch with the format **issue/[issue_number]-[issue-title]**: `git checkout -b issue/1-test-issue-for-documentation`
-4. Set up a new virtual environment ( `virtualenv env` ) and activate it (`source env/bin/activate`)
+4. Set up a new virtual environment ( `python3 -m venv ~/.virtualenvs/canvasapi` ) and activate it
 5. Install the required dependencies with `pip install -r dev_requirements.txt`
 
 From here, you can go about working on your issue you normally would. Please make sure to adhere to our [style guidelines for both code and docstrings](#code-style-guidelines). Once you're satisfied with the result, it's time to write a unit test for it.
@@ -74,7 +78,7 @@ Tests are a critical part of building applications, and we [pity the fool who do
 
 You'll notice our tests live in the creatively named `tests` directory. Within that directory, you'll see several files in the form `test_[class].py` and another directory named `fixtures`. Depending on the scope of the issue you're solving, you'll be writing two different kinds of tests.
 
-##### API Coverage Tests
+##### API coverage tests
 
 We use the [requests-mock](https://pypi.python.org/pypi/requests-mock) library to simulate API responses. Those mock responses live inside the `fixtures` directory in JSON files. Each file's name describes the endpoints that are contained within. For example, course endpoints live in `course.json`. These fixtures are loaded on demand in a given test. Let's look at `test_get_user` in `test_course.py` as an example:
 
@@ -196,17 +200,30 @@ TOTAL                             1586      0   100%
 
 Certain statements can be omitted from the coverage report by adding `# pragma: no cover` but this should be used conservatively. If your tests pass and your coverage is at 100%, you're ready to [submit a pull request](https://github.com/ucfopen/canvasapi/pulls)!
 
-#### Making a Pull Request
+#### Making a pull request
 
 Be sure to include the issue number in the title with a pound sign in front of it (#123) so we know which issue the code is addressing. Point the branch at `develop` and then submit it for review.
 
-## Code Style Guidelines
+## Code style guidelines
 
 We try to adhere to Python's [PEP 8](https://www.python.org/dev/peps/pep-0008/) specification as much as possible. In short, that means:
 
 * We use four spaces for indentation.
 * Lines should be around 80 characters long, but up to 99 is allowed. Once you get into the 85+ territory, consider breaking your code into separate lines.
 
+### Running code style checks
+
+The following tools can help you check your code for style correctness. We run these tools in our CI pipeline, so running them locally is a great way to speed up acceptance of your pull requests.
+
+You can use [pre-commit](https://pre-commit.com/) to force each check to run before you create a commit locally:
+
+```sh
+pip install pre-commit
+pre-commit install
+```
+
+Alternatively, each step can be run manually one-by-one, or all at once executing `./scripts/run_tests.sh`.
+
 We use `flake8` for linting:
 
 ```sh
@@ -225,6 +242,12 @@ We require methods to be in alphabetical order for ease of reading. Run this scr
 python scripts/alphabetic.py
 ```
 
+All endpoint methods should accept arbitrary keyword arguments to enable parameter pass-through to Canvas:
+
+```sh
+python scripts/find_missing_kwargs.py
+```
+
 ### Foolish consistency
 
 > A foolish consistency is the hobgoblin of little minds. -- Ralph Waldo Emerson
diff --git a/.gitignore b/.gitignore
index f6b23d7..109ce10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
 /docs/_build/
 /env/
 /env3/
+venv/
 /htmlcov/
 \#*#
 .vscode/settings.json
diff --git a/.travis.yml b/.travis.yml
index 24aba50..4b2fb8c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,6 +16,7 @@ install:
 script:
   - coverage run -m unittest discover
   - black --check canvasapi tests
+  - isort --check canvasapi tests
   - flake8 canvasapi tests
   - mdl . .github
   - python scripts/alphabetic.py
diff --git a/AUTHORS.md b/AUTHORS.md
index 78430e8..a20241a 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -16,8 +16,10 @@ Patches and Suggestions
 - [@amorqiu](https://github.com/amorqiu)
 - Andrew Gardener [@andrew-gardener](https://github.com/andrew-gardener)
 - Anthony Rodriguez [@AnthonyRodriguez726](https://github.com/AnthonyRodriguez726)
+- Ashutosh Saxena [@Xx-Ashutosh-xX](https://github.com/Xx-Ashutosh-xX)
 - Ben Liblit [@liblit](https://github.com/liblit)
 - Bill Wrbican [@wjw27](https://github.com/wjw27)
+- [@blepabyte](https://github.com/blepabyte)
 - Bradford Lynch [@bradfordlynch](https://github.com/bradfordlynch)
 - Brian Bennett [@bennettscience](https://github.com/bennettscience)
 - Bruce Spang [@brucespang](https://github.com/brucespang)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5e1b2d..5fc59c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,30 @@
 
 ## [Unreleased]
 
+## [2.1.0] - 2020-12-04
+
+### New Endpoint Coverage
+
+- Course TODO items (Thanks, [@onomou](https://github.com/onomou))
+- Create observer pairing code (Thanks, [@bennettscience](https://github.com/bennettscience))
+
+### General
+
+- Added missing documentation for the get_current_user method and clarifications to the `CurrentUser` class. (Thanks, [@Xx-Ashutosh-xX](https://github.com/Xx-Ashutosh-xX))
+- `Canvas.get_announcement` now has a required parameter `context_codes`, which accepts a list of course IDs or `Course` objects.
+- Updated contributing guide
+- Added missing documentation for the "Smart DateTimes" feature
+- Added basic troubleshooting guide to documentation
+
+### Bugfixes
+
+- Fixed an issue where an `Announcement` object sometimes didn't have an associated course ID. (Thanks, [@bennettscience](https://github.com/bennettscience))
+- Fixed an issue where an encoding problem could lead to file downloads hanging indefinitely. (Thanks, [@blepabyte](https://github.com/blepabyte))
+
+### Deprecation Warnings
+
+- The `enrollment_type` argument on `Course.enroll_user` is now deprecated. Pass this information to `enrollment[type]` as a keyword argument instead. e.g. `enroll_user(enrollment={'type': 'StudentEnrollment'})`
+
 ## [2.0.0] - 2020-08-14
 
 ### General
@@ -487,7 +511,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti
 - Fixed some incorrectly defined parameters
 - Fixed an issue where tests would fail due to an improperly configured requires block
 
-[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v2.0.0...develop
+[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v2.1.0...develop
+[2.1.0]: https://github.com/ucfopen/canvasapi/compare/v2.0.0...v2.1.0
 [2.0.0]: https://github.com/ucfopen/canvasapi/compare/v1.0.0...v2.0.0
 [1.0.0]: https://github.com/ucfopen/canvasapi/compare/v0.16.1...v1.0.0
 [0.16.1]: https://github.com/ucfopen/canvasapi/compare/v0.16.0...v0.16.1
diff --git a/README.md b/README.md
index 40245a0..ed36af6 100644
--- a/README.md
+++ b/README.md
@@ -13,15 +13,18 @@ CanvasAPI is a Python library for accessing Instructure’s [Canvas LMS API](htt
 
 ## Table of Contents
 
-* [Installation](#installation)
-* [Documentation](#documentation)
-* [Quickstart](#quickstart)
-* [Working with Canvas Objects](#working-with-canvas-objects)
-    * [Course Objects](#course-objects)
-    * [User Objects](#user-objects)
-    * [Paginated Lists](#paginated-lists)
-    * [Keyword Arguments](#keyword-arguments)
-* [Contact Us](#contact-us)
+* [CanvasAPI](#canvasapi)
+    * [Table of Contents](#table-of-contents)
+    * [Installation](#installation)
+    * [Documentation](#documentation)
+    * [Contributing](#contributing)
+    * [Quickstart](#quickstart)
+        * [Working with Canvas Objects](#working-with-canvas-objects)
+            * [Course objects](#course-objects)
+            * [User objects](#user-objects)
+            * [Paginated Lists](#paginated-lists)
+            * [Keyword arguments](#keyword-arguments)
+    * [Contact Us](#contact-us)
 
 ## Installation
 
@@ -33,6 +36,10 @@ You can install CanvasAPI with pip:
 
 Full documentation is available at [Read the Docs](http://canvasapi.readthedocs.io/).
 
+## Contributing
+
+Want to help us improve CanvasAPI? Check out our [Contributing Guide](.github/CONTRIBUTING.md) to learn about running CanvasAPI as a developer, picking issues to work on, submitting bug reports, contributing patches, and more.
+
 ## Quickstart
 
 Getting started with CanvasAPI is easy.
@@ -58,7 +65,7 @@ You can now use `canvas` to begin making API calls.
 
 ### Working with Canvas Objects
 
-CanvasAPI converts the JSON responses from the Canvas API into Python objects. These objects provide further access to the Canvas API. You can find a full breakdown of the methods these classes provide in our [class documentation](http://canvasapi.readthedocs.io/en/latest/class-reference.html). Below, you’ll find a few examples of common CanvasAPI use cases.
+CanvasAPI converts the JSON responses from the Canvas API into Python objects. These objects provide further access to the Canvas API. You can find a full breakdown of the methods these classes provide in our [class documentation](http://canvasapi.readthedocs.io/en/stable/class-reference.html). Below, you’ll find a few examples of common CanvasAPI use cases.
 
 #### Course objects
 
@@ -146,7 +153,7 @@ Most of Canvas’s API endpoints accept a variety of arguments. CanvasAPI allows
 >>> courses = user.get_courses(per_page=50)
 ```
 
-For a more detailed description of how CanvasAPI handles more complex keyword arguments, check out the [Keyword Argument Documentation](http://canvasapi.readthedocs.io/en/latest/keyword-args.html).
+For a more detailed description of how CanvasAPI handles more complex keyword arguments, check out the [Keyword Argument Documentation](http://canvasapi.readthedocs.io/en/stable/keyword-args.html).
 
 ## Contact Us
 
diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py
index c5d95fc..2d3cf91 100644
--- a/canvasapi/__init__.py
+++ b/canvasapi/__init__.py
@@ -4,4 +4,4 @@ from canvasapi.canvas import Canvas
 
 __all__ = ["Canvas"]
 
-__version__ = "2.0.0"
+__version__ = "2.1.0"
diff --git a/canvasapi/account.py b/canvasapi/account.py
index 1428dc7..3877d4e 100644
--- a/canvasapi/account.py
+++ b/canvasapi/account.py
@@ -1,8 +1,8 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.feature import Feature, FeatureFlag
-from canvasapi.grading_standard import GradingStandard
 from canvasapi.grading_period import GradingPeriod
+from canvasapi.grading_standard import GradingStandard
 from canvasapi.outcome_import import OutcomeImport
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.rubric import Rubric
diff --git a/canvasapi/assignment.py b/canvasapi/assignment.py
index 367bc2e..d987476 100644
--- a/canvasapi/assignment.py
+++ b/canvasapi/assignment.py
@@ -5,8 +5,7 @@ from canvasapi.peer_review import PeerReview
 from canvasapi.progress import Progress
 from canvasapi.submission import Submission
 from canvasapi.upload import Uploader
-from canvasapi.user import User
-from canvasapi.user import UserDisplay
+from canvasapi.user import User, UserDisplay
 from canvasapi.util import combine_kwargs, obj_or_id
 
 
diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py
index 2b668b0..962106e 100644
--- a/canvasapi/canvas.py
+++ b/canvasapi/canvas.py
@@ -73,7 +73,9 @@ class Canvas(object):
         """
 
         response = self.__requester.request(
-            "DELETE", "users/self/course_nicknames", _kwargs=combine_kwargs(**kwargs),
+            "DELETE",
+            "users/self/course_nicknames",
+            _kwargs=combine_kwargs(**kwargs),
         )
         return response.json().get("message") == "OK"
 
@@ -119,7 +121,9 @@ class Canvas(object):
         kwargs["event"] = event
 
         response = self.__requester.request(
-            "PUT", "conversations", _kwargs=combine_kwargs(**kwargs),
+            "PUT",
+            "conversations",
+            _kwargs=combine_kwargs(**kwargs),
         )
         return_progress = Progress(self.__requester, response.json())
         return return_progress
@@ -433,18 +437,40 @@ class Canvas(object):
         )
         return response.json()
 
-    def get_announcements(self, **kwargs):
+    def get_announcements(self, context_codes, **kwargs):
         """
         List announcements.
 
         :calls: `GET /api/v1/announcements \
         <https://canvas.instructure.com/doc/api/announcements.html#method.announcements_api.index>`_
 
+        :param context_codes: Course ID(s) or <Course> objects to request announcements from.
+        :type context_codes: list
+
         :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
                 :class:`canvasapi.discussion_topic.DiscussionTopic`
         """
         from canvasapi.discussion_topic import DiscussionTopic
 
+        if type(context_codes) is not list or len(context_codes) == 0:
+            raise RequiredFieldMissing("context_codes need to be passed as a list")
+
+        if isinstance(context_codes[0], str) and "course_" in context_codes[0]:
+            # Legacy support for context codes passed as list of `course_123`-style strings
+            kwargs["context_codes"] = context_codes
+        else:
+            # The type of object in `context_codes` is taken care of by obj_or_id, extracting
+            # the course ID from a <Course> object or by returning plain strings.
+            course_ids = [
+                obj_or_id(course_id, "context_codes", (Course,))
+                for course_id in context_codes
+            ]
+
+            # Set the **kwargs object vaue so it can be combined with others passed by the user.
+            kwargs["context_codes"] = [
+                f"course_{course_id}" for course_id in course_ids
+            ]
+
         return PaginatedList(
             DiscussionTopic,
             self.__requester,
@@ -566,7 +592,7 @@ class Canvas(object):
         <https://canvas.instructure.com/doc/api/comm_messages.html#method.comm_messages_api.index>`_
 
         :param user: The object or ID of the user.
-        :type user: :class: `canvasapi.user.User` or int
+        :type user: :class:`canvasapi.user.User` or int
 
         :returns: Paginated list containing messages sent to user
         :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
@@ -734,6 +760,14 @@ class Canvas(object):
         )
 
     def get_current_user(self):
+        """
+        Return a details of the current user.
+
+        :calls: `GET /api/v1/users/:user_id \
+        <https://canvas.instructure.com/doc/api/users.html#method.current_user.show>`_
+
+        :rtype: :class:`canvasapi.current_user.CurrentUser`
+        """
         return CurrentUser(self.__requester)
 
     def get_epub_exports(self, **kwargs):
diff --git a/canvasapi/canvas_object.py b/canvasapi/canvas_object.py
index 9fcef15..5a743eb 100644
--- a/canvasapi/canvas_object.py
+++ b/canvasapi/canvas_object.py
@@ -1,6 +1,7 @@
+import re
 from datetime import datetime
+
 import pytz
-import re
 
 DATE_PATTERN = re.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z")
 
diff --git a/canvasapi/content_migration.py b/canvasapi/content_migration.py
index 88ede57..fa5a792 100644
--- a/canvasapi/content_migration.py
+++ b/canvasapi/content_migration.py
@@ -147,9 +147,9 @@ class ContentMigration(CanvasObject):
             or :class:`canvasapi.course.Group`,
             or :class:`canvasapi.course.User`
         """
-        from canvasapi.group import Group
-        from canvasapi.course import Course
         from canvasapi.account import Account
+        from canvasapi.course import Course
+        from canvasapi.group import Group
         from canvasapi.user import User
 
         response = self._requester.request(
diff --git a/canvasapi/course.py b/canvasapi/course.py
index 4a5f2f3..36aac1d 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -1,3 +1,5 @@
+import warnings
+
 from canvasapi.assignment import Assignment, AssignmentGroup
 from canvasapi.blueprint import BlueprintSubscription
 from canvasapi.canvas_object import CanvasObject
@@ -5,35 +7,36 @@ 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.exceptions import RequiredFieldMissing
+from canvasapi.feature import Feature, FeatureFlag
+from canvasapi.folder import Folder
 from canvasapi.gradebook_history import (
     Day,
     Grader,
-    SubmissionVersion,
     SubmissionHistory,
+    SubmissionVersion,
 )
-from canvasapi.grading_standard import GradingStandard
 from canvasapi.grading_period import GradingPeriod
-from canvasapi.exceptions import RequiredFieldMissing
-from canvasapi.feature import Feature, FeatureFlag
-from canvasapi.folder import Folder
+from canvasapi.grading_standard import GradingStandard
 from canvasapi.license import License
 from canvasapi.outcome_import import OutcomeImport
 from canvasapi.page import Page
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
 from canvasapi.quiz import QuizExtension
-from canvasapi.tab import Tab
-from canvasapi.rubric import RubricAssociation, Rubric
+from canvasapi.rubric import Rubric, RubricAssociation
 from canvasapi.submission import GroupedSubmission, Submission
+from canvasapi.tab import Tab
+from canvasapi.todo import Todo
 from canvasapi.upload import Uploader
 from canvasapi.usage_rights import UsageRights
 from canvasapi.util import (
     combine_kwargs,
-    is_multivalued,
     file_or_path,
+    is_multivalued,
+    normalize_bool,
     obj_or_id,
     obj_or_str,
-    normalize_bool,
 )
 
 
@@ -109,7 +112,9 @@ class Course(CanvasObject):
         kwargs["event"] = "conclude"
 
         response = self._requester.request(
-            "DELETE", "courses/{}".format(self.id), _kwargs=combine_kwargs(**kwargs),
+            "DELETE",
+            "courses/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
         )
 
         return response.json().get("conclude")
@@ -571,7 +576,9 @@ class Course(CanvasObject):
         kwargs["event"] = "delete"
 
         response = self._requester.request(
-            "DELETE", "courses/{}".format(self.id), _kwargs=combine_kwargs(**kwargs),
+            "DELETE",
+            "courses/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
         )
         return response.json().get("delete")
 
@@ -636,7 +643,7 @@ class Course(CanvasObject):
 
         return response.status_code == 204
 
-    def enroll_user(self, user, enrollment_type, **kwargs):
+    def enroll_user(self, user, enrollment_type=None, **kwargs):
         """
         Create a new user enrollment for a course or a section.
 
@@ -646,14 +653,25 @@ class Course(CanvasObject):
         :param user: The object or ID of the user to enroll in this course.
         :type user: :class:`canvasapi.user.User` or int
         :param enrollment_type: The type of enrollment.
-        :type enrollment_type: str
+        :type enrollment_type: str, optional
         :rtype: :class:`canvasapi.enrollment.Enrollment`
         """
         from canvasapi.enrollment import Enrollment
         from canvasapi.user import User
 
         kwargs["enrollment[user_id]"] = obj_or_id(user, "user", (User,))
-        kwargs["enrollment[type]"] = enrollment_type
+
+        if enrollment_type:
+            warnings.warn(
+                (
+                    "The `enrollment_type` argument is deprecated and will be "
+                    "removed in a future version.\n"
+                    "Use `enrollment[type]` as a keyword argument instead. "
+                    "e.g. `enroll_user(enrollment={'type': 'StudentEnrollment'})`"
+                ),
+                DeprecationWarning,
+            )
+            kwargs["enrollment[type]"] = enrollment_type
 
         response = self._requester.request(
             "POST",
@@ -2083,6 +2101,25 @@ class Course(CanvasObject):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_todo_items(self, **kwargs):
+        """
+        Returns the current user's course-specific todo items.
+
+        :calls: `GET /api/v1/courses/:course_id/todo \
+        <https://canvas.instructure.com/doc/api/courses.html#method.courses.todo_items>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.todo.Todo`
+        """
+
+        return PaginatedList(
+            Todo,
+            self._requester,
+            "GET",
+            "courses/{}/todo".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_uncollated_submissions(self, **kwargs):
         """
         Gives a paginated, uncollated list of submission versions for all matching
diff --git a/canvasapi/custom_gradebook_columns.py b/canvasapi/custom_gradebook_columns.py
index 4791173..a9e5d64 100644
--- a/canvasapi/custom_gradebook_columns.py
+++ b/canvasapi/custom_gradebook_columns.py
@@ -1,9 +1,6 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.paginated_list import PaginatedList
-from canvasapi.util import (
-    combine_kwargs,
-    is_multivalued,
-)
+from canvasapi.util import combine_kwargs, is_multivalued
 
 
 class CustomGradebookColumn(CanvasObject):
diff --git a/canvasapi/discussion_topic.py b/canvasapi/discussion_topic.py
index 2a7dd50..c44631d 100644
--- a/canvasapi/discussion_topic.py
+++ b/canvasapi/discussion_topic.py
@@ -18,6 +18,13 @@ class DiscussionTopic(CanvasObject):
             return self.course_id
         elif hasattr(self, "group_id"):
             return self.group_id
+        elif hasattr(self, "context_code"):
+            if self.context_code.startswith("course_"):
+                self.course_id = self.context_code.split("_")[1]
+                return self.course_id
+            elif self.context_code.startswith("group_"):
+                self.group_id = self.context_code.split("_")[1]
+                return self.group_id
         else:
             raise ValueError("Discussion Topic does not have a course_id or group_id")
 
@@ -32,6 +39,11 @@ class DiscussionTopic(CanvasObject):
             return "course"
         elif hasattr(self, "group_id"):
             return "group"
+        elif hasattr(self, "context_code"):
+            if self.context_code.startswith("course"):
+                return "course"
+            elif self.context_code.startswith("group"):
+                return "group"
         else:
             raise ValueError("Discussion Topic does not have a course_id or group_id")
 
@@ -98,8 +110,8 @@ class DiscussionTopic(CanvasObject):
 
         :rtype: :class:`canvasapi.group.Group` or :class:`canvasapi.course.Course`
         """
-        from canvasapi.group import Group
         from canvasapi.course import Course
+        from canvasapi.group import Group
 
         response = self._requester.request(
             "GET",
diff --git a/canvasapi/file.py b/canvasapi/file.py
index ea68a52..81439f6 100644
--- a/canvasapi/file.py
+++ b/canvasapi/file.py
@@ -32,11 +32,15 @@ class File(CanvasObject):
         with open(location, "wb") as file_out:
             file_out.write(response.content)
 
-    def get_contents(self):
+    def get_contents(self, binary=False):
         """
         Download the contents of this file.
+        Pass binary=True to return a bytes object instead of a str.
 
-        :rtype: str
+        :rtype: str or bytes
         """
         response = self._requester.request("GET", _url=self.url)
-        return response.text
+        if binary:
+            return response.content
+        else:
+            return response.text
diff --git a/canvasapi/folder.py b/canvasapi/folder.py
index 76f852f..0b27083 100644
--- a/canvasapi/folder.py
+++ b/canvasapi/folder.py
@@ -1,7 +1,7 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.paginated_list import PaginatedList
-from canvasapi.util import combine_kwargs, obj_or_id
 from canvasapi.upload import Uploader
+from canvasapi.util import combine_kwargs, obj_or_id
 
 
 class Folder(CanvasObject):
diff --git a/canvasapi/group.py b/canvasapi/group.py
index 0309c09..a228748 100644
--- a/canvasapi/group.py
+++ b/canvasapi/group.py
@@ -1,8 +1,8 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.collaboration import Collaboration
 from canvasapi.discussion_topic import DiscussionTopic
-from canvasapi.folder import Folder
 from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.folder import Folder
 from canvasapi.license import License
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.tab import Tab
@@ -1049,8 +1049,8 @@ class GroupCategory(CanvasObject):
         :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.user.User`
             or :class:`canvasapi.progress.Progress`
         """
-        from canvasapi.user import User
         from canvasapi.progress import Progress
+        from canvasapi.user import User
 
         if sync:
             return PaginatedList(
diff --git a/canvasapi/page.py b/canvasapi/page.py
index b362cd0..e0840b8 100644
--- a/canvasapi/page.py
+++ b/canvasapi/page.py
@@ -56,8 +56,8 @@ class Page(CanvasObject):
 
         :rtype: :class:`canvasapi.group.Group` or :class:`canvasapi.course.Course`
         """
-        from canvasapi.group import Group
         from canvasapi.course import Course
+        from canvasapi.group import Group
 
         response = self._requester.request(
             "GET",
@@ -208,8 +208,8 @@ class PageRevision(CanvasObject):
 
         :rtype: :class:`canvasapi.group.Group` or :class:`canvasapi.course.Course`
         """
-        from canvasapi.group import Group
         from canvasapi.course import Course
+        from canvasapi.group import Group
 
         response = self._requester.request(
             "GET",
diff --git a/canvasapi/pairing_code.py b/canvasapi/pairing_code.py
new file mode 100644
index 0000000..1028540
--- /dev/null
+++ b/canvasapi/pairing_code.py
@@ -0,0 +1,6 @@
+from canvasapi.canvas_object import CanvasObject
+
+
+class PairingCode(CanvasObject):
+    def __str__(self):
+        return "{} - {}".format(self.user_id, self.code)
diff --git a/canvasapi/poll.py b/canvasapi/poll.py
index 45aa294..da36574 100644
--- a/canvasapi/poll.py
+++ b/canvasapi/poll.py
@@ -1,5 +1,5 @@
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.poll_choice import PollChoice
 from canvasapi.poll_session import PollSession
diff --git a/canvasapi/poll_choice.py b/canvasapi/poll_choice.py
index 600b79d..ccf64df 100644
--- a/canvasapi/poll_choice.py
+++ b/canvasapi/poll_choice.py
@@ -1,5 +1,5 @@
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.canvas_object import CanvasObject
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.util import combine_kwargs
 
 
diff --git a/canvasapi/poll_session.py b/canvasapi/poll_session.py
index 0167248..e03b435 100644
--- a/canvasapi/poll_session.py
+++ b/canvasapi/poll_session.py
@@ -1,7 +1,7 @@
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.canvas_object import CanvasObject
-from canvasapi.util import combine_kwargs, obj_or_id
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.poll_submission import PollSubmission
+from canvasapi.util import combine_kwargs, obj_or_id
 
 
 class PollSession(CanvasObject):
diff --git a/canvasapi/progress.py b/canvasapi/progress.py
index 5dafc93..eff7817 100644
--- a/canvasapi/progress.py
+++ b/canvasapi/progress.py
@@ -16,7 +16,9 @@ class Progress(CanvasObject):
         :rtype: :class:`canvasapi.progress.Progress`
         """
         response = self._requester.request(
-            "GET", "progress/{}".format(self.id), _kwargs=combine_kwargs(**kwargs),
+            "GET",
+            "progress/{}".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()
 
diff --git a/canvasapi/requester.py b/canvasapi/requester.py
index 8ea4b88..2d236ce 100644
--- a/canvasapi/requester.py
+++ b/canvasapi/requester.py
@@ -1,5 +1,5 @@
-from datetime import datetime
 import logging
+from datetime import datetime
 from pprint import pformat
 
 import requests
@@ -16,7 +16,6 @@ from canvasapi.exceptions import (
 )
 from canvasapi.util import clean_headers
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -157,7 +156,7 @@ class Requester(object):
             currently only the POST request of GraphQL is using this parameter.
             For all other methods it's just passed and ignored.
         :type json: `bool`
-        :rtype: str
+        :rtype: :class:`requests.Response`
         """
         full_url = _url if _url else "{}{}".format(self.base_url, endpoint)
 
@@ -218,9 +217,14 @@ class Requester(object):
         )
 
         try:
-            logger.debug("Data: {data}".format(data=pformat(response.json())))
-        except ValueError:
-            logger.debug("Data: {data}".format(data=pformat(response.text)))
+            logger.debug(
+                "Data: {data}".format(data=pformat(response.content.decode("utf-8")))
+            )
+        except UnicodeDecodeError:
+            logger.debug("Data: {data}".format(data=pformat(response.content)))
+        except AttributeError:
+            # response.content is None
+            logger.debug("No data")
 
         # Add response to internal cache
         if len(self._cache) > 4:
diff --git a/canvasapi/section.py b/canvasapi/section.py
index a74a25b..05fdb4f 100644
--- a/canvasapi/section.py
+++ b/canvasapi/section.py
@@ -2,7 +2,7 @@ from canvasapi.canvas_object import CanvasObject
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
 from canvasapi.submission import GroupedSubmission, Submission
-from canvasapi.util import combine_kwargs, obj_or_id, normalize_bool
+from canvasapi.util import combine_kwargs, normalize_bool, obj_or_id
 
 
 class Section(CanvasObject):
diff --git a/canvasapi/todo.py b/canvasapi/todo.py
new file mode 100644
index 0000000..d20d384
--- /dev/null
+++ b/canvasapi/todo.py
@@ -0,0 +1,6 @@
+from canvasapi.canvas_object import CanvasObject
+
+
+class Todo(CanvasObject):
+    def __str__(self):
+        return "Todo Item ({})".format(self.type)
diff --git a/canvasapi/user.py b/canvasapi/user.py
index a45fd27..e7612aa 100644
--- a/canvasapi/user.py
+++ b/canvasapi/user.py
@@ -3,8 +3,9 @@ from canvasapi.canvas_object import CanvasObject
 from canvasapi.communication_channel import CommunicationChannel
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.folder import Folder
-from canvasapi.paginated_list import PaginatedList
 from canvasapi.license import License
+from canvasapi.paginated_list import PaginatedList
+from canvasapi.pairing_code import PairingCode
 from canvasapi.upload import Uploader
 from canvasapi.usage_rights import UsageRights
 from canvasapi.util import combine_kwargs, obj_or_id, obj_or_str
@@ -118,6 +119,24 @@ class User(CanvasObject):
         )
         return Folder(self._requester, response.json())
 
+    def create_pairing_code(self, **kwargs):
+        """
+        Create a pairing code for this user.
+
+        :calls: `POST /api/v1/users/:user_id/observer_pairing_codes \
+            <https://canvas.instructure.com/doc/api/user_observees.html#method.observer_pairing_codes_api.create>`_
+
+        :rtype: :class:`canvasapi.pairing_code.PairingCode`
+        """
+
+        response = self._requester.request(
+            "POST",
+            "users/{}/observer_pairing_codes".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+        return PairingCode(self._requester, response.json())
+
     def edit(self, **kwargs):
         """
         Modify this user's information.
@@ -292,7 +311,9 @@ class User(CanvasObject):
         :rtype: dict
         """
         response = self._requester.request(
-            "GET", "users/{}/colors".format(self.id), _kwargs=combine_kwargs(**kwargs),
+            "GET",
+            "users/{}/colors".format(self.id),
+            _kwargs=combine_kwargs(**kwargs),
         )
         return response.json()
 
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index e95326d..cb5e00d 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -40,6 +40,7 @@ Class Reference
     outcome-ref
     outcome-import-ref
     page-ref
+    pairing-code-ref
     planner-ref
     poll-ref
     poll-choice-ref
@@ -54,6 +55,7 @@ Class Reference
     sis-import-ref
     submission-ref
     tab-ref
+    todo-ref
     upload-ref
     user-ref
     usage-rights-ref
diff --git a/docs/current-user-ref.rst b/docs/current-user-ref.rst
index 92246c0..f1b4de5 100644
--- a/docs/current-user-ref.rst
+++ b/docs/current-user-ref.rst
@@ -2,5 +2,9 @@
 CurrentUser
 ===========
 
+The :class:`canvasapi.current_user.CurrentUser` class is a subclass of :class:`canvasapi.user.User`, and thus also includes all of its methods.
+
+`Documentation for the User class <user-ref.html>`_
+
 .. autoclass:: canvasapi.current_user.CurrentUser
     :members:
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
index d85f69a..5fa8501 100644
--- a/docs/getting-started.rst
+++ b/docs/getting-started.rst
@@ -14,7 +14,7 @@ Usage
 Before using CanvasAPI, you'll need to instantiate a new Canvas object:
 
 .. code:: python
-    
+
     # Import the Canvas class
     from canvasapi import Canvas
 
@@ -121,3 +121,37 @@ Most of Canvas’s API endpoints accept a variety of arguments. CanvasAPI allows
     >>> courses = user.get_courses(per_page=50)
 
 For a more detailed description of how CanvasAPI handles more complex keyword arguments, check out the `Keyword Argument Documentation <keyword-args.html>`_.
+
+Smart DateTimes
+~~~~~~~~~~~~~~~
+
+CanvasAPI is set up to make working with datetime strings a little bit easier.
+
+If Canvas's documentation calls for an ISO 8601 format datetime string, you can pass in a `Python datetime object <https://docs.python.org/3/library/datetime.html>`_ instead. For example, updating the :code:`start_at` and :code:`end_at` dates of a course:
+
+
+.. code-block:: python
+
+    from datetime import datetime
+
+    start_date = datetime(2018, 1, 1, 0, 1)  # '2018-01-01T00:01Z'
+    end_date = datetime(2018, 12, 31, 11, 59)  # '2018-12-31T11:59Z'
+
+    course.update(
+        course={
+            'start_at': start_date,
+            'end_at': end_date,
+        }
+    )
+
+If Canvas returns an ISO 8601 formatted datetime string, CanvasAPI will automatically create a :code:`datetime` object from that string. This new :code:`datetime` object will be a the string attribute as the same key with :code:`_date` appended. For example, :code:`start_at` becomes :code:`start_at_date`. The attribute with the original string representation of the date is retained.
+
+.. code-block:: python
+
+    >>> course = canvas.get_course(1)
+    >>> course.start_at
+    '2014-02-11T16:38:00Z'
+    >>> course.start_at_date
+    datetime.datetime(2014, 2, 11, 16, 38, tzinfo=<UTC>)
+
+**Note**: At this time, the automatic conversion to `datetime` objects only works on strings with no UTC offset (ending in "Z").
diff --git a/docs/index.rst b/docs/index.rst
index ee8667d..fde738c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,6 +15,7 @@ Contents:
    examples
    keyword-args
    exceptions
+   troubleshooting
    debugging
    class-reference
    internal-classes
diff --git a/docs/pairing-code-ref.rst b/docs/pairing-code-ref.rst
new file mode 100644
index 0000000..a27b9c7
--- /dev/null
+++ b/docs/pairing-code-ref.rst
@@ -0,0 +1,6 @@
+===========
+PairingCode
+===========
+
+.. autoclass:: canvasapi.pairing_code.PairingCode
+    :members:
diff --git a/docs/todo-ref.rst b/docs/todo-ref.rst
new file mode 100644
index 0000000..b55351c
--- /dev/null
+++ b/docs/todo-ref.rst
@@ -0,0 +1,6 @@
+====
+Todo
+====
+
+.. autoclass:: canvasapi.todo.Todo
+    :members:
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
new file mode 100644
index 0000000..504f691
--- /dev/null
+++ b/docs/troubleshooting.rst
@@ -0,0 +1,79 @@
+Troubleshooting
+===============
+
+If you're just starting out with CanvasAPI, you may run into exceptions right out of the gate. Below are a few common issues to check for.
+
+URL Formatting
+--------------
+
+Confirm that the Canvas URL you're passing into the :class:`canvasapi.Canvas` object as ``base_url`` is properly formatted:
+
+* Starts with ``https://`` (Only use ``http`` if absolutely neccessary)
+* Does **NOT** include ``/api/v1/``
+* Does **NOT** have a trailing slash
+* Does **NOT** have leading or trailing whitespace
+
+Bad URL Examples (Avoid These!)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* :code:`"example.edu"`
+* :code:`"http://example.edu"`
+* :code:`"https://example.edu/api/v1/"`
+* :code:`"https://example.edu/"`
+* :code:`"https://canvas.example.edu/"`
+* :code:`"https://example.edu/canvas/"`
+* :code:`" https://example.edu"`
+* :code:`"https://example.edu "`
+
+Good URL Examples
+~~~~~~~~~~~~~~~~~
+
+* :code:`"https://example.edu"`
+* :code:`"https://canvas.example.edu"`
+* :code:`"https://example.edu/canvas"`
+
+Access Token
+------------
+
+CanvasAPI expects an access token, **NOT** a developer key. If you have a developer key, you'll need to follow the `OAuth2 process <https://canvas.instructure.com/doc/api/file.oauth.html>`_ to get the access token for a user.
+
+To create an access token as your user without a developer key, you can create one manually through the Canvas LMS user interface. Go to Account > Settings, then scroll down to the "Approved Integrations" section and click the "New Access Token" button.
+
+You can test your new access token manually with a third-party tool like `curl <https://curl.se/docs/manpage.html>`_ or `Postman <https://www.postman.com/>`_. To test with CanvasAPI, call :meth:`canvasapi.Canvas.get_current_user` and check if returns the appropriate user information.
+
+Ensure the token you provide to CanvasAPI is correctly formatted:
+
+* Does **NOT** have leading or trailing whitespace
+* Does **NOT** contain ``Bearer`` (CanvasAPI will add this for you, where appropriate)
+* Includes the shard ID, separated by ``~`` (if applicable)
+* Is a valid (non-expired) token
+* Is the full length token (usually 64 characters, not including the shard ID)
+
+Bad Access Token Examples (Avoid these!)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* :code:`" 7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u"`
+* :code:`"7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u "`
+* :code:`"Bearer 7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u"`
+* :code:`"1 7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u"`
+* :code:`"7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0"`
+
+Good Access Token Examples
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* :code:`"7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u"`
+* :code:`"1~7H155up3r53CR3770K3n15n07R34L4Nd5h0uLDN0783u53dpL34534nd7H4nKY0u"`
+
+User Permissions
+----------------
+
+You may recieve unexpected :class:`~canvasapi.exceptions.Unauthorized` or :class:`~canvasapi.exceptions.Forbidden` exceptions. It is also possible to get :class:`~canvasapi.exceptions.ResourceDoesNotExist` exceptions for objects that actually do exist. These are usually signs of the user lacking sufficient permissions to access the endpoint.
+
+To determine if the issue is related to a bad access token or insufficent permissions, use the test described in `Access Token`_ above. If that works properly but you still can't access the other endpoints, your user may not have the permissions required to use that endpoint. You may need to discuss your permission levels with your Canvas LMS Administrator.
+
+Still Have a Problem?
+---------------------
+
+If you've checked for and corrected these common mistakes but are still encountering an unexpected issue, please look at our `instructions for setting up logging in CanvasAPI <debugging.html>`_. This may help find the issue.
+
+If you need further assistance, please `create an issue on GitHub <https://github.com/ucfopen/canvasapi/issues/new/choose>`_ or join us in the ``#canvasapi`` channel on the `UCF Open Source Slack <https://ucf-open-slackin.herokuapp.com/>`_. There are lots of friendly folks willing to help you figure out what's going on!
diff --git a/setup.cfg b/setup.cfg
index 160e963..d690360 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,3 +3,6 @@ source = canvasapi/
 
 [flake8]
 max-line-length = 99
+
+[isort]
+profile=black


Test patch:
diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh
index 8d02b98..c8f3809 100755
--- a/scripts/run_tests.sh
+++ b/scripts/run_tests.sh
@@ -4,6 +4,7 @@ coverage run -m unittest discover
 coverage report
 coverage html
 black --check canvasapi tests
+isort --check canvasapi tests
 flake8 canvasapi tests
 mdl . .github
 python scripts/alphabetic.py
diff --git a/tests/fixtures/announcements.json b/tests/fixtures/announcements.json
index b637050..684540d 100755
--- a/tests/fixtures/announcements.json
+++ b/tests/fixtures/announcements.json
@@ -10,7 +10,8 @@
           "display_name": "John Doe"
         },
         "message": "Announcement Message #1",
-        "subscription_hold": "topic_is_announcement"
+        "subscription_hold": "topic_is_announcement",
+        "context_code": "course_1"
       },
       {
         "id": 2,
@@ -20,7 +21,30 @@
           "display_name": "John Smith"
         },
         "message": "Announcement Message #2",
-        "subscription_hold": "topic_is_announcement"
+        "subscription_hold": "topic_is_announcement",
+        "context_code": "course_1"
+      },
+      {
+        "id": 3,
+        "title": "Annoucement 3",
+        "author": {
+          "id": 3,
+          "display_name": "John Wick"
+        },
+        "message": "Announcement Message #3",
+        "subscription_hold": "topic_is_announcement",
+        "context_code": "group_1"
+      },
+      {
+        "id": 4,
+        "title": "Annoucement 4",
+        "author": {
+          "id": 3,
+          "display_name": "Jon Voight"
+        },
+        "message": "Announcement Message #3",
+        "subscription_hold": "topic_is_announcement",
+        "context_code": "course_2"
       }
     ],
     "status_code": 200
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index 8242170..32db324 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -641,6 +641,32 @@
             }
         ]
     },
+	"todo_items": {
+		"method": "GET",
+		"endpoint": "courses/1/todo",
+		"data": [
+			{
+				"type": "grading",
+				"assignment": {},
+				"ignore": ".. url ..",
+				"ignore_permanently": ".. url ..",
+				"html_url": ".. url ..",
+				"needs_grading_count": 3,
+				"context_type": "course",
+				"course_id": 1,
+				"group_id": null
+			},
+			{
+				"type": "submitting",
+				"assignment": {},
+				"ignore": ".. url ..",
+				"ignore_permanently": ".. url ..",
+				"html_url": ".. url ..",
+				"context_type": "course",
+				"course_id": 1
+			}
+		]
+	},
 	"get_uncollated_submissions": {
 		"method": "GET",
 		"endpoint": "courses/1/gradebook_history/feed",
diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json
index b180fc6..11b77b1 100644
--- a/tests/fixtures/user.json
+++ b/tests/fixtures/user.json
@@ -267,7 +267,18 @@
             "full_name": "Folder 1"
         },
         "status_code": 200
-    },
+	},
+	"observer_pairing_codes": {
+		"method": "POST",
+		"endpoint": "users/1/observer_pairing_codes",
+		"data": {
+			"user_id": 1,
+			"code": "abc123",
+			"expires_at": "2020-10-30T17:00:00Z",
+			"workflow_state": "active"
+		},
+		"status_code": 200
+	},
 	"get_user_assignments": {
 		"method": "GET",
 		"endpoint": "users/1/courses/1/assignments",
diff --git a/tests/test_account.py b/tests/test_account.py
index 160491b..9d3a0d7 100644
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -1,7 +1,7 @@
 import datetime
-import pytz
 import unittest
 
+import pytz
 import requests_mock
 
 from canvasapi import Canvas
@@ -13,13 +13,14 @@ from canvasapi.account import (
     Role,
     SSOSettings,
 )
-from canvasapi.authentication_provider import AuthenticationProvider
 from canvasapi.authentication_event import AuthenticationEvent
+from canvasapi.authentication_provider import AuthenticationProvider
+from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Course
 from canvasapi.enrollment import Enrollment
 from canvasapi.enrollment_term import EnrollmentTerm
-from canvasapi.external_tool import ExternalTool
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
+from canvasapi.external_tool import ExternalTool
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.grading_period import GradingPeriod
 from canvasapi.grading_standard import GradingStandard
@@ -32,7 +33,6 @@ from canvasapi.rubric import Rubric
 from canvasapi.scope import Scope
 from canvasapi.sis_import import SisImport
 from canvasapi.user import User
-from canvasapi.content_migration import ContentMigration, Migrator
 from tests import settings
 from tests.util import register_uris
 
diff --git a/tests/test_assignment.py b/tests/test_assignment.py
index 7f96109..e77a325 100644
--- a/tests/test_assignment.py
+++ b/tests/test_assignment.py
@@ -6,9 +6,9 @@ import requests_mock
 from canvasapi import Canvas
 from canvasapi.assignment import (
     Assignment,
+    AssignmentExtension,
     AssignmentGroup,
     AssignmentOverride,
-    AssignmentExtension,
 )
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
 from canvasapi.peer_review import PeerReview
@@ -16,7 +16,7 @@ from canvasapi.progress import Progress
 from canvasapi.submission import Submission
 from canvasapi.user import UserDisplay
 from tests import settings
-from tests.util import register_uris, cleanup_file
+from tests.util import cleanup_file, register_uris
 
 
 @requests_mock.Mocker()
diff --git a/tests/test_authentication_event.py b/tests/test_authentication_event.py
index 415bccc..7db7b10 100644
--- a/tests/test_authentication_event.py
+++ b/tests/test_authentication_event.py
@@ -26,9 +26,9 @@ class TestAuthenticationEvent(unittest.TestCase):
             )
             self.user = self.canvas.get_user(1)
 
-            self.authentication_event_account = self.account.get_authentication_events()[
-                0
-            ]
+            self.authentication_event_account = (
+                self.account.get_authentication_events()[0]
+            )
             self.authentication_event_login = self.login.get_authentication_events()[0]
             self.authentication_event_user = self.user.get_authentication_events()[0]
 
diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py
index 6b3c3b7..ca1f725 100644
--- a/tests/test_blueprint.py
+++ b/tests/test_blueprint.py
@@ -3,8 +3,7 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
-from canvasapi.blueprint import BlueprintMigration
-from canvasapi.blueprint import ChangeRecord
+from canvasapi.blueprint import BlueprintMigration, ChangeRecord
 from canvasapi.course import Course
 from canvasapi.paginated_list import PaginatedList
 from tests import settings
@@ -165,9 +164,9 @@ class TestBlueprintMigration(unittest.TestCase):
             self.blueprint = self.course.get_blueprint(1)
             self.blueprint_migration = self.blueprint.show_blueprint_migration(1)
             self.blueprint_subscription = self.course.list_blueprint_subscriptions()[0]
-            self.blueprint_imports = self.blueprint_subscription.list_blueprint_imports()[
-                0
-            ]
+            self.blueprint_imports = (
+                self.blueprint_subscription.list_blueprint_imports()[0]
+            )
             self.b_import = self.blueprint_subscription.show_blueprint_import(3)
 
     # __str__()
diff --git a/tests/test_canvas.py b/tests/test_canvas.py
index d27aa03..87ec42a 100644
--- a/tests/test_canvas.py
+++ b/tests/test_canvas.py
@@ -702,13 +702,60 @@ class TestCanvas(unittest.TestCase):
         self.assertEqual(progress.id, 1)
 
     # get_announcements()
-    def test_get_announcements(self, m):
+    def test_get_single_course_announcements(self, m):
         register_uris({"announcements": ["list_announcements"]}, m)
-        announcements = self.canvas.get_announcements()
+        announcements = self.canvas.get_announcements([1])
         announcement_list = [announcement for announcement in announcements]
+
         self.assertIsInstance(announcements, PaginatedList)
         self.assertIsInstance(announcement_list[0], DiscussionTopic)
-        self.assertEqual(len(announcement_list), 2)
+        self.assertEqual(len(announcement_list), 4)
+
+    def test_get_course_announcements_from_object(self, m):
+        register_uris(
+            {"course": ["get_by_id"], "announcements": ["list_announcements"]}, m
+        )
+        course = self.canvas.get_course(1)
+        announcements = self.canvas.get_announcements([course])
+
+        self.assertIsInstance(announcements, PaginatedList)
+
+    def test_get_course_announcements_from_mixed_list(self, m):
+        register_uris(
+            {"course": ["get_by_id"], "announcements": ["list_announcements"]}, m
+        )
+        course = self.canvas.get_course(1)
+        course_ids = [course, 2]
+        announcements = self.canvas.get_announcements(course_ids)
+
+        self.assertIsInstance(announcements, PaginatedList)
+
+    def test_get_announcements_fail(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.canvas.get_announcements([])
+        with self.assertRaises(RequiredFieldMissing):
+            self.canvas.get_announcements(1)
+
+    def test_multiple_course_announcements(self, m):
+        register_uris({"announcements": ["list_announcements"]}, m)
+        announcements = self.canvas.get_announcements([1, 2])
+        announcement_list = [announcement for announcement in announcements]
+
+        self.assertEqual(announcement_list[1].context_code, "course_1")
+        self.assertEqual(announcement_list[1]._parent_type, "course")
+        self.assertEqual(announcement_list[1]._parent_id, "1")
+
+        self.assertEqual(announcement_list[2].context_code, "group_1")
+        self.assertEqual(announcement_list[2]._parent_type, "group")
+        self.assertEqual(announcement_list[2]._parent_id, "1")
+
+    def test_course_announcements_legacy(self, m):
+        register_uris({"announcements": ["list_announcements"]}, m)
+        announcements = self.canvas.get_announcements(context_codes=["course_1"])
+
+        self.assertEqual(announcements[0].context_code, "course_1")
+        self.assertEqual(announcements[0]._parent_type, "course")
+        self.assertEqual(announcements[0]._parent_id, "1")
 
     # get_epub_exports()
     def test_get_epub_exports(self, m):
diff --git a/tests/test_content_migration.py b/tests/test_content_migration.py
index 8a81a22..553d0ae 100644
--- a/tests/test_content_migration.py
+++ b/tests/test_content_migration.py
@@ -191,11 +191,11 @@ class TestMigrationIssue(unittest.TestCase):
             self.content_migration_user = self.user.get_content_migration(1)
 
             self.migration_issue = self.content_migration.get_migration_issue(1)
-            self.migration_issue_course = self.content_migration_course.get_migration_issue(
-                1
+            self.migration_issue_course = (
+                self.content_migration_course.get_migration_issue(1)
             )
-            self.migration_issue_group = self.content_migration_group.get_migration_issue(
-                1
+            self.migration_issue_group = (
+                self.content_migration_group.get_migration_issue(1)
             )
             self.migration_issue_user = self.content_migration_user.get_migration_issue(
                 1
diff --git a/tests/test_course.py b/tests/test_course.py
index 8c30035..2fca3dc 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -1,32 +1,35 @@
 import unittest
-from urllib.parse import quote
 import uuid
+import warnings
+from urllib.parse import quote
 
 import requests
 import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.assignment import Assignment, AssignmentGroup, AssignmentOverride
-from canvasapi.blueprint import BlueprintSubscription
-from canvasapi.blueprint import BlueprintTemplate
-from canvasapi.course import Course, CourseNickname, Page, LatePolicy
+from canvasapi.blueprint import BlueprintSubscription, BlueprintTemplate
+from canvasapi.content_export import ContentExport
+from canvasapi.content_migration import ContentMigration, Migrator
+from canvasapi.course import Course, CourseNickname, LatePolicy, Page
+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,
-    Grader,
-    SubmissionVersion,
-    SubmissionHistory,
-)
-from canvasapi.grading_standard import GradingStandard
 from canvasapi.enrollment import Enrollment
-from canvasapi.course_epub_export import CourseEpubExport
-from canvasapi.exceptions import ResourceDoesNotExist, RequiredFieldMissing
+from canvasapi.exceptions import RequiredFieldMissing, ResourceDoesNotExist
 from canvasapi.external_feed import ExternalFeed
 from canvasapi.external_tool import ExternalTool
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.gradebook_history import (
+    Day,
+    Grader,
+    SubmissionHistory,
+    SubmissionVersion,
+)
 from canvasapi.grading_period import GradingPeriod
+from canvasapi.grading_standard import GradingStandard
 from canvasapi.group import Group, GroupCategory
 from canvasapi.license import License
 from canvasapi.module import Module
@@ -34,16 +37,14 @@ from canvasapi.outcome import OutcomeGroup, OutcomeLink
 from canvasapi.outcome_import import OutcomeImport
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.progress import Progress
-from canvasapi.quiz import Quiz, QuizExtension, QuizAssignmentOverrideSet
+from canvasapi.quiz import Quiz, QuizAssignmentOverrideSet, QuizExtension
 from canvasapi.rubric import Rubric, RubricAssociation
 from canvasapi.section import Section
 from canvasapi.submission import GroupedSubmission, Submission
 from canvasapi.tab import Tab
-from canvasapi.user import User
+from canvasapi.todo import Todo
 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 canvasapi.user import User
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
@@ -75,7 +76,8 @@ class TestCourse(unittest.TestCase):
     # column_data_bulk_update()
     def test_column_data_bulk_update(self, m):
         register_uris(
-            {"course": ["column_data_bulk_update"], "progress": ["course_progress"]}, m,
+            {"course": ["column_data_bulk_update"], "progress": ["course_progress"]},
+            m,
         )
         progress = self.course.column_data_bulk_update(
             column_data=[
@@ -202,20 +204,52 @@ class TestCourse(unittest.TestCase):
         register_uris(requires, m)
 
         enrollment_type = "TeacherEnrollment"
-        user_by_id = self.canvas.get_user(1)
-        enrollment_by_id = self.course.enroll_user(user_by_id, enrollment_type)
+
+        # by user ID
+        enrollment_by_id = self.course.enroll_user(
+            1, enrollment={"type": enrollment_type}
+        )
 
         self.assertIsInstance(enrollment_by_id, Enrollment)
         self.assertTrue(hasattr(enrollment_by_id, "type"))
         self.assertEqual(enrollment_by_id.type, enrollment_type)
 
-        user_by_obj = self.canvas.get_user(self.user)
-        enrollment_by_obj = self.course.enroll_user(user_by_obj, enrollment_type)
+        # by user object
+        enrollment_by_obj = self.course.enroll_user(
+            self.user, enrollment={"type": enrollment_type}
+        )
 
         self.assertIsInstance(enrollment_by_obj, Enrollment)
         self.assertTrue(hasattr(enrollment_by_obj, "type"))
         self.assertEqual(enrollment_by_obj.type, enrollment_type)
 
+    def test_enroll_user_legacy(self, m):
+        warnings.simplefilter("always", DeprecationWarning)
+
+        requires = {"course": ["enroll_user"], "user": ["get_by_id"]}
+        register_uris(requires, m)
+
+        enrollment_type = "TeacherEnrollment"
+
+        with warnings.catch_warnings(record=True) as warning_list:
+            # by user ID
+            enrollment_by_id = self.course.enroll_user(1, enrollment_type)
+
+            self.assertIsInstance(enrollment_by_id, Enrollment)
+            self.assertTrue(hasattr(enrollment_by_id, "type"))
+            self.assertEqual(enrollment_by_id.type, enrollment_type)
+
+            # by user object
+            enrollment_by_obj = self.course.enroll_user(self.user, enrollment_type)
+
+            self.assertIsInstance(enrollment_by_obj, Enrollment)
+            self.assertTrue(hasattr(enrollment_by_obj, "type"))
+            self.assertEqual(enrollment_by_obj.type, enrollment_type)
+
+        self.assertEqual(len(warning_list), 2)
+        self.assertEqual(warning_list[0].category, DeprecationWarning)
+        self.assertEqual(warning_list[1].category, DeprecationWarning)
+
     # get_recent_students()
     def test_get_recent_students(self, m):
         recent = {"course": ["get_recent_students", "get_recent_students_p2"]}
@@ -942,7 +976,8 @@ class TestCourse(unittest.TestCase):
     def test_create_external_tool_missing_params(self, m):
         with self.assertRaises(RequiredFieldMissing):
             self.course.create_external_tool(
-                consumer_key="key", shared_secret="secret",
+                consumer_key="key",
+                shared_secret="secret",
             )
 
     # get_collaborations
@@ -1139,6 +1174,15 @@ class TestCourse(unittest.TestCase):
         self.assertEqual(len(tab_list), 2)
         self.assertIsInstance(tab_list[0], Tab)
 
+    # get_todo_items()
+    def test_get_todo_items(self, m):
+        register_uris({"course": ["todo_items"]}, m)
+
+        todo_items = self.course.get_todo_items()
+        todo_list = [todo for todo in todo_items]
+
+        self.assertIsInstance(todo_list[0], Todo)
+
     # get_rubric
     def test_get_rubric(self, m):
         register_uris({"course": ["get_rubric_single"]}, m)
diff --git a/tests/test_discussion_topic.py b/tests/test_discussion_topic.py
index 7c1dea4..268bfa5 100644
--- a/tests/test_discussion_topic.py
+++ b/tests/test_discussion_topic.py
@@ -4,7 +4,7 @@ import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.course import Course
-from canvasapi.discussion_topic import DiscussionTopic, DiscussionEntry
+from canvasapi.discussion_topic import DiscussionEntry, DiscussionTopic
 from canvasapi.exceptions import Forbidden
 from canvasapi.group import Group
 from tests import settings
@@ -47,7 +47,6 @@ class TestDiscussionTopic(unittest.TestCase):
         discussion = self.discussion_topic.update()
         self.assertIsInstance(discussion, DiscussionTopic)
         self.assertTrue(hasattr(discussion, "course_id"))
-        self.assertEqual(discussion.course_id, 1)
 
     # post_entry()
     def test_post_entry(self, m):
diff --git a/tests/test_file.py b/tests/test_file.py
index 45df8df..ed7a569 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -6,8 +6,7 @@ import requests_mock
 from canvasapi import Canvas
 from canvasapi.file import File
 from tests import settings
-from tests.util import cleanup_file
-from tests.util import register_uris
+from tests.util import cleanup_file, register_uris
 
 
 @requests_mock.Mocker()
@@ -54,3 +53,5 @@ class TestFile(unittest.TestCase):
         register_uris({"file": ["file_contents"]}, m)
         contents = self.file.get_contents()
         self.assertEqual(contents, '"Hello there"')
+        contents_binary = self.file.get_contents(binary=True)
+        self.assertEqual(contents_binary, b'"Hello there"')
diff --git a/tests/test_folder.py b/tests/test_folder.py
index a7c1df3..4381e93 100644
--- a/tests/test_folder.py
+++ b/tests/test_folder.py
@@ -7,7 +7,7 @@ from canvasapi import Canvas
 from canvasapi.file import File
 from canvasapi.folder import Folder
 from tests import settings
-from tests.util import register_uris, cleanup_file
+from tests.util import cleanup_file, register_uris
 
 
 @requests_mock.Mocker()
diff --git a/tests/test_grading_period.py b/tests/test_grading_period.py
index 07aa7ca..8a76fcf 100644
--- a/tests/test_grading_period.py
+++ b/tests/test_grading_period.py
@@ -5,8 +5,8 @@ import requests_mock
 from canvasapi import Canvas
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.grading_period import GradingPeriod
-from tests.util import register_uris
 from tests import settings
+from tests.util import register_uris
 
 
 @requests_mock.Mocker()
diff --git a/tests/test_group.py b/tests/test_group.py
index 705dff9..576be82 100644
--- a/tests/test_group.py
+++ b/tests/test_group.py
@@ -1,24 +1,24 @@
 import unittest
-from urllib.parse import quote
 import uuid
+from urllib.parse import quote
 
 import requests
 import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.assignment import AssignmentOverride
-from canvasapi.group import Group, GroupMembership, GroupCategory
+from canvasapi.content_export import ContentExport
+from canvasapi.content_migration import ContentMigration, Migrator
 from canvasapi.course import Page
 from canvasapi.discussion_topic import DiscussionTopic
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.external_feed import ExternalFeed
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.group import Group, GroupCategory, GroupMembership
 from canvasapi.license import License
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.tab import Tab
-from canvasapi.content_migration import ContentMigration, Migrator
-from canvasapi.content_export import ContentExport
 from canvasapi.usage_rights import UsageRights
 from tests import settings
 from tests.util import cleanup_file, register_uris
@@ -715,8 +715,8 @@ class TestGroupCategory(unittest.TestCase):
 
     # assign_members()
     def test_assign_members(self, m):
-        from canvasapi.progress import Progress
         from canvasapi.paginated_list import PaginatedList
+        from canvasapi.progress import Progress
 
         requires = {
             "group": ["category_assign_members_true", "category_assign_members_false"]
diff --git a/tests/test_pairing_code.py b/tests/test_pairing_code.py
new file mode 100644
index 0000000..13dcf66
--- /dev/null
+++ b/tests/test_pairing_code.py
@@ -0,0 +1,28 @@
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.pairing_code import PairingCode
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestPairingCode(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {"user": ["get_by_id"]}
+            register_uris(requires, m)
+
+            self.user = self.canvas.get_user(1)
+
+    # __str__()
+    def test__str__(self, m):
+        register_uris({"user": ["observer_pairing_codes"]}, m)
+
+        pairing_code = self.user.create_pairing_code()
+        self.assertIsInstance(pairing_code, PairingCode)
+        self.assertEqual("1 - abc123", pairing_code.__str__())
diff --git a/tests/test_planner.py b/tests/test_planner.py
index abc9217..e0564df 100644
--- a/tests/test_planner.py
+++ b/tests/test_planner.py
@@ -2,8 +2,8 @@ import unittest
 
 import requests_mock
 
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.planner import PlannerNote, PlannerOverride
 from tests import settings
 from tests.util import register_uris
diff --git a/tests/test_poll.py b/tests/test_poll.py
index 468f257..154bccc 100644
--- a/tests/test_poll.py
+++ b/tests/test_poll.py
@@ -2,8 +2,8 @@ import unittest
 
 import requests_mock
 
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.poll import Poll
 from tests import settings
 from tests.util import register_uris
diff --git a/tests/test_poll_choice.py b/tests/test_poll_choice.py
index 0e87eab..ad1101e 100644
--- a/tests/test_poll_choice.py
+++ b/tests/test_poll_choice.py
@@ -2,8 +2,8 @@ import unittest
 
 import requests_mock
 
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.poll_choice import PollChoice
 from tests import settings
 from tests.util import register_uris
diff --git a/tests/test_poll_session.py b/tests/test_poll_session.py
index 2f8dd1e..c8ba241 100644
--- a/tests/test_poll_session.py
+++ b/tests/test_poll_session.py
@@ -2,8 +2,8 @@ import unittest
 
 import requests_mock
 
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.poll_session import PollSession
 from tests import settings
 from tests.util import register_uris
diff --git a/tests/test_poll_submission.py b/tests/test_poll_submission.py
index 58b289d..4cd8484 100644
--- a/tests/test_poll_submission.py
+++ b/tests/test_poll_submission.py
@@ -2,8 +2,8 @@ import unittest
 
 import requests_mock
 
-from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.poll_submission import PollSubmission
 from tests import settings
 from tests.util import register_uris
@@ -24,8 +24,8 @@ class TestPollSubmission(unittest.TestCase):
 
             self.poll = self.canvas.get_poll(1)
             self.poll.poll_session = self.poll.get_session(1)
-            self.poll.poll_session.poll_submission = self.poll.poll_session.get_submission(
-                1
+            self.poll.poll_session.poll_submission = (
+                self.poll.poll_session.get_submission(1)
             )
 
     # __str__()
diff --git a/tests/test_quiz.py b/tests/test_quiz.py
index b2929eb..45049f4 100644
--- a/tests/test_quiz.py
+++ b/tests/test_quiz.py
@@ -4,23 +4,23 @@ import requests_mock
 
 from canvasapi import Canvas
 from canvasapi.exceptions import RequiredFieldMissing
+from canvasapi.paginated_list import PaginatedList
 from canvasapi.quiz import (
     Quiz,
+    QuizAssignmentOverrideSet,
+    QuizExtension,
+    QuizQuestion,
+    QuizReport,
     QuizStatistic,
     QuizSubmission,
-    QuizSubmissionQuestion,
-    QuizQuestion,
-    QuizExtension,
     QuizSubmissionEvent,
-    QuizReport,
-    QuizAssignmentOverrideSet,
+    QuizSubmissionQuestion,
 )
 from canvasapi.quiz_group import QuizGroup
-from canvasapi.paginated_list import PaginatedList
+from canvasapi.submission import Submission
+from canvasapi.user import User
 from tests import settings
 from tests.util import register_uris
-from canvasapi.user import User
-from canvasapi.submission import Submission
 
 
 @requests_mock.Mocker()
diff --git a/tests/test_quiz_group.py b/tests/test_quiz_group.py
index f43e65e..cd38b73 100644
--- a/tests/test_quiz_group.py
+++ b/tests/test_quiz_group.py
@@ -3,9 +3,9 @@ import unittest
 import requests_mock
 
 from canvasapi import Canvas
+from canvasapi.exceptions import RequiredFieldMissing
 from tests import settings
 from tests.util import register_uris
-from canvasapi.exceptions import RequiredFieldMissing
 
 
 @requests_mock.Mocker()
diff --git a/tests/test_requester.py b/tests/test_requester.py
index 68b4a01..df59eb9 100644
--- a/tests/test_requester.py
+++ b/tests/test_requester.py
@@ -1,5 +1,5 @@
-from datetime import datetime
 import unittest
+from datetime import datetime
 from urllib.parse import quote
 
 import requests
@@ -32,6 +32,18 @@ class TestRequester(unittest.TestCase):
         response = self.requester.request("GET", "fake_get_request")
         self.assertEqual(response.status_code, 200)
 
+    def test_request_get_binary(self, m):
+        m.register_uri(
+            "GET",
+            settings.BASE_URL_WITH_VERSION + "get_binary_data",
+            content=b"\xff\xff\xff",
+            status_code=200,
+            headers={},
+        )
+
+        response = self.requester.request("GET", "get_binary_data")
+        self.assertEqual(response.content, b"\xff\xff\xff")
+
     def test_request_get_datetime(self, m):
         date = datetime.today()
 
diff --git a/tests/test_todo.py b/tests/test_todo.py
new file mode 100644
index 0000000..f72e81a
--- /dev/null
+++ b/tests/test_todo.py
@@ -0,0 +1,33 @@
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from canvasapi.todo import Todo
+from tests import settings
+
+
+@requests_mock.Mocker()
+class TestTodo(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        self.todo = Todo(
+            self.canvas._Canvas__requester,
+            {
+                "type": "grading",
+                "assignment": {},
+                "ignore": ".. url ..",
+                "ignore_permanently": ".. url ..",
+                "html_url": ".. url ..",
+                "needs_grading_count": 3,
+                "context_type": "course",
+                "course_id": 1,
+                "group_id": None,
+            },
+        )
+
+    def test_str(self, m):
+        test_str = str(self.todo)
+        self.assertIsInstance(test_str, str)
+        self.assertEqual(test_str, "Todo Item (grading)")
diff --git a/tests/test_user.py b/tests/test_user.py
index d46e26f..0391d3a 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -16,12 +16,13 @@ from canvasapi.enrollment import Enrollment
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.file import File
 from canvasapi.folder import Folder
-from canvasapi.login import Login
 from canvasapi.license import License
+from canvasapi.login import Login
 from canvasapi.page_view import PageView
 from canvasapi.paginated_list import PaginatedList
-from canvasapi.user import User
+from canvasapi.pairing_code import PairingCode
 from canvasapi.usage_rights import UsageRights
+from canvasapi.user import User
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
@@ -565,6 +566,14 @@ class TestUser(unittest.TestCase):
         self.assertIsInstance(root_folder_list[0], Folder)
         self.assertEqual("my_files", root_folder_list[0].name)
 
+    # create_pairing_code()
+    def test_create_pairing_code(self, m):
+        register_uris({"user": ["observer_pairing_codes"]}, m)
+
+        pairing_code = self.user.create_pairing_code()
+        self.assertIsInstance(pairing_code, PairingCode)
+        self.assertEqual("abc123", pairing_code.code)
+
 
 @requests_mock.Mocker()
 class TestUserDisplay(unittest.TestCase):
diff --git a/tests/test_util.py b/tests/test_util.py
index dbf5377..863d7e1 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -1,5 +1,6 @@
 import unittest
 import uuid
+from itertools import chain
 
 import requests_mock
 
@@ -9,14 +10,13 @@ from canvasapi.user import User
 from canvasapi.util import (
     clean_headers,
     combine_kwargs,
+    file_or_path,
     get_institution_url,
     is_multivalued,
+    normalize_bool,
     obj_or_id,
     obj_or_str,
-    file_or_path,
-    normalize_bool,
 )
-from itertools import chain
 from tests import settings
 from tests.util import cleanup_file, register_uris
 
diff --git a/tests/test_validate_docstrings.py b/tests/test_validate_docstrings.py
index 5138dc7..b9e16bd 100644
--- a/tests/test_validate_docstrings.py
+++ b/tests/test_validate_docstrings.py
@@ -1,15 +1,14 @@
-import re
 import io
+import re
+import unittest
+
+import requests_mock
 
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.folder import Folder
 from canvasapi.util import combine_kwargs, obj_or_id
 from scripts.validate_docstrings import validate_method
 
-import unittest
-
-import requests_mock
-
 
 # test_endpoint_docstrings
 @requests_mock.Mocker()
diff --git a/tests_requirements.txt b/tests_requirements.txt
index 06f873c..ecc22e1 100644
--- a/tests_requirements.txt
+++ b/tests_requirements.txt
@@ -3,4 +3,5 @@
 black; python_version >= '3.6'
 coverage
 flake8
+isort
 requests-mock