Skip to content

Commit

Permalink
Add mypy typings (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
libre-man authored Sep 8, 2020
1 parent acb10c9 commit 8ad9932
Show file tree
Hide file tree
Showing 41 changed files with 877 additions and 179 deletions.
10 changes: 8 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ language: python
sudo: false
cache: pip

install: pip install tox
matrix:
include:
- python: 2.7
Expand All @@ -20,7 +19,14 @@ matrix:
- python: 3.8
env:
- TOX_ENV=py38
script: tox -e $TOX_ENV

install:
- pip install tox
- "if ! [[ $TRAVIS_PYTHON_VERSION = '2.7' ]]; then pip install mypy; fi"

script:
- "if ! [[ $TRAVIS_PYTHON_VERSION = '2.7' ]]; then mypy pylti1p3/ && mypy --py2 pylti1p3/; fi"
- tox -e $TOX_ENV

notifications:
email:
Expand Down
4 changes: 4 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ disable=
# python2-compatible
useless-object-inheritance,

# Conditional imports for type checking
unused-import,
import-error,

[REPORTS]

# Set the output format. Available formats are text, parseable, colorized, msvs
Expand Down
10 changes: 8 additions & 2 deletions pylti1p3/actions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import typing as t

if t.TYPE_CHECKING:
from typing_extensions import Final


class Action(object):
OIDC_LOGIN = 'oidc_login'
MESSAGE_LAUNCH = 'message_launch'
OIDC_LOGIN = 'oidc_login' # type: Final
MESSAGE_LAUNCH = 'message_launch' # type: Final
49 changes: 43 additions & 6 deletions pylti1p3/assignments_grades.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import typing as t

from .exception import LtiException
from .lineitem import LineItem

if t.TYPE_CHECKING:
from .service_connector import ServiceConnector, _ServiceConnectorResponse
from .grade import Grade
from mypy_extensions import TypedDict
from typing_extensions import Literal

_AssignmentsGradersData = TypedDict('_AssignmentsGradersData', {
'scope': t.List[Literal['https://purl.imsglobal.org/spec/lti-ags/scope/score',
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly']],
'lineitems': str,
'lineitem': str,
}, total=False)


class AssignmentsGradesService(object):
_service_connector = None
_service_data = None
_service_connector = None # type: ServiceConnector
_service_data = None # type: _AssignmentsGradersData

def __init__(self, service_connector, service_data):
# type: (ServiceConnector, _AssignmentsGradersData) -> None
self._service_connector = service_connector
self._service_data = service_data

def put_grade(self, grade, line_item=None):
# type: (Grade, t.Optional[LineItem]) -> _ServiceConnectorResponse
if "https://purl.imsglobal.org/spec/lti-ags/scope/score" not in self._service_data['scope']:
raise LtiException('Missing required scope')

Expand All @@ -27,6 +45,7 @@ def put_grade(self, grade, line_item=None):
line_item = self.find_or_create_lineitem(line_item)
score_url = line_item.get_id()

assert score_url is not None
score_url = self._add_url_path_ending(score_url, 'scores')
return self._service_connector.make_service_request(
self._service_data['scope'],
Expand All @@ -37,6 +56,7 @@ def put_grade(self, grade, line_item=None):
)

def get_lineitems(self):
# type: () -> list
if "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem" not in self._service_data['scope']:
raise LtiException('Missing required scope')

Expand All @@ -45,9 +65,12 @@ def get_lineitems(self):
self._service_data['lineitems'],
accept='application/vnd.ims.lis.v2.lineitemcontainer+json'
)
if not isinstance(line_items['body'], list):
raise LtiException('Unknown response type received for line items')
return line_items['body']

def find_lineitem_by_id(self, ln_id):
# type: (t.Optional[str]) -> t.Optional[LineItem]
line_items = self.get_lineitems()

for line_item in line_items:
Expand All @@ -57,6 +80,7 @@ def find_lineitem_by_id(self, ln_id):
return None

def find_lineitem_by_tag(self, tag):
# type: (t.Optional[str]) -> t.Optional[LineItem]
line_items = self.get_lineitems()

for line_item in line_items:
Expand All @@ -66,10 +90,13 @@ def find_lineitem_by_tag(self, tag):
return None

def find_or_create_lineitem(self, new_line_item, find_by='tag'):
# type: (LineItem, Literal['tag', 'id']) -> LineItem
if find_by == 'tag':
line_item = self.find_lineitem_by_tag(new_line_item.get_tag())
tag = new_line_item.get_tag()
line_item = self.find_lineitem_by_tag(tag)
elif find_by == 'id':
line_item = self.find_lineitem_by_id(new_line_item.get_id())
line_id = new_line_item.get_id()
line_item = self.find_lineitem_by_id(line_id)
else:
raise LtiException('Invalid "find_by" value: ' + str(find_by))

Expand All @@ -84,28 +111,38 @@ def find_or_create_lineitem(self, new_line_item, find_by='tag'):
content_type='application/vnd.ims.lis.v2.lineitem+json',
accept='application/vnd.ims.lis.v2.lineitem+json'
)
if not isinstance(created_line_item['body'], dict):
raise LtiException('Unknown response type received for create line item')
return LineItem(created_line_item['body'])

def get_grades(self, line_item):
# type: (LineItem) -> list
line_item_id = line_item.get_id()
line_item_tag = line_item.get_tag()

find_by = None
find_by = None # type: t.Optional[Literal['id', 'tag']]
if line_item_id:
find_by = 'id'
elif line_item_tag:
find_by = 'tag'
else:
raise LtiException('Received LineItem did not contain a tag or id')

line_item = self.find_or_create_lineitem(line_item, find_by=find_by)
results_url = self._add_url_path_ending(line_item.get_id(), 'results')
line_item_id = line_item.get_id()
assert line_item_id is not None
results_url = self._add_url_path_ending(line_item_id, 'results')
scores = self._service_connector.make_service_request(
self._service_data['scope'],
results_url,
accept='application/vnd.ims.lis.v2.resultcontainer+json'
)
if not isinstance(scores['body'], list):
raise LtiException('Unknown response type received for results')
return scores['body']

def _add_url_path_ending(self, url, url_path_ending):
# type: (str, str) -> str
if '?' in url:
url_parts = url.split('?')
new_url = url_parts[0]
Expand Down
17 changes: 10 additions & 7 deletions pylti1p3/contrib/django/cookie.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import django
import sys
import typing as t

import django # type: ignore
from pylti1p3.cookie import CookieService
try:
import Cookie
except ImportError:
import http.cookies as Cookie

if sys.version_info[0] > 2:
import http.cookies as Cookie # type: ignore
else:
import Cookie # type: ignore

# Add support for the SameSite attribute (obsolete when PY37 is unsupported).
# pylint: disable=protected-access
if 'samesite' not in Cookie.Morsel._reserved:
Cookie.Morsel._reserved.setdefault('samesite', 'SameSite')
if 'samesite' not in Cookie.Morsel._reserved: # type: ignore
Cookie.Morsel._reserved.setdefault('samesite', 'SameSite') # type: ignore


class DjangoCookieService(CookieService):
Expand Down
2 changes: 1 addition & 1 deletion pylti1p3/contrib/django/launch_data_storage/cache.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.core.cache import caches
from django.core.cache import caches # type: ignore
from pylti1p3.launch_data_storage.cache import CacheDataStorage


Expand Down
2 changes: 2 additions & 0 deletions pylti1p3/contrib/django/lti1p3_tool_config/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# mypy: ignore-errors
from django.contrib import admin

from .models import LtiTool, LtiToolKey


Expand Down
2 changes: 1 addition & 1 deletion pylti1p3/contrib/django/lti1p3_tool_config/apps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.apps import AppConfig
from django.apps import AppConfig # type: ignore


class PyLTI1p3ToolConfig(AppConfig):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# mypy: ignore-errors
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
Expand Down
5 changes: 3 additions & 2 deletions pylti1p3/contrib/django/lti1p3_tool_config/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# mypy: ignore-errors
import json

from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.core.validators import URLValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
from pylti1p3.registration import Registration


Expand Down
3 changes: 2 additions & 1 deletion pylti1p3/contrib/django/oidc_login.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.http import HttpResponse
from django.http import HttpResponse # type: ignore
from pylti1p3.oidc_login import OIDCLogin
from pylti1p3.request import Request

from .cookie import DjangoCookieService
from .redirect import DjangoRedirect
from .request import DjangoRequest
Expand Down
5 changes: 3 additions & 2 deletions pylti1p3/contrib/django/redirect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.http import HttpResponse
from django.shortcuts import redirect
from django.http import HttpResponse # type: ignore
from django.shortcuts import redirect # type: ignore
from pylti1p3.redirect import Redirect


Expand All @@ -8,6 +8,7 @@ class DjangoRedirect(Redirect):
_cookie_service = None

def __init__(self, location, cookie_service=None):
super(DjangoRedirect, self).__init__()
self._location = location
self._cookie_service = cookie_service

Expand Down
4 changes: 4 additions & 0 deletions pylti1p3/contrib/django/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class DjangoRequest(Request):
_post_only = False
_default_params = None

@property
def session(self):
return self._request.session

def __init__(self, request, post_only=False, default_params=None):
self.set_request(request)
self._post_only = post_only
Expand Down
1 change: 1 addition & 0 deletions pylti1p3/contrib/flask/redirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class FlaskRedirect(Redirect):
_cookie_service = None

def __init__(self, location, cookie_service=None):
super(FlaskRedirect, self).__init__()
self._location = location
self._cookie_service = cookie_service

Expand Down
10 changes: 8 additions & 2 deletions pylti1p3/contrib/flask/request.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
from flask import request, session as flask_session
import typing as t

from flask import request
from flask import session as flask_session
from pylti1p3.request import Request

if t.TYPE_CHECKING:
from pylti1p3.request import SessionLike


class FlaskRequest(Request):
session = None
session = None # type: SessionLike
_cookies = None
_request_data = None
_request_is_secure = None

def __init__(self, cookies=None, session=None, request_data=None, request_is_secure=None):
super(FlaskRequest, self).__init__()
self._cookies = request.cookies if cookies is None else cookies
self.session = flask_session if session is None else session
self._request_is_secure = request.is_secure if request_is_secure is None else request_is_secure
Expand Down
Empty file added pylti1p3/contrib/py.typed
Empty file.
5 changes: 4 additions & 1 deletion pylti1p3/cookie.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import typing as t
from abc import ABCMeta, abstractmethod


class CookieService(object):
__metaclass__ = ABCMeta
_cookie_prefix = 'lti1p3'
_cookie_prefix = 'lti1p3' # type: str

@abstractmethod
def get_cookie(self, name):
# type: (str) -> t.Optional[str]
raise NotImplementedError

@abstractmethod
def set_cookie(self, name, value, exp=3600):
# type: (str, str, int) -> None
raise NotImplementedError
16 changes: 11 additions & 5 deletions pylti1p3/cookies_allowed_check.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import json
import typing as t


class CookiesAllowedCheckPage(object):
_params = {}
_protocol = 'http'
_main_text = ''
_click_text = ''
_loading_text = ''
_params = {} # type: t.Mapping[str, str]
_protocol = 'http' # type: str
_main_text = '' # type: str
_click_text = '' # type: str
_loading_text = '' # type: str

def __init__(self, params, protocol, main_text, click_text, loading_text, *args, **kwargs):
# type: (t.Mapping[str, str], str, str, str, str, *None, **None) -> None
# pylint: disable=unused-argument
self._params = params
self._protocol = protocol
Expand All @@ -17,6 +19,7 @@ def __init__(self, params, protocol, main_text, click_text, loading_text, *args,
self._loading_text = loading_text

def get_css_block(self):
# type: () -> str
css_block = """\
body {
font-family: Geneva, Arial, Helvetica, sans-serif;
Expand All @@ -25,6 +28,7 @@ def get_css_block(self):
return css_block

def get_js_block(self):
# type: () -> str
js_block = """\
var siteProtocol = '%s';
var urlParams = %s;
Expand Down Expand Up @@ -84,9 +88,11 @@ def get_js_block(self):
return js_block

def get_header_block(self):
# type: () -> str
return ''

def get_html(self):
# type: () -> str
html = """\
<!DOCTYPE html>
<html lang="en">
Expand Down
Loading

0 comments on commit 8ad9932

Please sign in to comment.