diff --git a/.travis.yml b/.travis.yml index 7cc7cab61..86b87c5b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,16 @@ branches: only: - master python: - - "2.7" + - "3.5" cache: pip install: - - pip install -r requirements.txt - - pip install -r django_requirements.txt + - pip install tox - pip install codecov +env: + - TOX_ENV=py27 + - TOX_ENV=py35 + - TOX_ENV=docs script: - - make test doc quality + - tox -e $TOX_ENV after_success: - codecov diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57983c284..b0895312e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,8 +4,20 @@ Change history for XBlock These are notable changes in XBlock. -0.4 - In Progress ------------------ +1.0 - Python 3 +-------------- + +* Introduce Python 3 compatibility to the xblock code base. + This does not enable Python 2 codebases (like edx-platform) to load xblocks + written in Python 3, but it lays the groundwork for future migrations. + +0.5 - ??? +--------- + +No notes provided. + +0.4 +--- * Separate Fragment class out into new web-fragments package diff --git a/Makefile b/Makefile index a304bc3f1..b67e6f41d 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ -test: - coverage run -m nose - -docs: - cd doc && make html - quality: pep8 script/max_pylint_violations + pylint --py3k xblock package: - python setup.py register sdist upload \ No newline at end of file + python setup.py register sdist upload + +test: + tox -e py27,py35 + +docs: + tox -e docs diff --git a/django_requirements.txt b/django_requirements.txt index 50f918b59..dc7b1be21 100644 --- a/django_requirements.txt +++ b/django_requirements.txt @@ -1,4 +1,4 @@ # Install Django requirements, if we're using the optional Django-integrated # parts of XBlock Django >= 1.8, < 1.9 -django-pyfs +django-pyfs >= 1.0.5 diff --git a/doc/conf.py b/doc/conf.py index 81b9439d2..176f820b0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,7 +13,7 @@ import sys import os -from path import path +from path import Path as path import sys import mock @@ -223,4 +223,4 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' -exclude_patterns = ['api/*', 'links.rst'] \ No newline at end of file +exclude_patterns = ['api/*', 'links.rst'] diff --git a/doc/requirements.txt b/doc/requirements.txt index 424e86ad5..38a3c3997 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,4 +4,5 @@ lazy sphinx sphinxcontrib-napoleon==0.2.6 +sphinx_rtd_theme==0.1.9 path.py diff --git a/pylintrc b/pylintrc index 43d937fe4..8fc862696 100644 --- a/pylintrc +++ b/pylintrc @@ -19,7 +19,7 @@ persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=caniusepython3.pylint_checker [MESSAGES CONTROL] @@ -46,8 +46,11 @@ disable= star-args, abstract-class-not-used, abstract-class-little-used, + abstract-class-instantiated, no-init, too-many-lines, + no-else-return, + arguments-differ, no-self-use, too-many-ancestors, too-many-instance-attributes, @@ -56,7 +59,9 @@ disable= too-many-return-statements, too-many-branches, too-many-arguments, - too-many-locals + too-many-locals, + duplicate-code, + len-as-condition, [REPORTS] @@ -93,9 +98,6 @@ comment=no [BASIC] -# Required attributes for module, separated by a comma -required-attributes= - # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input @@ -147,6 +149,7 @@ no-docstring-rgx=__.*__|test_.+|setUp|tearDown # ones are exempt. docstring-min-length=-1 +extension-pkg-whitelist=lxml [FORMAT] @@ -228,10 +231,6 @@ additional-builtins= [CLASSES] -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp diff --git a/requirements.txt b/requirements.txt index 427e883a7..87074b664 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,13 +12,14 @@ markupsafe mock nose coverage -# pinning astroid b/c it's throwing errors on 2.7.8 altho doc says it shouldn't -astroid == 1.2.1 -pylint == 1.3.1 +astroid +pylint rednose pep8 +caniusepython3 diff-cover >= 0.2.1 ddt==0.8.0 +tox # For docs -r doc/requirements.txt @@ -28,6 +29,7 @@ cookiecutter # For web fragments web-fragments +#git+https://github.com/edx/web-fragments.git@0.1.1#egg=web_fragments==0.1.1 # Our own XBlocks -e . diff --git a/script/max_pylint_violations b/script/max_pylint_violations index 9383b9e54..659d95fa3 100755 --- a/script/max_pylint_violations +++ b/script/max_pylint_violations @@ -1,5 +1,5 @@ #!/bin/bash -DEFAULT_MAX=7 +DEFAULT_MAX=6 pylint xblock | tee /tmp/pylint-xblock.log ERR=`grep -E "^[C|R|W|E]:" /tmp/pylint-xblock.log | wc -l` diff --git a/setup.cfg b/setup.cfg index 4cb0bc000..52138a3d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,4 @@ rednose=1 [pep8] ignore=E501,E402 -exclude=doc/* +exclude=doc/*,.tox/ diff --git a/setup.py b/setup.py index fc3e304f8..312c343b6 100755 --- a/setup.py +++ b/setup.py @@ -4,14 +4,17 @@ Set up for XBlock """ +from __future__ import absolute_import, division, print_function, unicode_literals + +import codecs import os.path from setuptools import setup -version_file = os.path.join(os.path.dirname(__file__), 'xblock/VERSION.txt') +VERSION_FILE = os.path.join(os.path.dirname(__file__), 'xblock/VERSION.txt') setup( name='XBlock', - version=open(version_file).read().strip(), + version=codecs.open(VERSION_FILE, encoding='ascii').read().strip(), description='XBlock Core Library', packages=[ 'xblock', @@ -33,7 +36,7 @@ 'web-fragments', ], extras_require={ - 'django': ['django-pyfs'] + 'django': ['django-pyfs >= 1.0.5'] }, license='Apache 2.0', classifiers=[ @@ -47,5 +50,7 @@ 'Natural Language :: English', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", ] ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..d0b2f2e99 --- /dev/null +++ b/tox.ini @@ -0,0 +1,33 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27, py35, docs + +[testenv] +setenv = + COVERAGE_FILE={envdir}/.coverage +passenv = + CI + TRAVIS + TRAVIS_* +deps = + -rrequirements.txt + -rdjango_requirements.txt +commands = + coverage run -m nose + coverage xml -o {env:TRAVIS_BUILD_DIR}/coverage.xml + make quality +whitelist_externals = make + +[testenv:docs] +basepython=python2.7 +changedir={toxinidir}/doc +deps= + sphinx + -rrequirements.txt + -rdjango_requirements.txt + -rdoc/requirements.txt +commands=make html diff --git a/xblock/VERSION.txt b/xblock/VERSION.txt index 8f0916f76..3eefcb9dd 100644 --- a/xblock/VERSION.txt +++ b/xblock/VERSION.txt @@ -1 +1 @@ -0.5.0 +1.0.0 diff --git a/xblock/__init__.py b/xblock/__init__.py index 27a1cc562..fe100d755 100644 --- a/xblock/__init__.py +++ b/xblock/__init__.py @@ -2,8 +2,15 @@ XBlock Courseware Components """ +# For backwards compatability, provide the XBlockMixin in xblock.fields +# without causing a circular import + +from __future__ import absolute_import, division, print_function, unicode_literals + +import codecs import os import warnings + import xblock.core import xblock.fields @@ -25,4 +32,4 @@ def __init__(self, *args, **kwargs): VERSION_FILE = os.path.join(os.path.dirname(__file__), 'VERSION.txt') -__version__ = open(VERSION_FILE).read().strip() +__version__ = codecs.open(VERSION_FILE, encoding='ascii').read().strip() diff --git a/xblock/core.py b/xblock/core.py index dbe31f576..85da0bc3a 100644 --- a/xblock/core.py +++ b/xblock/core.py @@ -5,13 +5,18 @@ and used by all runtimes. """ + +from __future__ import absolute_import, division, print_function, unicode_literals + from collections import defaultdict import inspect import os import warnings import pkg_resources +import six +import xblock.exceptions from xblock.exceptions import DisallowedFileError from xblock.fields import String, List, Scope from xblock.internal import class_lazy @@ -91,6 +96,9 @@ def open_local_resource(cls, uri): unauthorized resource. """ + if isinstance(uri, six.binary_type): + uri = uri.decode('utf-8') + # If no resources_dir is set, then this XBlock cannot serve local resources. if cls.resources_dir is None: raise DisallowedFileError("This XBlock is not configured to serve local resources") @@ -276,8 +284,8 @@ def aside_view_declaration(self, view_name): Returns: either the function or None """ - if view_name in self._combined_asides: - return getattr(self, self._combined_asides[view_name]) + if view_name in self._combined_asides: # pylint: disable=unsupported-membership-test + return getattr(self, self._combined_asides[view_name]) # pylint: disable=unsubscriptable-object else: return None @@ -288,11 +296,7 @@ def needs_serialization(self): If all of the aside's data is empty or a default value, then the aside shouldn't be serialized as XML at all. """ - return any([field.is_set_on(self) for field in self.fields.itervalues()]) - - -# Maintain backwards compatibility -import xblock.exceptions + return any(field.is_set_on(self) for field in six.itervalues(self.fields)) class KeyValueMultiSaveError(xblock.exceptions.KeyValueMultiSaveError): diff --git a/xblock/django/request.py b/xblock/django/request.py index 6955b84c0..6a43e0a57 100644 --- a/xblock/django/request.py +++ b/xblock/django/request.py @@ -1,9 +1,13 @@ """Helpers for WebOb requests and responses.""" -import webob +from __future__ import absolute_import, division, print_function, unicode_literals + from collections import MutableMapping +from itertools import chain, repeat from lazy import lazy -from itertools import chain, repeat, izip + +import six +import webob from webob.multidict import MultiDict, NestedMultiDict, NoVars @@ -20,7 +24,7 @@ def webob_to_django_response(webob_response): return django_response -class HeaderDict(MutableMapping): +class HeaderDict(MutableMapping, six.Iterator): """ Provide a dictionary view of the HTTP headers in a Django request.META dictionary that translates the @@ -77,8 +81,8 @@ def querydict_to_multidict(query_dict, wrap=None): """ wrap = wrap or (lambda val: val) return MultiDict(chain.from_iterable( - izip(repeat(key), (wrap(v) for v in vals)) - for key, vals in query_dict.iterlists() + six.moves.zip(repeat(key), (wrap(v) for v in vals)) + for key, vals in six.iterlists(query_dict) )) diff --git a/xblock/exceptions.py b/xblock/exceptions.py index 66dc72fc7..3d1aaa82e 100644 --- a/xblock/exceptions.py +++ b/xblock/exceptions.py @@ -1,6 +1,9 @@ """ Module for all xblock exception classes """ + +from __future__ import absolute_import, division, print_function, unicode_literals + from webob import Response try: import simplejson as json # pylint: disable=F0401 @@ -14,7 +17,8 @@ class XBlockNotFoundError(Exception): """ def __init__(self, usage_id): # Exception is an old-style class, so can't use super - Exception.__init__(self, "Unable to load an xblock for usage_id {!r}".format(usage_id)) + Exception.__init__(self) + self.message = "Unable to load an xblock for usage_id {!r}".format(usage_id) class XBlockSaveError(Exception): @@ -30,8 +34,9 @@ def __init__(self, saved_fields, dirty_fields, message=None): `dirty_fields` - a set of fields that were left dirty after the save """ # Exception is an old-style class, so can't use super - Exception.__init__(self, message) + Exception.__init__(self) + self.message = message self.saved_fields = saved_fields self.dirty_fields = dirty_fields @@ -58,13 +63,14 @@ class InvalidScopeError(Exception): Raised to indicated that operating on the supplied scope isn't allowed by a KeyValueStore """ def __init__(self, invalid_scope, valid_scopes=None): + super(InvalidScopeError, self).__init__() if valid_scopes: - super(InvalidScopeError, self).__init__("Invalid scope: {}. Valid scopes are: {}".format( + self.message = "Invalid scope: {}. Valid scopes are: {}".format( invalid_scope, valid_scopes, - )) + ) else: - super(InvalidScopeError, self).__init__("Invalid scope: {}".format(invalid_scope)) + self.message = "Invalid scope: {}".format(invalid_scope) class NoSuchViewError(Exception): @@ -79,7 +85,8 @@ def __init__(self, block, view_name): :param view_name: The name of the view that couldn't be found """ # Can't use super because Exception is an old-style class - Exception.__init__(self, "Unable to find view {!r} on block {!r}".format(view_name, block)) + Exception.__init__(self) + self.message = "Unable to find view {!r} on block {!r}".format(view_name, block) class NoSuchHandlerError(Exception): @@ -124,9 +131,10 @@ def get_response(self, **kwargs): the Response. """ return Response( - json.dumps({"error": self.message}), + json.dumps({"error": self.message}), # pylint: disable=exception-message-attribute status_code=self.status_code, content_type="application/json", + charset="utf-8", **kwargs ) diff --git a/xblock/field_data.py b/xblock/field_data.py index 0cb8b5391..a76cc9e3b 100644 --- a/xblock/field_data.py +++ b/xblock/field_data.py @@ -5,21 +5,24 @@ simple. """ +from __future__ import absolute_import, division, print_function, unicode_literals + + import copy from abc import ABCMeta, abstractmethod from collections import defaultdict +import six + from xblock.exceptions import InvalidScopeError -class FieldData(object): +class FieldData(six.with_metaclass(ABCMeta, object)): """ An interface allowing access to an XBlock's field values indexed by field names. """ - __metaclass__ = ABCMeta - @abstractmethod def get(self, block, name): """ @@ -87,7 +90,7 @@ def set_many(self, block, update_dict): :param update_dict: A map of field names to their new values :type update_dict: dict """ - for key, value in update_dict.items(): + for key, value in six.iteritems(update_dict): self.set(block, key, value) def default(self, block, name): # pylint: disable=unused-argument @@ -161,10 +164,10 @@ def set(self, block, name, value): def set_many(self, block, update_dict): update_dicts = defaultdict(dict) - for key, value in update_dict.items(): + for key, value in six.iteritems(update_dict): update_dicts[self._field_data(block, key)][key] = value - for field_data, update_dict in update_dicts.items(): - field_data.set_many(block, update_dict) + for field_data, new_update_dict in six.iteritems(update_dicts): + field_data.set_many(block, new_update_dict) def delete(self, block, name): self._field_data(block, name).delete(block, name) diff --git a/xblock/fields.py b/xblock/fields.py index d270ee340..2e78b7c77 100644 --- a/xblock/fields.py +++ b/xblock/fields.py @@ -6,6 +6,8 @@ """ +from __future__ import absolute_import, division, print_function, unicode_literals + from collections import namedtuple import copy import datetime @@ -155,6 +157,7 @@ def scopes(cls): ScopeBase = namedtuple('ScopeBase', 'user block name') +@six.python_2_unicode_compatible class Scope(ScopeBase): """ Defines six types of scopes to be used: `content`, `settings`, @@ -188,12 +191,12 @@ class Scope(ScopeBase): the points scored by all users attempting a problem. """ - content = ScopeBase(UserScope.NONE, BlockScope.DEFINITION, u'content') - settings = ScopeBase(UserScope.NONE, BlockScope.USAGE, u'settings') - user_state = ScopeBase(UserScope.ONE, BlockScope.USAGE, u'user_state') - preferences = ScopeBase(UserScope.ONE, BlockScope.TYPE, u'preferences') - user_info = ScopeBase(UserScope.ONE, BlockScope.ALL, u'user_info') - user_state_summary = ScopeBase(UserScope.ALL, BlockScope.USAGE, u'user_state_summary') + content = ScopeBase(UserScope.NONE, BlockScope.DEFINITION, 'content') + settings = ScopeBase(UserScope.NONE, BlockScope.USAGE, 'settings') + user_state = ScopeBase(UserScope.ONE, BlockScope.USAGE, 'user_state') + preferences = ScopeBase(UserScope.ONE, BlockScope.TYPE, 'preferences') + user_info = ScopeBase(UserScope.ONE, BlockScope.ALL, 'user_info') + user_state_summary = ScopeBase(UserScope.ALL, BlockScope.USAGE, 'user_state_summary') @classmethod def named_scopes(cls): @@ -222,19 +225,22 @@ def __new__(cls, user, block, name=None): """Create a new Scope, with an optional name.""" if name is None: - name = u'{}_{}'.format(user, block) + name = '{}_{}'.format(user, block) return ScopeBase.__new__(cls, user, block, name) children = Sentinel('Scope.children') parent = Sentinel('Scope.parent') - def __unicode__(self): + def __str__(self): return self.name def __eq__(self, other): return isinstance(other, Scope) and self.user == other.user and self.block == other.block + def __hash__(self): + return hash(('xblock.fields.Scope', self.user, self.block)) + class ScopeIds(namedtuple('ScopeIds', 'user_id block_type def_id usage_id')): """ @@ -444,7 +450,7 @@ def _check_or_enforce_type(self, value): try: new_value = self.enforce_type(value) except: # pylint: disable=bare-except - message = u"The value {} could not be enforced ({})".format( + message = "The value {!r} could not be enforced ({})".format( value, traceback.format_exc().splitlines()[-1]) warnings.warn(message, FailingEnforceTypeWarning, stacklevel=3) else: @@ -453,7 +459,7 @@ def _check_or_enforce_type(self, value): except TypeError: equal = False if not equal: - message = u"The value {} would be enforced to {}".format( + message = "The value {!r} would be enforced to {!r}".format( value, new_value) warnings.warn(message, ModifyingEnforceTypeWarning, stacklevel=3) @@ -467,7 +473,7 @@ def _calculate_unique_id(self, xblock): for the field in its given scope. """ key = scope_key(self, xblock) - return hashlib.sha1(key).hexdigest() + return hashlib.sha1(key.encode('utf-8')).hexdigest() def _get_default_value_to_cache(self, xblock): """ @@ -615,8 +621,11 @@ def to_string(self, value): """ self._warn_deprecated_outside_JSONField() value = json.dumps( - self.to_json(value), indent=2, - sort_keys=True, separators=(',', ': ')) + self.to_json(value), + indent=2, + sort_keys=True, + separators=(',', ': '), + ) return value def from_string(self, serialized): @@ -754,7 +763,9 @@ def __init__(self, help=None, default=UNSET, scope=Scope.content, display_name=N **kwargs) def from_json(self, value): - if isinstance(value, basestring): + if isinstance(value, six.binary_type): + value = value.decode('ascii', errors='replace') + if isinstance(value, six.text_type): return value.lower() == 'true' else: return bool(value) @@ -779,6 +790,18 @@ def from_json(self, value): enforce_type = from_json + def to_string(self, value): + """ + In python3, json.dumps() cannot sort keys of different types, + so preconvert None to 'null'. + """ + self.enforce_type(value) + if isinstance(value, dict) and None in value: + value = value.copy() + value['null'] = value[None] + del value[None] + return super(Dict, self).to_string(value) + class List(JSONField): """ @@ -834,19 +857,13 @@ class String(JSONField): """ MUTABLE = False - VALID_CONTROLS = {u'\n', u'\r', u'\t'} + VALID_CONTROLS = {'\n', '\r', '\t'} - def _valid_unichar(self, character): + def _valid_char(self, character): """ Strip invalid control characters from a unicode text object. """ - return unicodedata.category(character)[0] != u'C' or character in self.VALID_CONTROLS - - def _valid_bytechar(self, character): - """ - Strip invalid control characters from a bytestring object. - """ - return ord(character) >= 32 or character.decode('ascii', errors='replace') in self.VALID_CONTROLS + return unicodedata.category(character)[0] != 'C' or character in self.VALID_CONTROLS def _sanitize(self, value): """ @@ -854,10 +871,10 @@ def _sanitize(self, value): https://www.w3.org/TR/xml/#charsets Leave all other characters. """ + if isinstance(value, six.binary_type): + value = value.decode('utf-8') if isinstance(value, six.text_type): - new_value = u''.join(ch for ch in value if self._valid_unichar(ch)) - elif isinstance(value, six.binary_type): - new_value = b''.join(ch for ch in value if self._valid_bytechar(ch)) + new_value = ''.join(ch for ch in value if self._valid_char(ch)) else: return value # The new string will be equivalent to the original string if no control characters are present. @@ -865,7 +882,7 @@ def _sanitize(self, value): return value if value == new_value else new_value def from_json(self, value): - if value is None or isinstance(value, basestring): + if value is None or isinstance(value, (six.binary_type, six.text_type)): return self._sanitize(value) else: raise TypeError('Value stored in a String must be None or a string, found %s' % type(value)) @@ -876,6 +893,8 @@ def from_string(self, value): def to_string(self, value): """String gets serialized and deserialized without quote marks.""" + if isinstance(value, six.binary_type): + value = value.decode('utf-8') return self.to_json(value) @property @@ -929,7 +948,10 @@ def from_json(self, value): if value is None: return None - if isinstance(value, basestring): + if isinstance(value, six.binary_type): + value = value.decode('utf-8') + + if isinstance(value, six.text_type): # Parser interprets empty string as now by default if value == "": return None @@ -1054,23 +1076,23 @@ def scope_key(instance, xblock): if instance.scope.user == UserScope.NONE or instance.scope.user == UserScope.ALL: pass elif instance.scope.user == UserScope.ONE: - scope_key_dict['user'] = unicode(xblock.scope_ids.user_id) + scope_key_dict['user'] = six.text_type(xblock.scope_ids.user_id) else: raise NotImplementedError() if instance.scope.block == BlockScope.TYPE: - scope_key_dict['block'] = unicode(xblock.scope_ids.block_type) + scope_key_dict['block'] = six.text_type(xblock.scope_ids.block_type) elif instance.scope.block == BlockScope.USAGE: - scope_key_dict['block'] = unicode(xblock.scope_ids.usage_id) + scope_key_dict['block'] = six.text_type(xblock.scope_ids.usage_id) elif instance.scope.block == BlockScope.DEFINITION: - scope_key_dict['block'] = unicode(xblock.scope_ids.def_id) + scope_key_dict['block'] = six.text_type(xblock.scope_ids.def_id) elif instance.scope.block == BlockScope.ALL: pass else: raise NotImplementedError() - replacements = list(itertools.product("._-", "._-")) - substitution_list = dict(zip("./\\,_ +:-", ("".join(x) for x in replacements))) + replacements = itertools.product("._-", "._-") + substitution_list = dict(six.moves.zip("./\\,_ +:-", ("".join(x) for x in replacements))) # Above runs in 4.7us, and generates a list of common substitutions: # {' ': '_-', '+': '-.', '-': '--', ',': '_.', '/': '._', '.': '..', ':': '-_', '\\': '.-', '_': '__'} @@ -1079,7 +1101,7 @@ def scope_key(instance, xblock): def encode(char): """ Replace all non-alphanumeric characters with -n- where n - is their UTF8 code. + is their Unicode codepoint. TODO: Test for UTF8 which is not ASCII """ if char.isalnum(): diff --git a/xblock/fragment.py b/xblock/fragment.py index f2c7e1432..44ed96179 100644 --- a/xblock/fragment.py +++ b/xblock/fragment.py @@ -2,6 +2,8 @@ Makes the Fragment class available through the old namespace location. """ +from __future__ import absolute_import, division, print_function, unicode_literals + import warnings import web_fragments.fragment diff --git a/xblock/internal.py b/xblock/internal.py index 4c7a5d9f3..ba88dc310 100644 --- a/xblock/internal.py +++ b/xblock/internal.py @@ -1,9 +1,14 @@ """ Internal machinery used to make building XBlock family base classes easier. """ + +from __future__ import absolute_import, division, print_function, unicode_literals + import functools import inspect +import six + class LazyClassProperty(object): """ @@ -38,7 +43,7 @@ class NamedAttributesMetaclass(type): def __new__(mcs, name, bases, attrs): # Iterate over the attrs before they're bound to the class # so that we don't accidentally trigger any __get__ methods - for attr_name, attr in attrs.iteritems(): + for attr_name, attr in six.iteritems(attrs): if Nameable.needs_name(attr): attr.__name__ = attr_name @@ -58,8 +63,8 @@ class Nameable(object): :class:`.NamedAttributesMetaclass`, will be assigned a `__name__` attribute based on what class attribute they are bound to. """ - __slots__ = ('__name__') - + if six.PY2: + __slots__ = ('__name__',) __name__ = None @staticmethod diff --git a/xblock/mixins.py b/xblock/mixins.py index 4022be6a9..ac5c5b9a0 100644 --- a/xblock/mixins.py +++ b/xblock/mixins.py @@ -3,16 +3,19 @@ functionality, such as ScopeStorage, RuntimeServices, and Handlers. """ +from __future__ import absolute_import, division, print_function, unicode_literals + + +from collections import OrderedDict +import copy import functools import inspect import logging -from lxml import etree -import copy -from collections import OrderedDict - -import json import warnings +import json +from lxml import etree +import six from webob import Response from xblock.exceptions import JsonHandlerError, KeyValueMultiSaveError, XBlockSaveError, FieldDataDeprecationWarning @@ -68,7 +71,7 @@ def wrapper(self, request, suffix=''): if isinstance(response, Response): return response else: - return Response(json.dumps(response), content_type='application/json') + return Response(json.dumps(response), content_type='application/json', charset='utf8') return wrapper @classmethod @@ -153,15 +156,14 @@ def service_declaration(cls, service_name): Returns: One of "need", "want", or None. """ - return cls._combined_services.get(service_name) + return cls._combined_services.get(service_name) # pylint: disable=no-member @RuntimeServicesMixin.needs('field-data') -class ScopedStorageMixin(RuntimeServicesMixin): +class ScopedStorageMixin(six.with_metaclass(NamedAttributesMetaclass, RuntimeServicesMixin)): """ This mixin provides scope for Fields and the associated Scoped storage. """ - __metaclass__ = NamedAttributesMetaclass @class_lazy def fields(cls): # pylint: disable=no-self-argument @@ -308,14 +310,16 @@ def __repr__(self): # Since this is not understood by static analysis, silence this error. # pylint: disable=E1101 attrs = [] - for field in self.fields.values(): + for field in six.itervalues(self.fields): try: value = getattr(self, field.name) - except Exception: # pylint: disable=W0703 + except Exception: # pylint: disable=broad-except # Ensure we return a string, even if unanticipated exceptions. attrs.append(" %s=???" % (field.name,)) else: - if isinstance(value, basestring): + if isinstance(value, six.binary_type): + value = value.decode('utf-8', errors='escape') + if isinstance(value, six.text_type): value = value.strip() if len(value) > 40: value = value[:37] + "..." @@ -345,11 +349,10 @@ def __new__(mcs, name, bases, attrs): return super(ChildrenModelMetaclass, mcs).__new__(mcs, name, bases, attrs) -class HierarchyMixin(ScopedStorageMixin): +class HierarchyMixin(six.with_metaclass(ChildrenModelMetaclass, ScopedStorageMixin)): """ This adds Fields for parents and children. """ - __metaclass__ = ChildrenModelMetaclass parent = Reference(help='The id of the parent of this XBlock', default=None, scope=Scope.parent) @@ -460,7 +463,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): block.runtime.add_node_as_child(block, child, id_generator) # Attributes become fields. - for name, value in node.items(): + for name, value in node.items(): # lxml has no iteritems cls._set_field_if_present(block, name, value, {}) # Text content becomes "content", if such a field exists. @@ -483,7 +486,7 @@ def add_xml_to_node(self, node): node.set('xblock-family', self.entry_point) # Set node attributes based on our fields. - for field_name, field in self.fields.iteritems(): + for field_name, field in self.fields.items(): if field_name in ('children', 'parent', 'content'): continue if field.is_set_on(self) or field.force_export: @@ -516,7 +519,7 @@ def _set_field_if_present(cls, block, name, value, attrs): else: setattr(block, name, value) else: - logging.warn("XBlock %s does not contain field %s", type(block), name) + logging.warning("XBlock %s does not contain field %s", type(block), name) def _add_field(self, node, field_name, field): """ diff --git a/xblock/plugin.py b/xblock/plugin.py index b683d699a..9c597b0ec 100644 --- a/xblock/plugin.py +++ b/xblock/plugin.py @@ -1,9 +1,11 @@ -"""Generic plugin support so we can find XBlocks. +""" +Generic plugin support so we can find XBlocks. This code is in the Runtime layer. - """ +from __future__ import absolute_import, division, print_function, unicode_literals + import functools import itertools import logging @@ -173,7 +175,7 @@ def test_the_thing(): def _decorator(func): # pylint: disable=C0111 @functools.wraps(func) def _inner(*args, **kwargs): # pylint: disable=C0111 - global PLUGIN_CACHE + global PLUGIN_CACHE # pylint: disable=global-statement old = list(cls.extra_entry_points) old_cache = PLUGIN_CACHE diff --git a/xblock/reference/plugins.py b/xblock/reference/plugins.py index 3fe1f3f30..3499da931 100644 --- a/xblock/reference/plugins.py +++ b/xblock/reference/plugins.py @@ -7,6 +7,8 @@ Much of this still needs to be organized. """ +from __future__ import absolute_import, division, print_function, unicode_literals + try: from django.core.exceptions import ImproperlyConfigured except ImportError: @@ -25,7 +27,7 @@ class ImproperlyConfigured(Exception): except ImportError: djpyfs = None # pylint: disable=invalid-name except ImproperlyConfigured: - print "Warning! Django is not correctly configured." + print("Warning! Django is not correctly configured.") djpyfs = None # pylint: disable=invalid-name from xblock.fields import Field, NO_CACHE_VALUE diff --git a/xblock/reference/user_service.py b/xblock/reference/user_service.py index 42cb15bb0..039342cd2 100644 --- a/xblock/reference/user_service.py +++ b/xblock/reference/user_service.py @@ -2,6 +2,8 @@ This file supports the XBlock service that returns data about users. """ +from __future__ import absolute_import, division, print_function, unicode_literals + from xblock.reference.plugins import Service diff --git a/xblock/run_script.py b/xblock/run_script.py index 02d4d72c7..0f0f2c6b8 100644 --- a/xblock/run_script.py +++ b/xblock/run_script.py @@ -1,7 +1,13 @@ -"""Script execution for script fragments in content.""" +""" +Script execution for script fragments in content. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals import textwrap +import six + def run_script(pycode): """Run the Python in `pycode`, and return a dict of the resulting globals.""" @@ -13,6 +19,6 @@ def run_script(pycode): # execute it. globs = {} - exec pycode in globs, globs # pylint: disable=W0122 + six.exec_(pycode, globs, globs) # pylint: disable=W0122 return globs diff --git a/xblock/runtime.py b/xblock/runtime.py index fa4ac6113..3ce24f6bb 100644 --- a/xblock/runtime.py +++ b/xblock/runtime.py @@ -2,20 +2,27 @@ Machinery to make the common case easy when building new runtimes """ +from __future__ import absolute_import, division, print_function, unicode_literals + +from abc import ABCMeta, abstractmethod +from collections import namedtuple import functools import gettext +from io import BytesIO, StringIO import itertools -import markupsafe +import json +import logging import re import threading import warnings -from abc import ABCMeta, abstractmethod from lxml import etree -from StringIO import StringIO +import markupsafe +import six -from collections import namedtuple from web_fragments.fragment import Fragment + +from xblock.core import XBlock, XBlockAside, XML_NAMESPACES from xblock.fields import Field, BlockScope, Scope, ScopeIds, UserScope from xblock.field_data import FieldData from xblock.exceptions import ( @@ -26,19 +33,14 @@ NoSuchDefinition, FieldDataDeprecationWarning, ) -from xblock.core import XBlock, XBlockAside, XML_NAMESPACES -import logging -import json log = logging.getLogger(__name__) -class KeyValueStore(object): +class KeyValueStore(six.with_metaclass(ABCMeta, object)): """The abstract interface for Key Value Stores.""" - __metaclass__ = ABCMeta - class Key(namedtuple("Key", "scope, user_id, block_scope_id, field_name, block_family")): """ Keys are structured to retain information about the scope of the data. @@ -85,7 +87,7 @@ def set_many(self, update_dict): :update_dict: field_name, field_value pairs for all cached changes """ - for key, value in update_dict.iteritems(): + for key, value in six.iteritems(update_dict): self.set(key, value) @@ -223,7 +225,7 @@ def set_many(self, block, update_dict): updated_dict = {} # Generate a new dict with the correct mappings. - for (key, value) in update_dict.items(): + for (key, value) in six.iteritems(update_dict): updated_dict[self._key(block, key)] = value self._kvs.set_many(updated_dict) @@ -243,9 +245,8 @@ def default(self, block, name): DbModel = KvsFieldData # pylint: disable=C0103 -class IdReader(object): +class IdReader(six.with_metaclass(ABCMeta, object)): """An abstract object that stores usages and definitions.""" - __metaclass__ = ABCMeta @abstractmethod def get_usage_id_from_aside(self, aside_id): @@ -326,9 +327,8 @@ def get_aside_type_from_definition(self, aside_id): raise NotImplementedError() -class IdGenerator(object): +class IdGenerator(six.with_metaclass(ABCMeta, object)): """An abstract object that creates usage and definition ids""" - __metaclass__ = ABCMeta @abstractmethod def create_aside(self, definition_id, usage_id, aside_type): @@ -438,13 +438,11 @@ def get_aside_type_from_usage(self, aside_id): return aside_id.aside_type -class Runtime(object): +class Runtime(six.with_metaclass(ABCMeta, object)): """ Access to the runtime environment for XBlocks. """ - __metaclass__ = ABCMeta - # Abstract methods @abstractmethod def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): @@ -678,7 +676,11 @@ def parse_xml_string(self, xml, id_generator=None): ) id_generator = id_generator or self.id_generator - return self.parse_xml_file(StringIO(xml), id_generator) + if isinstance(xml, six.binary_type): + io_type = BytesIO + else: + io_type = StringIO + return self.parse_xml_file(io_type(xml), id_generator) def parse_xml_file(self, fileobj, id_generator=None): """Parse an open XML file, returning a usage id.""" @@ -766,7 +768,7 @@ def export_to_xml(self, block, xmlfile): aside_node = etree.Element("unknown_root", nsmap=XML_NAMESPACES) aside.add_xml_to_node(aside_node) block.append(aside_node) - tree.write(xmlfile, xml_declaration=True, pretty_print=True, encoding="utf8") + tree.write(xmlfile, xml_declaration=True, pretty_print=True, encoding='utf-8') def add_block_as_child_node(self, block, node): """ @@ -893,8 +895,10 @@ def _wrap_ele(self, block, view, frag, extra_data=None): # for at least a little while so as not to adversely effect developers. # pmitros/Jun 28, 2014. if hasattr(frag, 'json_init_args') and frag.json_init_args is not None: - json_init = u''.format(data=json.dumps(frag.json_init_args)) + json_init = ( + '' + ).format(data=json.dumps(frag.json_init_args)) block_css_entrypoint = block.entry_point.replace('.', '-') css_classes = [ @@ -902,9 +906,9 @@ def _wrap_ele(self, block, view, frag, extra_data=None): '{}-{}'.format(block_css_entrypoint, view), ] - html = u"
{body}{js}
".format( + html = "
{body}{js}
".format( markupsafe.escape(' '.join(css_classes)), - properties="".join(" data-%s='%s'" % item for item in data.items()), + properties="".join(" data-%s='%s'" % item for item in list(data.items())), body=frag.body_html(), js=json_init) @@ -1086,7 +1090,7 @@ class BadPath(Exception): """Bad path exception thrown when path cannot be found.""" pass results = self.query(block) - ROOT, SEP, WORD, FINAL = range(4) # pylint: disable=C0103 + ROOT, SEP, WORD, FINAL = six.moves.range(4) # pylint: disable=C0103 state = ROOT lexer = RegexLexer( ("dotdot", r"\.\."), @@ -1150,7 +1154,7 @@ def _family_id_to_superclass(self, family_id): for family in [XBlock, XBlockAside]: if family_id == family.entry_point: return family - raise ValueError(u'No such family: {}'.format(family_id)) + raise ValueError('No such family: {}'.format(family_id)) class ObjectAggregator(object): @@ -1233,7 +1237,7 @@ def mix(self, cls): # created a class before we got the lock, we don't # overwrite it return _CLASS_CACHE.setdefault(mixin_key, type( - base_class.__name__ + 'WithMixins', + base_class.__name__ + str('WithMixins'), # type() requires native str (base_class, ) + mixins, {'unmixed_class': base_class} )) @@ -1278,6 +1282,37 @@ def strftime(self, dtime, format): # pylint: disable=redefined-builtin Locale-aware strftime, with format short-cuts. """ format = self.STRFTIME_FORMATS.get(format + "_FORMAT", format) - if isinstance(format, unicode): + if six.PY2 and isinstance(format, six.text_type): format = format.encode("utf8") - return dtime.strftime(format).decode("utf8") + timestring = dtime.strftime(format) + if six.PY2: + timestring = timestring.decode("utf8") + return timestring + + @property + def ugettext(self): + """ + Dispatch to the appropriate gettext method to handle text objects. + + Note that under python 3, this uses `gettext()`, while under python 2, + it uses `ugettext()`. This should not be used with bytestrings. + """ + # pylint: disable=no-member + if six.PY2: + return self._translations.ugettext + else: + return self._translations.gettext + + @property + def ungettext(self): + """ + Dispatch to the appropriate ngettext method to handle text objects. + + Note that under python 3, this uses `ngettext()`, while under python 2, + it uses `ungettext()`. This should not be used with bytestrings. + """ + # pylint: disable=no-member + if six.PY2: + return self._translations.ungettext + else: + return self._translations.ngettext diff --git a/xblock/test/django/test_request.py b/xblock/test/django/test_request.py index b7c6df575..dd73d4b1d 100644 --- a/xblock/test/django/test_request.py +++ b/xblock/test/django/test_request.py @@ -4,22 +4,28 @@ responses. """ +from __future__ import absolute_import, division, print_function, unicode_literals + # Set up Django settings import os +from unittest import TestCase + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xblock.test.settings") -# Django isn't always available, so skip tests if it isn't. -from nose.plugins.skip import SkipTest +# pylint: disable=wrong-import-position try: from django.test.client import RequestFactory # pylint: disable=import-error HAS_DJANGO = True except ImportError: HAS_DJANGO = False -from unittest import TestCase +# Django isn't always available, so skip tests if it isn't. +from nose.plugins.skip import SkipTest + from webob import Response from xblock.django.request import django_to_webob_request, webob_to_django_response +# pylint: enable=wrong-import-position class TestDjangoWebobRequest(TestCase): @@ -38,10 +44,10 @@ def test_post_already_read(self): dj_req = self.req_factory.post('dummy_url', data={'foo': 'bar'}) # Read from POST before constructing the webob request - self.assertEquals(dj_req.POST.getlist('foo'), ['bar']) # pylint: disable=no-member + self.assertEqual(dj_req.POST.getlist('foo'), ['bar']) # pylint: disable=no-member webob_req = django_to_webob_request(dj_req) - self.assertEquals(webob_req.POST.getall('foo'), ['bar']) + self.assertEqual(webob_req.POST.getall('foo'), ['bar']) class TestDjangoWebobResponse(TestCase): @@ -61,31 +67,31 @@ def _as_django(self, *args, **kwargs): return webob_to_django_response(Response(*args, **kwargs)) def test_status_code(self): - self.assertEquals(self._as_django(status=200).status_code, 200) - self.assertEquals(self._as_django(status=404).status_code, 404) - self.assertEquals(self._as_django(status=500).status_code, 500) + self.assertEqual(self._as_django(status=200).status_code, 200) + self.assertEqual(self._as_django(status=404).status_code, 404) + self.assertEqual(self._as_django(status=500).status_code, 500) def test_content(self): - self.assertEquals(self._as_django(body=u"foo").content, "foo") - self.assertEquals(self._as_django(app_iter=(c for c in u"foo")).content, "foo") - self.assertEquals(self._as_django(body="foo", charset="utf-8").content, "foo") + self.assertEqual(self._as_django(body="foo").content, b"foo") + self.assertEqual(self._as_django(app_iter=(c for c in "foo")).content, b"foo") + self.assertEqual(self._as_django(body=b"foo", charset="utf-8").content, b"foo") - encoded_snowman = u"\N{SNOWMAN}".encode('utf-8') - self.assertEquals(self._as_django(body=encoded_snowman, charset="utf-8").content, encoded_snowman) + encoded_snowman = "\N{SNOWMAN}".encode('utf-8') + self.assertEqual(self._as_django(body=encoded_snowman, charset="utf-8").content, encoded_snowman) def test_headers(self): self.assertIn('X-Foo', self._as_django(headerlist=[('X-Foo', 'bar')])) - self.assertEquals(self._as_django(headerlist=[('X-Foo', 'bar')])['X-Foo'], 'bar') + self.assertEqual(self._as_django(headerlist=[('X-Foo', 'bar')])['X-Foo'], 'bar') def test_content_types(self): # JSON content type (no charset should be returned) - self.assertEquals( + self.assertEqual( self._as_django(content_type='application/json')['Content-Type'], 'application/json' ) # HTML content type (UTF-8 charset should be returned) - self.assertEquals( + self.assertEqual( self._as_django(content_type='text/html')['Content-Type'], 'text/html; charset=UTF-8' ) diff --git a/xblock/test/settings.py b/xblock/test/settings.py index 2fe8b649e..68c04edd5 100644 --- a/xblock/test/settings.py +++ b/xblock/test/settings.py @@ -1,4 +1,7 @@ """Django settings for toy runtime project.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + import os DEBUG = True diff --git a/xblock/test/test_asides.py b/xblock/test/test_asides.py index be475aedd..24e02bab1 100644 --- a/xblock/test/test_asides.py +++ b/xblock/test/test_asides.py @@ -1,22 +1,28 @@ """ Test XBlock Aside """ + +from __future__ import absolute_import, division, print_function, unicode_literals + from unittest import TestCase + +import six + from web_fragments.fragment import Fragment + from xblock.core import XBlockAside, XBlock from xblock.fields import ScopeIds, Scope, String from xblock.runtime import DictKeyValueStore, KvsFieldData from xblock.test.test_runtime import TestXBlock from xblock.test.tools import TestRuntime from xblock.test.test_parsing import Leaf, XmlTestMixin -from timeit import itertools class TestAside(XBlockAside): """ Test xblock aside class """ - FRAG_CONTENT = u"

