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"
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, 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('A