Skip to content

Commit

Permalink
Merge pull request #71 from noisecapella/gs/python_3
Browse files Browse the repository at this point in the history
Added Python 3 compatibility
  • Loading branch information
clintonb committed Apr 1, 2016
2 parents cee052f + 1e2bcfc commit 67513bd
Show file tree
Hide file tree
Showing 22 changed files with 249 additions and 214 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[run]
branch = True
source = ./opaque_keys
source = ./opaque_keys
omit = ./.tox/*
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ David Baumgold <[email protected]>
Gabe Mulley <[email protected]>
Braden MacDonald <[email protected]>
Clinton Blackburn <[email protected]>
George Schneeloch <[email protected]>
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ requirements:
pip install -r requirements.txt

test:
coverage run -m nose
tox
37 changes: 21 additions & 16 deletions opaque_keys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
from abc import ABCMeta, abstractmethod
from functools import total_ordering

from six import (
iteritems,
python_2_unicode_compatible,
text_type,
viewkeys,
viewitems,
with_metaclass,
)
from stevedore.enabled import EnabledExtensionManager


Expand All @@ -34,8 +42,9 @@ def __new__(mcs, name, bases, attrs):
return super(OpaqueKeyMetaclass, mcs).__new__(mcs, name, bases, attrs)


@python_2_unicode_compatible
@total_ordering
class OpaqueKey(object):
class OpaqueKey(with_metaclass(OpaqueKeyMetaclass)):
"""
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
Expand Down Expand Up @@ -89,7 +98,6 @@ class constructor will not validate any of the ``KEY_FIELDS`` arguments, and wil
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', 'deprecated')

KEY_FIELDS = []
Expand Down Expand Up @@ -156,7 +164,7 @@ def _to_deprecated_string(self):

# ============= SERIALIZATION ==============

def __unicode__(self):
def __str__(self):
"""
Serialize this :class:`OpaqueKey`, in the form ``<CANONICAL_NAMESPACE>:<value of _to_string>``.
"""
Expand Down Expand Up @@ -265,13 +273,13 @@ def __init__(self, *args, **kwargs):

# 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)
self.deprecated = kwargs.pop('deprecated', False) # pylint: disable=assigning-non-slot

if self.CHECKED_INIT:
self._checked_init(*args, **kwargs)
else:
self._unchecked_init(**kwargs)
self._initialized = True
self._initialized = True # pylint: disable=assigning-non-slot

def _checked_init(self, *args, **kwargs):
"""
Expand All @@ -285,13 +293,13 @@ def _checked_init(self, *args, **kwargs):
))

keyed_args = dict(zip(self.KEY_FIELDS, args))
overlapping_args = keyed_args.viewkeys() & kwargs.viewkeys()
overlapping_args = viewkeys(keyed_args) & viewkeys(kwargs)
if overlapping_args:
raise TypeError('__init__() got multiple values for keyword argument {!r}'.format(overlapping_args[0]))

keyed_args.update(kwargs)

for key in keyed_args.viewkeys():
for key in viewkeys(keyed_args):
if key not in self.KEY_FIELDS:
raise TypeError('__init__() got an unexpected argument {!r}'.format(key))

Expand All @@ -301,7 +309,7 @@ def _unchecked_init(self, **kwargs):
"""
Set all kwargs as attributes.
"""
for key, value in kwargs.viewitems():
for key, value in viewitems(kwargs):
setattr(self, key, value)

def replace(self, **kwargs):
Expand All @@ -315,7 +323,7 @@ def replace(self, **kwargs):
existing_values = {key: getattr(self, key) for key in self.KEY_FIELDS} # pylint: disable=no-member
existing_values['deprecated'] = self.deprecated

if all(value == existing_values[key] for (key, value) in kwargs.iteritems()):
if all(value == existing_values[key] for (key, value) in iteritems(kwargs)):
return self

existing_values.update(kwargs)
Expand All @@ -325,7 +333,7 @@ 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)
super(OpaqueKey, self).__setattr__(name, value) # pylint: disable=no-member

def __delattr__(self, name):
raise AttributeError("Can't delete {!r}. OpaqueKeys are immutable.".format(name))
Expand All @@ -348,8 +356,8 @@ def __setstate__(self, state_dict):
for key in state_dict:
if key in self.KEY_FIELDS: # pylint: disable=no-member
setattr(self, key, state_dict[key])
self.deprecated = state_dict['deprecated']
self._initialized = True
self.deprecated = state_dict['deprecated'] # pylint: disable=assigning-non-slot
self._initialized = True # pylint: disable=assigning-non-slot

def __getstate__(self):
# used by pickle to get fields on an unpickled object
Expand Down Expand Up @@ -380,9 +388,6 @@ def __lt__(self, other):
def __hash__(self):
return hash(self._key)

def __str__(self):
return unicode(self).encode('utf-8')

def __repr__(self):
return '{}({})'.format(
self.__class__.__name__,
Expand All @@ -391,4 +396,4 @@ def __repr__(self):

def __len__(self):
"""Return the number of characters in the serialized OpaqueKey"""
return len(unicode(self))
return len(text_type(self))
5 changes: 3 additions & 2 deletions opaque_keys/edx/asides.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
store scoped data alongside the definition and usage of the particular XBlock usage that they're
commenting on.
"""
from six import text_type

from opaque_keys.edx.keys import AsideDefinitionKey, AsideUsageKey, DefinitionKey, UsageKey

Expand Down Expand Up @@ -90,7 +91,7 @@ def _to_string(self):
This serialization should not include the namespace prefix.
"""
return u'{}::{}'.format(_encode(unicode(self.definition_key)), _encode(unicode(self.aside_type)))
return u'{}::{}'.format(_encode(text_type(self.definition_key)), _encode(text_type(self.aside_type)))


class AsideUsageKeyV1(AsideUsageKey): # pylint: disable=abstract-method
Expand Down Expand Up @@ -179,4 +180,4 @@ def _to_string(self):
This serialization should not include the namespace prefix.
"""
return u'{}::{}'.format(_encode(unicode(self.usage_key)), _encode(unicode(self.aside_type)))
return u'{}::{}'.format(_encode(text_type(self.usage_key)), _encode(text_type(self.aside_type)))
3 changes: 2 additions & 1 deletion opaque_keys/edx/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import json
from abc import abstractmethod, abstractproperty
from six import text_type

from opaque_keys import OpaqueKey

Expand Down Expand Up @@ -207,7 +208,7 @@ class i4xEncoder(json.JSONEncoder): # pylint: disable=invalid-name
"""
def default(self, key): # pylint: disable=method-hidden
if isinstance(key, OpaqueKey):
return unicode(key)
return text_type(key)
super(i4xEncoder, self).default(key)


Expand Down
48 changes: 17 additions & 31 deletions opaque_keys/edx/locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import warnings
from abc import abstractproperty
from six import string_types, text_type

from bson.errors import InvalidId
from bson.objectid import ObjectId
Expand Down Expand Up @@ -45,12 +46,6 @@ class Locator(OpaqueKey):
ALLOWED_ID_CHARS = r'[\w\-~.:]'
DEPRECATED_ALLOWED_ID_CHARS = r'[\w\-~.:%]'

def __str__(self):
"""
str(self) returns something like this: "mit.eecs.6002x"
"""
return unicode(self).encode('utf-8')

@abstractproperty
def version(self): # pragma: no cover
"""
Expand Down Expand Up @@ -159,9 +154,6 @@ class CourseLocator(BlockLocatorBase, CourseKey): # pylint: disable=abstract-m
# Characters that are forbidden in the deprecated format
INVALID_CHARS_DEPRECATED = re.compile(r"[^\w.%-]", re.UNICODE)

# stubs to fake out the abstractproperty class instrospection and allow treatment as attrs in instances
org = None

def __init__(self, org=None, course=None, run=None, branch=None, version_guid=None, deprecated=False, **kwargs):
"""
Construct a CourseLocator
Expand Down Expand Up @@ -223,7 +215,7 @@ def __init__(self, org=None, course=None, run=None, branch=None, version_guid=No
def _check_location_part(cls, val, regexp): # pylint: disable=missing-docstring
if val is None:
return
if not isinstance(val, basestring):
if not isinstance(val, string_types):
raise InvalidKeyError(cls, "{!r} is not a string".format(val))
if regexp.search(val) is not None:
raise InvalidKeyError(cls, "Invalid characters in {!r}.".format(val))
Expand Down Expand Up @@ -278,7 +270,7 @@ def html_id(self):
place, but I have no way to override. We should clearly define the purpose and restrictions of this
(e.g., I'm assuming periods are fine).
"""
return unicode(self)
return text_type(self)

def make_usage_key(self, block_type, block_id):
return BlockUsageLocator(
Expand Down Expand Up @@ -367,7 +359,7 @@ def to_deprecated_string(self):
DeprecationWarning,
stacklevel=2
)
return unicode(self)
return text_type(self)

@classmethod
def _from_deprecated_string(cls, serialized):
Expand Down Expand Up @@ -421,12 +413,6 @@ class LibraryLocator(BlockLocatorBase, CourseKey):
__slots__ = KEY_FIELDS
CHECKED_INIT = False

# declare our fields explicitly to avoid pylint warnings
org = None
library = None
branch = None
version_guid = None

def __init__(self, org=None, library=None, branch=None, version_guid=None, **kwargs):
"""
Construct a LibraryLocator
Expand Down Expand Up @@ -472,7 +458,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):
if self.version_guid is None and (self.org is None or self.library is None): # pylint: disable=no-member
raise InvalidKeyError(self.__class__, "Either version_guid or org and library should be set")

@property
Expand All @@ -489,7 +475,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
return self.library # pylint: disable=no-member

@property
def version(self):
Expand All @@ -502,7 +488,7 @@ def version(self):
DeprecationWarning,
stacklevel=2
)
return self.version_guid
return self.version_guid # pylint: disable=no-member

@classmethod
def _from_string(cls, serialized):
Expand All @@ -526,7 +512,7 @@ def html_id(self):
"""
Return an id which can be used on an html page as an id attr of an html element.
"""
return unicode(self)
return text_type(self)

def make_usage_key(self, block_type, block_id):
return LibraryUsageLocator(
Expand Down Expand Up @@ -579,12 +565,12 @@ def _to_string(self):
Return a string representing this location.
"""
parts = []
if self.library:
if self.library: # pylint: disable=no-member
parts.extend([self.org, self.course])
if self.branch:
parts.append(u"{prefix}@{branch}".format(prefix=self.BRANCH_PREFIX, branch=self.branch))
if self.version_guid:
parts.append(u"{prefix}@{guid}".format(prefix=self.VERSION_PREFIX, guid=self.version_guid))
if self.branch: # pylint: disable=no-member
parts.append(u"{prefix}@{branch}".format(prefix=self.BRANCH_PREFIX, branch=self.branch)) # pylint: disable=no-member
if self.version_guid: # pylint: disable=no-member
parts.append(u"{prefix}@{guid}".format(prefix=self.VERSION_PREFIX, guid=self.version_guid)) # pylint: disable=no-member
return u"+".join(parts)

def _to_deprecated_string(self):
Expand Down Expand Up @@ -977,7 +963,7 @@ def to_deprecated_string(self):
DeprecationWarning,
stacklevel=2
)
return unicode(self)
return text_type(self)

@classmethod
def _from_deprecated_string(cls, serialized):
Expand Down Expand Up @@ -1176,7 +1162,7 @@ class DefinitionLocator(Locator, DefinitionKey):
definition_id = None

def __init__(self, block_type, definition_id, deprecated=False): # pylint: disable=unused-argument
if isinstance(definition_id, basestring):
if isinstance(definition_id, string_types):
try:
definition_id = self.as_object_id(definition_id)
except ValueError:
Expand All @@ -1188,7 +1174,7 @@ def _to_string(self):
Return a string representing this location.
unicode(self) returns something like this: "519665f6223ebd6980884f2b+type+problem"
"""
return u"{}+{}@{}".format(unicode(self.definition_id), self.BLOCK_TYPE_PREFIX, self.block_type)
return u"{}+{}@{}".format(text_type(self.definition_id), self.BLOCK_TYPE_PREFIX, self.block_type)

URL_RE = re.compile(
r"^(?P<definition_id>[A-F0-9]+)\+{}@(?P<block_type>{ALLOWED_ID_CHARS}+)$".format(
Expand Down Expand Up @@ -1298,7 +1284,7 @@ def to_deprecated_string(self):
DeprecationWarning,
stacklevel=2
)
return unicode(self)
return text_type(self)

@property
def tag(self):
Expand Down
4 changes: 2 additions & 2 deletions opaque_keys/edx/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def assertDeprecationWarning(self, count=1):
"""Asserts that the contained code raises `count` deprecation warnings"""
with warnings.catch_warnings(record=True) as caught:
yield
self.assertEquals(count,
len([warning for warning in caught if issubclass(warning.category, DeprecationWarning)]))
self.assertEqual(count,
len([warning for warning in caught if issubclass(warning.category, DeprecationWarning)]))


class LocatorBaseTest(TestCase):
Expand Down
Loading

0 comments on commit 67513bd

Please sign in to comment.