diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dc84e024..265289b3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,81 +2,132 @@ Thanks for your interest in contributing! -Below you'll find guidelines for contributing that will keep our codebase clean and happy. +Below you'll find guidelines for contributing that will keep our codebase clean +and happy. ## 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) +- [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) + - [Type hinting](#type-hinting) + - [Type hint fomatting](#type-hint-formatting) + - [Type hints in Classes](#type-hints-in-classes) + - [Python 3.6](#python-3.6-type-hints) + - [Python 3.7+](#python-3.7+-type-hints) ## How can I contribute? ### 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). +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). Here are a few things worth mentioning when making a report: -* What **version** of CanvasAPI are you running? (`pip show canvasapi`) -* What **version** of Python are you using? (`python --version`) -* What steps can be taken to **reproduce the issue**? -* **Detail matters.** Try not to be too be verbose, but generally the more information, the better! +- What **version** of CanvasAPI are you running? (`pip show canvasapi`) +- What **version** of Python are you using? (`python --version`) +- What steps can be taken to **reproduce the issue**? +- **Detail matters.** Try not to be too be verbose, but generally the more + information, the better! ### Resolving issues -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/). - -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. +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/). + +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. ### Making your first contribution #### 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. - -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` -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`) +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. + +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` +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`) 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. +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. #### Writing tests -Tests are a critical part of building applications, and we [pity the fool who doesn't write them](https://blog.codinghorror.com/i-pity-the-fool-who-doesnt-write-unit-tests/). Unit tests help us monitor the health of the code checked into the repository and they provide a nice overview at the progress we make. Due to the size and nature of the library, it's unrealistic for us to manually test each component. Because of this, we require pull requests to A) have tests associated with the changes being made and B) pass those and all other tests. +Tests are a critical part of building applications, and we +[pity the fool who doesn't write them](https://blog.codinghorror.com/i-pity-the-fool-who-doesnt-write-unit-tests/). +Unit tests help us monitor the health of the code checked into the repository +and they provide a nice overview at the progress we make. Due to the size and +nature of the library, it's unrealistic for us to manually test each component. +Because of this, we require pull requests to A) have tests associated with the +changes being made and B) pass those and all other tests. -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. +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 -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: +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: ```python # get_user() @@ -95,7 +146,9 @@ Breakdown: # get_user() ``` -It is common to have multiple tests for a single method. All related tests should be grouped together under a single comment with the name of the method being tested. +It is common to have multiple tests for a single method. All related tests +should be grouped together under a single comment with the name of the method +being tested. --- @@ -103,7 +156,9 @@ It is common to have multiple tests for a single method. All related tests shoul def test_get_user(self, m): ``` -This is a standard Python `unittest` test method with one addition: the `m` variable is passed to all methods with names starting with `test`. `m` is a Mocker object that can be used to override the routing of HTTP requests. +This is a standard Python `unittest` test method with one addition: the `m` +variable is passed to all methods with names starting with `test`. `m` is a +Mocker object that can be used to override the routing of HTTP requests. --- @@ -111,7 +166,12 @@ This is a standard Python `unittest` test method with one addition: the `m` vari register_uris({'course': ['get_user']}, m) ``` -The `register_uris` function tells a mocker object which fixtures to load. It takes in two arguments: a dictionary describing which fixtures to load, and a mocker object. The dictionary keys represent which file the desired fixtures are located in. The values are lists containing each desired fixture from that particular file. The example above will register the `get_user` fixture in `course.json`. +The `register_uris` function tells a mocker object which fixtures to load. It +takes in two arguments: a dictionary describing which fixtures to load, and a +mocker object. The dictionary keys represent which file the desired fixtures are +located in. The values are lists containing each desired fixture from that +particular file. The example above will register the `get_user` fixture in +`course.json`. Example Fixture: @@ -127,7 +187,9 @@ Example Fixture: }, ``` -When this fixture is loaded, all `GET` requests to a url matching `courses/1/users/1` will return a status code of 200 and the provided user data for John Doe. +When this fixture is loaded, all `GET` requests to a url matching +`courses/1/users/1` will return a status code of 200 and the provided user data +for John Doe. --- @@ -138,11 +200,16 @@ self.assertIsInstance(user, User) self.assertTrue(hasattr(user, 'name')) ``` -The rest is basic unit testing. Call the function to be tested, and assert various outcomes. If necessary, multiple tests can written for a single method. All related tests should appear together under the same comment, as described earlier. +The rest is basic unit testing. Call the function to be tested, and assert +various outcomes. If necessary, multiple tests can written for a single method. +All related tests should appear together under the same comment, as described +earlier. --- -It is common to need certain object(s) for multiple tests. For example, most methods in `test_course.py` require a `Course` object. In this case, save a course to the class in `self.course` for later use. +It is common to need certain object(s) for multiple tests. For example, most +methods in `test_course.py` require a `Course` object. In this case, save a +course to the class in `self.course` for later use. Do this in the `setUp` class method: @@ -161,7 +228,9 @@ with requests_mock.Mocker() as m: self.user = self.canvas.get_user(1) ``` -Since `setUp` is not a test method, it does not automatically get passed a Mocker object `m`. To use the mocker, all relevant code needs to be inside a `with` statement: +Since `setUp` is not a test method, it does not automatically get passed a +Mocker object `m`. To use the mocker, all relevant code needs to be inside a +`with` statement: ```python with requests_mock.Mocker() as m: @@ -169,15 +238,26 @@ with requests_mock.Mocker() as m: ##### Engine tests -Not all of CanvasAPI relies on networking. While these pieces are few and far between, we still need to verify that they're performing correctly. Writing tests for engine-level code is just as important as user-facing code and is a bit easier. You'll just need to follow the same process as you would for API tests, minus the fixtures. +Not all of CanvasAPI relies on networking. While these pieces are few and far +between, we still need to verify that they're performing correctly. Writing +tests for engine-level code is just as important as user-facing code and is a +bit easier. You'll just need to follow the same process as you would for API +tests, minus the fixtures. #### Running tests / coverage reports -Once you've written test case(s) for your issue, you'll need to run the test to verify that your changes are passing and haven't interfered with any other part of the library. +Once you've written test case(s) for your issue, you'll need to run the test to +verify that your changes are passing and haven't interfered with any other part +of the library. -You'll do this by running `coverage run -m unittest discover` from the main `canvasapi` directory. If your tests pass, you're ready to run a coverage report! +You'll do this by running `coverage run -m unittest discover` from the main +`canvasapi` directory. If your tests pass, you're ready to run a coverage +report! -Coverage reports tell us how much of our code is actually being tested. As of right now, we're happily maintaining 100% code coverage (🎉!) and our goal is to keep it there. Ensure you've covered your changes entirely by running `coverage report`. Your output should look something like this: +Coverage reports tell us how much of our code is actually being tested. As of +right now, we're happily maintaining 100% code coverage (🎉!) and our goal is to +keep it there. Ensure you've covered your changes entirely by running +`coverage report`. Your output should look something like this: ```Formatted Name Stmts Miss Cover @@ -194,18 +274,25 @@ canvasapi/util.py 29 0 100% 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)! +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 -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. +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 -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 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. +- 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. We use `flake8` for linting: @@ -213,13 +300,15 @@ We use `flake8` for linting: flake8 canvasapi tests ``` -We use `black` for auto-formatting. When you run the command below, `black` will automatically convert your code to our desired style. +We use `black` for auto-formatting. When you run the command below, `black` will +automatically convert your code to our desired style. ```sh black canvasapi tests ``` -We require methods to be in alphabetical order for ease of reading. Run this script to confirm order: +We require methods to be in alphabetical order for ease of reading. Run this +script to confirm order: ```sh python scripts/alphabetic.py @@ -229,38 +318,53 @@ python scripts/alphabetic.py > A foolish consistency is the hobgoblin of little minds. -- Ralph Waldo Emerson -An important tenet of PEP8 is to not get hung up on PEP8. While we try to be as PEP8 compliant as possible, maintaining the consistency of the project is more important than modifying an existing style choice. +An important tenet of PEP8 is to not get hung up on PEP8. While we try to be as +PEP8 compliant as possible, maintaining the consistency of the project is more +important than modifying an existing style choice. Below you'll find several established styles that'll help you along the way. ### Method docstrings -Method docstrings should include a description, a link to the related API endpoint (if available), parameter name, parameter description, and parameter type, return description (if available), and return type. They should be included in the following order: +Method docstrings should include a description, a link to the related API +endpoint (if available), parameter name, parameter description, and parameter +type, return description (if available), and return type. They should be +included in the following order: #### Descriptions -A description should be a concise, *action* statement (use "*write* a good docstring" over "*writes* a good docstring") that describes the method. Generally, the official API documentation's description is usable (make sure it's an **action statement** though). Special functionality should be documented. +A description should be a concise, _action_ statement (use "_write_ a good +docstring" over "_writes_ a good docstring") that describes the method. +Generally, the official API documentation's description is usable (make sure +it's an **action statement** though). Special functionality should be +documented. #### Links to related API endpoints -A link to a related API endpoint is denoted with `:calls:`. CanvasAPI uses Sphinx to automatically generate documentation, so we can provide a link to an API endpoint with the reStructuredText syntax: +A link to a related API endpoint is denoted with `:calls:`. CanvasAPI uses +Sphinx to automatically generate documentation, so we can provide a link to an +API endpoint with the reStructuredText syntax: ```rst :calls: `THE TEXT OF THE HYPERLINK \ `_ ``` -Hyperlink text should match the text underneath the endpoint in the official Canvas API documentation. Generally, that looks like this: +Hyperlink text should match the text underneath the endpoint in the official +Canvas API documentation. Generally, that looks like this: ```rst :calls: `HTTP_METHOD /api/v1/endpoint/:variable ``` -**Note**: It's okay to go over 80 characters for the URL, it can't be helped. Use a backslash to split the hyperlink text from the actual URL to limit line length. +**Note**: It's okay to go over 80 characters for the URL, it can't be helped. +Use a backslash to split the hyperlink text from the actual URL to limit line +length. #### Parameters -Parameters should be listed in the order that they appear in the method prototype. They should take on the following form: +Parameters should be listed in the order that they appear in the method +prototype. They should take on the following form: ```rst :param PARAMETER_NAME: PARAMETER_DESCRIPTION. @@ -269,7 +373,8 @@ Parameters should be listed in the order that they appear in the method prototyp #### Returns -**Return description** should be listed first, if available. This should be included to clarify a returned value, for example: +**Return description** should be listed first, if available. This should be +included to clarify a returned value, for example: ```python def uncheck_box(box_id): @@ -281,9 +386,13 @@ def uncheck_box(box_id): """ ``` -In most cases, the return value is easy to infer based on the type and the description given in the docstring. `:returns:` is only necessary to clarify ambiguous cases. +In most cases, the return value is easy to infer based on the type and the +description given in the docstring. `:returns:` is only necessary to clarify +ambiguous cases. -**Return type** should always be included when a value is returned. If it's not a primitive type (`int`, `str`, `bool`, `list`, etc.) a fully-qualified class name should be included: +**Return type** should always be included when a value is returned. If it's not +a primitive type (`int`, `str`, `bool`, `list`, etc.) a fully-qualified class +name should be included: ```rst :rtype: :class:`canvasapi.user.User` @@ -341,3 +450,124 @@ def clear_course_nicknames(self): :rtype: bool """ ``` + +### Type Annotations + +Type annotations were added in Python 3.6 and allow maintainers to run static +checks on code to identify and fix potential `TypeError` bugs before runtime. +Type annotations should be added to maintain consistency across modules. + +#### Type hint formatting + +Type annotations follow the general format below to define expected types for +method parameters and returns. + +```python +def my_function(param1: str, param2: str) -> str: + return f'You submitted {param1} and {param2}!" +``` + +#### Type Hints in Classes + +Classes instantiated with an `__init__()` method are expected to return `None`. + +```python +class MyClass(): + + __init__(self, param1: str, param2: int) -> None: + self.param1: param1, + self.param2: param2 +``` + +Class methods, on the other hand, can be annotated with a specific return type. +In the example below, the `get_course()` method returns an instance of `Course`: + +```python +class Canvas(): + + def get_course( + self, + course: Union[int, Course], + use_sis_id: bool = False, + **kwargs: Optional[dict], + ) -> Course: ... +``` + +Often, a method can have multiple valid parameters. In `get_course`, the +`course` param can be either an `int` or a `Course` object. The special `Union` +type allows you to accept multiple types for a given parameter. Every method has +an optional `**kwargs` argument which can be annotated by importing the +`Optional` type from the `typing` package: + +```python +from typing import Optional, Union +``` + +Both types accept a list of _other_ types which would be valid in the function: + +```python +def my_function(param1: Union[str, int], **kwargs: Optional[str, int]) -> Any: ... +``` + +#### Python 3.6 type hints + +Modules are often imported when necessary in specific class methods rather than +at the top of the file. This can cause problems because the imports happen as +the code is executed, not when the program is initialized. This will cause +errors to show when the function is returning a valid value: + +```python +""" +In this example, would have an error in the static check because +it has not been imported before the function definition. However, this is a valid +function because it is instantiated by the time the function returns. +""" +def get_appointment_group( + self, appointment_group: Union[int, AppointmentGroup], **kwargs: Optional[dict] + ) -> AppointmentGroup: + + from canvasapi.appointment_group import AppointmentGroup + + appointment_group_id = obj_or_id( + appointment_group, "appointment_group", (AppointmentGroup,) + ) + + response = self.__requester.request( + "GET", + "appointment_groups/{}".format(appointment_group_id), + _kwargs=combine_kwargs(**kwargs), + ) + return AppointmentGroup(self.__requester, response.json()) +``` + +To get around this problem, the `typing` package has a `TYPE_CHECKING` module +which allows you to define imports for static checks that will be ignored at +runtime. Annotate the class name as a string and mypy will recognize it as a +valid class. + +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from canvasapi.appointment_group import AppointmentGroup + +""" +The error in the above code block will no longer be displayed because the static checker +does the import at the top of the file. +""" +def get_appointment_group( + self, appointment_group: Union[int, 'AppointmentGroup'], **kwargs: Optional[dict] + ) -> 'AppointmentGroup': ... +``` + +#### Python 3.7+ type hints + +Python 3.7 by added the ability to defer module loading. Instead of adding a +conditional check, import the new `annotations` module: + +```python +from __future__ import annotations +``` + +The `annotations` module is in the standard library in Python 3.10 and can be +imported directly. diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index dc8fae6d..9af33457 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -1,3 +1,4 @@ +from typing import Union, Optional, List, TYPE_CHECKING import warnings from canvasapi.account import Account @@ -15,13 +16,25 @@ from canvasapi.user import User from canvasapi.util import combine_kwargs, get_institution_url, obj_or_id +# Necessary to support type hinting in Python 3.6 +# Starting in 3.7, module deffered loading is available in __future__.annotations +if TYPE_CHECKING: + from canvasapi.progress import Progress + from canvasapi.appointment_group import AppointmentGroup + from canvasapi.calendar_event import CalendarEvent + from canvasapi.conversation import Conversation + from canvasapi.planner import PlannerNote, PlannerOverride + from canvasapi.poll import Poll + from canvasapi.course import CourseNickname + from canvasapi.outcome import Outcome, OutcomeGroup + class Canvas(object): """ The main class to be instantiated to provide access to Canvas's API. """ - def __init__(self, base_url, access_token): + def __init__(self, base_url: str, access_token: str) -> None: """ :param base_url: The base URL of the Canvas instance's API. :type base_url: str @@ -60,7 +73,7 @@ def __init__(self, base_url, access_token): self.__requester = Requester(base_url, access_token) - def clear_course_nicknames(self, **kwargs): + def clear_course_nicknames(self, **kwargs: Optional[dict]) -> bool: """ Remove all stored course nicknames. @@ -79,7 +92,12 @@ def clear_course_nicknames(self, **kwargs): ) return response.json().get("message") == "OK" - def conversations_batch_update(self, conversation_ids, event, **kwargs): + def conversations_batch_update( + self, + conversation_ids: List[str], + event: str, + **kwargs: Union[str, List[str], Optional[dict]], + ) -> "Progress": """ :calls: `PUT /api/v1/conversations \ @@ -128,7 +146,7 @@ def conversations_batch_update(self, conversation_ids, event, **kwargs): return_progress = Progress(self.__requester, response.json()) return return_progress - def conversations_get_running_batches(self, **kwargs): + def conversations_get_running_batches(self, **kwargs: Optional[dict]) -> dict: """ Returns any currently running conversation batches for the current user. Conversation batches are created when a bulk private message is sent @@ -147,7 +165,7 @@ def conversations_get_running_batches(self, **kwargs): return response.json() - def conversations_mark_all_as_read(self, **kwargs): + def conversations_mark_all_as_read(self, **kwargs: Optional[dict]) -> bool: """ Mark all conversations as read. @@ -161,7 +179,7 @@ def conversations_mark_all_as_read(self, **kwargs): ) return response.json() == {} - def conversations_unread_count(self, **kwargs): + def conversations_unread_count(self, **kwargs: Optional[dict]) -> dict: """ Get the number of unread conversations for the current user @@ -177,7 +195,7 @@ def conversations_unread_count(self, **kwargs): return response.json() - def create_account(self, **kwargs): + def create_account(self, **kwargs: Optional[dict]) -> Account: """ Create a new root account. @@ -191,7 +209,9 @@ def create_account(self, **kwargs): ) return Account(self.__requester, response.json()) - def create_appointment_group(self, appointment_group, **kwargs): + def create_appointment_group( + self, appointment_group: dict, **kwargs: Optional[dict] + ) -> "AppointmentGroup": """ Create a new Appointment Group. @@ -230,7 +250,9 @@ def create_appointment_group(self, appointment_group, **kwargs): return AppointmentGroup(self.__requester, response.json()) - def create_calendar_event(self, calendar_event, **kwargs): + def create_calendar_event( + self, calendar_event: dict, **kwargs: Union[str, dict, Optional[dict]] + ) -> "CalendarEvent": """ Create a new Calendar Event. @@ -256,7 +278,12 @@ def create_calendar_event(self, calendar_event, **kwargs): return CalendarEvent(self.__requester, response.json()) - def create_conversation(self, recipients, body, **kwargs): + def create_conversation( + self, + recipients: List[str], + body: str, + **kwargs: Union[str, list, Optional[dict]], + ) -> List["Conversation"]: """ Create a new Conversation. @@ -282,7 +309,7 @@ def create_conversation(self, recipients, body, **kwargs): ) return [Conversation(self.__requester, convo) for convo in response.json()] - def create_group(self, **kwargs): + def create_group(self, **kwargs: Optional[dict]) -> Group: """ Create a group @@ -296,7 +323,7 @@ def create_group(self, **kwargs): ) return Group(self.__requester, response.json()) - def create_planner_note(self, **kwargs): + def create_planner_note(self, **kwargs: Optional[dict]) -> "PlannerNote": """ Create a planner note for the current user @@ -312,7 +339,12 @@ def create_planner_note(self, **kwargs): ) return PlannerNote(self.__requester, response.json()) - def create_planner_override(self, plannable_type, plannable_id, **kwargs): + def create_planner_override( + self, + plannable_type: str, + plannable_id: Union[int, "PlannerOverride"], + **kwargs: Union[str, int, Optional[dict]], + ) -> "PlannerOverride": """ Create a planner override for the current user @@ -343,7 +375,9 @@ def create_planner_override(self, plannable_type, plannable_id, **kwargs): ) return PlannerOverride(self.__requester, response.json()) - def create_poll(self, poll, **kwargs): + def create_poll( + self, poll: List[dict], **kwargs: Union[list, Optional[dict]] + ) -> "Poll": """ Create a new poll for the current user. @@ -372,7 +406,12 @@ def create_poll(self, poll, **kwargs): ) return Poll(self.__requester, response.json()["polls"][0]) - def get_account(self, account, use_sis_id=False, **kwargs): + def get_account( + self, + account: Union[int, Account], + use_sis_id: bool = False, + **kwargs: Optional[dict], + ) -> Account: """ Retrieve information on an individual account. @@ -399,7 +438,7 @@ def get_account(self, account, use_sis_id=False, **kwargs): ) return Account(self.__requester, response.json()) - def get_accounts(self, **kwargs): + def get_accounts(self, **kwargs: Optional[dict]) -> PaginatedList: """ List accounts that the current user can view or manage. @@ -421,7 +460,7 @@ def get_accounts(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_activity_stream_summary(self, **kwargs): + def get_activity_stream_summary(self, **kwargs: Optional[dict]) -> dict: """ Return a summary of the current user's global activity stream. @@ -437,7 +476,11 @@ def get_activity_stream_summary(self, **kwargs): ) return response.json() - def get_announcements(self, courses, **kwargs): + def get_announcements( + self, + courses: List[Union[int, Course]], + **kwargs: Union[List[str], Optional[dict]], + ) -> PaginatedList: """ List announcements. @@ -473,7 +516,11 @@ def get_announcements(self, courses, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_appointment_group(self, appointment_group, **kwargs): + def get_appointment_group( + self, + appointment_group: Union[int, "AppointmentGroup"], + **kwargs: Optional[dict], + ) -> "AppointmentGroup": """ Return single Appointment Group by id @@ -498,7 +545,7 @@ def get_appointment_group(self, appointment_group, **kwargs): ) return AppointmentGroup(self.__requester, response.json()) - def get_appointment_groups(self, **kwargs): + def get_appointment_groups(self, **kwargs: Optional[dict]) -> PaginatedList: """ List appointment groups. @@ -518,7 +565,7 @@ def get_appointment_groups(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_brand_variables(self, **kwargs): + def get_brand_variables(self, **kwargs: Optional[dict]) -> dict: """ Get account brand variables @@ -533,7 +580,9 @@ def get_brand_variables(self, **kwargs): ) return response.json() - def get_calendar_event(self, calendar_event, **kwargs): + def get_calendar_event( + self, calendar_event: Union[int, "CalendarEvent"], **kwargs: Optional[dict] + ) -> "CalendarEvent": """ Return single Calendar Event by id @@ -558,7 +607,7 @@ def get_calendar_event(self, calendar_event, **kwargs): ) return CalendarEvent(self.__requester, response.json()) - def get_calendar_events(self, **kwargs): + def get_calendar_events(self, **kwargs: Optional[dict]) -> PaginatedList: """ List calendar events. @@ -578,7 +627,9 @@ def get_calendar_events(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_comm_messages(self, user, **kwargs): + def get_comm_messages( + self, user: Union[int, User], **kwargs: Union[int, Optional[dict]] + ) -> PaginatedList: """ Retrieve a paginated list of messages sent to a user. @@ -604,7 +655,9 @@ def get_comm_messages(self, user, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_conversation(self, conversation, **kwargs): + def get_conversation( + self, conversation: Union[int, "Conversation"], **kwargs: Optional[dict] + ) -> "Conversation": """ Return single Conversation @@ -627,7 +680,7 @@ def get_conversation(self, conversation, **kwargs): ) return Conversation(self.__requester, response.json()) - def get_conversations(self, **kwargs): + def get_conversations(self, **kwargs: Optional[dict]) -> PaginatedList: """ Return list of conversations for the current user, most resent ones first. @@ -647,7 +700,12 @@ def get_conversations(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_course(self, course, use_sis_id=False, **kwargs): + def get_course( + self, + course: Union[int, Course], + use_sis_id: bool = False, + **kwargs: Optional[dict], + ) -> Course: """ Retrieve a course by its ID. @@ -674,7 +732,7 @@ def get_course(self, course, use_sis_id=False, **kwargs): ) return Course(self.__requester, response.json()) - def get_course_accounts(self, **kwargs): + def get_course_accounts(self, **kwargs: Optional[dict]) -> PaginatedList: """ List accounts that the current user can view through their admin course enrollments (Teacher, TA or designer enrollments). @@ -696,7 +754,9 @@ def get_course_accounts(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_course_nickname(self, course, **kwargs): + def get_course_nickname( + self, course: Union[int, Course], **kwargs: Optional[dict] + ) -> "CourseNickname": """ Return the nickname for the given course. @@ -719,7 +779,7 @@ def get_course_nickname(self, course, **kwargs): ) return CourseNickname(self.__requester, response.json()) - def get_course_nicknames(self, **kwargs): + def get_course_nicknames(self, **kwargs: Optional[dict]) -> PaginatedList: """ Return all course nicknames set by the current account. @@ -739,7 +799,7 @@ def get_course_nicknames(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_courses(self, **kwargs): + def get_courses(self, **kwargs: Optional[dict]) -> PaginatedList: """ Return a list of active courses for the current user. @@ -753,7 +813,7 @@ def get_courses(self, **kwargs): Course, self.__requester, "GET", "courses", _kwargs=combine_kwargs(**kwargs) ) - def get_current_user(self): + def get_current_user(self) -> CurrentUser: """ Return a details of the current user. @@ -764,7 +824,7 @@ def get_current_user(self): """ return CurrentUser(self.__requester) - def get_epub_exports(self, **kwargs): + def get_epub_exports(self, **kwargs: Optional[dict]) -> PaginatedList: """ Return a list of epub exports for the associated course. @@ -784,7 +844,7 @@ def get_epub_exports(self, **kwargs): kwargs=combine_kwargs(**kwargs), ) - def get_file(self, file, **kwargs): + def get_file(self, file: Union[int, File], **kwargs: Optional[dict]) -> File: """ Return the standard attachment json object for a file. @@ -803,7 +863,9 @@ def get_file(self, file, **kwargs): ) return File(self.__requester, response.json()) - def get_folder(self, folder, **kwargs): + def get_folder( + self, folder: Union[int, Folder], **kwargs: Optional[dict] + ) -> Folder: """ Return the details for a folder @@ -822,7 +884,12 @@ def get_folder(self, folder, **kwargs): ) return Folder(self.__requester, response.json()) - def get_group(self, group, use_sis_id=False, **kwargs): + def get_group( + self, + group: Union[int, Group], + use_sis_id: bool = False, + **kwargs: Optional[dict], + ) -> Group: """ Return the data for a single group. If the caller does not have permission to view the group a 401 will be returned. @@ -852,7 +919,9 @@ def get_group(self, group, use_sis_id=False, **kwargs): ) return Group(self.__requester, response.json()) - def get_group_category(self, category, **kwargs): + def get_group_category( + self, category: Union[int, GroupCategory], **kwargs: Optional[dict] + ) -> GroupCategory: """ Get a single group category. @@ -873,7 +942,11 @@ def get_group_category(self, category, **kwargs): ) return GroupCategory(self.__requester, response.json()) - def get_group_participants(self, appointment_group, **kwargs): + def get_group_participants( + self, + appointment_group: Union[int, "AppointmentGroup"], + **kwargs: Optional[dict], + ) -> PaginatedList: """ List student group participants in this appointment group. @@ -900,7 +973,9 @@ def get_group_participants(self, appointment_group, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_outcome(self, outcome, **kwargs): + def get_outcome( + self, outcome: Union[int, "Outcome"], **kwargs: Optional[dict] + ) -> "Outcome": """ Returns the details of the outcome with the given id. @@ -921,7 +996,9 @@ def get_outcome(self, outcome, **kwargs): ) return Outcome(self.__requester, response.json()) - def get_outcome_group(self, group, **kwargs): + def get_outcome_group( + self, group: Union[int, "OutcomeGroup"], **kwargs: Optional[dict] + ) -> "OutcomeGroup": """ Returns the details of the Outcome Group with the given id. @@ -946,7 +1023,9 @@ def get_outcome_group(self, group, **kwargs): return OutcomeGroup(self.__requester, response.json()) - def get_planner_note(self, planner_note, **kwargs): + def get_planner_note( + self, planner_note: Union[int, "PlannerNote"], **kwargs: Optional[dict] + ) -> "PlannerNote": """ Retrieve a planner note for the current user @@ -975,7 +1054,7 @@ def get_planner_note(self, planner_note, **kwargs): return PlannerNote(self.__requester, response.json()) - def get_planner_notes(self, **kwargs): + def get_planner_notes(self, **kwargs: Optional[dict]) -> PaginatedList: """ Retrieve the paginated list of planner notes @@ -995,7 +1074,9 @@ def get_planner_notes(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_planner_override(self, planner_override, **kwargs): + def get_planner_override( + self, planner_override: Union[int, "PlannerOverride"], **kwargs: Optional[dict] + ) -> "PlannerOverride": """ Retrieve a planner override for the current user @@ -1028,7 +1109,7 @@ def get_planner_override(self, planner_override, **kwargs): return PlannerOverride(self.__requester, response.json()) - def get_planner_overrides(self, **kwargs): + def get_planner_overrides(self, **kwargs: Optional[dict]) -> PaginatedList: """ Retrieve a planner override for the current user @@ -1048,7 +1129,7 @@ def get_planner_overrides(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_poll(self, poll, **kwargs): + def get_poll(self, poll: Union[int, "Poll"], **kwargs: Optional[dict]) -> "Poll": """ Get a single poll, based on the poll id. @@ -1068,7 +1149,7 @@ def get_poll(self, poll, **kwargs): ) return Poll(self.__requester, response.json()["polls"][0]) - def get_polls(self, **kwargs): + def get_polls(self, **kwargs: Optional[dict]) -> PaginatedList: """ Returns a paginated list of polls for the current user @@ -1089,7 +1170,9 @@ def get_polls(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def get_progress(self, progress, **kwargs): + def get_progress( + self, progress: Union[int, "Progress"], **kwargs: Optional[dict] + ) -> "Progress": """ Get a specific progress. @@ -1111,7 +1194,7 @@ def get_progress(self, progress, **kwargs): ) return Progress(self.__requester, response.json()) - def get_root_outcome_group(self, **kwargs): + def get_root_outcome_group(self, **kwargs: Optional[dict]) -> "OutcomeGroup": """ Redirect to root outcome group for context @@ -1128,7 +1211,12 @@ def get_root_outcome_group(self, **kwargs): ) return OutcomeGroup(self.__requester, response.json()) - def get_section(self, section, use_sis_id=False, **kwargs): + def get_section( + self, + section: Union[int, Section], + use_sis_id: bool = False, + **kwargs: Optional[dict], + ) -> Section: """ Get details about a specific section. @@ -1147,6 +1235,7 @@ def get_section(self, section, use_sis_id=False, **kwargs): section_id = section uri_str = "sections/sis_section_id:{}" else: + # TODO: Fix this with overloading? section_id = obj_or_id(section, "section", (Section,)) uri_str = "sections/{}" @@ -1155,7 +1244,7 @@ def get_section(self, section, use_sis_id=False, **kwargs): ) return Section(self.__requester, response.json()) - def get_todo_items(self, **kwargs): + def get_todo_items(self, **kwargs: Optional[dict]) -> dict: """ Return the current user's list of todo items, as seen on the user dashboard. @@ -1169,7 +1258,7 @@ def get_todo_items(self, **kwargs): ) return response.json() - def get_upcoming_events(self, **kwargs): + def get_upcoming_events(self, **kwargs: Optional[dict]) -> dict: """ Return the current user's upcoming events, i.e. the same things shown in the dashboard 'Coming Up' sidebar. @@ -1184,7 +1273,9 @@ def get_upcoming_events(self, **kwargs): ) return response.json() - def get_user(self, user, id_type=None, **kwargs): + def get_user( + self, user: Union[int, User], id_type: str = None, **kwargs: Optional[dict] + ) -> User: """ Retrieve a user by their ID. `id_type` denotes which endpoint to try as there are several different IDs that can pull the same user record from Canvas. @@ -1216,7 +1307,11 @@ def get_user(self, user, id_type=None, **kwargs): ) return User(self.__requester, response.json()) - def get_user_participants(self, appointment_group, **kwargs): + def get_user_participants( + self, + appointment_group: Union[int, "AppointmentGroup"], + **kwargs: Optional[dict], + ) -> PaginatedList: """ List user participants in this appointment group. @@ -1243,7 +1338,9 @@ def get_user_participants(self, appointment_group, **kwargs): _kwargs=combine_kwargs(**kwargs), ) - def graphql(self, query, variables=None, **kwargs): + def graphql( + self, query: str, variables: dict = None, **kwargs: Optional[dict] + ) -> dict: """ Makes a GraphQL formatted request to Canvas @@ -1270,7 +1367,12 @@ def graphql(self, query, variables=None, **kwargs): return response.json() - def reserve_time_slot(self, calendar_event, participant_id=None, **kwargs): + def reserve_time_slot( + self, + calendar_event: Union[int, "CalendarEvent"], + participant_id: int = None, + **kwargs: Optional[dict], + ) -> "CalendarEvent": """ Return single Calendar Event by id @@ -1303,7 +1405,7 @@ def reserve_time_slot(self, calendar_event, participant_id=None, **kwargs): ) return CalendarEvent(self.__requester, response.json()) - def search_accounts(self, **kwargs): + def search_accounts(self, **kwargs: Optional[dict]) -> dict: """ Return a list of up to 5 matching account domains. Partial matches on name and domain are supported. @@ -1318,7 +1420,7 @@ def search_accounts(self, **kwargs): ) return response.json() - def search_all_courses(self, **kwargs): + def search_all_courses(self, **kwargs: Optional[dict]) -> List[dict]: """ List all the courses visible in the public index. Returns a list of dicts, each containing a single course. @@ -1333,7 +1435,7 @@ def search_all_courses(self, **kwargs): ) return response.json() - def search_recipients(self, **kwargs): + def search_recipients(self, **kwargs: Union[Optional[str], Optional[dict]]) -> List: """ Find valid recipients (users, courses and groups) that the current user can send messages to. @@ -1352,7 +1454,12 @@ def search_recipients(self, **kwargs): ) return response.json() - def set_course_nickname(self, course, nickname, **kwargs): + def set_course_nickname( + self, + course: Union[int, Course], + nickname: str, + **kwargs: Union[str, Optional[dict]], + ) -> "CourseNickname": """ Set a nickname for the given course. This will replace the course's name in the output of subsequent API calls, as diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..fe442682 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +python_version = 3.6 + +[mypy-canvasapi] +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True