Aside rendered

" + FRAG_CONTENT = "

Aside rendered

" content = String(default="default_value", scope=Scope.content) data2 = String(default="default_value", scope=Scope.user_state) @@ -31,7 +37,7 @@ class TestInheritedAside(TestAside): """ XBlock Aside that inherits an aside view function from its parent. """ - FRAG_CONTENT = u"

Inherited aside rendered

" + FRAG_CONTENT = "

Inherited aside rendered

" class AsideRuntimeSetup(TestCase): @@ -61,10 +67,10 @@ def test_render_aside(self): Test that rendering the xblock renders its aside """ - frag = self.runtime.render(self.tester, 'student_view', [u"ignore"]) + frag = self.runtime.render(self.tester, 'student_view', ["ignore"]) self.assertIn(TestAside.FRAG_CONTENT, frag.body_html()) - frag = self.runtime.render(self.tester, 'author_view', [u"ignore"]) + frag = self.runtime.render(self.tester, 'author_view', ["ignore"]) self.assertNotIn(TestAside.FRAG_CONTENT, frag.body_html()) @XBlockAside.register_temp_plugin(TestAside) @@ -74,11 +80,11 @@ def test_inherited_aside_view(self): Test that rendering the xblock renders its aside (when the aside view is inherited). """ - frag = self.runtime.render(self.tester, 'student_view', [u"ignore"]) + frag = self.runtime.render(self.tester, 'student_view', ["ignore"]) self.assertIn(TestAside.FRAG_CONTENT, frag.body_html()) self.assertIn(TestInheritedAside.FRAG_CONTENT, frag.body_html()) - frag = self.runtime.render(self.tester, 'author_view', [u"ignore"]) + frag = self.runtime.render(self.tester, 'author_view', ["ignore"]) self.assertNotIn(TestAside.FRAG_CONTENT, frag.body_html()) self.assertNotIn(TestInheritedAside.FRAG_CONTENT, frag.body_html()) @@ -130,7 +136,7 @@ def _assert_xthing_equal(self, first, second): """ self.assertEqual(first.scope_ids.block_type, second.scope_ids.block_type) self.assertEqual(first.fields, second.fields) - for field in first.fields.itervalues(): + for field in six.itervalues(first.fields): self.assertEqual(field.read_from(first), field.read_from(second), field) def _test_roundrip_of(self, block): @@ -139,7 +145,7 @@ def _test_roundrip_of(self, block): """ restored = self.parse_xml_to_block(self.export_xml_for_block(block)) self._assert_xthing_equal(block, restored) - for first, second in itertools.izip(self.runtime.get_asides(block), self.runtime.get_asides(restored)): + for first, second in six.moves.zip(self.runtime.get_asides(block), self.runtime.get_asides(restored)): self._assert_xthing_equal(first, second) @XBlockAside.register_temp_plugin(TestAside, 'test_aside') diff --git a/xblock/test/test_core.py b/xblock/test/test_core.py index aae156564..22a289527 100644 --- a/xblock/test/test_core.py +++ b/xblock/test/test_core.py @@ -4,15 +4,18 @@ metaclassing, field access, caching, serialization, and bulk saves. """ +from __future__ import absolute_import, division, print_function, unicode_literals + # Allow accessing protected members for testing purposes -# pylint: disable=W0212 -from mock import patch, MagicMock, Mock +# pylint: disable=protected-access from datetime import datetime import json import re import unittest import ddt +from mock import patch, MagicMock, Mock +import six from webob import Response from xblock.core import XBlock @@ -29,9 +32,15 @@ from xblock.runtime import Runtime from xblock.test.tools import ( - assert_equals, assert_raises, assert_raises_regexp, - assert_not_equals, assert_false, - WarningTestMixin, TestRuntime, + assert_equals, + assert_in, + assert_is_instance, + assert_raises, + assert_raises_regexp, + assert_not_equals, + assert_false, + WarningTestMixin, + TestRuntime, ) @@ -588,7 +597,7 @@ def test_xblock_save_one(): def fake_set_many(block, update_dict): # pylint: disable=unused-argument """Mock update method that throws a KeyValueMultiSaveError indicating that only one field was correctly saved.""" - raise KeyValueMultiSaveError([update_dict.keys()[0]]) + raise KeyValueMultiSaveError([next(iter(update_dict))]) field_tester = setup_save_failure(fake_set_many) @@ -822,9 +831,9 @@ class HasParent(XBlock): def test_json_handler_basic(): test_self = Mock() test_data = {"foo": "bar", "baz": "quux"} - test_data_json = json.dumps(test_data) + test_data_json = ['{"foo": "bar", "baz": "quux"}', '{"baz": "quux", "foo": "bar"}'] test_suffix = "suff" - test_request = Mock(method="POST", body=test_data_json) + test_request = Mock(method="POST", body=test_data_json[0]) @XBlock.json_handler def test_func(self, request, suffix): @@ -835,7 +844,7 @@ def test_func(self, request, suffix): response = test_func(test_self, test_request, test_suffix) assert_equals(response.status_code, 200) - assert_equals(response.body, test_data_json) + assert_in(response.body.decode('utf-8'), test_data_json) assert_equals(response.content_type, "application/json") @@ -849,7 +858,7 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument response = test_func(Mock(), test_request, "dummy_suffix") # pylint: disable=no-member assert_equals(response.status_code, 400) - assert_equals(json.loads(response.body), {"error": "Invalid JSON"}) + assert_equals(json.loads(response.body.decode('utf-8')), {"error": "Invalid JSON"}) assert_equals(response.content_type, "application/json") @@ -863,7 +872,7 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument response = test_func(Mock(), test_request, "dummy_suffix") # pylint: disable=no-member assert_equals(response.status_code, 405) - assert_equals(json.loads(response.body), {"error": "Method must be POST"}) + assert_equals(json.loads(response.body.decode('utf-8')), {"error": "Method must be POST"}) assert_equals(list(response.allow), ["POST"]) @@ -877,7 +886,7 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument response = test_func(Mock(), test_request, "dummy_suffix") # pylint: disable=no-member assert_equals(response.status_code, 400) - assert_equals(json.loads(response.body), {"error": "Invalid JSON"}) + assert_equals(json.loads(response.body.decode('utf-8')), {"error": "Invalid JSON"}) assert_equals(response.content_type, "application/json") @@ -892,7 +901,7 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument response = test_func(Mock(), test_request, "dummy_suffix") # pylint: disable=assignment-from-no-return assert_equals(response.status_code, test_status_code) - assert_equals(json.loads(response.body), {"error": test_message}) + assert_equals(json.loads(response.body.decode('utf-8')), {"error": test_message}) assert_equals(response.content_type, "application/json") @@ -904,7 +913,7 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument return Response(body="not JSON", status=418, content_type="text/plain") response = test_func(Mock(), test_request, "dummy_suffix") - assert_equals(response.body, "not JSON") + assert_equals(response.ubody, "not JSON") assert_equals(response.status_code, 418) assert_equals(response.content_type, "text/plain") @@ -917,8 +926,8 @@ def test_func(self, request, suffix): # pylint: disable=unused-argument return Response(request=request) response = test_func(Mock(), test_request, "dummy_suffix") - for request_part in response.request: - assert_equals(type(request_part), unicode) + for request_part in response.request: # pylint: disable=not-an-iterable + assert_is_instance(request_part, six.text_type) @ddt.ddt @@ -944,12 +953,26 @@ def stub_resource_stream(self, module, name): "public/js/vendor/jNotify.jQuery.min.js", "public/something.foo", # Unknown file extension is fine "public/a/long/PATH/no-problem=here$123.ext", - "public/ℓιвяαяу.js", + "public/\N{SNOWMAN}.js", ) def test_open_good_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) with patch('pkg_resources.resource_stream', self.stub_resource_stream): assert loadable.open_local_resource(uri) == "!" + uri + "!" + assert loadable.open_local_resource(uri.encode('utf-8')) == "!" + uri + "!" + + @ddt.data( + "public/hey.js".encode('utf-8'), + "public/sub/hey.js".encode('utf-8'), + "public/js/vendor/jNotify.jQuery.min.js".encode('utf-8'), + "public/something.foo".encode('utf-8'), # Unknown file extension is fine + "public/a/long/PATH/no-problem=here$123.ext".encode('utf-8'), + "public/\N{SNOWMAN}.js".encode('utf-8'), + ) + def test_open_good_local_resource_binary(self, uri): + loadable = self.LoadableXBlock(None, scope_ids=None) + with patch('pkg_resources.resource_stream', self.stub_resource_stream): + assert loadable.open_local_resource(uri) == "!" + uri.decode('utf-8') + "!" @ddt.data( "public/../secret.js", @@ -958,12 +981,28 @@ def test_open_good_local_resource(self, uri): "../public/no-no.bad", "image.png", ".git/secret.js", - "static/ℓιвяαяу.js", + "static/\N{SNOWMAN}.js", ) def test_open_bad_local_resource(self, uri): loadable = self.LoadableXBlock(None, scope_ids=None) with patch('pkg_resources.resource_stream', self.stub_resource_stream): - msg = ".*: %s" % re.escape(repr(uri)) + msg_pattern = ".*: %s" % re.escape(repr(uri)) + with assert_raises_regexp(DisallowedFileError, msg_pattern): + loadable.open_local_resource(uri) + + @ddt.data( + "public/../secret.js".encode('utf-8'), + "public/.git/secret.js".encode('utf-8'), + "static/secret.js".encode('utf-8'), + "../public/no-no.bad".encode('utf-8'), + "image.png".encode('utf-8'), + ".git/secret.js".encode('utf-8'), + "static/\N{SNOWMAN}.js".encode('utf-8'), + ) + def test_open_bad_local_resource_binary(self, uri): + loadable = self.LoadableXBlock(None, scope_ids=None) + with patch('pkg_resources.resource_stream', self.stub_resource_stream): + msg = ".*: %s" % re.escape(repr(uri.decode('utf-8'))) with assert_raises_regexp(DisallowedFileError, msg): loadable.open_local_resource(uri) @@ -973,14 +1012,14 @@ def test_open_bad_local_resource(self, uri): "public/js/vendor/jNotify.jQuery.min.js", "public/something.foo", # Unknown file extension is fine "public/a/long/PATH/no-problem=here$123.ext", - "public/ℓιвяαяу.js", + "public/\N{SNOWMAN}.js", "public/foo.js", "public/.git/secret.js", "static/secret.js", "../public/no-no.bad", "image.png", ".git/secret.js", - "static/ℓιвяαяу.js", + "static/\N{SNOWMAN}.js", ) def test_open_local_resource_with_no_resources_dir(self, uri): unloadable = self.UnloadableXBlock(None, scope_ids=None) diff --git a/xblock/test/test_field_data.py b/xblock/test/test_field_data.py index 5b73f7d23..b555f8d2e 100644 --- a/xblock/test/test_field_data.py +++ b/xblock/test/test_field_data.py @@ -2,13 +2,14 @@ Tests of the utility FieldData's defined by xblock """ +from __future__ import absolute_import, division, print_function, unicode_literals + from mock import Mock from xblock.core import XBlock from xblock.exceptions import InvalidScopeError from xblock.fields import Scope, String from xblock.field_data import SplitFieldData, ReadOnlyFieldData - from xblock.test.tools import assert_false, assert_raises, assert_equals, TestRuntime diff --git a/xblock/test/test_fields.py b/xblock/test/test_fields.py index c10425d06..beea14176 100644 --- a/xblock/test/test_fields.py +++ b/xblock/test/test_fields.py @@ -4,6 +4,8 @@ # pylint: disable=abstract-class-instantiated, protected-access +from __future__ import absolute_import, division, print_function, unicode_literals + from contextlib import contextmanager import datetime as dt import itertools @@ -16,6 +18,7 @@ from lxml import etree from mock import Mock import pytz +import six from xblock.core import XBlock, Scope from xblock.field_data import DictFieldData @@ -61,7 +64,7 @@ def assertDeprecationWarning(self, count=1): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always", DeprecationWarning) yield - self.assertEquals(count, sum( + self.assertEqual(count, sum( 1 for warning in caught if issubclass(warning.category, DeprecationWarning) )) @@ -232,13 +235,13 @@ def test_error(self): self.assertJSONOrSetTypeError({}) def test_control_characters_filtered(self): - self.assertJSONOrSetGetEquals(u'', u'\v') self.assertJSONOrSetGetEquals('', '\v') + self.assertJSONOrSetGetEquals('', b'\v') with self.assertRaises(AssertionError): - self.assertJSONOrSetGetEquals('\v', u'') + self.assertJSONOrSetGetEquals('\v', b'') with self.assertRaises(AssertionError): - self.assertJSONOrSetGetEquals(u'\v', '') - self.assertJSONOrSetGetEquals(u'\n\r\t', u'\n\v\r\b\t') + self.assertJSONOrSetGetEquals('\v', '') + self.assertJSONOrSetGetEquals('\n\r\t', '\n\v\r\b\t') @ddt.ddt @@ -249,11 +252,11 @@ class XMLStringTest(FieldTest): FIELD_TO_TEST = XMLString @ddt.data( - u'Hello', - u'Hello', - u'', - '', - '\xc8\x88', + 'Hello', + 'Hello', + '', + b'', + b'\xc8\x88', None ) def test_json_equals(self, input_text): @@ -262,14 +265,15 @@ def test_json_equals(self, input_text): @ddt.data( 'text', - '', - '', 'with text', 'trailing text', 'text', - '', + '', + b'', + b'', ) def test_bad_xml(self, input_text): # pylint: disable=no-member @@ -706,16 +710,16 @@ class SentinelTest(unittest.TestCase): """ def test_equality(self): base = Sentinel('base') - self.assertEquals(base, base) - self.assertEquals(base, Sentinel('base')) - self.assertNotEquals(base, Sentinel('foo')) - self.assertNotEquals(base, 'base') + self.assertEqual(base, base) + self.assertEqual(base, Sentinel('base')) + self.assertNotEqual(base, Sentinel('foo')) + self.assertNotEqual(base, 'base') def test_hashing(self): base = Sentinel('base') a_dict = {base: True} - self.assertEquals(a_dict[Sentinel('base')], True) - self.assertEquals(a_dict[base], True) + self.assertEqual(a_dict[Sentinel('base')], True) + self.assertEqual(a_dict[base], True) self.assertNotIn(Sentinel('foo'), a_dict) self.assertNotIn('base', a_dict) @@ -730,14 +734,14 @@ def assert_to_string(self, _type, value, string): Helper method: checks if _type's to_string given instance of _type returns expected string """ result = _type().to_string(value) - self.assertEquals(result, string) + self.assertEqual(result, string) def assert_from_string(self, _type, string, value): """ Helper method: checks if _type's from_string given string representation of type returns expected value """ result = _type().from_string(string) - self.assertEquals(result, value) + self.assertEqual(result, value) # Serialisation test data that is tested both ways, i.e. whether serialisation of the value # yields the string and deserialisation of the string yields the value. @@ -802,7 +806,7 @@ def test_both_directions(self, _type, value, string): (Float, -10.0, r"-10|-10\.0*")) def test_to_string_regexp_matches(self, _type, value, regexp): result = _type().to_string(value) - self.assertRegexpMatches(result, regexp) + six.assertRegex(self, result, regexp) # Test data for non-canonical serialisations of values that we should be able to correctly # deserialise. These values are not serialised to the representation given here for various @@ -898,5 +902,5 @@ def test_float_from_NaN_is_nan(self): # pylint: disable=invalid-name ['{"foo":"bar"}', '[1, 2, 3]', 'baz', '1.abc', 'defg'])) def test_from_string_errors(self, _type, string): """ Cases that raises various exceptions.""" - with self.assertRaises(StandardError): + with self.assertRaises(Exception): _type().from_string(string) diff --git a/xblock/test/test_fields_api.py b/xblock/test/test_fields_api.py index 4be8b2e25..91cd5a9dc 100644 --- a/xblock/test/test_fields_api.py +++ b/xblock/test/test_fields_api.py @@ -24,8 +24,12 @@ particular combination of initial conditions that we want to test) """ +from __future__ import absolute_import, division, print_function, unicode_literals + import copy + from mock import Mock, patch +import six from xblock.core import XBlock from xblock.fields import Integer, List, String, ScopeIds, UNIQUE_ID @@ -409,7 +413,7 @@ def __init__(self, storage, sequence): self._sequence = sequence def default(self, block, name): - return next(self._sequence) + return next(iter(self._sequence)) class StaticDefaultTestCases(UniversalTestCases, DefaultValueProperties): @@ -466,7 +470,7 @@ class TestImmutableWithComputedDefault(ImmutableTestCases, ComputedDefaultTestCa @property def default_iterator(self): - return iter(xrange(1000)) + return six.moves.range(1000) class TestMutableWithStaticDefault(MutableTestCases, StaticDefaultTestCases, DefaultValueMutationProperties): @@ -483,7 +487,7 @@ class TestMutableWithComputedDefault(MutableTestCases, ComputedDefaultTestCases, @property def default_iterator(self): - return ([None] * i for i in xrange(1000)) + return ([None] * i for i in six.moves.range(1000)) class TestImmutableWithInitialValue(ImmutableTestCases, InitialValueProperties): @@ -558,11 +562,15 @@ def setUp(self): test_name += "And" + noop_prefix.__name__ test_classes = (noop_prefix, ) + test_classes - vars()[test_name] = type(test_name, test_classes, {'__test__': True}) + vars()[test_name] = type( + str(test_name), # First argument must be native string type + test_classes, + {'__test__': True}, + ) # If we don't delete the loop variables, then they leak into the global namespace # and cause the last class looped through to be tested twice. Surprise! -# pylint: disable=W0631 +# pylint: disable=undefined-loop-variable del operation_backend del noop_prefix del base_test_case diff --git a/xblock/test/test_fragment.py b/xblock/test/test_fragment.py index 592f68a90..f435c3098 100644 --- a/xblock/test/test_fragment.py +++ b/xblock/test/test_fragment.py @@ -4,6 +4,8 @@ Note: this class has been deprecated in favor of web_fragments.fragment.Fragment """ +from __future__ import absolute_import, division, print_function, unicode_literals + from unittest import TestCase from xblock.fragment import Fragment @@ -17,7 +19,7 @@ def test_fragment(self): """ Test the delegated Fragment class. """ - TEST_HTML = u'

