-
Notifications
You must be signed in to change notification settings - Fork 14
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
Conversation
lms/models/application_instance.py
Outdated
@@ -80,7 +80,7 @@ class ApplicationInstance(CreatedUpdatedMixin, Base): | |||
organization_id = sa.Column( | |||
sa.Integer(), sa.ForeignKey("organization.id"), nullable=True | |||
) | |||
organization = sa.orm.relationship("Organization") | |||
organization: sa.Mapped["Organization"] = sa.orm.relationship("Organization") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll eventually should type all the models as they are at the core of the codebase.
lms/product/plugin/plugin.py
Outdated
grouping: GroupingPlugin = _LazyPlugin() | ||
course_copy: CourseCopyPlugin = _LazyPlugin() | ||
misc: MiscPlugin = _LazyPlugin() | ||
grouping: GroupingPlugin = _LazyPlugin() # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code it's a bit magical. Not worth trying to fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the comment be # type:ignore
?
grouping: GroupingPlugin = _LazyPlugin() # type: ignore | |
grouping: GroupingPlugin = _LazyPlugin() # type:ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# type: ignore
is canonical.
Using # type: ignore
here works fine as mypy does still use the annotated type (GroupingPlugin
) in other contexts. Sometimes in TypeScript silencing type errors can cause unexpected issues in code which uses the result of the expression where the check was suppressed. An alternative approach is to use typing.cast
:
diff --git a/lms/product/plugin/plugin.py b/lms/product/plugin/plugin.py
index 66dcfea02..44cdf2a07 100644
--- a/lms/product/plugin/plugin.py
+++ b/lms/product/plugin/plugin.py
@@ -1,4 +1,5 @@
from dataclasses import dataclass
+from typing import cast
from lms.product.plugin.course_copy import CourseCopyPlugin
from lms.product.plugin.grouping import GroupingPlugin
@@ -31,9 +32,9 @@ class Plugins:
setattr(instance, self.plugin_name, plugin) # Overwrite the attr
return plugin
- grouping: GroupingPlugin = _LazyPlugin() # type: ignore
- course_copy: CourseCopyPlugin = _LazyPlugin() # type:ignore
- misc: MiscPlugin = _LazyPlugin() # type:ignore
+ grouping = cast(GroupingPlugin, _LazyPlugin())
+ course_copy = cast(CourseCopyPlugin, _LazyPlugin())
+ misc = cast(MiscPlugin, _LazyPlugin())
def __init__(self, request, plugin_config: PluginConfig):
self._request = request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went for the type casting 👍
@@ -219,7 +219,7 @@ def list_collection(self, collection_id: str) -> list[File]: | |||
|
|||
return files | |||
|
|||
def _api_request(self, path: str, schema_cls: RequestsResponseSchema) -> dict: | |||
def _api_request(self, path: str, schema_cls: Type[RequestsResponseSchema]) -> dict: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object type vs passing a class.
@@ -45,12 +45,12 @@ def bulk_upsert( | |||
|
|||
index_elements_columns = [getattr(model_class, c) for c in index_elements] | |||
|
|||
stmt = insert(model_class).values(values) | |||
stmt = stmt.on_conflict_do_update( | |||
base = insert(model_class).values(values) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two had different types, use different names.
@@ -92,9 +92,9 @@ def get_document_url( | |||
url = urlparse(url) | |||
params = parse_qs(url.query) | |||
if end_page: | |||
params["end_page"] = end_page | |||
params["end_page"] = end_page # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parse_qs seems to return dict[str, list[str]]
""" | ||
Whether or not this schema validates collections of objects by default. | ||
|
||
If this is ``None`` then marshmallow's default behavior will be used -- the | ||
If this is ``False`` then marshmallow's default behavior will be used -- the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -147,7 +147,6 @@ python_version = 3.11 | |||
|
|||
disable_error_code = [ | |||
"arg-type", | |||
"assignment", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan is to remove codes from here one by one.
lms/services/http.py
Outdated
@@ -7,7 +7,7 @@ class HTTPService: | |||
"""Send HTTP requests with `requests` and receive the responses.""" | |||
|
|||
# This is here mostly to let auto-spec know about it in the tests | |||
session: Session = None | |||
session: Session = None # typing: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the default makes the test fail, which is the reason to have this in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a look through the Python docs for create_autospec
, which suggests some alternative ways of handling this. I include the diff below to illustrate, but # typing: ignore
is simpler.
diff --git a/lms/services/http.py b/lms/services/http.py
index 8d0e73fca..8c282b3c5 100644
--- a/lms/services/http.py
+++ b/lms/services/http.py
@@ -6,8 +6,7 @@ from lms.services.exceptions import ExternalRequestError
class HTTPService:
"""Send HTTP requests with `requests` and receive the responses."""
- # This is here mostly to let auto-spec know about it in the tests
- session: Session = None # typing: ignore
+ session: Session
"""The underlying requests Session."""
def __init__(self):
@@ -15,7 +14,6 @@ class HTTPService:
# requests and urllib3 connection pooling is used (which means that
# underlying TCP connections are re-used when making multiple requests
# to the same host, e.g. pagination).
-
# See https://docs.python-requests.org/en/latest/user/advanced/#session-objects
self.session = Session()
diff --git a/tests/unit/lms/services/jstor/service_test.py b/tests/unit/lms/services/jstor/service_test.py
index d5152d0c3..ea4d86ad1 100644
--- a/tests/unit/lms/services/jstor/service_test.py
+++ b/tests/unit/lms/services/jstor/service_test.py
@@ -5,7 +5,7 @@ from unittest.mock import sentinel
import pytest
from lms.services import ExternalRequestError
-from lms.services.jstor.service import ArticleNotFound, JSTORService
+from lms.services.jstor.service import ArticleNotFound, HTTPService, JSTORService
from tests import factories
API_URL = "http://jstore_api.example.com"
@@ -200,7 +200,12 @@ class TestJSTORService:
@pytest.fixture(autouse=True)
def HTTPService(self, patch):
- return patch("lms.services.jstor.service.HTTPService")
+ class HTTPServiceForTest(HTTPService):
+ session = None
+
+ return patch(
+ "lms.services.jstor.service.HTTPService", autospec=HTTPServiceForTest
+ )
@pytest.fixture(autouse=True)
def JWTService(self, patch):
diff --git a/tests/unit/lms/services/vitalsource/_client_test.py b/tests/unit/lms/services/vitalsource/_client_test.py
index a0c43921e..9da609b36 100644
--- a/tests/unit/lms/services/vitalsource/_client_test.py
+++ b/tests/unit/lms/services/vitalsource/_client_test.py
@@ -7,6 +7,7 @@ from requests import Request
from lms.services.exceptions import ExternalRequestError
from lms.services.vitalsource._client import (
BookNotFound,
+ HTTPService,
VitalSourceClient,
VitalSourceConfigurationError,
_VSUserAuth,
@@ -253,9 +254,13 @@ class TestVitalSourceClient:
@pytest.fixture(autouse=True)
def http_service(self, patch):
- HTTPService = patch("lms.services.vitalsource._client.HTTPService")
+ class HTTPServiceForTest(HTTPService):
+ session = None
- return HTTPService.return_value
+ cls = patch(
+ "lms.services.vitalsource._client.HTTPService", autospec=HTTPServiceForTest
+ )
+ return cls.return_value
@pytest.fixture
def _VSUserAuth(self, patch):
diff --git a/tests/unit/services.py b/tests/unit/services.py
index 6e71bc381..a43513842 100644
--- a/tests/unit/services.py
+++ b/tests/unit/services.py
@@ -1,6 +1,7 @@
from unittest import mock
import pytest
+from requests import Session
from lms.product.plugin.course_copy import (
CourseCopyFilesHelper,
@@ -234,9 +235,11 @@ def h_api(mock_service):
@pytest.fixture
def http_service(mock_service):
- http_service = mock_service(HTTPService, service_name="http")
- http_service.request.return_value = factories.requests.Response()
+ class HTTPServiceForTest(HTTPService):
+ session = mock.create_autospec(Session, spec_set=True, instance=True)
+ http_service = mock_service(HTTPServiceForTest, service_name="http")
+ http_service.request.return_value = factories.requests.Response()
return http_service
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we then define the type as Session | None
?
09ee5fe
to
48937ae
Compare
|
||
|
||
@dataclass | ||
class Blackboard(Product): | ||
"""A product for Blackboard specific settings and tweaks.""" | ||
|
||
family: Product.Family = Product.Family.BLACKBOARD |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Access these more directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the changes here the one thing I think we might want to look into a better pattern for is avoiding adding extra | None
s for dataclass fields which are initialized in __post_init__
since that will lead to unnecessary checks in code that handles constructed dataclass instances.
@@ -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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@@ -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: |
There was a problem hiding this comment.
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.
@@ -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 |
There was a problem hiding this comment.
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.
lms/product/plugin/plugin.py
Outdated
grouping: GroupingPlugin = _LazyPlugin() | ||
course_copy: CourseCopyPlugin = _LazyPlugin() | ||
misc: MiscPlugin = _LazyPlugin() | ||
grouping: GroupingPlugin = _LazyPlugin() # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# type: ignore
is canonical.
Using # type: ignore
here works fine as mypy does still use the annotated type (GroupingPlugin
) in other contexts. Sometimes in TypeScript silencing type errors can cause unexpected issues in code which uses the result of the expression where the check was suppressed. An alternative approach is to use typing.cast
:
diff --git a/lms/product/plugin/plugin.py b/lms/product/plugin/plugin.py
index 66dcfea02..44cdf2a07 100644
--- a/lms/product/plugin/plugin.py
+++ b/lms/product/plugin/plugin.py
@@ -1,4 +1,5 @@
from dataclasses import dataclass
+from typing import cast
from lms.product.plugin.course_copy import CourseCopyPlugin
from lms.product.plugin.grouping import GroupingPlugin
@@ -31,9 +32,9 @@ class Plugins:
setattr(instance, self.plugin_name, plugin) # Overwrite the attr
return plugin
- grouping: GroupingPlugin = _LazyPlugin() # type: ignore
- course_copy: CourseCopyPlugin = _LazyPlugin() # type:ignore
- misc: MiscPlugin = _LazyPlugin() # type:ignore
+ grouping = cast(GroupingPlugin, _LazyPlugin())
+ course_copy = cast(CourseCopyPlugin, _LazyPlugin())
+ misc = cast(MiscPlugin, _LazyPlugin())
def __init__(self, request, plugin_config: PluginConfig):
self._request = request
lms/services/http.py
Outdated
@@ -7,7 +7,7 @@ class HTTPService: | |||
"""Send HTTP requests with `requests` and receive the responses.""" | |||
|
|||
# This is here mostly to let auto-spec know about it in the tests | |||
session: Session = None | |||
session: Session = None # typing: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a look through the Python docs for create_autospec
, which suggests some alternative ways of handling this. I include the diff below to illustrate, but # typing: ignore
is simpler.
diff --git a/lms/services/http.py b/lms/services/http.py
index 8d0e73fca..8c282b3c5 100644
--- a/lms/services/http.py
+++ b/lms/services/http.py
@@ -6,8 +6,7 @@ from lms.services.exceptions import ExternalRequestError
class HTTPService:
"""Send HTTP requests with `requests` and receive the responses."""
- # This is here mostly to let auto-spec know about it in the tests
- session: Session = None # typing: ignore
+ session: Session
"""The underlying requests Session."""
def __init__(self):
@@ -15,7 +14,6 @@ class HTTPService:
# requests and urllib3 connection pooling is used (which means that
# underlying TCP connections are re-used when making multiple requests
# to the same host, e.g. pagination).
-
# See https://docs.python-requests.org/en/latest/user/advanced/#session-objects
self.session = Session()
diff --git a/tests/unit/lms/services/jstor/service_test.py b/tests/unit/lms/services/jstor/service_test.py
index d5152d0c3..ea4d86ad1 100644
--- a/tests/unit/lms/services/jstor/service_test.py
+++ b/tests/unit/lms/services/jstor/service_test.py
@@ -5,7 +5,7 @@ from unittest.mock import sentinel
import pytest
from lms.services import ExternalRequestError
-from lms.services.jstor.service import ArticleNotFound, JSTORService
+from lms.services.jstor.service import ArticleNotFound, HTTPService, JSTORService
from tests import factories
API_URL = "http://jstore_api.example.com"
@@ -200,7 +200,12 @@ class TestJSTORService:
@pytest.fixture(autouse=True)
def HTTPService(self, patch):
- return patch("lms.services.jstor.service.HTTPService")
+ class HTTPServiceForTest(HTTPService):
+ session = None
+
+ return patch(
+ "lms.services.jstor.service.HTTPService", autospec=HTTPServiceForTest
+ )
@pytest.fixture(autouse=True)
def JWTService(self, patch):
diff --git a/tests/unit/lms/services/vitalsource/_client_test.py b/tests/unit/lms/services/vitalsource/_client_test.py
index a0c43921e..9da609b36 100644
--- a/tests/unit/lms/services/vitalsource/_client_test.py
+++ b/tests/unit/lms/services/vitalsource/_client_test.py
@@ -7,6 +7,7 @@ from requests import Request
from lms.services.exceptions import ExternalRequestError
from lms.services.vitalsource._client import (
BookNotFound,
+ HTTPService,
VitalSourceClient,
VitalSourceConfigurationError,
_VSUserAuth,
@@ -253,9 +254,13 @@ class TestVitalSourceClient:
@pytest.fixture(autouse=True)
def http_service(self, patch):
- HTTPService = patch("lms.services.vitalsource._client.HTTPService")
+ class HTTPServiceForTest(HTTPService):
+ session = None
- return HTTPService.return_value
+ cls = patch(
+ "lms.services.vitalsource._client.HTTPService", autospec=HTTPServiceForTest
+ )
+ return cls.return_value
@pytest.fixture
def _VSUserAuth(self, patch):
diff --git a/tests/unit/services.py b/tests/unit/services.py
index 6e71bc381..a43513842 100644
--- a/tests/unit/services.py
+++ b/tests/unit/services.py
@@ -1,6 +1,7 @@
from unittest import mock
import pytest
+from requests import Session
from lms.product.plugin.course_copy import (
CourseCopyFilesHelper,
@@ -234,9 +235,11 @@ def h_api(mock_service):
@pytest.fixture
def http_service(mock_service):
- http_service = mock_service(HTTPService, service_name="http")
- http_service.request.return_value = factories.requests.Response()
+ class HTTPServiceForTest(HTTPService):
+ session = mock.create_autospec(Session, spec_set=True, instance=True)
+ http_service = mock_service(HTTPServiceForTest, service_name="http")
+ http_service.request.return_value = factories.requests.Response()
return http_service
lms/services/http.py
Outdated
@@ -7,7 +7,7 @@ class HTTPService: | |||
"""Send HTTP requests with `requests` and receive the responses.""" | |||
|
|||
# This is here mostly to let auto-spec know about it in the tests | |||
session: Session = None | |||
session: Session = None # typing: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we then define the type as Session | None
?
Add type annotations to columns that SQLAlchemy can't infer.
The python typecheker (mypy) can be run with
make typecheck
. It's not enabled in eithermake sure
or in CI.Fix some low-hanging-fruit issues and add a few
ignores
in more complicated ones.