Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Miscellaneous python typing fixes #6122

Merged
merged 15 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lms/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class _Setting:
"""The properties of a setting and how to read it."""

name: str
read_from: str = None
value_mapper: Callable = None
read_from: str | None = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using str | None here has the downside that mypy will ask you to add checks that this is non-null whenever using an initialized instance. If I understand correctly, this value is only None during construction? There are some Stack Overflow posts discussing this issue, such as https://stackoverflow.com/q/74621969/434243.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep looking for a good solution in a different PR.

The feature we are after is for dataclass fields that can be used during init but post__init provides a default if that's missing.

value_mapper: Callable | None = None

def __post_init__(self):
if not self.read_from:
Expand Down
2 changes: 1 addition & 1 deletion lms/events/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def from_instance(cls, instance, **kwargs):
)

type: EventType.Type = EventType.Type.AUDIT_TRAIL
change: ModelChange = None
change: ModelChange | None = None

def _get_data(self):
"""
Expand Down
1 change: 1 addition & 0 deletions lms/events/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
@subscriber(BaseEvent)
def handle_event(event: BaseEvent):
"""Record the event in the Event model's table."""
assert event.request
event.request.find_service(EventService).insert_event(event)
2 changes: 1 addition & 1 deletion lms/models/_mixins/public_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class PublicIdMixin:
should be used instead which provides a fully qualified id.
"""

public_id_model_code = None
public_id_model_code: str | None = None
"""The short code which identifies this type of model."""

@hybrid_property
Expand Down
7 changes: 3 additions & 4 deletions lms/models/application_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sqlalchemy as sa
from pyramid.settings import asbool
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand Down Expand Up @@ -96,15 +97,13 @@ class ApplicationInstance(CreatedUpdatedMixin, Base):
developer_key = sa.Column(sa.Unicode)
developer_secret = sa.Column(sa.LargeBinary)
aes_cipher_iv = sa.Column(sa.LargeBinary)
provisioning = sa.Column(
provisioning: Mapped[bool] = mapped_column(
sa.Boolean(),
default=True,
server_default=sa.sql.expression.true(),
nullable=False,
)

settings = sa.Column(
"settings",
settings: Mapped[ApplicationSettings] = mapped_column(
ApplicationSettings.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
Expand Down
10 changes: 5 additions & 5 deletions lms/models/assignment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand Down Expand Up @@ -50,17 +51,16 @@ class Assignment(CreatedUpdatedMixin, Base):
)
"""Assignment this one was copied from."""

document_url = sa.Column(sa.Unicode, nullable=False)
document_url: Mapped[str] = mapped_column(sa.Unicode, nullable=False)
"""The URL of the document to be annotated for this assignment."""

extra = sa.Column(
"extra",
extra: Mapped[MutableDict] = mapped_column(
MutableDict.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
)

is_gradable = sa.Column(
is_gradable: Mapped[bool] = mapped_column(
sa.Boolean(),
default=False,
server_default=sa.sql.expression.false(),
Expand All @@ -74,7 +74,7 @@ class Assignment(CreatedUpdatedMixin, Base):
description = sa.Column(sa.Unicode, nullable=True)
"""The resource link description from LTI params."""

deep_linking_uuid = sa.Column(sa.Unicode, nullable=True)
deep_linking_uuid: Mapped[str | None] = mapped_column(sa.Unicode, nullable=True)
"""UUID that identifies the deep linking that created this assignment."""

def get_canvas_mapped_file_id(self, file_id):
Expand Down
4 changes: 2 additions & 2 deletions lms/models/course.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models.json_settings import JSONSettings
Expand Down Expand Up @@ -27,8 +28,7 @@ class LegacyCourse(Base):
#: settings belong to.
authority_provided_id = sa.Column(sa.UnicodeText(), primary_key=True)

settings = sa.Column(
"settings",
settings: Mapped[JSONSettings] = mapped_column(
JSONSettings.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
Expand Down
3 changes: 2 additions & 1 deletion lms/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base, varchar_enum

Expand Down Expand Up @@ -127,7 +128,7 @@ class EventData(Base):
)
event = sa.orm.relationship("Event")

data = sa.Column(
data: Mapped[MutableDict] = mapped_column(
"extra",
MutableDict.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
Expand Down
5 changes: 4 additions & 1 deletion lms/models/group_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base

Expand Down Expand Up @@ -87,7 +88,9 @@ class GroupInfo(Base):
custom_canvas_course_id = sa.Column(sa.UnicodeText())

#: A dict of info about this group.
_info = sa.Column("info", MutableDict.as_mutable(JSONB))
_info: Mapped[MutableDict | None] = mapped_column(
"info", MutableDict.as_mutable(JSONB)
)

@property
def _safe_info(self):
Expand Down
7 changes: 3 additions & 4 deletions lms/models/grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base, varchar_enum
from lms.models._mixins import CreatedUpdatedMixin
Expand Down Expand Up @@ -113,15 +114,13 @@ class Type(str, Enum):

type = varchar_enum(Type, nullable=False)

settings = sa.Column(
"settings",
settings: Mapped[JSONSettings] = mapped_column(
JSONSettings.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
)

extra = sa.Column(
"extra",
extra: Mapped[MutableDict] = mapped_column(
MutableDict.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
Expand Down
8 changes: 5 additions & 3 deletions lms/models/json_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def set(self, group, key, value):
# pylint:disable=unsupported-assignment-operation
super().setdefault(group, {})[key] = value

def set_secret(self, aes_service, group, key, value: str):
def set_secret(self, aes_service, group, key, value: str) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that there is no type inference for return types, even functions that return None. Hopefully this might happen in future.

"""
Store a setting as a secret.