Hello, world!

' + TEST_HTML = u'

Hello, world!

' # pylint: disable=invalid-name fragment = Fragment() fragment.add_content(TEST_HTML) self.assertEqual(fragment.body_html(), TEST_HTML) diff --git a/xblock/test/test_internal.py b/xblock/test/test_internal.py index ce7b5985f..0a18976cc 100644 --- a/xblock/test/test_internal.py +++ b/xblock/test/test_internal.py @@ -1,7 +1,11 @@ """Tests of the xblock.internal module.""" +from __future__ import absolute_import, division, print_function, unicode_literals + from unittest import TestCase +import six + from xblock.internal import class_lazy, NamedAttributesMetaclass, Nameable @@ -41,9 +45,8 @@ def __set__(self, instance, value): pass -class NamingTester(object): +class NamingTester(six.with_metaclass(NamedAttributesMetaclass, object)): """Class with several descriptors that should get names.""" - __metaclass__ = NamedAttributesMetaclass test_descriptor = TestDescriptor() test_getset_descriptor = TestGetSetDescriptor() @@ -68,20 +71,20 @@ class TestNamedDescriptorsMetaclass(TestCase): "Tests of the NamedDescriptorsMetaclass." def test_named_descriptor(self): - self.assertEquals('test_descriptor', NamingTester.test_descriptor.__name__) + self.assertEqual('test_descriptor', NamingTester.test_descriptor.__name__) def test_named_getset_descriptor(self): - self.assertEquals('test_getset_descriptor', NamingTester.test_getset_descriptor.__name__) + self.assertEqual('test_getset_descriptor', NamingTester.test_getset_descriptor.__name__) def test_inherited_naming(self): - self.assertEquals('test_descriptor', InheritedNamingTester.test_descriptor.__name__) - self.assertEquals('inherited', InheritedNamingTester.inherited.__name__) + self.assertEqual('test_descriptor', InheritedNamingTester.test_descriptor.__name__) + self.assertEqual('inherited', InheritedNamingTester.inherited.__name__) def test_unnamed_attribute(self): self.assertFalse(hasattr(NamingTester.test_nonnameable, '__name__')) def test_method(self): - self.assertEquals('meth', NamingTester.meth.__name__) + self.assertEqual('meth', NamingTester.meth.__name__) def test_prop(self): self.assertFalse(hasattr(NamingTester.prop, '__name__')) diff --git a/xblock/test/test_json_conversion.py b/xblock/test/test_json_conversion.py index 826a56258..2f5599e50 100644 --- a/xblock/test/test_json_conversion.py +++ b/xblock/test/test_json_conversion.py @@ -2,8 +2,12 @@ Tests asserting that ModelTypes convert to and from json when working with ModelDatas """ + # Allow inspection of private class members -# pylint: disable=W0212 +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function, unicode_literals + from mock import Mock from xblock.core import XBlock diff --git a/xblock/test/test_mixins.py b/xblock/test/test_mixins.py index 63be332fd..1a6abd0e9 100644 --- a/xblock/test/test_mixins.py +++ b/xblock/test/test_mixins.py @@ -1,12 +1,17 @@ """ Tests of the XBlock-family functionality mixins """ -import ddt as ddt + +from __future__ import absolute_import, division, print_function, unicode_literals + from datetime import datetime -import pytz +from unittest import TestCase + +import ddt as ddt from lxml import etree import mock -from unittest import TestCase +import pytz +import six from xblock.core import XBlock, XBlockAside from xblock.fields import List, Scope, Integer, String, ScopeIds, UNIQUE_ID, DateTime @@ -187,7 +192,7 @@ def an_unsupported_view(self): ("multi_featured_view", "functionality2", True), ("multi_featured_view", "bogus_functionality", False), ): - self.assertEquals( + self.assertEqual( test_xblock.has_support(getattr(test_xblock, view_name), functionality), expected_result ) @@ -212,7 +217,7 @@ def has_support(self, view, functionality): ("functionality_supported_view", "a_functionality", True), ("functionality_supported_view", "bogus_functionality", False), ): - self.assertEquals( + self.assertEqual( test_xblock.has_support(getattr(test_xblock, view_name, None), functionality), expected_result ) @@ -269,13 +274,13 @@ def _make_block(self, block_type=None): def _assert_node_attributes(self, node, expected_attributes, entry_point=None): """ Checks XML node attributes to match expected_attributes""" - node_attributes = node.keys() + node_attributes = list(node.keys()) node_attributes.remove('xblock-family') self.assertEqual(node.get('xblock-family'), entry_point if entry_point else self.TestXBlock.entry_point) self.assertEqual(set(node_attributes), set(expected_attributes.keys())) - for key, value in expected_attributes.iteritems(): + for key, value in six.iteritems(expected_attributes): if value != UNIQUE_ID: self.assertEqual(node.get(key), value) else: @@ -299,7 +304,7 @@ def test_no_fields_set_add_xml_to_node(self): node = etree.Element(self.test_xblock_tag) # Precondition check: no fields are set. - for field_name in self.test_xblock.fields.keys(): + for field_name in six.iterkeys(self.test_xblock.fields): self.assertFalse(self.test_xblock.fields[field_name].is_set_on(self.test_xblock)) self.test_xblock.add_xml_to_node(node) diff --git a/xblock/test/test_parsing.py b/xblock/test/test_parsing.py index 0de56064a..146f5807f 100644 --- a/xblock/test/test_parsing.py +++ b/xblock/test/test_parsing.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- -"""Test XML parsing in XBlocks.""" +""" +Test XML parsing in XBlocks. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals import re -import StringIO import textwrap import unittest + import ddt -import mock from lxml import etree +import mock +import six from xblock.core import XBlock, XML_NAMESPACES from xblock.fields import Scope, String, Integer, Dict, List @@ -19,7 +24,7 @@ def get_namespace_attrs(): """ Returns string suitable to be used as an xmlns parameters in XBlock XML representation """ - return " ".join('xmlns:{}="{}"'.format(k, v) for k, v in XML_NAMESPACES.items()) + return " ".join('xmlns:{}="{}"'.format(k, v) for k, v in six.iteritems(XML_NAMESPACES)) class Leaf(XBlock): @@ -75,8 +80,8 @@ def parse_xml(cls, node, runtime, keys, id_generator): if child.tag is not etree.Comment: block.runtime.add_node_as_child(block, child, id_generator) # Now build self.inner_xml from the XML of node's children - # We can't just call tostring() on each child because it adds xmlns: attributes - xml_str = etree.tostring(node) + # We can't just call tounicode() on each child because it adds xmlns: attributes + xml_str = etree.tounicode(node) block.inner_xml = xml_str[xml_str.index('>') + 1:xml_str.rindex('<')] return block @@ -104,12 +109,12 @@ def parse_xml_to_block(self, xml): def export_xml_for_block(self, block): """A helper to return the XML string for a block.""" - output = StringIO.StringIO() + output = six.BytesIO() self.runtime.export_to_xml(block, output) return output.getvalue() -class XmlTest(XmlTestMixin): +class XmlTest(XmlTestMixin, unittest.TestCase): """Helpful things for XML tests.""" def setUp(self): super(XmlTest, self).setUp() @@ -187,11 +192,11 @@ def test_comments_in_field_preserved(self): self.assertEqual(len(block.children), 2) xml = self.export_xml_for_block(block) - self.assertIn('ACE', xml) + self.assertIn(b'ACE', xml) block_imported = self.parse_xml_to_block(xml) self.assertEqual( block_imported.inner_xml, - "ACE" + 'ACE', ) @XBlock.register_temp_plugin(Leaf) @@ -207,9 +212,9 @@ def test_customized_parsing(self): @XBlock.register_temp_plugin(Leaf) def test_parse_unicode(self): - block = self.parse_xml_to_block(u"") + block = self.parse_xml_to_block("") self.assertIsInstance(block, Leaf) - self.assertEqual(block.data1, u'\u2603') + self.assertEqual(block.data1, '\u2603') @ddt.ddt @@ -220,15 +225,24 @@ class ExportTest(XmlTest, unittest.TestCase): def test_dead_simple_export(self): block = self.parse_xml_to_block("") xml = self.export_xml_for_block(block) - self.assertRegexpMatches( - xml.strip(), - r"\<\?xml version='1.0' encoding='UTF8'\?\>\n\" + self.assertIn( + b"\n") + xml = self.export_xml_for_block(block) + self.assertIn( + b"\n @@ -240,12 +254,14 @@ def test_export_then_import(self): Some text content. - """)) + """ + block = self.parse_xml_to_block(textwrap.dedent(block_body).encode('utf-8')) xml = self.export_xml_for_block(block) block_imported = self.parse_xml_to_block(xml) # Crude checks that the XML is correct. The exact form of the XML # isn't important. + xml = xml.decode('utf-8') self.assertEqual(xml.count("container"), 4) self.assertEqual(xml.count("child1"), 2) self.assertEqual(xml.count("child2"), 1) @@ -263,10 +279,10 @@ def test_dict_and_list_as_attribute(self): - """)) + """).encode('utf-8')) - self.assertEquals(block.dictionary, {"foo": "bar"}) - self.assertEquals(block.sequence, ["one", "two", "three"]) + self.assertEqual(block.dictionary, {"foo": "bar"}) + self.assertEqual(block.sequence, ["one", "two", "three"]) @XBlock.register_temp_plugin(LeafWithOption) def test_export_then_import_with_options(self): @@ -284,7 +300,7 @@ def test_export_then_import_with_options(self): - some string - """)) + """).encode('utf-8')) xml = self.export_xml_for_block(block) block_imported = self.parse_xml_to_block(xml) @@ -292,13 +308,13 @@ def test_export_then_import_with_options(self): self.assertEqual(block_imported.data3, {"child": 1, "with custom option": True}) self.assertEqual(block_imported.data4, [1.23, True, "some string"]) - self.assertEqual(xml.count("child1"), 1) + self.assertEqual(xml.count(b"child1"), 1) self.assertTrue(blocks_are_equivalent(block, block_imported)) @XBlock.register_temp_plugin(LeafWithOption) def test_dict_and_list_export_format(self): xml = textwrap.dedent("""\ - + [ 1.23, @@ -311,10 +327,16 @@ def test_dict_and_list_export_format(self): } """) % get_namespace_attrs() - block = self.parse_xml_to_block(xml) + block = self.parse_xml_to_block(xml.encode('utf-8')) exported_xml = self.export_xml_for_block(block) - - self.assertEqual(xml, exported_xml) + self.assertIn( + '[\n 1.23,\n true,\n "some string"\n]\n', + exported_xml.decode('utf-8') + ) + self.assertIn( + '{\n "child": 1,\n "with custom option": true\n}\n', + exported_xml.decode('utf-8') + ) @XBlock.register_temp_plugin(Leaf) @ddt.data( @@ -324,7 +346,7 @@ def test_dict_and_list_export_format(self): "eccentricity" ) def test_unknown_field_as_attribute_raises_warning(self, parameter_name): - with mock.patch('logging.warn') as patched_warn: + with mock.patch('logging.warning') as patched_warn: block = self.parse_xml_to_block("".format(parameter_name)) patched_warn.assert_called_once_with("XBlock %s does not contain field %s", type(block), parameter_name) @@ -341,7 +363,7 @@ def test_unknown_field_as_node_raises_warning(self, parameter_name): Some completely irrelevant data """) % (get_namespace_attrs(), parameter_name, parameter_name) - with mock.patch('logging.warn') as patched_warn: + with mock.patch('logging.warning') as patched_warn: block = self.parse_xml_to_block(xml) patched_warn.assert_called_once_with("XBlock %s does not contain field %s", type(block), parameter_name) @@ -360,27 +382,35 @@ def create_block(self, block_type): @XBlock.register_temp_plugin(LeafWithDictAndList) def test_string_roundtrip(self): - """ Test correctly serializes-deserializes List and Dicts with plain string contents """ + """ + Test correctly serializes-deserializes List and Dicts with byte string + contents in Python 2. + + In Python 3, this behavior is unsupported. dict and list elements + cannot be bytes objects. + """ block = self.create_block("leafwithdictandlist") - expected_seq = ['1', '2'] - expected_dict = {'1': '1', 'ping': 'ack'} + expected_seq = [b'1', b'2'] + expected_dict = {b'1': b'1', b'ping': b'ack'} block.sequence = expected_seq block.dictionary = expected_dict - xml = self.export_xml_for_block(block) + if six.PY3: + self.assertRaises(TypeError, self.export_xml_for_block, block) + else: + xml = self.export_xml_for_block(block) + parsed = self.parse_xml_to_block(xml) - parsed = self.parse_xml_to_block(xml) - - self.assertEqual(parsed.sequence, expected_seq) - self.assertEqual(parsed.dictionary, expected_dict) + self.assertEqual(parsed.sequence, expected_seq) + self.assertEqual(parsed.dictionary, expected_dict) @XBlock.register_temp_plugin(LeafWithDictAndList) def test_unicode_roundtrip(self): """ Test correctly serializes-deserializes List and Dicts with unicode contents """ block = self.create_block("leafwithdictandlist") - expected_seq = [u'1', u'2'] - expected_dict = {u'1': u'1', u'ping': u'ack'} + expected_seq = ['1', '2'] + expected_dict = {'1': '1', 'ping': 'ack'} block.sequence = expected_seq block.dictionary = expected_dict xml = self.export_xml_for_block(block) @@ -404,7 +434,7 @@ def test_integers_roundtrip(self): self.assertEqual(parsed.sequence, expected_seq) self.assertNotEqual(parsed.dictionary, expected_dict) - self.assertEqual(parsed.dictionary, {str(key): value for key, value in expected_dict.items()}) + self.assertEqual(parsed.dictionary, {six.text_type(key): value for key, value in six.iteritems(expected_dict)}) @XBlock.register_temp_plugin(LeafWithDictAndList) def test_none_contents_roundtrip(self): diff --git a/xblock/test/test_plugin.py b/xblock/test/test_plugin.py index ba764150d..c2abbdb58 100644 --- a/xblock/test/test_plugin.py +++ b/xblock/test/test_plugin.py @@ -2,10 +2,15 @@ Test xblock/core/plugin.py """ +from __future__ import absolute_import, division, print_function, unicode_literals + from mock import patch, Mock from xblock.test.tools import ( - assert_is, assert_raises_regexp, assert_equals) + assert_is, + assert_raises_regexp, + assert_equals +) from xblock.core import XBlock from xblock import plugin @@ -79,7 +84,7 @@ def _num_plugins_cached(): """ Returns the number of plugins that have been cached. """ - return len(plugin.PLUGIN_CACHE.keys()) + return len(plugin.PLUGIN_CACHE) @XBlock.register_temp_plugin(AmbiguousBlock1, "thumbs") diff --git a/xblock/test/test_runtime.py b/xblock/test/test_runtime.py index 7ddc1eac1..60acafdb8 100644 --- a/xblock/test/test_runtime.py +++ b/xblock/test/test_runtime.py @@ -1,16 +1,20 @@ # -*- coding: utf-8 -*- """Tests the features of xblock/runtime""" -# Allow tests to access private members of classes -# pylint: disable=W0212 + +from __future__ import absolute_import, division, print_function, unicode_literals + +# pylint: disable=protected-access from collections import namedtuple from datetime import datetime -from mock import Mock, patch from unittest import TestCase +from mock import Mock, patch +import six + from web_fragments.fragment import Fragment + from xblock.core import XBlock, XBlockMixin -from xblock.fields import BlockScope, Scope, String, ScopeIds, List, UserScope, Integer from xblock.exceptions import ( NoSuchDefinition, NoSuchHandlerError, @@ -19,6 +23,7 @@ NoSuchViewError, FieldDataDeprecationWarning, ) +from xblock.fields import BlockScope, Scope, String, ScopeIds, List, UserScope, Integer from xblock.runtime import ( DictKeyValueStore, IdReader, @@ -106,34 +111,13 @@ def fallback_view(self, view_name, context): if view_name == 'test_fallback_view': return Fragment(self.preferences) else: - return Fragment(u"{} default".format(view_name)) + return Fragment("{} default".format(view_name)) # Allow this tuple to be named as if it were a class TestUsage = namedtuple('TestUsage', 'id, def_id') # pylint: disable=C0103 -def check_field(collection, field): - """ - Test method. - - Asserts that the given `field` is present in `collection`. - Sets the field to a new value and asserts that the update properly occurs. - Deletes the new value, and asserts that the default value is properly restored. - """ - print "Getting %s from %r" % (field.name, collection) - assert_equals(field.default, getattr(collection, field.name)) - new_value = 'new ' + field.name - print "Setting %s to %s on %r" % (field.name, new_value, collection) - setattr(collection, field.name, new_value) - print "Checking %s on %r" % (field.name, collection) - assert_equals(new_value, getattr(collection, field.name)) - print "Deleting %s from %r" % (field.name, collection) - delattr(collection, field.name) - print "Back to defaults for %s in %r" % (field.name, collection) - assert_equals(field.default, getattr(collection, field.name)) - - def test_db_model_keys(): # Tests that updates to fields are properly recorded in the KeyValueStore, # and that the keys have been constructed correctly @@ -144,7 +128,7 @@ def test_db_model_keys(): assert_false(field_data.has(tester, 'not a field')) - for field in tester.fields.values(): + for field in six.itervalues(tester.fields): new_value = 'new ' + field.name assert_false(field_data.has(tester, field.name)) if isinstance(field, List): @@ -155,7 +139,7 @@ def test_db_model_keys(): tester.save() # Make sure everything saved correctly - for field in tester.fields.values(): + for field in six.itervalues(tester.fields): assert_true(field_data.has(tester, field.name)) def get_key_value(scope, user_id, block_scope_id, field_name): @@ -225,7 +209,7 @@ def test_querypath_parsing(): mrun = MockRuntimeForQuerying() block = Mock() mrun.querypath(block, "..//@hello") - print mrun.mock_query.mock_calls + print(mrun.mock_query.mock_calls) expected = Mock() expected.parent().descendants().attr("hello") assert mrun.mock_query.mock_calls == expected.mock_calls @@ -236,38 +220,38 @@ def test_runtime_handle(): key_store = DictKeyValueStore() field_data = KvsFieldData(key_store) - runtime = TestRuntime(services={'field-data': field_data}) - tester = TestXBlock(runtime, scope_ids=Mock(spec=ScopeIds)) + test_runtime = TestRuntime(services={'field-data': field_data}) + basic_tester = TestXBlock(test_runtime, scope_ids=Mock(spec=ScopeIds)) runtime = MockRuntimeForQuerying() # string we want to update using the handler update_string = "user state update" - assert_equals(runtime.handle(tester, 'existing_handler', update_string), + assert_equals(runtime.handle(basic_tester, 'existing_handler', update_string), 'I am the existing test handler') - assert_equals(tester.user_state, update_string) + assert_equals(basic_tester.user_state, update_string) # when the handler needs to use the fallback as given name can't be found new_update_string = "new update" - assert_equals(runtime.handle(tester, 'test_fallback_handler', new_update_string), + assert_equals(runtime.handle(basic_tester, 'test_fallback_handler', new_update_string), 'I have been handled') - assert_equals(tester.user_state, new_update_string) + assert_equals(basic_tester.user_state, new_update_string) # request to use a handler which doesn't have XBlock.handler decoration # should use the fallback new_update_string = "new update" - assert_equals(runtime.handle(tester, 'handler_without_correct_decoration', new_update_string), + assert_equals(runtime.handle(basic_tester, 'handler_without_correct_decoration', new_update_string), 'gone to fallback') - assert_equals(tester.user_state, new_update_string) + assert_equals(basic_tester.user_state, new_update_string) # handler can't be found & no fallback handler supplied, should throw an exception - tester = TestXBlockNoFallback(runtime, scope_ids=Mock(spec=ScopeIds)) + no_fallback_tester = TestXBlockNoFallback(runtime, scope_ids=Mock(spec=ScopeIds)) ultimate_string = "ultimate update" with assert_raises(NoSuchHandlerError): - runtime.handle(tester, 'test_nonexistant_fallback_handler', ultimate_string) + runtime.handle(no_fallback_tester, 'test_nonexistant_fallback_handler', ultimate_string) # request to use a handler which doesn't have XBlock.handler decoration # and no fallback should raise NoSuchHandlerError with assert_raises(NoSuchHandlerError): - runtime.handle(tester, 'handler_without_correct_decoration', 'handled') + runtime.handle(no_fallback_tester, 'handler_without_correct_decoration', 'handled') def test_runtime_render(): @@ -279,7 +263,7 @@ def test_runtime_render(): usage_id = runtime.id_generator.create_usage(def_id) tester = TestXBlock(runtime, scope_ids=ScopeIds('user', block_type, def_id, usage_id)) # string we want to update using the handler - update_string = u"user state update" + update_string = "user state update" # test against the student view frag = runtime.render(tester, 'student_view', [update_string]) @@ -287,22 +271,22 @@ def test_runtime_render(): assert_equals(tester.preferences, update_string) # test against the fallback view - update_string = u"new update" + update_string = "new update" frag = runtime.render(tester, 'test_fallback_view', [update_string]) assert_in(update_string, frag.body_html()) assert_equals(tester.preferences, update_string) # test block-first - update_string = u"penultimate update" + update_string = "penultimate update" frag = tester.render('student_view', [update_string]) assert_in(update_string, frag.body_html()) assert_equals(tester.preferences, update_string) # test against the no-fallback XBlock - update_string = u"ultimate update" - tester = TestXBlockNoFallback(Mock(), scope_ids=Mock(spec=ScopeIds)) + update_string = "ultimate update" + no_fallback_tester = TestXBlockNoFallback(Mock(), scope_ids=Mock(spec=ScopeIds)) with assert_raises(NoSuchViewError): - runtime.render(tester, 'test_nonexistent_view', [update_string]) + runtime.render(no_fallback_tester, 'test_nonexistent_view', [update_string]) class SerialDefaultKVS(DictKeyValueStore): @@ -408,7 +392,7 @@ class Dynamic(object): Object for testing that sets attrs based on __init__ kwargs """ def __init__(self, **kwargs): - for name, value in kwargs.items(): + for name, value in six.iteritems(kwargs): setattr(self, name, value) @@ -495,7 +479,7 @@ def test_unmixed_class(self): assert_is(FieldTester, self.mixologist.mix(FieldTester).unmixed_class) def test_mixin_fields(self): - assert_is(FirstMixin.fields['field'], FirstMixin.field) + assert_is(FirstMixin.fields['field'], FirstMixin.field) # pylint: disable=unsubscriptable-object def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) @@ -536,22 +520,22 @@ def student_view(self, _context): def assert_equals_unicode(str1, str2): """`str1` equals `str2`, and both are Unicode strings.""" assert_equals(str1, str2) - assert isinstance(str1, unicode) - assert isinstance(str2, unicode) + assert isinstance(str1, six.text_type) + assert isinstance(str2, six.text_type) i18n = self.runtime.service(self, "i18n") - assert_equals_unicode(u"Welcome!", i18n.ugettext("Welcome!")) + assert_equals_unicode("Welcome!", i18n.ugettext("Welcome!")) - assert_equals_unicode(u"Plural", i18n.ungettext("Singular", "Plural", 0)) - assert_equals_unicode(u"Singular", i18n.ungettext("Singular", "Plural", 1)) - assert_equals_unicode(u"Plural", i18n.ungettext("Singular", "Plural", 2)) + assert_equals_unicode("Plural", i18n.ungettext("Singular", "Plural", 0)) + assert_equals_unicode("Singular", i18n.ungettext("Singular", "Plural", 1)) + assert_equals_unicode("Plural", i18n.ungettext("Singular", "Plural", 2)) when = datetime(2013, 2, 14, 22, 30, 17) - assert_equals_unicode(u"2013-02-14", i18n.strftime(when, "%Y-%m-%d")) - assert_equals_unicode(u"Feb 14, 2013", i18n.strftime(when, "SHORT_DATE")) - assert_equals_unicode(u"Thursday, February 14, 2013", i18n.strftime(when, "LONG_DATE")) - assert_equals_unicode(u"Feb 14, 2013 at 22:30", i18n.strftime(when, "DATE_TIME")) - assert_equals_unicode(u"10:30:17 PM", i18n.strftime(when, "TIME")) + assert_equals_unicode("2013-02-14", i18n.strftime(when, "%Y-%m-%d")) + assert_equals_unicode("Feb 14, 2013", i18n.strftime(when, "SHORT_DATE")) + assert_equals_unicode("Thursday, February 14, 2013", i18n.strftime(when, "LONG_DATE")) + assert_equals_unicode("Feb 14, 2013 at 22:30", i18n.strftime(when, "DATE_TIME")) + assert_equals_unicode("10:30:17 PM", i18n.strftime(when, "TIME")) # secret_service is available. assert_equals(self.runtime.service(self, "secret_service"), 17) @@ -587,8 +571,8 @@ def test_ugettext_calls(): """ runtime = TestRuntime() block = XBlockWithServices(runtime, scope_ids=Mock(spec=[])) - assert_equals(block.ugettext('test'), u'test') - assert_true(isinstance(block.ugettext('test'), unicode)) + assert_equals(block.ugettext('test'), 'test') + assert_true(isinstance(block.ugettext('test'), six.text_type)) # NoSuchServiceError exception should raise if i18n is none/empty. runtime = TestRuntime(services={ @@ -690,7 +674,7 @@ def test_passed_field_data(self): with self.assertWarns(FieldDataDeprecationWarning): runtime = TestRuntime(Mock(spec=IdReader), field_data) with self.assertWarns(FieldDataDeprecationWarning): - self.assertEquals(runtime.field_data, field_data) + self.assertEqual(runtime.field_data, field_data) def test_set_field_data(self): field_data = Mock(spec=FieldData) @@ -698,4 +682,4 @@ def test_set_field_data(self): with self.assertWarns(FieldDataDeprecationWarning): runtime.field_data = field_data with self.assertWarns(FieldDataDeprecationWarning): - self.assertEquals(runtime.field_data, field_data) + self.assertEqual(runtime.field_data, field_data) diff --git a/xblock/test/test_test_tools.py b/xblock/test/test_test_tools.py index e09cf6128..4e7749f4d 100644 --- a/xblock/test/test_test_tools.py +++ b/xblock/test/test_test_tools.py @@ -1,21 +1,23 @@ -"""Tests of our testing tools. +""" +Tests of our testing tools. "The only code you have to test is the code you want to work." - """ -from abc import ABCMeta, abstractmethod +from __future__ import absolute_import, division, print_function, unicode_literals +from abc import ABCMeta, abstractmethod import unittest +import six + + from xblock.test.tools import unabc -class Abstract(object): +class Abstract(six.with_metaclass(ABCMeta, object)): """Our test subject: an abstract class with two abstract methods.""" - __metaclass__ = ABCMeta - def concrete(self, arg): """This is available as-is on all subclasses.""" return arg * arg + 3 @@ -47,7 +49,7 @@ class TestUnAbc(unittest.TestCase): """Test the @unabc decorator.""" def test_cant_abstract(self): - with self.assertRaisesRegexp(TypeError, r"Can't instantiate .*"): + with six.assertRaisesRegex(self, TypeError, r"Can't instantiate .*"): Abstract() def test_concrete(self): @@ -56,14 +58,14 @@ def test_concrete(self): def test_concrete_absmeth(self): conc = ForceConcrete() - with self.assertRaisesRegexp(NotImplementedError, r"absmeth1 isn't implemented"): + with six.assertRaisesRegex(self, NotImplementedError, r"absmeth1 isn't implemented"): conc.absmeth1() - with self.assertRaisesRegexp(NotImplementedError, r"absmeth2 isn't implemented"): + with six.assertRaisesRegex(self, NotImplementedError, r"absmeth2 isn't implemented"): conc.absmeth2() def test_concrete_absmeth_message(self): conc = ForceConcreteMessage() - with self.assertRaisesRegexp(NotImplementedError, r"Sorry, no absmeth1"): + with six.assertRaisesRegex(self, NotImplementedError, r"Sorry, no absmeth1"): conc.absmeth1() - with self.assertRaisesRegexp(NotImplementedError, r"Sorry, no absmeth2"): + with six.assertRaisesRegex(self, NotImplementedError, r"Sorry, no absmeth2"): conc.absmeth2() diff --git a/xblock/test/test_user_service.py b/xblock/test/test_user_service.py index e5ee415af..588170061 100644 --- a/xblock/test/test_user_service.py +++ b/xblock/test/test_user_service.py @@ -1,7 +1,13 @@ """ Tests for the UserService """ + +from __future__ import absolute_import, division, print_function, unicode_literals + import collections + +import six + from xblock.reference.user_service import XBlockUser, UserService from xblock.test.tools import assert_equals, assert_raises, assert_is_instance, assert_false @@ -29,7 +35,7 @@ def test_dummy_user_service_current_user(): assert_equals(current_user.full_name, "tester") # assert that emails is an Iterable but not a string assert_is_instance(current_user.emails, collections.Iterable) - assert_false(isinstance(current_user.emails, basestring)) + assert_false(isinstance(current_user.emails, (six.text_type, six.binary_type))) # assert that opt_attrs is a Mapping assert_is_instance(current_user.opt_attrs, collections.Mapping) diff --git a/xblock/test/test_validation.py b/xblock/test/test_validation.py index 387b528c2..a5069ace0 100644 --- a/xblock/test/test_validation.py +++ b/xblock/test/test_validation.py @@ -2,6 +2,8 @@ Test xblock/validation.py """ +from __future__ import absolute_import, division, print_function, unicode_literals + import unittest from xblock.test.tools import assert_raises @@ -18,20 +20,20 @@ def test_bad_parameters(self): Test that `TypeError`s are thrown for bad input parameters. """ with assert_raises(TypeError): - ValidationMessage("unknown type", u"Unknown type info") + ValidationMessage("unknown type", "Unknown type info") with assert_raises(TypeError): - ValidationMessage(ValidationMessage.WARNING, "Non-unicode message") + ValidationMessage(ValidationMessage.WARNING, b"Non-unicode message") def test_to_json(self): """ Test the `to_json` method. """ - expected = {"type": ValidationMessage.ERROR, "text": u"Error message"} - self.assertEqual(expected, ValidationMessage(ValidationMessage.ERROR, u"Error message").to_json()) + expected = {"type": ValidationMessage.ERROR, "text": "Error message"} + self.assertEqual(expected, ValidationMessage(ValidationMessage.ERROR, "Error message").to_json()) - expected = {"type": ValidationMessage.WARNING, "text": u"Warning message"} - self.assertEqual(expected, ValidationMessage(ValidationMessage.WARNING, u"Warning message").to_json()) + expected = {"type": ValidationMessage.WARNING, "text": "Warning message"} + self.assertEqual(expected, ValidationMessage(ValidationMessage.WARNING, "Warning message").to_json()) class ValidationTest(unittest.TestCase): @@ -48,7 +50,7 @@ def test_empty(self): self.assertTrue(validation.empty) self.assertTrue(validation) - validation.add(ValidationMessage(ValidationMessage.ERROR, u"Error message")) + validation.add(ValidationMessage(ValidationMessage.ERROR, "Error message")) self.assertFalse(validation.empty) self.assertFalse(validation) @@ -57,19 +59,19 @@ def test_add_messages(self): Test the behavior of adding the messages from another `Validation` object to this instance. """ validation_1 = Validation("id") - validation_1.add(ValidationMessage(ValidationMessage.ERROR, u"Error message")) + validation_1.add(ValidationMessage(ValidationMessage.ERROR, "Error message")) validation_2 = Validation("id") - validation_2.add(ValidationMessage(ValidationMessage.WARNING, u"Warning message")) + validation_2.add(ValidationMessage(ValidationMessage.WARNING, "Warning message")) validation_1.add_messages(validation_2) self.assertEqual(2, len(validation_1.messages)) self.assertEqual(ValidationMessage.ERROR, validation_1.messages[0].type) - self.assertEqual(u"Error message", validation_1.messages[0].text) + self.assertEqual("Error message", validation_1.messages[0].text) self.assertEqual(ValidationMessage.WARNING, validation_1.messages[1].type) - self.assertEqual(u"Warning message", validation_1.messages[1].text) + self.assertEqual("Warning message", validation_1.messages[1].text) def test_add_messages_error(self): """ @@ -92,14 +94,14 @@ def test_to_json(self): } self.assertEqual(expected, validation.to_json()) - validation.add(ValidationMessage(ValidationMessage.ERROR, u"Error message")) - validation.add(ValidationMessage(ValidationMessage.WARNING, u"Warning message")) + validation.add(ValidationMessage(ValidationMessage.ERROR, "Error message")) + validation.add(ValidationMessage(ValidationMessage.WARNING, "Warning message")) expected = { "xblock_id": "id", "messages": [ - {"type": ValidationMessage.ERROR, "text": u"Error message"}, - {"type": ValidationMessage.WARNING, "text": u"Warning message"} + {"type": ValidationMessage.ERROR, "text": "Error message"}, + {"type": ValidationMessage.WARNING, "text": "Warning message"} ], "empty": False } diff --git a/xblock/test/tools.py b/xblock/test/tools.py index fb6f083e5..96dde6174 100644 --- a/xblock/test/tools.py +++ b/xblock/test/tools.py @@ -1,15 +1,19 @@ """ Tools for testing XBlocks """ -import warnings + +from __future__ import absolute_import, division, print_function, unicode_literals from contextlib import contextmanager from functools import partial +import warnings + +import six # nose.tools has convenient assert methods, but it defines them in a clever way # that baffles pylint. Import them all here so we can keep the pylint clutter # out of the rest of our files. -from nose.tools import ( # pylint: disable=W0611,E0611 +from nose.tools import ( # pylint: disable=no-name-in-module,unused-import assert_true, assert_false, assert_equals, assert_not_equals, assert_is, assert_is_not, @@ -47,7 +51,7 @@ def blocks_are_equivalent(block1, block2): if len(block1.children) != len(block2.children): return False - for child_id1, child_id2 in zip(block1.children, block2.children): + for child_id1, child_id2 in six.moves.zip(block1.children, block2.children): if child_id1 == child_id2: # Equal ids mean they must be equal, check the next child. continue @@ -71,6 +75,7 @@ def dummy_method(self, *args, **kwargs): # pylint: disable=unused-argument return dummy_method for ab_name in cls.__abstractmethods__: + print(cls, ab_name) setattr(cls, ab_name, make_dummy_method(ab_name)) cls.__abstractmethods__ = () @@ -130,3 +135,15 @@ def __init__(self, *args, **kwargs): kwargs.setdefault('id_reader', memory_id_manager) kwargs.setdefault('id_generator', memory_id_manager) super(TestRuntime, self).__init__(*args, **kwargs) + + def handler_url(self, *args, **kwargs): + raise NotImplementedError + + def local_resource_url(self, *args, **kwargs): + raise NotImplementedError + + def publish(self, *args, **kwargs): + raise NotImplementedError + + def resource_url(self, *args, **kwargs): + raise NotImplementedError diff --git a/xblock/test/toy_runtime.py b/xblock/test/toy_runtime.py index 2ce9a023a..b67c4ff81 100644 --- a/xblock/test/toy_runtime.py +++ b/xblock/test/toy_runtime.py @@ -1,4 +1,7 @@ """A very basic toy runtime for XBlock tests.""" + +from __future__ import absolute_import, division, print_function, unicode_literals + import logging try: @@ -6,6 +9,8 @@ except ImportError: import json +import six + from xblock.fields import Scope from xblock.runtime import ( KvsFieldData, KeyValueStore, Runtime, MemoryIdManager @@ -83,7 +88,7 @@ def set_many(self, update_dict): `update_dict`: A dictionary of keys: values. This method sets the value of each key to the specified new value. """ - for key, value in update_dict.items(): + for key, value in six.iteritems(update_dict): # We just call `set` directly here, because this is an in-memory representation # thus we don't concern ourselves with bulk writes. self.set(key, value) diff --git a/xblock/validation.py b/xblock/validation.py index 7d82219d7..ca371832f 100644 --- a/xblock/validation.py +++ b/xblock/validation.py @@ -2,6 +2,10 @@ Validation information for an xblock instance. """ +from __future__ import absolute_import, division, print_function, unicode_literals + +import six + class ValidationMessage(object): """ @@ -18,12 +22,12 @@ def __init__(self, message_type, message_text): Create a new message. Args: - message_type (str): The type associated with this message. Most be included in `TYPES`. + message_type (unicode): The type associated with this message. Must be included in `TYPES`. message_text (unicode): The textual message. """ if message_type not in self.TYPES: raise TypeError("Unknown message_type: " + message_type) - if not isinstance(message_text, unicode): + if not isinstance(message_text, six.text_type): raise TypeError("Message text must be unicode") self.type = message_type self.text = message_text @@ -112,7 +116,7 @@ def to_json(self): dict: A dict representation that is json-serializable. """ return { - "xblock_id": unicode(self.xblock_id), + "xblock_id": six.text_type(self.xblock_id), "messages": [message.to_json() for message in self.messages], "empty": self.empty }