Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
ed7a631c
Commit
ed7a631c
authored
May 23, 2014
by
Calen Pennington
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Extract opaque_keys into a separate library
[LMS-2571]
parent
8809c5f0
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1 additions
and
477 deletions
+1
-477
common/lib/opaque_keys/opaque_keys/__init__.py
+0
-275
common/lib/opaque_keys/opaque_keys/tests/__init__.py
+0
-0
common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py
+0
-182
common/lib/opaque_keys/setup.py
+0
-19
requirements/edx/github.txt
+1
-0
requirements/edx/local.txt
+0
-1
No files found.
common/lib/opaque_keys/opaque_keys/__init__.py
deleted
100644 → 0
View file @
8809c5f0
"""
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 ``<CANONICAL_NAMESPACE>:<value of _to_string>``.
"""
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
)
common/lib/opaque_keys/opaque_keys/tests/__init__.py
deleted
100644 → 0
View file @
8809c5f0
common/lib/opaque_keys/opaque_keys/tests/test_opaque_keys.py
deleted
100644 → 0
View file @
8809c5f0
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
common/lib/opaque_keys/setup.py
deleted
100644 → 0
View file @
8809c5f0
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'
,
]
}
)
requirements/edx/github.txt
View file @
ed7a631c
...
...
@@ -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
requirements/edx/local.txt
View file @
ed7a631c
...
...
@@ -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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment