Skip to content

Latest commit

 

History

History
366 lines (253 loc) · 16.6 KB

CONTRIBUTING.md

File metadata and controls

366 lines (253 loc) · 16.6 KB

Contributing to CanvasAPI

Thanks for your interest in contributing!

Below you'll find guidelines for contributing that will keep our codebase clean and happy.

Table of Contents

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.

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!

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:

  • api coverage: covering new endpoints or updating existing ones.
  • backstage: issues affecting the repository or project internals rather than user-facing features.
  • bug: happy little code accidents.
  • canvas: confirmed to be an issue with the Canvas LMS rather than the CanvasAPI library.
  • documentation: issues relating to Documentation. Specifically, any of the .md files or our class reference docs.
  • enhancement: updates to the engine to improve performance or add new functionality.
  • help wanted: we need your help to figure these out!
  • major: difficult or major changes or additions that require familiarity with the library.
  • question: issues that aren't reporting functionality or requesting improvement but requesting clarification on existing behavior
  • simple: 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 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 Python 3 (pip / venv) and git installed and are using a terminal. If not, please set those up before continuing.

  1. Fork CanvasAPI on GitHub (see the docs here)
  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 ( 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. 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. 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.

API coverage tests

We use the 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:

# get_user()
def test_get_user(self, m):
    register_uris({'course': ['get_user']}, m)

    user = self.course.get_user(1)

    self.assertIsInstance(user, User)
    self.assertTrue(hasattr(user, 'name'))

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.


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.


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.

Example Fixture:

"get_user": {
    "method": "GET",
    "endpoint": "courses/1/users/1",
    "data": {
        "id": 1,
        "name": "John Doe"
    },
    "status_code": 200
},

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.


user = self.course.get_user(1)

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.


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:

with requests_mock.Mocker() as m:
    requires = {
        'course': ['get_by_id', 'get_page'],
        'quiz': ['get_by_id'],
        'user': ['get_by_id']
    }
    register_uris(requires, m)

    self.course = self.canvas.get_course(1)
    self.page = self.course.get_page('my-url')
    self.quiz = self.course.get_quiz(1)
    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:

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.

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.

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:

Name                             Stmts   Miss  Cover
----------------------------------------------------
canvasapi/__init__.py                3      0   100%
canvasapi/account.py               166      0   100%
canvasapi/appointment_group.py      17      0   100%
canvasapi/assignment.py             24      0   100%
[...]
canvasapi/upload.py                 29      0   100%
canvasapi/user.py                  101      0   100%
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!

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

We try to adhere to Python's PEP 8 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 to force each check to run before you create a commit locally:

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:

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.

black canvasapi tests

We require methods to be in alphabetical order for ease of reading. Run this script to confirm order:

python scripts/alphabetic.py

All endpoint methods should accept arbitrary keyword arguments to enable parameter pass-through to Canvas:

python scripts/find_missing_kwargs.py

Foolish consistency

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.

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:

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.

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:

:calls: `THE TEXT OF THE HYPERLINK \
    <https://the.url/to/use/>`_

Hyperlink text should match the text underneath the endpoint in the official Canvas API documentation. Generally, that looks like this:

: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.

Parameters

Parameters should be listed in the order that they appear in the method prototype. They should take on the following form:

:param PARAMETER_NAME: PARAMETER_DESCRIPTION.
:type PARAMETER_NAME: PYTHON_TYPE

Returns

Return description should be listed first, if available. This should be included to clarify a returned value, for example:

def uncheck_box(box_id):
    """
    Uncheck the box with the given ID.

    :returns: True if the box was successfully unchecked, False otherwise.
    :rtype: bool
    """

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:

:rtype: :class:`canvasapi.user.User`

In the event a PaginatedList is returned:

:rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.user.User`

Docstring Examples

Here are some real world examples of how docstrings should be formatted:

def get_account(self, account_id):
    """
    Retrieve information on an individual account.

    :calls: `GET /api/v1/accounts/:id \
    <https://canvas.instructure.com/doc/api/accounts.html#method.accounts.show>`_

    :param account_id: The ID of the account to retrieve.
    :type account_id: int
    :rtype: :class:`canvasapi.account.Account`
    """
def get_accounts(self, **kwargs):
    """
    List accounts that the current user can view or manage.

    Typically, students and teachers will get an empty list in
    response. Only account admins can view the accounts that they
    are in.

    :calls: `GET /api/v1/accounts \
    <https://canvas.instructure.com/doc/api/accounts.html#method.accounts.index>`_

    :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.account.Account`
    """
def clear_course_nicknames(self):
    """
    Remove all stored course nicknames.

    :calls: `DELETE /api/v1/users/self/course_nicknames \
    <https://canvas.instructure.com/doc/api/users.html#method.course_nicknames.delete>`_

    :returns: True if the nicknames were cleared, False otherwise.
    :rtype: bool
    """