From e0ee525df4395f3795501fdc3a75a7ecdbedcb1f Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Mon, 21 Aug 2023 12:51:26 -0700 Subject: [PATCH] feat: Add type hints (#256) --- CHANGELOG.rst | 4 + mypy.ini | 6 + opaque_keys/__init__.py | 78 +++---- opaque_keys/edx/django/models.py | 14 +- opaque_keys/edx/django/tests/models.py | 1 - opaque_keys/edx/django/tests/test_models.py | 7 +- opaque_keys/edx/keys.py | 57 +++-- opaque_keys/edx/locations.py | 4 +- opaque_keys/edx/locator.py | 206 ++++++++++-------- opaque_keys/edx/tests/__init__.py | 1 - opaque_keys/edx/tests/test_course_locators.py | 4 +- .../edx/tests/test_library_locators.py | 23 +- .../edx/tests/test_library_usage_locators.py | 3 +- opaque_keys/edx/tests/test_properties.py | 1 - opaque_keys/py.typed | 0 opaque_keys/tests/strategies.py | 35 ++- pytest.ini | 2 +- requirements/base.in | 1 + requirements/base.txt | 2 + requirements/django-test.txt | 6 + requirements/doc.txt | 6 + requirements/test.in | 3 +- requirements/test.txt | 17 +- setup.py | 1 + tox.ini | 1 + 25 files changed, 254 insertions(+), 229 deletions(-) create mode 100644 mypy.ini create mode 100644 opaque_keys/py.typed diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0d3624e7..f605f8af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,7 @@ +# 2.5.0 + +* Added python type hints + # 2.4.0 * Added Support for Django 4.2 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..9fd34f72 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +python_version = 3.8 +follow_imports = normal +ignore_missing_imports = True +allow_untyped_globals = False +files = opaque_keys diff --git a/opaque_keys/__init__.py b/opaque_keys/__init__.py index e9b32440..64e88a4e 100644 --- a/opaque_keys/__init__.py +++ b/opaque_keys/__init__.py @@ -6,14 +6,15 @@ an application, while concealing the particulars of the serialization formats, and allowing new serialization formats to be installed transparently. """ +from __future__ import annotations from abc import ABCMeta, abstractmethod +from collections import defaultdict from functools import total_ordering -# pylint: disable=wrong-import-order -from _collections import defaultdict from stevedore.enabled import EnabledExtensionManager +from typing_extensions import Self # For python 3.11 plus, can just use "from typing import Self" -__version__ = '2.4.0' +__version__ = '2.5.0' class InvalidKeyError(Exception): @@ -94,15 +95,16 @@ class constructor will not validate any of the ``KEY_FIELDS`` arguments, and wil """ __slots__ = ('_initialized', 'deprecated') - KEY_FIELDS = [] - CANONICAL_NAMESPACE = None + KEY_FIELDS: tuple[str, ...] + CANONICAL_NAMESPACE: str + KEY_TYPE: str NAMESPACE_SEPARATOR = ':' - CHECKED_INIT = True + CHECKED_INIT: bool = True # ============= ABSTRACT METHODS ============== @classmethod @abstractmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str): """ Return an instance of `cls` parsed from its `serialized` form. @@ -117,7 +119,7 @@ def _from_string(cls, serialized): raise NotImplementedError() @abstractmethod - def _to_string(self): + def _to_string(self) -> str: """ Return a serialization of `self`. @@ -142,7 +144,7 @@ def _from_deprecated_string(cls, serialized): InvalidKeyError: Should be raised if `serialized` is not a valid serialized key understood by `cls`. """ - raise NotImplementedError() + raise AttributeError("The specified key type does not have a deprecated version.") def _to_deprecated_string(self): """ @@ -154,21 +156,21 @@ def _to_deprecated_string(self): This serialization should not include the namespace prefix. """ - raise NotImplementedError() + raise AttributeError("The specified key type does not have a deprecated version.") # ============= SERIALIZATION ============== - def __str__(self): + def __str__(self) -> str: """ Serialize this :class:`OpaqueKey`, in the form ``:``. """ if self.deprecated: # no namespace on deprecated return self._to_deprecated_string() - return self.NAMESPACE_SEPARATOR.join([self.CANONICAL_NAMESPACE, self._to_string()]) # pylint: disable=no-member + return self.NAMESPACE_SEPARATOR.join([self.CANONICAL_NAMESPACE, self._to_string()]) @classmethod - def from_string(cls, serialized): + def from_string(cls, serialized: str) -> Self: """ Return a :class:`OpaqueKey` object deserialized from the `serialized` argument. This object will be an instance @@ -192,12 +194,12 @@ def from_string(cls, serialized): raise InvalidKeyError(cls, serialized) return key_class._from_string(rest) except InvalidKeyError as error: - if hasattr(cls, 'deprecated_fallback') and issubclass(cls.deprecated_fallback, cls): - return cls.deprecated_fallback._from_deprecated_string(serialized) + if hasattr(cls, 'deprecated_fallback') and issubclass(cls.deprecated_fallback, cls): # type: ignore + return cls.deprecated_fallback._from_deprecated_string(serialized) # type: ignore raise InvalidKeyError(cls, serialized) from error @classmethod - def _separate_namespace(cls, serialized): + def _separate_namespace(cls, serialized: str): """ Return the namespace from a serialized :class:`OpaqueKey`, and the rest of the key. @@ -220,7 +222,7 @@ def _separate_namespace(cls, serialized): return (namespace, rest) @classmethod - def get_namespace_plugin(cls, namespace): + def get_namespace_plugin(cls, namespace: str): """ Return the registered OpaqueKey subclass of cls for the supplied namespace """ @@ -240,17 +242,17 @@ def get_namespace_plugin(cls, namespace): # a particular unknown namespace (like i4x) raise InvalidKeyError(cls, f'{namespace}:*') from error - LOADED_DRIVERS = defaultdict() # If you change default, change test_default_deprecated + LOADED_DRIVERS: dict[type[OpaqueKey], EnabledExtensionManager] = defaultdict() @classmethod - def _drivers(cls): + def _drivers(cls: type[OpaqueKey]): """ Return a driver manager for all key classes that are subclasses of `cls`. """ if cls not in cls.LOADED_DRIVERS: cls.LOADED_DRIVERS[cls] = EnabledExtensionManager( - cls.KEY_TYPE, # pylint: disable=no-member + cls.KEY_TYPE, check_func=lambda extension: issubclass(extension.plugin, cls), invoke_on_load=False, ) @@ -268,17 +270,16 @@ def set_deprecated_fallback(cls, fallback): # ============= VALUE SEMANTICS ============== def __init__(self, *args, **kwargs): # The __init__ expects child classes to implement KEY_FIELDS - # pylint: disable=no-member # a flag used to indicate that this instance was deserialized from the # deprecated form and should serialize to the deprecated form - self.deprecated = kwargs.pop('deprecated', False) # pylint: disable=assigning-non-slot + self.deprecated = kwargs.pop('deprecated', False) if self.CHECKED_INIT: self._checked_init(*args, **kwargs) else: self._unchecked_init(**kwargs) - self._initialized = True # pylint: disable=assigning-non-slot + self._initialized = True def _checked_init(self, *args, **kwargs): """ @@ -318,7 +319,7 @@ def replace(self, **kwargs): Subclasses should override this if they have required properties that aren't included in their ``KEY_FIELDS``. """ - existing_values = {key: getattr(self, key) for key in self.KEY_FIELDS} # pylint: disable=no-member + existing_values = {key: getattr(self, key) for key in self.KEY_FIELDS} existing_values['deprecated'] = self.deprecated if all(value == existing_values[key] for (key, value) in kwargs.items()): @@ -331,7 +332,7 @@ def __setattr__(self, name, value): if getattr(self, '_initialized', False): raise AttributeError(f"Can't set {name!r}. OpaqueKeys are immutable.") - super().__setattr__(name, value) # pylint: disable=no-member + super().__setattr__(name, value) def __delattr__(self, name): raise AttributeError(f"Can't delete {name!r}. OpaqueKeys are immutable.") @@ -352,44 +353,43 @@ def __deepcopy__(self, memo): def __setstate__(self, state_dict): # used by pickle to set fields on an unpickled object for key in state_dict: - if key in self.KEY_FIELDS: # pylint: disable=no-member + if key in self.KEY_FIELDS: setattr(self, key, state_dict[key]) - self.deprecated = state_dict['deprecated'] # pylint: disable=assigning-non-slot - self._initialized = True # pylint: disable=assigning-non-slot + self.deprecated = state_dict['deprecated'] + self._initialized = True def __getstate__(self): # used by pickle to get fields on an unpickled object pickleable_dict = {} - for key in self.KEY_FIELDS: # pylint: disable=no-member + for key in self.KEY_FIELDS: pickleable_dict[key] = getattr(self, key) pickleable_dict['deprecated'] = self.deprecated return pickleable_dict @property - def _key(self): + def _key(self) -> tuple: """Returns a tuple of key fields""" - # pylint: disable=no-member return tuple(getattr(self, field) for field in self.KEY_FIELDS) + (self.CANONICAL_NAMESPACE, self.deprecated) - def __eq__(self, other): - return isinstance(other, OpaqueKey) and self._key == other._key # pylint: disable=protected-access + def __eq__(self, other) -> bool: + return isinstance(other, OpaqueKey) and self._key == other._key - def __ne__(self, other): + def __ne__(self, other) -> bool: return not self == other - def __lt__(self, other): + def __lt__(self, other) -> bool: if (self.KEY_FIELDS, self.CANONICAL_NAMESPACE, self.deprecated) != (other.KEY_FIELDS, other.CANONICAL_NAMESPACE, other.deprecated): raise TypeError(f"{self!r} is incompatible with {other!r}") - return self._key < other._key # pylint: disable=protected-access + return self._key < other._key - def __hash__(self): + def __hash__(self) -> int: return hash(self._key) - def __repr__(self): + def __repr__(self) -> str: key_field_repr = ', '.join(repr(getattr(self, key)) for key in self.KEY_FIELDS) return f'{self.__class__.__name__}({key_field_repr})' - def __len__(self): + def __len__(self) -> int: """Return the number of characters in the serialized OpaqueKey""" return len(str(self)) diff --git a/opaque_keys/edx/django/models.py b/opaque_keys/edx/django/models.py index 21661bf6..4effd974 100644 --- a/opaque_keys/edx/django/models.py +++ b/opaque_keys/edx/django/models.py @@ -2,7 +2,7 @@ Useful django models for implementing XBlock infrastructure in django. If Django is unavailable, none of the classes below will work as intended. """ -# pylint: disable=abstract-method +from __future__ import annotations import logging import warnings @@ -16,6 +16,7 @@ CharField = object IsNull = object +from opaque_keys import OpaqueKey from opaque_keys.edx.keys import BlockTypeKey, CourseKey, LearningContextKey, UsageKey @@ -41,7 +42,7 @@ def __set__(self, obj, value): obj.__dict__[self.field.name] = self.field.to_python(value) -# pylint: disable=missing-docstring,unused-argument +# pylint: disable=unused-argument class CreatorMixin: """ Mixin class to provide SubfieldBase functionality to django fields. @@ -77,7 +78,6 @@ def _strip_value(value, lookup='exact'): return stripped_value -# pylint: disable=logging-format-interpolation class OpaqueKeyField(CreatorMixin, CharField): """ A django field for storing OpaqueKeys. @@ -92,7 +92,7 @@ class OpaqueKeyField(CreatorMixin, CharField): description = "An OpaqueKey object, saved to the DB in the form of a string." Empty = object() - KEY_CLASS = None + KEY_CLASS: type[OpaqueKey] def __init__(self, *args, **kwargs): if self.KEY_CLASS is None: @@ -100,7 +100,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def to_python(self, value): + def to_python(self, value): # pylint: disable=missing-function-docstring if value is self.Empty or value is None: return None @@ -128,13 +128,12 @@ def to_python(self, value): return self.KEY_CLASS.from_string(value) return value - def get_prep_value(self, value): + def get_prep_value(self, value): # pylint: disable=missing-function-docstring if value is self.Empty or value is None: return '' # CharFields should use '' as their empty value, rather than None if isinstance(value, str): value = self.KEY_CLASS.from_string(value) - # pylint: disable=isinstance-second-argument-not-valid-type assert isinstance(value, self.KEY_CLASS), f"{value} is not an instance of {self.KEY_CLASS}" serialized_key = str(_strip_value(value)) if serialized_key.endswith('\n'): @@ -178,7 +177,6 @@ def get_prep_lookup(self): try: - # pylint: disable=no-member OpaqueKeyField.register_lookup(OpaqueKeyFieldEmptyLookupIsNull) except AttributeError: # Django was not imported diff --git a/opaque_keys/edx/django/tests/models.py b/opaque_keys/edx/django/tests/models.py index d8b7699f..6dd0011e 100644 --- a/opaque_keys/edx/django/tests/models.py +++ b/opaque_keys/edx/django/tests/models.py @@ -34,7 +34,6 @@ def __eq__(self, obj): return self.text == obj.text -# pylint: disable=missing-docstring class ExampleField(CreatorMixin, CharField): """A simple Django Field to assist in testing the CreatorMixin class.""" diff --git a/opaque_keys/edx/django/tests/test_models.py b/opaque_keys/edx/django/tests/test_models.py index 1992096e..b0273097 100644 --- a/opaque_keys/edx/django/tests/test_models.py +++ b/opaque_keys/edx/django/tests/test_models.py @@ -23,7 +23,6 @@ def enable_db_access_for_all_tests(db): """Enable DB access for all tests.""" -# pylint: disable=no-member class TestCreatorMixin(TestCase): """Tests of the CreatorMixin class.""" def setUp(self): @@ -33,7 +32,7 @@ def setUp(self): def test_char_field_is_converted_to_container(self): expected = Container('key-1').transform() - self.assertEqual(expected, self.model.key.transform()) + self.assertEqual(expected, self.model.key.transform()) # pylint: disable=no-member def test_load_model_from_db(self): fetched_model = ExampleModel.objects.get(key='key-1') @@ -48,8 +47,8 @@ class EmptyKeyClassField(OpaqueKeyField): class TestOpaqueKeyField(TestCase): """Tests the implementation of OpaqueKeyField methods.""" def test_null_key_class_raises_value_error(self): - with self.assertRaises(ValueError): - EmptyKeyClassField() + with self.assertRaises(AttributeError): + EmptyKeyClassField() # AttributeError: 'EmptyKeyClassField' object has no attribute 'KEY_CLASS' def test_to_python_trailing_newline_stripped(self): field = ComplexModel()._meta.get_field('course_key') diff --git a/opaque_keys/edx/keys.py b/opaque_keys/edx/keys.py index c9457d2c..ba574ddc 100644 --- a/opaque_keys/edx/keys.py +++ b/opaque_keys/edx/keys.py @@ -1,15 +1,17 @@ """ -; OpaqueKey abstract classes for edx-platform object types (courses, definitions, usages, and assets). """ +from __future__ import annotations import json from abc import abstractmethod import warnings +from typing_extensions import Self # For python 3.11 plus, can just use "from typing import Self" + from opaque_keys import OpaqueKey -class LearningContextKey(OpaqueKey): +class LearningContextKey(OpaqueKey): # pylint: disable=abstract-method """ An :class:`opaque_keys.OpaqueKey` identifying a course, a library, a program, a website, or some other collection of content where learning @@ -30,13 +32,6 @@ class LearningContextKey(OpaqueKey): # just isinstance(key, CourseKey) is_course = False - def make_definition_usage(self, definition_key, usage_id=None): - """ - Return a usage key, given the given the specified definition key and - usage_id. - """ - raise NotImplementedError() - class CourseKey(LearningContextKey): """ @@ -47,7 +42,7 @@ class CourseKey(LearningContextKey): @property @abstractmethod - def org(self): # pragma: no cover + def org(self) -> str | None: # pragma: no cover """ The organization that this course belongs to. """ @@ -55,7 +50,7 @@ def org(self): # pragma: no cover @property @abstractmethod - def course(self): # pragma: no cover + def course(self) -> str | None: # pragma: no cover """ The name for this course. @@ -65,7 +60,7 @@ def course(self): # pragma: no cover @property @abstractmethod - def run(self): # pragma: no cover + def run(self) -> str | None: # pragma: no cover """ The run for this course. @@ -74,7 +69,7 @@ def run(self): # pragma: no cover raise NotImplementedError() @abstractmethod - def make_usage_key(self, block_type, block_id): # pragma: no cover + def make_usage_key(self, block_type: str, block_id: str) -> UsageKey: # pragma: no cover """ Return a usage key, given the given the specified block_type and block_id. @@ -84,7 +79,7 @@ def make_usage_key(self, block_type, block_id): # pragma: no cover raise NotImplementedError() @abstractmethod - def make_asset_key(self, asset_type, path): # pragma: no cover + def make_asset_key(self, asset_type: str, path: str) -> AssetKey: # pragma: no cover """ Return an asset key, given the given the specified path. @@ -103,7 +98,7 @@ class DefinitionKey(OpaqueKey): @property @abstractmethod - def block_type(self): # pragma: no cover + def block_type(self) -> str: # pragma: no cover """ The XBlock type of this definition. """ @@ -119,14 +114,14 @@ class CourseObjectMixin: @property @abstractmethod - def course_key(self): # pragma: no cover + def course_key(self) -> CourseKey: # pragma: no cover """ Return the :class:`CourseKey` for the course containing this usage. """ raise NotImplementedError() @abstractmethod - def map_into_course(self, course_key): # pragma: no cover + def map_into_course(self, course_key: CourseKey) -> Self: # pragma: no cover """ Return a new :class:`UsageKey` or :class:`AssetKey` representing this usage inside the course identified by the supplied :class:`CourseKey`. It returns the same type as @@ -150,7 +145,7 @@ class AssetKey(CourseObjectMixin, OpaqueKey): @property @abstractmethod - def asset_type(self): # pragma: no cover + def asset_type(self) -> str: # pragma: no cover """ Return what type of asset this is. """ @@ -158,7 +153,7 @@ def asset_type(self): # pragma: no cover @property @abstractmethod - def path(self): # pragma: no cover + def path(self) -> str: # pragma: no cover """ Return the path for this asset. """ @@ -174,7 +169,7 @@ class UsageKey(CourseObjectMixin, OpaqueKey): @property @abstractmethod - def definition_key(self): # pragma: no cover + def definition_key(self) -> DefinitionKey: # pragma: no cover """ Return the :class:`DefinitionKey` for the XBlock containing this usage. """ @@ -182,7 +177,7 @@ def definition_key(self): # pragma: no cover @property @abstractmethod - def block_type(self): + def block_type(self) -> str: """ The XBlock type of this usage. """ @@ -190,14 +185,14 @@ def block_type(self): @property @abstractmethod - def block_id(self): + def block_id(self) -> str: """ The name of this usage. """ raise NotImplementedError() @property - def context_key(self): + def context_key(self) -> LearningContextKey: """ Get the learning context key (LearningContextKey) for this XBlock usage. """ @@ -224,7 +219,7 @@ class UsageKeyV2(UsageKey): @property @abstractmethod - def context_key(self): + def context_key(self) -> LearningContextKey: """ Get the learning context key (LearningContextKey) for this XBlock usage. May be a course key, library key, or some other LearningContextKey type. @@ -232,7 +227,7 @@ def context_key(self): raise NotImplementedError() @property - def definition_key(self): + def definition_key(self) -> DefinitionKey: """ Returns the definition key for this usage. For the newer V2 key types, this cannot be done with the key alone, so it's necessary to ask the @@ -245,11 +240,11 @@ def definition_key(self): ) @property - def course_key(self): + def course_key(self) -> CourseKey: warnings.warn("Use .context_key instead of .course_key", DeprecationWarning, stacklevel=2) - return self.context_key + return self.context_key # type: ignore - def map_into_course(self, course_key): + def map_into_course(self, course_key: CourseKey) -> Self: """ Implement map_into_course for API compatibility. Shouldn't be used in new code. @@ -312,7 +307,7 @@ class i4xEncoder(json.JSONEncoder): # pylint: disable=invalid-name If provided as the cls to json.dumps, will serialize and Locations as i4x strings and other keys using the unicode strings. """ - def default(self, o): # pylint: disable=arguments-differ, method-hidden + def default(self, o): if isinstance(o, OpaqueKey): return str(o) super().default(o) @@ -329,7 +324,7 @@ class BlockTypeKey(OpaqueKey): @property @abstractmethod - def block_family(self): + def block_family(self) -> str: """ Return the block-family identifier (the entry-point used to load that block family). @@ -338,7 +333,7 @@ def block_family(self): @property @abstractmethod - def block_type(self): + def block_type(self) -> str: """ Return the block_type of this block (the key in the entry-point to load the block with). diff --git a/opaque_keys/edx/locations.py b/opaque_keys/edx/locations.py index d3264e22..2087e439 100644 --- a/opaque_keys/edx/locations.py +++ b/opaque_keys/edx/locations.py @@ -1,7 +1,7 @@ """ Deprecated OpaqueKey implementations used by XML and Mongo modulestores """ - +from __future__ import annotations import re import warnings @@ -67,7 +67,7 @@ def replace(self, **kwargs): class LocationBase: """Deprecated. Base class for :class:`Location` and :class:`AssetLocation`""" - DEPRECATED_TAG = None # Subclasses should define what DEPRECATED_TAG is + DEPRECATED_TAG: str | None = None # Subclasses should define what DEPRECATED_TAG is @classmethod def _deprecation_warning(cls): diff --git a/opaque_keys/edx/locator.py b/opaque_keys/edx/locator.py index 5d3a87c5..7960b43a 100644 --- a/opaque_keys/edx/locator.py +++ b/opaque_keys/edx/locator.py @@ -1,16 +1,18 @@ """ Identifier for course resources. """ - +from __future__ import annotations import inspect import logging import re +from typing import Any import warnings from uuid import UUID from bson.errors import InvalidId from bson.objectid import ObjectId from bson.son import SON +from typing_extensions import Self # For python 3.11 plus, can just use "from typing import Self" from opaque_keys import OpaqueKey, InvalidKeyError from opaque_keys.edx.keys import AssetKey, CourseKey, DefinitionKey, LearningContextKey, UsageKey, UsageKeyV2 @@ -22,7 +24,7 @@ class LocalId: """ Class for local ids for non-persisted xblocks (which can have hardcoded block_ids if necessary) """ - def __init__(self, block_id=None): + def __init__(self, block_id: str | None = None): self.block_id = block_id super().__init__() @@ -55,7 +57,7 @@ def version(self): # pragma: no cover raise NotImplementedError() @classmethod - def as_object_id(cls, value): + def as_object_id(cls, value: Any) -> ObjectId: """ Attempts to cast value as a bson.objectid.ObjectId. @@ -73,7 +75,7 @@ class CheckFieldMixin: Mixin that provides handy methods for checking field types/values. """ @classmethod - def _check_key_string_field(cls, field_name, value, regexp=re.compile(r'^[a-zA-Z0-9_\-.]+$')): + def _check_key_string_field(cls, field_name: str, value: str, regexp=re.compile(r'^[a-zA-Z0-9_\-.]+$')): """ Helper method to verify that a key's string field(s) meet certain requirements: @@ -125,7 +127,7 @@ class BlockLocatorBase(Locator): URL_RE = re.compile('^' + URL_RE_SOURCE + r'\Z', re.VERBOSE | re.UNICODE) @classmethod - def parse_url(cls, string): # pylint: disable=redefined-outer-name + def parse_url(cls, string: str) -> dict[str, str]: """ If it can be parsed as a version_guid with no preceding org + offering, returns a dict with key 'version_guid' and the value, @@ -139,10 +141,10 @@ def parse_url(cls, string): # pylint: disable=redefined-outer-name match = cls.URL_RE.match(string) if not match: raise InvalidKeyError(cls, string) - return match.groupdict() + return match.groupdict() # type: ignore -class CourseLocator(BlockLocatorBase, CourseKey): # pylint: disable=abstract-method +class CourseLocator(BlockLocatorBase, CourseKey): """ Examples of valid CourseLocator specifications: CourseLocator(version_guid=ObjectId('519665f6223ebd6980884f2b')) @@ -158,18 +160,30 @@ class CourseLocator(BlockLocatorBase, CourseKey): # pylint: disable=abstract-m the persistence layer may raise exceptions if the given version != the current such version of the course. """ - - # pylint: disable=no-member - CANONICAL_NAMESPACE = 'course-v1' KEY_FIELDS = ('org', 'course', 'run', 'branch', 'version_guid') + org: str | None + course: str | None + run: str | None + branch: str | None + version_guid: ObjectId | None + __slots__ = KEY_FIELDS CHECKED_INIT = False # Characters that are forbidden in the deprecated format INVALID_CHARS_DEPRECATED = re.compile(r"[^\w.%-]", re.UNICODE) - def __init__(self, org=None, course=None, run=None, branch=None, version_guid=None, deprecated=False, **kwargs): + def __init__( + self, + org: str | None = None, + course: str | None = None, + run: str | None = None, + branch: str | None = None, + version_guid: str | ObjectId | None = None, + deprecated: bool = False, + **kwargs, + ): """ Construct a CourseLocator @@ -191,7 +205,7 @@ def __init__(self, org=None, course=None, run=None, branch=None, version_guid=No for part in (org, course, run): self._check_location_part(part, self.INVALID_CHARS_DEPRECATED) - fields = [org, course] + fields: list[str] = [org, course] # type: ignore # Deprecated style allowed to have None for run and branch, and allowed to have '' for run if run: fields.append(run) @@ -227,7 +241,7 @@ def __init__(self, org=None, course=None, run=None, branch=None, version_guid=No raise InvalidKeyError(self.__class__, "Either version_guid or org, course, and run should be set") @classmethod - def _check_location_part(cls, val, regexp): # pylint: disable=missing-docstring + def _check_location_part(cls, val, regexp): # pylint: disable=missing-function-docstring if val is None: return if not isinstance(val, str): @@ -236,7 +250,7 @@ def _check_location_part(cls, val, regexp): # pylint: disable=missing-docstring raise InvalidKeyError(cls, f"Invalid characters in {val!r}.") @property - def version(self): + def version(self) -> str | None: """ Deprecated. The ambiguously named field from CourseLocation which code expects to find. Equivalent to version_guid. @@ -326,7 +340,7 @@ def version_agnostic(self): """ return self.replace(version_guid=None) - def course_agnostic(self): + def course_agnostic(self) -> Self: """ We only care about the locator's version not its course. Returns a copy of itself without any course info. @@ -336,7 +350,7 @@ def course_agnostic(self): """ return self.replace(org=None, course=None, run=None, branch=None) - def for_branch(self, branch): + def for_branch(self, branch: str | None) -> Self: """ Return a new CourseLocator for another branch of the same course (also version agnostic) """ @@ -344,32 +358,32 @@ def for_branch(self, branch): raise InvalidKeyError(self.__class__, "Branches must have full course ids not just versions") return self.replace(branch=branch, version_guid=None) - def for_version(self, version_guid): + def for_version(self, version_guid: str) -> Self: """ Return a new CourseLocator for another version of the same course and branch. Usually used when the head is updated (and thus the course x branch now points to this version) """ return self.replace(version_guid=version_guid) - def _to_string(self): + def _to_string(self) -> str: """ Return a string representing this location. """ - parts = [] + parts: list[str] = [] if self.course and self.run: - parts.extend([self.org, self.course, self.run]) + parts.extend([self.org, self.course, self.run]) # type: ignore if self.branch: parts.append(f"{self.BRANCH_PREFIX}@{self.branch}") if self.version_guid: parts.append(f"{self.VERSION_PREFIX}@{self.version_guid}") return "+".join(parts) - def _to_deprecated_string(self): + def _to_deprecated_string(self) -> str: """Returns an 'old-style' course id, represented as 'org/course/run'""" - return '/'.join([self.org, self.course, self.run]) + return '/'.join([self.org, self.course, self.run]) # type: ignore @classmethod - def _from_deprecated_string(cls, serialized): + def _from_deprecated_string(cls, serialized: str) -> Self: """ Return an instance of `cls` parsed from its deprecated `serialized` form. @@ -388,7 +402,7 @@ def _from_deprecated_string(cls, serialized): if serialized.count('/') != 2: raise InvalidKeyError(cls, serialized) - return cls(*serialized.split('/'), deprecated=True) + return cls(*serialized.split('/'), deprecated=True) # type: ignore CourseKey.set_deprecated_fallback(CourseLocator) @@ -419,11 +433,21 @@ class LibraryLocator(BlockLocatorBase, CourseKey): CANONICAL_NAMESPACE = 'library-v1' RUN = 'library' # For backwards compatibility, LibraryLocators have a read-only 'run' property equal to this KEY_FIELDS = ('org', 'library', 'branch', 'version_guid') + library: str + branch: str + version_guid: ObjectId __slots__ = KEY_FIELDS CHECKED_INIT = False is_course = False # These keys inherit from CourseKey for historical reasons but are not courses - def __init__(self, org=None, library=None, branch=None, version_guid=None, **kwargs): + def __init__( + self, + org: str | None = None, + library: str | None = None, + branch: str | None = None, + version_guid: str | None = None, + **kwargs, + ): """ Construct a LibraryLocator @@ -468,7 +492,7 @@ def __init__(self, org=None, library=None, branch=None, version_guid=None, **kwa **kwargs ) - if self.version_guid is None and (self.org is None or self.library is None): # pylint: disable=no-member + if self.version_guid is None and (self.org is None or self.library is None): raise InvalidKeyError(self.__class__, "Either version_guid or org and library should be set") @property @@ -485,7 +509,7 @@ def course(self): Deprecated. Return a 'course' for compatibility with CourseLocator. """ warnings.warn("Accessing 'course' on a LibraryLocator is deprecated.", DeprecationWarning, stacklevel=2) - return self.library # pylint: disable=no-member + return self.library @property def version(self): @@ -498,7 +522,7 @@ def version(self): DeprecationWarning, stacklevel=2 ) - return self.version_guid # pylint: disable=no-member + return self.version_guid @classmethod def _from_string(cls, serialized): @@ -575,12 +599,12 @@ def _to_string(self): Return a string representing this location. """ parts = [] - if self.library: # pylint: disable=no-member - parts.extend([self.org, self.library]) # pylint: disable=no-member - if self.branch: # pylint: disable=no-member - parts.append(f"{self.BRANCH_PREFIX}@{self.branch}") # pylint: disable=no-member - if self.version_guid: # pylint: disable=no-member - parts.append(f"{self.VERSION_PREFIX}@{self.version_guid}") # pylint: disable=no-member + if self.library: + parts.extend([self.org, self.library]) + if self.branch: + parts.append(f"{self.BRANCH_PREFIX}@{self.branch}") + if self.version_guid: + parts.append(f"{self.VERSION_PREFIX}@{self.version_guid}") return "+".join(parts) def _to_deprecated_string(self): @@ -626,9 +650,9 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): DEPRECATED_TAG = 'i4x' # to combine Locations with BlockUsageLocators - # fake out class introspection as this is an attr in this class's instances - course_key = None - block_type = None + course_key: CourseLocator + block_type: str + block_id: str DEPRECATED_URL_RE = re.compile(""" i4x:// @@ -661,7 +685,7 @@ def __init__(self, course_key, block_type, block_id, **kwargs): super().__init__(course_key=course_key, block_type=block_type, block_id=block_id, **kwargs) - def replace(self, **kwargs): + def replace(self, **kwargs) -> Self: # BlockUsageLocator allows for the replacement of 'KEY_FIELDS' in 'self.course_key'. # This includes the deprecated 'KEY_FIELDS' of CourseLocator `'revision'` and `'version'`. course_key_kwargs = {} @@ -693,7 +717,7 @@ def _clean(cls, value, invalid): return re.sub('_+', '_', invalid.sub('_', value)) @classmethod - def clean(cls, value): + def clean(cls, value: str) -> str: """ Should only be called on deprecated-style values @@ -702,7 +726,7 @@ def clean(cls, value): return cls._clean(value, cls.DEPRECATED_INVALID_CHARS) @classmethod - def clean_keeping_underscores(cls, value): + def clean_keeping_underscores(cls, value: str) -> str: """ Should only be called on deprecated-style values @@ -713,7 +737,7 @@ def clean_keeping_underscores(cls, value): return cls.DEPRECATED_INVALID_CHARS.sub('_', value) @classmethod - def clean_for_url_name(cls, value): + def clean_for_url_name(cls, value: str) -> str: """ Should only be called on deprecated-style values @@ -722,7 +746,7 @@ def clean_for_url_name(cls, value): return cls._clean(value, cls.DEPRECATED_INVALID_CHARS_NAME) @classmethod - def clean_for_html(cls, value): + def clean_for_html(cls, value: str) -> str: """ Should only be called on deprecated-style values @@ -732,7 +756,7 @@ def clean_for_html(cls, value): return cls._clean(value, cls.DEPRECATED_INVALID_HTML_CHARS) @classmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str) -> Self: """ Requests CourseLocator to deserialize its part and then adds the local deserialization of block """ @@ -744,7 +768,7 @@ def _from_string(cls, serialized): raise InvalidKeyError(cls, serialized) return cls(course_key, parsed_parts.get('block_type'), block_id) - def version_agnostic(self): + def version_agnostic(self) -> Self: """ We don't care if the locator's version is not the current head; so, avoid version conflict by reducing info. @@ -755,7 +779,7 @@ def version_agnostic(self): """ return self.replace(course_key=self.course_key.version_agnostic()) - def course_agnostic(self): + def course_agnostic(self) -> Self: """ We only care about the locator's version not its course. Returns a copy of itself without any course info. @@ -1046,11 +1070,10 @@ class LibraryUsageLocator(BlockUsageLocator): CANONICAL_NAMESPACE = 'lib-block-v1' KEY_FIELDS = ('library_key', 'block_type', 'block_id') - # fake out class introspection as this is an attr in this class's instances - library_key = None - block_type = None + library_key: LibraryLocator + block_type: str - def __init__(self, library_key, block_type, block_id, **kwargs): + def __init__(self, library_key: LibraryLocator, block_type: str, block_id: str, **kwargs): """ Construct a LibraryUsageLocator """ @@ -1073,7 +1096,6 @@ def __init__(self, library_key, block_type, block_id, **kwargs): ) from error # We skip the BlockUsageLocator init and go to its superclass: - # pylint: disable=bad-super-call super(BlockUsageLocator, self).__init__(library_key=library_key, block_type=block_type, block_id=block_id, **kwargs) @@ -1179,10 +1201,10 @@ class DefinitionLocator(Locator, DefinitionKey): CHECKED_INIT = False # override the abstractproperty - block_type = None - definition_id = None + block_type: str + definition_id: ObjectId - def __init__(self, block_type, definition_id, deprecated=False): # pylint: disable=unused-argument + def __init__(self, block_type: str, definition_id: ObjectId | str, deprecated: bool = False): # pylint: disable=unused-argument if isinstance(definition_id, str): try: definition_id = self.as_object_id(definition_id) @@ -1190,7 +1212,7 @@ def __init__(self, block_type, definition_id, deprecated=False): # pylint: di raise InvalidKeyError(DefinitionLocator, definition_id) from error super().__init__(definition_id=definition_id, block_type=block_type, deprecated=False) - def _to_string(self): + def _to_string(self) -> str: """ Return a string representing this location. unicode(self) returns something like this: "519665f6223ebd6980884f2b+type+problem" @@ -1204,7 +1226,7 @@ def _to_string(self): ) @classmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str) -> Self: """ Return a DefinitionLocator parsing the given serialized string :param serialized: matches the string to @@ -1213,14 +1235,14 @@ def _from_string(cls, serialized): if not parse: raise InvalidKeyError(cls, serialized) - parse = parse.groupdict() - if parse['definition_id']: - parse['definition_id'] = cls.as_object_id(parse['definition_id']) + data = parse.groupdict() + if data['definition_id']: + data['definition_id'] = cls.as_object_id(data['definition_id']) - return cls(**{key: parse.get(key) for key in cls.KEY_FIELDS}) + return cls(**{key: data.get(key) for key in cls.KEY_FIELDS}) # type: ignore @property - def version(self): + def version(self) -> ObjectId: """ Returns the ObjectId referencing this specific location. """ @@ -1295,7 +1317,6 @@ def _to_deprecated_string(self): /c4x/org/course/category/name """ - # pylint: disable=missing-format-attribute url = ( f"/{self.DEPRECATED_TAG}/{self.course_key.org}/{self.course_key.course}" f"/{self.block_type}/{self.block_id}" @@ -1384,13 +1405,23 @@ class BundleDefinitionLocator(CheckFieldMixin, DefinitionKey): """ CANONICAL_NAMESPACE = 'bundle-olx' KEY_FIELDS = ('bundle_uuid', 'block_type', 'olx_path', '_version_or_draft') + bundle_uuid: UUID + olx_path: str + _version_or_draft: int | str + __slots__ = KEY_FIELDS CHECKED_INIT = False OLX_PATH_REGEXP = re.compile(r'^[\w\-./]+$', flags=re.UNICODE) - # pylint: disable=no-member - - def __init__(self, bundle_uuid, block_type, olx_path, bundle_version=None, draft_name=None, _version_or_draft=None): + def __init__( + self, + bundle_uuid: UUID | str, + block_type: str, + olx_path: str, + bundle_version: int | None = None, + draft_name: str | None = None, + _version_or_draft: str | int | None = None, + ): """ Instantiate a new BundleDefinitionLocator """ @@ -1427,7 +1458,7 @@ def __init__(self, bundle_uuid, block_type, olx_path, bundle_version=None, draft ) @property - def bundle_version(self): + def bundle_version(self) -> int | None: """ Get the Blockstore bundle version number, or None if a Blockstore draft name has been specified instead. @@ -1435,14 +1466,14 @@ def bundle_version(self): return self._version_or_draft if isinstance(self._version_or_draft, int) else None @property - def draft_name(self): + def draft_name(self) -> str | None: """ Get the Blockstore draft name, or None if a Blockstore bundle version number has been specified instead. """ return self._version_or_draft if not isinstance(self._version_or_draft, int) else None - def _to_string(self): + def _to_string(self) -> str: """ Return a string representing this BundleDefinitionLocator """ @@ -1451,10 +1482,11 @@ def _to_string(self): )) @classmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str) -> Self: """ Return a BundleDefinitionLocator by parsing the given serialized string """ + _version_or_draft: int | str try: (bundle_uuid_str, _version_or_draft, block_type, olx_path) = serialized.split(':', 3) except ValueError as error: @@ -1502,14 +1534,14 @@ class LibraryLocatorV2(CheckFieldMixin, LearningContextKey): """ CANONICAL_NAMESPACE = 'lib' KEY_FIELDS = ('org', 'slug') + org: str + slug: str __slots__ = KEY_FIELDS CHECKED_INIT = False # Allow library slugs to contain unicode characters SLUG_REGEXP = re.compile(r'^[\w\-.]+$', flags=re.UNICODE) - # pylint: disable=no-member - def __init__(self, org, slug): """ Construct a LibraryLocatorV2 @@ -1518,14 +1550,14 @@ def __init__(self, org, slug): self._check_key_string_field("slug", slug, regexp=self.SLUG_REGEXP) super().__init__(org=org, slug=slug) - def _to_string(self): + def _to_string(self) -> str: """ Serialize this key as a string """ return ":".join((self.org, self.slug)) @classmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str) -> Self: """ Instantiate this key from a serialized string """ @@ -1535,18 +1567,7 @@ def _from_string(cls, serialized): except (ValueError, TypeError) as error: raise InvalidKeyError(cls, serialized) from error - def make_definition_usage(self, definition_key, usage_id=None): - """ - Return a usage key, given the given the specified definition key and - usage_id. - """ - return LibraryUsageLocatorV2( - lib_key=self, - block_type=definition_key.block_type, - usage_id=usage_id, - ) - - def for_branch(self, branch): + def for_branch(self, branch: str | None): """ Compatibility helper. Some code calls .for_branch(None) on course keys. By implementing this, @@ -1567,15 +1588,16 @@ class LibraryUsageLocatorV2(CheckFieldMixin, UsageKeyV2): """ CANONICAL_NAMESPACE = 'lb' # "Library Block" KEY_FIELDS = ('lib_key', 'block_type', 'usage_id') + lib_key: LibraryLocatorV2 + usage_id: str + __slots__ = KEY_FIELDS CHECKED_INIT = False # Allow usage IDs to contian unicode characters USAGE_ID_REGEXP = re.compile(r'^[\w\-.]+$', flags=re.UNICODE) - # pylint: disable=no-member - - def __init__(self, lib_key, block_type, usage_id): + def __init__(self, lib_key: LibraryLocatorV2, block_type: str, usage_id: str): """ Construct a LibraryUsageLocatorV2 """ @@ -1590,24 +1612,24 @@ def __init__(self, lib_key, block_type, usage_id): ) @property - def context_key(self): + def context_key(self) -> LibraryLocatorV2: return self.lib_key @property - def block_id(self): + def block_id(self) -> str: """ Get the 'block ID' which is another name for the usage ID. """ return self.usage_id - def _to_string(self): + def _to_string(self) -> str: """ Serialize this key as a string """ return ":".join((self.lib_key.org, self.lib_key.slug, self.block_type, self.usage_id)) @classmethod - def _from_string(cls, serialized): + def _from_string(cls, serialized: str) -> Self: """ Instantiate this key from a serialized string """ @@ -1618,7 +1640,7 @@ def _from_string(cls, serialized): except (ValueError, TypeError) as error: raise InvalidKeyError(cls, serialized) from error - def html_id(self): + def html_id(self) -> str: """ Return an id which can be used on an html page as an id attr of an html element. This is only in here for backwards-compatibility with XModules; diff --git a/opaque_keys/edx/tests/__init__.py b/opaque_keys/edx/tests/__init__.py index db90366b..1b9e93ae 100644 --- a/opaque_keys/edx/tests/__init__.py +++ b/opaque_keys/edx/tests/__init__.py @@ -20,7 +20,6 @@ def setUp(self): warnings.simplefilter('always', DeprecationWarning) # Manually exit the catch_warnings context manager when the test is done - # pylint: disable-next=unnecessary-dunder-call self.addCleanup(self.cws.__exit__) @contextmanager diff --git a/opaque_keys/edx/tests/test_course_locators.py b/opaque_keys/edx/tests/test_course_locators.py index 0a7f447a..b01cd7c0 100644 --- a/opaque_keys/edx/tests/test_course_locators.py +++ b/opaque_keys/edx/tests/test_course_locators.py @@ -79,7 +79,7 @@ def test_course_constructor_bad_version_guid(self): CourseLocator(version_guid=None) def test_course_constructor_version_guid(self): - # pylint: disable=no-member,protected-access + # pylint: disable=protected-access # generate a random location test_id_1 = ObjectId() @@ -234,7 +234,7 @@ def test_course_constructor_package_id_separate_branch(self): branch=test_branch, ) - # pylint: disable=no-member,protected-access + # pylint: disable=protected-access self.assertEqual(testobj.branch, test_branch) self.assertEqual(testobj._to_string(), expected_urn) diff --git a/opaque_keys/edx/tests/test_library_locators.py b/opaque_keys/edx/tests/test_library_locators.py index d7023982..a0a3ad89 100644 --- a/opaque_keys/edx/tests/test_library_locators.py +++ b/opaque_keys/edx/tests/test_library_locators.py @@ -1,8 +1,7 @@ """ Tests of LibraryLocators """ - -import itertools # pylint: disable=wrong-import-order +import itertools from unittest import TestCase import ddt @@ -48,12 +47,12 @@ def test_lib_key_constructor(self): code = 'test-problem-bank' lib_key = LibraryLocator(org=org, library=code) self.assertEqual(lib_key.org, org) - self.assertEqual(lib_key.library, code) # pylint: disable=no-member + self.assertEqual(lib_key.library, code) with self.assertDeprecationWarning(): self.assertEqual(lib_key.course, code) with self.assertDeprecationWarning(): self.assertEqual(lib_key.run, 'library') - self.assertEqual(lib_key.branch, None) # pylint: disable=no-member + self.assertEqual(lib_key.branch, None) def test_constructor_using_course(self): org = 'TestX' @@ -62,7 +61,7 @@ def test_constructor_using_course(self): with self.assertDeprecationWarning(): lib_key2 = LibraryLocator(org=org, course=code) self.assertEqual(lib_key, lib_key2) - self.assertEqual(lib_key2.library, code) # pylint: disable=no-member + self.assertEqual(lib_key2.library, code) def test_version_property_deprecated(self): lib_key = CourseKey.from_string('library-v1:TestX+lib1+version@519665f6223ebd6980884f2b') @@ -120,11 +119,11 @@ def test_lib_key_branch_support(self): branch = 'future-purposes-perhaps' lib_key = LibraryLocator(org=org, library=code, branch=branch) self.assertEqual(lib_key.org, org) - self.assertEqual(lib_key.library, code) # pylint: disable=no-member - self.assertEqual(lib_key.branch, branch) # pylint: disable=no-member + self.assertEqual(lib_key.library, code) + self.assertEqual(lib_key.branch, branch) lib_key2 = CourseKey.from_string(str(lib_key)) self.assertEqual(lib_key, lib_key2) - self.assertEqual(lib_key.branch, branch) # pylint: disable=no-member + self.assertEqual(lib_key.branch, branch) def test_for_branch(self): lib_key = LibraryLocator(org='TestX', library='test', branch='initial') @@ -139,7 +138,7 @@ def test_for_branch(self): def test_version_only_lib_key(self): version_only_lib_key = LibraryLocator(version_guid=ObjectId('519665f6223ebd6980884f2b')) self.assertEqual(version_only_lib_key.org, None) - self.assertEqual(version_only_lib_key.library, None) # pylint: disable=no-member + self.assertEqual(version_only_lib_key.library, None) with self.assertRaises(InvalidKeyError): version_only_lib_key.for_branch("test") @@ -182,10 +181,10 @@ def test_lib_key_constructor_version_guid(self, version_id): version_id_obj = ObjectId(version_id) lib_key = LibraryLocator(version_guid=version_id) - self.assertEqual(lib_key.version_guid, version_id_obj) # pylint: disable=no-member + self.assertEqual(lib_key.version_guid, version_id_obj) self.assertEqual(lib_key.org, None) - self.assertEqual(lib_key.library, None) # pylint: disable=no-member - self.assertEqual(str(lib_key.version_guid), version_id_str) # pylint: disable=no-member + self.assertEqual(lib_key.library, None) + self.assertEqual(str(lib_key.version_guid), version_id_str) # Allow access to _to_string # pylint: disable=protected-access expected_str = '@'.join((lib_key.VERSION_PREFIX, version_id_str)) diff --git a/opaque_keys/edx/tests/test_library_usage_locators.py b/opaque_keys/edx/tests/test_library_usage_locators.py index c8090f41..90e24f23 100644 --- a/opaque_keys/edx/tests/test_library_usage_locators.py +++ b/opaque_keys/edx/tests/test_library_usage_locators.py @@ -1,8 +1,7 @@ """ Tests of LibraryUsageLocator """ - -import itertools # pylint: disable=wrong-import-order +import itertools from unittest import TestCase import ddt diff --git a/opaque_keys/edx/tests/test_properties.py b/opaque_keys/edx/tests/test_properties.py index 03336cd4..ee88b824 100644 --- a/opaque_keys/edx/tests/test_properties.py +++ b/opaque_keys/edx/tests/test_properties.py @@ -22,7 +22,6 @@ # composite strategies confuse pylint (because they silently provide the `draw` # argument), so we stop it from complaining about them. -# pylint: disable=no-value-for-parameter def insert(string, index, character): diff --git a/opaque_keys/py.typed b/opaque_keys/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/opaque_keys/tests/strategies.py b/opaque_keys/tests/strategies.py index bff9b544..1432a270 100644 --- a/opaque_keys/tests/strategies.py +++ b/opaque_keys/tests/strategies.py @@ -112,7 +112,7 @@ def classdispatch(func): """ dispatcher = singledispatch(func) - def wrapper(*args, **kw): # pylint: disable=missing-docstring + def wrapper(*args, **kw): return dispatcher.dispatch(args[0])(*args, **kw) wrapper.register = dispatcher.register @@ -150,11 +150,11 @@ def _aside_v1_exclusions(draw, strategy): @fields_for_key.register(AsideDefinitionKeyV1) @cacheable -def _fields_for_aside_def_key_v1(cls, field): # pylint: disable=missing-docstring +def _fields_for_aside_def_key_v1(cls, field): # pylint: disable=missing-function-docstring if field == 'deprecated': return strategies.just(False) if field == 'definition_key': - return _aside_v1_exclusions( # pylint: disable=no-value-for-parameter + return _aside_v1_exclusions( keys_of_type(DefinitionKey, blacklist=AsideDefinitionKey) ) return fields_for_key(super(AsideDefinitionKeyV1, cls).__class__, field) @@ -162,11 +162,11 @@ def _fields_for_aside_def_key_v1(cls, field): # pylint: disable=missing-docstri @fields_for_key.register(AsideUsageKeyV1) @cacheable -def _fields_for_aside_usage_key_v1(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_aside_usage_key_v1(cls, field): # pylint: disable=missing-function-docstring if field == 'deprecated': return strategies.just(False) if field == 'usage_key': - return _aside_v1_exclusions( # pylint: disable=no-value-for-parameter + return _aside_v1_exclusions( keys_of_type(UsageKey, blacklist=AsideUsageKey) ) return fields_for_key(super(AsideUsageKeyV1, cls).__class__, field) @@ -174,7 +174,7 @@ def _fields_for_aside_usage_key_v1(cls, field): # pylint: disable=missing-docst @fields_for_key.register(AsideDefinitionKeyV2) @cacheable -def _fields_for_aside_def_key_v2(cls, field): # pylint: disable=missing-docstring +def _fields_for_aside_def_key_v2(cls, field): # pylint: disable=missing-function-docstring if field == 'deprecated': return strategies.just(False) if field == 'definition_key': @@ -184,7 +184,7 @@ def _fields_for_aside_def_key_v2(cls, field): # pylint: disable=missing-docstri @fields_for_key.register(AsideUsageKeyV2) @cacheable -def _fields_for_aside_usage_key_v2(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_aside_usage_key_v2(cls, field): # pylint: disable=missing-function-docstring if field == 'deprecated': return strategies.just(False) if field == 'usage_key': @@ -194,7 +194,7 @@ def _fields_for_aside_usage_key_v2(cls, field): # pylint: disable=missing-docst @fields_for_key.register(LibraryLocator) @cacheable -def _fields_for_library_locator(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_library_locator(cls, field): # pylint: disable=missing-function-docstring if field == 'version_guid': return version_guids() if field in ('org', 'library', 'branch'): @@ -206,7 +206,7 @@ def _fields_for_library_locator(cls, field): # pylint: disable=missing-docstrin @fields_for_key.register(LibraryLocatorV2) @cacheable -def _fields_for_library_locator_v2(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_library_locator_v2(cls, field): # pylint: disable=missing-function-docstring if field == 'org': return ascii_identifier() if field == 'slug': @@ -216,7 +216,7 @@ def _fields_for_library_locator_v2(cls, field): # pylint: disable=missing-docst @fields_for_key.register(DefinitionLocator) @cacheable -def _fields_for_definition_locator(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_definition_locator(cls, field): # pylint: disable=missing-function-docstring if field == 'definition_id': return version_guids() if field == 'block_type': @@ -226,7 +226,7 @@ def _fields_for_definition_locator(cls, field): # pylint: disable=missing-docst @fields_for_key.register(BundleDefinitionLocator) @cacheable -def _fields_for_bundle_def_locator(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_bundle_def_locator(cls, field): # pylint: disable=missing-function-docstring if field == 'bundle_uuid': return strategies.uuids() if field == 'block_type': @@ -240,7 +240,7 @@ def _fields_for_bundle_def_locator(cls, field): # pylint: disable=missing-docst @fields_for_key.register(LibraryUsageLocatorV2) @cacheable -def _fields_for_library_locator_v2(cls, field): # pylint: disable=missing-docstring, function-redefined +def _fields_for_library_usage_locator_v2(cls, field): # pylint: disable=missing-function-docstring if field == 'lib_key': return instances_of_key(LibraryLocatorV2) if field == 'block_type': @@ -277,8 +277,7 @@ def instances_of_key(cls, **kwargs): @instances_of_key.register(BlockTypeKeyV1) @cacheable -def _instances_of_block_type_key(cls, **kwargs): # pylint: disable=missing-docstring, function-redefined - +def _instances_of_block_type_key(cls, **kwargs): return strategies.builds( cls, block_family=kwargs.get('block_family', ( @@ -293,7 +292,7 @@ def _instances_of_block_type_key(cls, **kwargs): # pylint: disable=missing-docs @instances_of_key.register(CourseLocator) @cacheable -def _instances_of_course_locator(cls, **kwargs): # pylint: disable=missing-docstring, function-redefined +def _instances_of_course_locator(cls, **kwargs): return strategies.builds( cls, @@ -315,7 +314,7 @@ def _instances_of_course_locator(cls, **kwargs): # pylint: disable=missing-docs @instances_of_key.register(BlockUsageLocator) @cacheable -def _instances_of_block_usage(cls, **kwargs): # pylint: disable=missing-docstring, function-redefined +def _instances_of_block_usage(cls, **kwargs): # pylint: disable=missing-function-docstring def locator_for_course(course_key): """ @@ -343,7 +342,7 @@ def locator_for_course(course_key): @instances_of_key.register(LibraryUsageLocator) @cacheable -def _instances_of_library_usage(cls, **kwargs): # pylint: disable=missing-docstring, function-redefined +def _instances_of_library_usage(cls, **kwargs): return strategies.builds( cls, @@ -355,7 +354,7 @@ def _instances_of_library_usage(cls, **kwargs): # pylint: disable=missing-docst @instances_of_key.register(DeprecatedLocation) @cacheable -def _instances_of_deprecated_loc(cls, **kwargs): # pylint: disable=missing-docstring, function-redefined +def _instances_of_deprecated_loc(cls, **kwargs): return strategies.builds( cls, diff --git a/pytest.ini b/pytest.ini index e18661dc..7ff32be3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] DJANGO_SETTINGS_MODULE = settings.test -addopts = --cov . --cov-config .coveragerc --pep8 --pylint --cov-report term --cov-report html -n auto +addopts = --cov . --cov-config .coveragerc --cov-report term --cov-report html -n auto norecursedirs = .git .tox .* CVS _darcs {arch} *.egg pep8ignore = */migrations/* ALL diff --git a/requirements/base.in b/requirements/base.in index 3fdcd1de..88fa1359 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,3 +4,4 @@ # migration guide here: https://pymongo.readthedocs.io/en/4.0/migrate-to-pymongo4.html pymongo>=2.7.2,<4.0 stevedore>=0.14.1 +typing-extensions diff --git a/requirements/base.txt b/requirements/base.txt index cdb44e7f..81d96464 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,3 +10,5 @@ pymongo==3.12.3 # via -r requirements/base.in stevedore==4.0.0 # via -r requirements/base.in +typing-extensions==4.3.0 + # via -r requirements/base.in diff --git a/requirements/django-test.txt b/requirements/django-test.txt index 56c45383..c1562d07 100644 --- a/requirements/django-test.txt +++ b/requirements/django-test.txt @@ -77,6 +77,12 @@ more-itertools==8.14.0 # via # -r requirements/test.txt # pytest +mypy==1.4.1 + # via -r requirements/test.txt +mypy-extensions==1.0.0 + # via + # -r requirements/test.txt + # mypy packaging==21.3 # via # -r requirements/test.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index e3b063aa..22525c4c 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -105,6 +105,12 @@ more-itertools==8.14.0 # via # -r requirements/test.txt # pytest +mypy==1.4.1 + # via -r requirements/test.txt +mypy-extensions==1.0.0 + # via + # -r requirements/test.txt + # mypy packaging==21.3 # via # -r requirements/test.txt diff --git a/requirements/test.in b/requirements/test.in index 8da82cdc..3fdaac32 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -8,9 +8,8 @@ ddt edx-lint hypothesis mock +mypy pycodestyle pytest pytest-cov -pytest-pep8 -pytest-pylint pytest-xdist diff --git a/requirements/test.txt b/requirements/test.txt index bcd7d98f..3f9c3c55 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -53,14 +53,16 @@ mock==4.0.3 # via -r requirements/test.in more-itertools==8.14.0 # via pytest +mypy==1.4.1 + # via -r requirements/test.in +mypy-extensions==1.0.0 + # via mypy packaging==21.3 # via pytest pbr==5.10.0 # via # -r requirements/base.txt # stevedore -pep8==1.7.1 - # via pytest-pep8 platformdirs==2.5.2 # via pylint pluggy==0.13.1 @@ -79,7 +81,6 @@ pylint==2.14.5 # pylint-celery # pylint-django # pylint-plugin-utils - # pytest-pylint pylint-celery==0.3 # via edx-lint pylint-django==2.5.3 @@ -99,19 +100,11 @@ pytest==5.4.3 # pytest-cache # pytest-cov # pytest-forked - # pytest-pep8 - # pytest-pylint # pytest-xdist -pytest-cache==1.0 - # via pytest-pep8 pytest-cov==3.0.0 # via -r requirements/test.in pytest-forked==1.4.0 # via pytest-xdist -pytest-pep8==1.0.6 - # via -r requirements/test.in -pytest-pylint==0.18.0 - # via -r requirements/test.in pytest-xdist==1.34.0 # via # -c requirements/constraints.txt @@ -132,8 +125,6 @@ stevedore==4.0.0 # code-annotations text-unidecode==1.3 # via python-slugify -toml==0.10.2 - # via pytest-pylint tomli==2.0.1 # via # coverage diff --git a/setup.py b/setup.py index e8350014..f5c46ebd 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,7 @@ def get_version(*file_paths): ], # We are including the tests because other libraries do use mixins from them. packages=find_packages(), + include_package_data=True, license='AGPL-3.0', install_requires=load_requirements('requirements/base.in'), extras_require={ diff --git a/tox.ini b/tox.ini index d43af4f8..5b5a6e43 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ commands = pytest --disable-pytest-warnings --ignore=opaque_keys/edx/django {pos [testenv:quality] commands = pycodestyle --config=.pep8 opaque_keys + mypy pylint --rcfile=pylintrc opaque_keys [testenv:docs]