Expand All @@ -112,12 +112,14 @@ def set_secret(self, aes_service, group, key, value: str):
:param value: The value to set
"""
aes_iv = aes_service.build_iv()
value = aes_service.encrypt(aes_iv, value)
encrypted_value: bytes = aes_service.encrypt(aes_iv, value)

# Store both the setting and the IV
# We can't store the bytes directly in JSON so we store it as base64
# pylint:disable=unsupported-assignment-operation
super().setdefault(group, {})[key] = base64.b64encode(value).decode("utf-8")
super().setdefault(group, {})[key] = base64.b64encode(encrypted_value).decode(
"utf-8"
)
super().setdefault(group, {})[f"{key}_aes_iv"] = base64.b64encode(
aes_iv
).decode("utf-8")
Expand Down
5 changes: 3 additions & 2 deletions lms/models/jwt_oauth2_token.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand Down Expand Up @@ -44,7 +45,7 @@ class JWTOAuth2Token(CreatedUpdatedMixin, Base):
scopes = sa.Column(sa.UnicodeText(), nullable=False)

# The OAuth 2.0 access token, as received from the authorization server.
access_token = sa.Column(sa.UnicodeText(), nullable=False)
access_token: Mapped[str] = mapped_column(sa.UnicodeText(), nullable=False)

# Time at which the toke will expire
expires_at = sa.Column(sa.DateTime, nullable=False)
expires_at = mapped_column(sa.DateTime, nullable=False)
2 changes: 1 addition & 1 deletion lms/models/lti_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class LTIUser: # pylint: disable=too-many-instance-attributes
request.lti_session or similar with contains the user and any other relevant values.
"""

application_instance: ApplicationInstance = None
application_instance: ApplicationInstance | None = None
"""Application instance this user belongs to"""

email: str = ""
Expand Down
8 changes: 4 additions & 4 deletions lms/models/organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin, PublicIdMixin
Expand All @@ -19,10 +20,10 @@ class Organization(CreatedUpdatedMixin, PublicIdMixin, Base):
name = sa.Column(sa.UnicodeText(), nullable=True)
"""Human readable name for the organization."""

enabled = sa.Column(sa.Boolean(), nullable=False, default=True)
enabled: Mapped[bool] = mapped_column(sa.Boolean(), nullable=False, default=True)
"""Is this organization allowed to use LMS?"""

