From ed7a631c8024411a6ef24ecc9c05b385c71e2501 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 23 May 2014 14:47:51 -0400 Subject: [PATCH] Extract opaque_keys into a separate library [LMS-2571] --- .../lib/opaque_keys/opaque_keys/__init__.py | 275 ------------------ .../opaque_keys/opaque_keys/tests/__init__.py | 0 .../opaque_keys/tests/test_opaque_keys.py | 182 ------------ common/lib/opaque_keys/setup.py | 19 -- requirements/edx/github.txt | 1 + requirements/edx/local.txt | 1 - 6 files changed, 1 insertion(+), 477 deletions(-) delete mode 100644 common/lib/opaque_keys/opaque_keys/__init__.py delete mode 100644 common/lib/opaque_keys/opaque_keys/tests/__init__.py delete mode 100644 common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py delete mode 100644 common/lib/opaque_keys/setup.py diff --git a/common/lib/opaque_keys/opaque_keys/__init__.py b/common/lib/opaque_keys/opaque_keys/__init__.py deleted file mode 100644 index bd097638f553..000000000000 --- a/common/lib/opaque_keys/opaque_keys/__init__.py +++ /dev/null @@ -1,275 +0,0 @@ -""" -Defines the :class:`OpaqueKey` class, to be used as the base-class for -implementing pluggable OpaqueKeys. - -These keys are designed to provide a limited, forward-evolveable interface to -an application, while concealing the particulars of the serialization -formats, and allowing new serialization formats to be installed transparently. -""" -from abc import ABCMeta, abstractmethod, abstractproperty -from copy import deepcopy -from collections import namedtuple -from functools import total_ordering - -from stevedore.enabled import EnabledExtensionManager - - -class InvalidKeyError(Exception): - """ - Raised to indicated that a serialized key isn't valid (wasn't able to be parsed - by any available providers). - """ - def __init__(self, key_class, serialized): - super(InvalidKeyError, self).__init__(u'{}: {}'.format(key_class, serialized)) - - -class OpaqueKeyMetaclass(ABCMeta): - """ - Metaclass for :class:`OpaqueKey`. Sets the default value for the values in ``KEY_FIELDS`` to - ``None``. - """ - def __new__(mcs, name, bases, attrs): - if '__slots__' not in attrs: - for field in attrs.get('KEY_FIELDS', []): - attrs.setdefault(field, None) - return super(OpaqueKeyMetaclass, mcs).__new__(mcs, name, bases, attrs) - - -@total_ordering -class OpaqueKey(object): - """ - A base-class for implementing pluggable opaque keys. Individual key subclasses identify - particular types of resources, without specifying the actual form of the key (or - its serialization). - - There are two levels of expected subclasses: Key type definitions, and key implementations - - :: - - OpaqueKey - | - Key type - | - Key implementation - - The key type base class must define the class property ``KEY_TYPE``, which identifies - which ``entry_point`` namespace the keys implementations should be registered with. - - The KeyImplementation classes must define the following: - - ``CANONICAL_NAMESPACE`` - Identifies the key namespace for the particular key implementation - (when serializing). Key implementations must be registered using the - ``CANONICAL_NAMESPACE`` as their entry_point name, but can also be registered - with other names for backwards compatibility. - - ``KEY_FIELDS`` - A list of attribute names that will be used to establish object - identity. Key implementation instances will compare equal iff all of - their ``KEY_FIELDS`` match, and will not compare equal to instances - of different KeyImplementation classes (even if the ``KEY_FIELDS`` match). - These fields must be hashable. - - ``_to_string`` - Serialize the key into a unicode object. This should not include the namespace - prefix (``CANONICAL_NAMESPACE``). - - ``_from_string`` - Construct an instance of this :class:`OpaqueKey` from a unicode object. The namespace - will already have been parsed. - - OpaqueKeys will not have optional constructor parameters (due to the implementation of - ``KEY_FIELDS``), by default. However, an implementation class can provide a default, - as long as it passes that default to a call to ``super().__init__``. - - :class:`OpaqueKey` objects are immutable. - - Serialization of an :class:`OpaqueKey` is performed by using the :func:`unicode` builtin. - Deserialization is performed by the :meth:`from_string` method. - """ - __metaclass__ = OpaqueKeyMetaclass - __slots__ = ('_initialized') - - NAMESPACE_SEPARATOR = u':' - - @classmethod - @abstractmethod - def _from_string(cls, serialized): - """ - Return an instance of `cls` parsed from its `serialized` form. - - Args: - cls: The :class:`OpaqueKey` subclass. - serialized (unicode): A serialized :class:`OpaqueKey`, with namespace already removed. - - Raises: - InvalidKeyError: Should be raised if `serialized` is not a valid serialized key - understood by `cls`. - """ - raise NotImplementedError() - - @abstractmethod - def _to_string(self): - """ - Return a serialization of `self`. - - This serialization should not include the namespace prefix. - """ - raise NotImplementedError() - - @classmethod - def _separate_namespace(cls, serialized): - """ - Return the namespace from a serialized :class:`OpaqueKey`, and - the rest of the key. - - Args: - serialized (unicode): A serialized :class:`OpaqueKey`. - - Raises: - MissingNamespace: Raised when no namespace can be - extracted from `serialized`. - """ - namespace, _, rest = serialized.partition(cls.NAMESPACE_SEPARATOR) - - # If ':' isn't found in the string, then the source string - # is returned as the first result (i.e. namespace) - if namespace == serialized: - raise InvalidKeyError(cls, serialized) - - return (namespace, rest) - - def __init__(self, *args, **kwargs): - # pylint: disable=no-member - if len(args) + len(kwargs) != len(self.KEY_FIELDS): - raise TypeError('__init__() takes exactly {} arguments ({} given)'.format( - len(self.KEY_FIELDS), - len(args) + len(kwargs) - )) - - keyed_args = dict(zip(self.KEY_FIELDS, args)) - - overlapping_args = keyed_args.viewkeys() & kwargs.viewkeys() - if overlapping_args: - raise TypeError('__init__() got multiple values for keyword argument {!r}'.format(overlapping_args[0])) - - keyed_args.update(kwargs) - - for key, value in keyed_args.viewitems(): - if key not in self.KEY_FIELDS: - raise TypeError('__init__() got an unexpected argument {!r}'.format(key)) - - setattr(self, key, value) - self._initialized = True - - def replace(self, **kwargs): - """ - Return: a new :class:`OpaqueKey` with ``KEY_FIELDS`` specified in ``kwargs`` replaced - their corresponding values. - """ - existing_values = {key: getattr(self, key) for key in self.KEY_FIELDS} # pylint: disable=no-member - existing_values.update(kwargs) - return type(self)(**existing_values) - - def __setattr__(self, name, value): - if getattr(self, '_initialized', False): - raise AttributeError("Can't set {!r}. OpaqueKeys are immutable.".format(name)) - - super(OpaqueKey, self).__setattr__(name, value) - - def __delattr__(self, name): - raise AttributeError("Can't delete {!r}. OpaqueKeys are immutable.".format(name)) - - def __unicode__(self): - """ - Serialize this :class:`OpaqueKey`, in the form ``:``. - """ - return self.NAMESPACE_SEPARATOR.join([self.CANONICAL_NAMESPACE, self._to_string()]) # pylint: disable=no-member - - def __copy__(self): - return self.replace() - - def __deepcopy__(self, memo): - return self.replace(**{ - key: deepcopy(getattr(self, key), memo) for key in self.KEY_FIELDS # pylint: disable=no-member - }) - - 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 - setattr(self, key, state_dict[key]) - - 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 - pickleable_dict[key] = getattr(self, key) - return pickleable_dict - - @property - def _key(self): - """Returns a tuple of key fields""" - return tuple(getattr(self, field) for field in self.KEY_FIELDS) # pylint: disable=no-member - - def __eq__(self, other): - return ( - type(self) == type(other) and - self._key == other._key # pylint: disable=protected-access - ) - - def __ne__(self, other): - return not (self == other) - - def __lt__(self, other): - if type(self) != type(other): - raise TypeError() - return self._key < other._key # pylint: disable=protected-access - - def __hash__(self): - return hash(self._key) - - def __str__(self): - return unicode(self).encode('utf-8') - - def __repr__(self): - return '{}({})'.format( - self.__class__.__name__, - ', '.join(repr(getattr(self, key)) for key in self.KEY_FIELDS) # pylint: disable=no-member - ) - - def __len__(self): - """Return the number of characters in the serialized OpaqueKey""" - return len(unicode(self)) - - @classmethod - def _drivers(cls): - """ - Return a driver manager for all key classes that are - subclasses of `cls`. - """ - return EnabledExtensionManager( - cls.KEY_TYPE, # pylint: disable=no-member - check_func=lambda extension: issubclass(extension.plugin, cls), - invoke_on_load=False, - ) - - @classmethod - def from_string(cls, serialized): - """ - Return a :class:`OpaqueKey` object deserialized from - the `serialized` argument. This object will be an instance - of a subclass of the `cls` argument. - - Args: - serialized: A stringified form of a :class:`OpaqueKey` - """ - if serialized is None: - raise InvalidKeyError(cls, serialized) - - # pylint: disable=protected-access - namespace, rest = cls._separate_namespace(serialized) - try: - return cls._drivers()[namespace].plugin._from_string(rest) - except KeyError: - raise InvalidKeyError(cls, serialized) diff --git a/common/lib/opaque_keys/opaque_keys/tests/__init__.py b/common/lib/opaque_keys/opaque_keys/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py b/common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py deleted file mode 100644 index 4f9c8ef5a90c..000000000000 --- a/common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py +++ /dev/null @@ -1,182 +0,0 @@ -import copy -import json -from unittest import TestCase -from stevedore.extension import Extension -from mock import Mock - -from opaque_keys import OpaqueKey, InvalidKeyError - - -def _mk_extension(name, cls): - return Extension( - name, - Mock(name='entry_point_{}'.format(name)), - cls, - Mock(name='obj_{}'.format(name)), - ) - - -class DummyKey(OpaqueKey): - """ - Key type for testing - """ - KEY_TYPE = 'opaque_keys.testing' - __slots__ = () - - -class HexKey(DummyKey): - KEY_FIELDS = ('value',) - __slots__ = KEY_FIELDS - - def _to_string(self): - return hex(self._value) - - @classmethod - def _from_string(cls, serialized): - if not serialized.startswith('0x'): - raise InvalidKeyError(cls, serialized) - try: - return cls(int(serialized, 16)) - except (ValueError, TypeError): - raise InvalidKeyError(cls, serialized) - - -class Base10Key(DummyKey): - KEY_FIELDS = ('value',) - # Deliberately not using __slots__, to test both cases - - def _to_string(self): - return unicode(self._value) - - @classmethod - def _from_string(cls, serialized): - try: - return cls(int(serialized)) - except (ValueError, TypeError): - raise InvalidKeyError(cls, serialized) - - -class DictKey(DummyKey): - KEY_FIELDS = ('value',) - __slots__ = KEY_FIELDS - - def _to_string(self): - return json.dumps(self._value) - - @classmethod - def _from_string(cls, serialized): - try: - return cls(json.loads(serialized)) - except (ValueError, TypeError): - raise InvalidKeyError(cls, serialized) - - -class KeyTests(TestCase): - def test_namespace_from_string(self): - hex_key = DummyKey.from_string('hex:0x10') - self.assertIsInstance(hex_key, HexKey) - self.assertEquals(hex_key.value, 16) - - base_key = DummyKey.from_string('base10:15') - self.assertIsInstance(base_key, Base10Key) - self.assertEquals(base_key.value, 15) - - def test_unknown_namespace(self): - with self.assertRaises(InvalidKeyError): - DummyKey.from_string('no_namespace:0x10') - - def test_no_namespace_from_string(self): - with self.assertRaises(InvalidKeyError): - DummyKey.from_string('0x10') - - with self.assertRaises(InvalidKeyError): - DummyKey.from_string('15') - - def test_immutability(self): - key = HexKey(10) - - with self.assertRaises(AttributeError): - key.value = 11 # pylint: disable=attribute-defined-outside-init - - def test_equality(self): - self.assertEquals(DummyKey.from_string('hex:0x10'), DummyKey.from_string('hex:0x10')) - self.assertNotEquals(DummyKey.from_string('hex:0x10'), DummyKey.from_string('base10:16')) - - def test_constructor(self): - with self.assertRaises(TypeError): - HexKey() - - with self.assertRaises(TypeError): - HexKey(foo='bar') - - with self.assertRaises(TypeError): - HexKey(10, 20) - - with self.assertRaises(TypeError): - HexKey(value=10, bar=20) - - self.assertEquals(HexKey(10).value, 10) - self.assertEquals(HexKey(value=10).value, 10) - - def test_replace(self): - hex10 = HexKey(10) - hex11 = hex10.replace(value=11) - hex_copy = hex10.replace() - - self.assertNotEquals(id(hex10), id(hex11)) - self.assertNotEquals(id(hex10), id(hex_copy)) - self.assertNotEquals(hex10, hex11) - self.assertEquals(hex10, hex_copy) - self.assertEquals(HexKey(10), hex10) - self.assertEquals(HexKey(11), hex11) - - def test_copy(self): - original = DictKey({'foo': 'bar'}) - copied = copy.copy(original) - deep = copy.deepcopy(original) - - self.assertEquals(original, copied) - self.assertNotEquals(id(original), id(copied)) - self.assertEquals(id(original.value), id(copied.value)) - - self.assertEquals(original, deep) - self.assertNotEquals(id(original), id(deep)) - self.assertNotEquals(id(original.value), id(deep.value)) - - self.assertEquals(copy.deepcopy([original]), [original]) - - def test_subclass(self): - with self.assertRaises(InvalidKeyError): - HexKey.from_string('base10:15') - - with self.assertRaises(InvalidKeyError): - Base10Key.from_string('hex:0x10') - - def test_ordering(self): - ten = HexKey(value=10) - eleven = HexKey(value=11) - - self.assertLess(ten, eleven) - self.assertLessEqual(ten, ten) - self.assertLessEqual(ten, eleven) - self.assertGreater(eleven, ten) - self.assertGreaterEqual(eleven, eleven) - self.assertGreaterEqual(eleven, ten) - - def test_non_ordering(self): - # Verify that different key types aren't comparable - ten = HexKey(value=10) - twelve = Base10Key(value=12) - - # pylint: disable=pointless-statement - with self.assertRaises(TypeError): - ten < twelve - - with self.assertRaises(TypeError): - ten > twelve - - with self.assertRaises(TypeError): - ten <= twelve - - with self.assertRaises(TypeError): - ten >= twelve diff --git a/common/lib/opaque_keys/setup.py b/common/lib/opaque_keys/setup.py deleted file mode 100644 index eaf0ec489718..000000000000 --- a/common/lib/opaque_keys/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup - -setup( - name="opaque_keys", - version="0.1", - packages=[ - "opaque_keys", - ], - install_requires=[ - "stevedore" - ], - entry_points={ - 'opaque_keys.testing': [ - 'base10 = opaque_keys.tests.test_opaque_keys:Base10Key', - 'hex = opaque_keys.tests.test_opaque_keys:HexKey', - 'dict = opaque_keys.tests.test_opaque_keys:DictKey', - ] - } -) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index ac135296f4ff..560d1ec64666 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -27,6 +27,7 @@ -e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock -e git+https://github.com/edx/edx-ora2.git@release-2014-05-23T16.59#egg=edx-ora2 +-e git+https://github.com/edx/opaque-keys.git@1f5ab1abd8273559795b0460e74658e7cd8adc8d#egg=opaque-keys # Prototype XBlocks for limited roll-outs and user testing. These are not for general use. -e git+https://github.com/pmitros/ConceptXBlock.git@2376fde9ebdd83684b78dde77ef96361c3bd1aa0#egg=concept-xblock diff --git a/requirements/edx/local.txt b/requirements/edx/local.txt index e56d8dc8ace6..0e775d04e3fa 100644 --- a/requirements/edx/local.txt +++ b/requirements/edx/local.txt @@ -3,7 +3,6 @@ -e common/lib/calc -e common/lib/capa -e common/lib/chem --e common/lib/opaque_keys -e common/lib/sandbox-packages -e common/lib/symmath -e common/lib/xmodule