parent_id = sa.Column(
parent_id: Mapped[int | None] = mapped_column(
sa.Integer(),
sa.ForeignKey("organization.id", ondelete="cascade"),
nullable=True,
Expand All @@ -39,8 +40,7 @@ class Organization(CreatedUpdatedMixin, PublicIdMixin, Base):
)
"""Get any application instances associated with this organization."""

settings = sa.Column(
"settings",
settings: Mapped[JSONSettings] = mapped_column(
JSONSettings.as_mutable(JSONB),
server_default=sa.text("'{}'::jsonb"),
nullable=False,
Expand Down
2 changes: 1 addition & 1 deletion lms/models/public_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PublicId:
app_code: str = "lms"
"""Code representing the product this model is in."""

instance_id: str = None
instance_id: str | None = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above about data class fields which are only null during initialization. It would be good to find a pattern so code which only handles initialized instances doesn't need unnecessary non-null checks.

"""Identifier for the specific model instance."""

def __post_init__(self):
Expand Down
3 changes: 2 additions & 1 deletion lms/models/rsa_key.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import uuid

import sqlalchemy as sa
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand All @@ -23,7 +24,7 @@ class RSAKey(CreatedUpdatedMixin, Base):
aes_cipher_iv = sa.Column(sa.LargeBinary)
"""IV for the private key AES encryption"""

expired = sa.Column(
expired: Mapped[bool] = mapped_column(
sa.Boolean(),
default=False,
server_default=sa.sql.expression.false(),
Expand Down
5 changes: 3 additions & 2 deletions lms/models/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sqlalchemy as sa
from sqlalchemy.orm import mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand Down Expand Up @@ -40,8 +41,8 @@ class User(CreatedUpdatedMixin, Base):
h_userid = sa.Column(sa.Unicode, nullable=False)
"""The H userid which is created from LTI provided values."""

email = sa.Column(sa.Unicode, nullable=True)
email = mapped_column(sa.Unicode, nullable=True)
"""Email address of the user"""

display_name = sa.Column(sa.Unicode, nullable=True)
display_name = mapped_column(sa.Unicode, nullable=True)
"""The user's display name."""
3 changes: 2 additions & 1 deletion lms/models/user_preferences.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import Column, Integer, Unicode, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from lms.db import Base
from lms.models._mixins import CreatedUpdatedMixin
Expand All @@ -11,7 +12,7 @@ class UserPreferences(CreatedUpdatedMixin, Base):

id = Column(Integer, autoincrement=True, primary_key=True)
h_userid = Column(Unicode, nullable=False, unique=True)
preferences = Column(
preferences: Mapped[MutableDict] = mapped_column(
MutableDict.as_mutable(JSONB),
server_default=text("'{}'::jsonb"),
nullable=False,
Expand Down
4 changes: 2 additions & 2 deletions lms/product/blackboard/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from lms.product.blackboard._plugin.course_copy import BlackboardCourseCopyPlugin
from lms.product.blackboard._plugin.grouping import BlackboardGroupingPlugin
from lms.product.blackboard._plugin.misc import BlackboardMiscPlugin
from lms.product.product import PluginConfig, Product, Routes
from lms.product.product import Family, PluginConfig, Product, Routes


@dataclass
class Blackboard(Product):
"""A product for Blackboard specific settings and tweaks."""

family: Product.Family = Product.Family.BLACKBOARD
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access these more directly

family: Family = Family.BLACKBOARD

route: Routes = Routes(
oauth2_authorize="blackboard_api.oauth.authorize",
Expand Down
4 changes: 2 additions & 2 deletions lms/product/canvas/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from lms.product.canvas._plugin.course_copy import CanvasCourseCopyPlugin
from lms.product.canvas._plugin.grouping import CanvasGroupingPlugin
from lms.product.canvas._plugin.misc import CanvasMiscPlugin
from lms.product.product import PluginConfig, Product, Routes
from lms.product.product import Family, PluginConfig, Product, Routes


@dataclass
class Canvas(Product):
"""A product for Canvas specific settings and tweaks."""

family: Product.Family = Product.Family.CANVAS
family: Family = Family.CANVAS

route: Routes = Routes(
oauth2_authorize="canvas_api.oauth.authorize",
Expand Down
4 changes: 2 additions & 2 deletions lms/product/d2l/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from lms.product.d2l._plugin.course_copy import D2LCourseCopyPlugin
from lms.product.d2l._plugin.grouping import D2LGroupingPlugin
from lms.product.d2l._plugin.misc import D2LMiscPlugin
from lms.product.product import PluginConfig, Product, Routes
from lms.product.product import Family, PluginConfig, Product, Routes


@dataclass
class D2L(Product):
"""A product for D2L specific settings and tweaks."""

family: Product.Family = Product.Family.D2L
family: Family = Family.D2L

plugin_config: PluginConfig = PluginConfig(
grouping=D2LGroupingPlugin, misc=D2LMiscPlugin, course_copy=D2LCourseCopyPlugin
Expand Down
4 changes: 2 additions & 2 deletions lms/product/moodle/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from lms.product.moodle._plugin.course_copy import MoodleCourseCopyPlugin
from lms.product.moodle._plugin.grouping import MoodleGroupingPlugin
from lms.product.moodle._plugin.misc import MoodleMiscPlugin
from lms.product.product import PluginConfig, Product, Routes
from lms.product.product import Family, PluginConfig, Product, Routes


@dataclass
class Moodle(Product):
family: Product.Family = Product.Family.MOODLE
family: Family = Family.MOODLE

plugin_config: PluginConfig = PluginConfig(
grouping=MoodleGroupingPlugin,
Expand Down
Loading
Loading