Browse Source

Add initial (experimental) Python 3 support.

All tests* currently pass under Python 2.7 and Python 3.3 - 3.5. This
does not mean that Python 3 support is stable, however. As a plan
forward, we should encourage interested users to run Hyde with Python 3,
report bugs, and submit patches. This change references the Python 3
Support issue on GitHub (#270). Once this becomes a bit more tested and
stable, that issue should be closed, and Hyde should announce official,
stable support for Python 3 (i.e. new changes must also support 3,
issues with running hyde under 3 should be considered bugs rather than
feature requests, etc.).

Changes:

- Add ``hyde/_compat.py`` for 2-to-3 compatibility helpers.
- Replace uses of 2-specific code with their ``hyde._compat`` versions.
- Tests remain largely unchanged (for good reason). The exceptions here
  are to ``print`` function calls, and ``str`` type usage, and
  compatibility imports.
- Replace explicit calls to ``foo.next()`` with ``next(foo)``. This
  keeps code portable between Python 2's ``next`` and Python 3's
  ``__next__`` methods without the need for any compatibility functions
  and is the recommended API for interacting with these methods.
- Replace deprecated (and, in Python 3, removed) ``except Exception, e:``
  statements with their updated ``except Exception as e:`` versions.
- Replace print statements with print function calls (because of how
  parentheses work in Python, this is transparently
  cross-version-compatible).
- Add Python 3.3, 3.4, and 3.5 to ``tox.ini``, ``.travis.yml``, and
  classifiers in ``setup.py``.
- Add ``{posargs}`` to ``tox.ini`` command invocations to improve usage
  during development.
- Add Python 3 note in changelog.

* asciidoc does not support Python 3, and fixing that far beyond the
scope of this task, so the test that deals with it is conditionally
skipped when run using versions of Python 3.
main
Jon Banafato 9 years ago
parent
commit
40f6e7e26d
35 changed files with 297 additions and 162 deletions
  1. +4
    -1
      .travis.yml
  2. +6
    -0
      CHANGELOG.rst
  3. +91
    -0
      hyde/_compat.py
  4. +5
    -5
      hyde/exceptions.py
  5. +9
    -9
      hyde/ext/plugins/css.py
  6. +1
    -0
      hyde/ext/plugins/depends.py
  7. +8
    -7
      hyde/ext/plugins/images.py
  8. +7
    -6
      hyde/ext/plugins/js.py
  9. +7
    -7
      hyde/ext/plugins/meta.py
  10. +2
    -1
      hyde/ext/plugins/sphinx.py
  11. +1
    -0
      hyde/ext/plugins/structure.py
  12. +8
    -8
      hyde/ext/publishers/dvcs.py
  13. +3
    -2
      hyde/ext/publishers/pyfs.py
  14. +3
    -2
      hyde/ext/publishers/pypi.py
  15. +2
    -1
      hyde/ext/publishers/ssh.py
  16. +29
    -27
      hyde/ext/templates/jinja.py
  17. +1
    -1
      hyde/generator.py
  18. +3
    -1
      hyde/layout.py
  19. +6
    -5
      hyde/model.py
  20. +7
    -7
      hyde/plugin.py
  21. +3
    -3
      hyde/publisher.py
  22. +14
    -11
      hyde/server.py
  23. +14
    -14
      hyde/site.py
  24. +6
    -4
      hyde/template.py
  25. +5
    -3
      hyde/util.py
  26. +10
    -6
      setup.py
  27. +4
    -4
      tests/ext/test_sass.py
  28. +2
    -2
      tests/ext/test_textlinks.py
  29. +15
    -14
      tests/test_initialize.py
  30. +8
    -1
      tests/test_jinja2template.py
  31. +2
    -1
      tests/test_layout.py
  32. +2
    -2
      tests/test_plugin.py
  33. +3
    -3
      tests/test_site.py
  34. +3
    -1
      tests/util.py
  35. +3
    -3
      tox.ini

+ 4
- 1
.travis.yml View File

@@ -2,7 +2,7 @@ sudo: false

language: python
python:
- "2.7"
- "3.5"

addons:
apt:
@@ -40,6 +40,9 @@ install:
# Run each tox environment separately
env:
- TOX_ENV=py27
- TOX_ENV=py33
- TOX_ENV=py34
- TOX_ENV=py35
- TOX_ENV=pep8

before_script:


+ 6
- 0
CHANGELOG.rst View File

@@ -1,3 +1,9 @@
Version TBA
===========

* Experimental Python 3 support


Version 0.8.9 (2015-11-09)
===========================================================



+ 91
- 0
hyde/_compat.py View File

@@ -0,0 +1,91 @@
"""2/3 compatibility module for Hyde."""

# This module is for cross-version compatibility. As such, several
# assignments and import will look invalid to checkers like flake8.
# These lines are being marked with ``# NOQA`` to allow flake8 checking
# to pass.

import sys

PY3 = sys.version_info.major == 3

if PY3:
# Imports that have moved.
from collections import UserDict # NOQA
from functools import reduce # NOQA
from http.server import HTTPServer, SimpleHTTPRequestHandler # NOQA
from io import StringIO # NOQA
from urllib import parse # NOQA
from urllib.parse import quote, unquote # NOQA

# Types that have changed name.
filter = filter # NOQA
input = input # NOQA
basestring = str # NOQA
str = str # NOQA
zip = zip # NOQA

def execfile(filename, globals, locals):
"""Python 3 replacement for ``execfile``."""
# Credit: 2to3 and this StackOverflow answer
# (http://stackoverflow.com/a/437857/841994) take similar
# approaches.
with open(filename) as f:
code = compile(f.read(), filename, 'exec')
exec(code, globals, locals)

def reraise(tp, value, tb=None):
"""Reraise exceptions."""
if getattr(value, '__traceback__', tb) is not tb:
raise value.with_traceback(tb)
raise value

else:
# Imports that have moved.
from itertools import ifilter as filter, izip as zip # NOQA
reduce = reduce
from BaseHTTPServer import HTTPServer # NOQA
from SimpleHTTPServer import SimpleHTTPRequestHandler # NOQA
from cStringIO import StringIO # NOQA
from UserDict import IterableUserDict as UserDict # NOQA
import urlparse as parse # NOQA
from urllib import quote, unquote # NOQA

# Types that have changed name.
input = raw_input # NOQA
basestring = basestring # NOQA
str = unicode # NOQA
execfile = execfile # NOQA

exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')


def iteritems(d):
"""Return iterable items from a dict."""
if hasattr(d, 'iteritems'):
return d.iteritems()
else:
return iter(d.items())


def with_metaclass(meta, *bases):
"""Assign a metaclass in a 2/3 compatible fashion."""
# Note: borrowed from https://github.com/dirn/Simon/
# This requires a bit of explanation: the basic idea is to make a
# dummy metaclass for one level of class instantiation that replaces
# itself with the actual metaclass. Because of internal type checks
# we also need to make sure that we downgrade the custom metaclass
# for one level to something closer to type (that's why __call__ and
# __init__ comes back from type etc.).
#
# This has the advantage over six.with_metaclass in that it does not
# introduce dummy classes into the final MRO.
class metaclass(meta):
__call__ = type.__call__
__init__ = type.__init__

def __new__(cls, name, this_bases, d):
if this_bases is None:
return type.__new__(cls, name, (), d)
return meta(name, bases, d)
return metaclass('DummyMetaClass', None, {})

+ 5
- 5
hyde/exceptions.py View File

@@ -1,10 +1,10 @@
class HydeException(Exception):
from hyde._compat import reraise


"""
Base class for exceptions from hyde
"""
class HydeException(Exception):
"""Base class for exceptions from hyde."""

@staticmethod
def reraise(message, exc_info):
_, _, tb = exc_info
raise HydeException(message), None, tb
reraise(HydeException, HydeException(message), tb)

+ 9
- 9
hyde/ext/plugins/css.py View File

@@ -3,7 +3,7 @@
CSS plugins
"""

from hyde._compat import str
from hyde.plugin import CLTransformer, Plugin
from hyde.exceptions import HydeException

@@ -109,9 +109,9 @@ class LessCSSPlugin(CLTransformer):
less = self.app
source = File.make_temp(text)
target = File.make_temp('')
args = [unicode(less)]
args = [str(less)]
args.extend(self.process_args(supported))
args.extend([unicode(source), unicode(target)])
args.extend([str(source), str(target)])
try:
self.call_app(args)
except subprocess.CalledProcessError:
@@ -223,9 +223,9 @@ class StylusPlugin(CLTransformer):
source = File.make_temp(text.strip())
supported = [("compress", "c"), ("include", "I")]

args = [unicode(stylus)]
args = [str(stylus)]
args.extend(self.process_args(supported))
args.append(unicode(source))
args.append(str(source))
try:
self.call_app(args)
except subprocess.CalledProcessError:
@@ -251,7 +251,7 @@ class CleverCSSPlugin(Plugin):
super(CleverCSSPlugin, self).__init__(site)
try:
import clevercss
except ImportError, e:
except ImportError as e:
raise HydeException('Unable to import CleverCSS: ' + e.message)
else:
self.clevercss = clevercss
@@ -329,7 +329,7 @@ class SassyCSSPlugin(Plugin):
super(SassyCSSPlugin, self).__init__(site)
try:
import scss
except ImportError, e:
except ImportError as e:
raise HydeException('Unable to import pyScss: ' + e.message)
else:
self.scss = scss
@@ -419,7 +419,7 @@ class SassPlugin(Plugin):
super(SassPlugin, self).__init__(site)
try:
import sass
except ImportError, e:
except ImportError as e:
raise HydeException('Unable to import libsass: ' + e.message)
else:
self.sass = sass
@@ -489,6 +489,6 @@ class SassPlugin(Plugin):
self.logger.error(resource)
try:
return self.sass.compile(string=text, **options)
except Exception, exc:
except Exception as exc:
self.logger.error(exc)
raise

+ 1
- 0
hyde/ext/plugins/depends.py View File

@@ -5,6 +5,7 @@ Depends plugin
/// Experimental: Not working yet.
"""

from hyde._compat import basestring
from hyde.plugin import Plugin




+ 8
- 7
hyde/ext/plugins/images.py View File

@@ -14,6 +14,7 @@ import re

from fswrap import File

from hyde._compat import str
from hyde.exceptions import HydeException


@@ -27,7 +28,7 @@ class PILPlugin(Plugin):
# No pillow
try:
import Image
except ImportError, e:
except ImportError as e:
raise HydeException('Unable to load PIL: ' + e.message)

self.Image = Image
@@ -442,9 +443,9 @@ class JPEGOptimPlugin(CLTransformer):
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
jpegoptim = self.app
args = [unicode(jpegoptim)]
args = [str(jpegoptim)]
args.extend(self.process_args(supported))
args.extend(["-q", unicode(target)])
args.extend(["-q", str(target)])
self.call_app(args)


@@ -499,9 +500,9 @@ class JPEGTranPlugin(CLTransformer):
resource.relative_deploy_path))
target = File.make_temp('')
jpegtran = self.app
args = [unicode(jpegtran)]
args = [str(jpegtran)]
args.extend(self.process_args(supported))
args.extend(["-outfile", unicode(target), unicode(source)])
args.extend(["-outfile", str(target), str(source)])
self.call_app(args)
target.copy_to(source)
target.delete()
@@ -570,7 +571,7 @@ class OptiPNGPlugin(CLTransformer):
target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
optipng = self.app
args = [unicode(optipng)]
args = [str(optipng)]
args.extend(self.process_args(supported))
args.extend([unicode(target)])
args.extend([str(target)])
self.call_app(args)

+ 7
- 6
hyde/ext/plugins/js.py View File

@@ -5,6 +5,7 @@ JavaScript plugins
import subprocess
import sys

from hyde._compat import str
from hyde.exceptions import HydeException
from hyde.plugin import CLTransformer

@@ -79,9 +80,9 @@ class UglifyPlugin(CLTransformer):
uglify = self.app
source = File.make_temp(text)
target = File.make_temp('')
args = [unicode(uglify)]
args = [str(uglify)]
args.extend(self.process_args(supported))
args.extend(["-o", unicode(target), unicode(source)])
args.extend(["-o", str(target), str(source)])
self.call_app(args)
out = target.read_all()
return out
@@ -127,9 +128,9 @@ class RequireJSPlugin(CLTransformer):

rjs = self.app
target = File.make_temp('')
args = [unicode(rjs)]
args = [str(rjs)]
args.extend(
['-o', unicode(resource), ("out=" + target.fully_expanded_path)])
['-o', str(resource), ("out=" + target.fully_expanded_path)])

try:
self.call_app(args)
@@ -184,6 +185,6 @@ class CoffeePlugin(CLTransformer):

coffee = self.app
source = File.make_temp(text)
args = [unicode(coffee)]
args.extend(["-c", "-p", unicode(source)])
args = [str(coffee)]
args.extend(["-c", "-p", str(source)])
return self.call_app(args)

+ 7
- 7
hyde/ext/plugins/meta.py View File

@@ -5,11 +5,11 @@ Contains classes and utilities related to meta data in hyde.

from collections import namedtuple
from functools import partial
from itertools import ifilter
from operator import attrgetter
import re
import sys

from hyde._compat import basestring, filter, iteritems, str
from hyde.exceptions import HydeException
from hyde.model import Expando
from hyde.plugin import Plugin
@@ -263,7 +263,7 @@ def get_tagger_sort_method(site):


def walk_resources_tagged_with(node, tag):
tags = set(unicode(tag).split('+'))
tags = set(str(tag).split('+'))
walker = get_tagger_sort_method(node.site)
for resource in walker():
try:
@@ -329,7 +329,7 @@ class TaggerPlugin(Plugin):
except AttributeError:
tag_meta = {}

for tagname, meta in tag_meta.iteritems():
for tagname, meta in iteritems(tag_meta):
# Don't allow name and resources in meta
if 'resources' in meta:
del(meta['resources'])
@@ -376,7 +376,7 @@ class TaggerPlugin(Plugin):

self.logger.debug("Generating archives for tags")

for name, config in archive_config.to_dict().iteritems():
for name, config in iteritems(archive_config.to_dict()):
self._create_tag_archive(config)

def _create_tag_archive(self, config):
@@ -413,7 +413,7 @@ extends: false
{%% set walker = source['walk_resources_tagged_with_%(tag)s'] %%}
{%% extends "%(template)s" %%}
"""
for tagname, tag in self.site.tagger.tags.to_dict().iteritems():
for tagname, tag in iteritems(self.site.tagger.tags.to_dict()):
tag_data = {
"tag": tagname,
"node": source.name,
@@ -482,8 +482,8 @@ def sort_method(node, settings=None):

excluder_ = partial(attributes_checker, attributes=attr)

resources = ifilter(lambda x: excluder_(x) and filter_(x),
node.walk_resources())
resources = filter(lambda x: excluder_(x) and filter_(x),
node.walk_resources())
return sorted(resources,
key=attrgetter(*attr),
reverse=reverse)


+ 2
- 1
hyde/ext/plugins/sphinx.py View File

@@ -44,6 +44,7 @@ import os
import json
import tempfile

from hyde._compat import execfile, iteritems
from hyde.plugin import Plugin
from hyde.model import Expando
from hyde.ext.plugins.meta import MetaPlugin as _MetaPlugin
@@ -166,7 +167,7 @@ class SphinxPlugin(Plugin):
if not settings.block_map:
output.append(sphinx_output["body"])
else:
for (nm, content) in sphinx_output.iteritems():
for (nm, content) in iteritems(sphinx_output):
try:
block = getattr(settings.block_map, nm)
except AttributeError:


+ 1
- 0
hyde/ext/plugins/structure.py View File

@@ -3,6 +3,7 @@
Plugins related to structure
"""

from hyde._compat import reduce
from hyde.ext.plugins.meta import Metadata
from hyde.plugin import Plugin
from hyde.site import Resource


+ 8
- 8
hyde/ext/publishers/dvcs.py View File

@@ -3,14 +3,14 @@ Contains classes and utilities that help publishing a hyde website to
distributed version control systems.
"""

from hyde._compat import str, with_metaclass
from hyde.publisher import Publisher

import abc
from subprocess import Popen, PIPE


class DVCS(Publisher):
__metaclass__ = abc.ABCMeta
class DVCS(with_metaclass(abc.ABCMeta, Publisher)):

def initialize(self, settings):
self.settings = settings
@@ -62,7 +62,7 @@ class Git(DVCS):

def add(self, path="."):
cmd = Popen('git add "%s"' % path,
cwd=unicode(self.path), stdout=PIPE, shell=True)
cwd=str(self.path), stdout=PIPE, shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
raise Exception(cmdresult)
@@ -70,7 +70,7 @@ class Git(DVCS):
def pull(self):
self.switch(self.branch)
cmd = Popen("git pull origin %s" % self.branch,
cwd=unicode(self.path),
cwd=str(self.path),
stdout=PIPE,
shell=True)
cmdresult = cmd.communicate()[0]
@@ -79,7 +79,7 @@ class Git(DVCS):

def push(self):
cmd = Popen("git push origin %s" % self.branch,
cwd=unicode(self.path), stdout=PIPE,
cwd=str(self.path), stdout=PIPE,
shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
@@ -87,7 +87,7 @@ class Git(DVCS):

def commit(self, message):
cmd = Popen('git commit -a -m"%s"' % message,
cwd=unicode(self.path), stdout=PIPE, shell=True)
cwd=str(self.path), stdout=PIPE, shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
raise Exception(cmdresult)
@@ -95,14 +95,14 @@ class Git(DVCS):
def switch(self, branch):
self.branch = branch
cmd = Popen('git checkout %s' % branch,
cwd=unicode(self.path), stdout=PIPE, shell=True)
cwd=str(self.path), stdout=PIPE, shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
raise Exception(cmdresult)

def merge(self, branch):
cmd = Popen('git merge %s' % branch,
cwd=unicode(self.path), stdout=PIPE, shell=True)
cwd=str(self.path), stdout=PIPE, shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
raise Exception(cmdresult)

+ 3
- 2
hyde/ext/publishers/pyfs.py View File

@@ -15,6 +15,7 @@ import getpass
import hashlib


from hyde._compat import basestring, input
from hyde.publisher import Publisher

from commando.util import getLoggerWithNullHandler
@@ -47,8 +48,8 @@ class PyFS(Publisher):
def prompt_for_credentials(self):
credentials = {}
if "%(username)s" in self.url:
print "Username: ",
credentials["username"] = raw_input().strip()
print("Username: ",)
credentials["username"] = input().strip()
if "%(password)s" in self.url:
credentials["password"] = getpass.getpass("Password: ")
if credentials:


+ 3
- 2
hyde/ext/publishers/pypi.py View File

@@ -13,6 +13,7 @@ import urlparse
from base64 import standard_b64encode
import ConfigParser

from hyde._compat import input
from hyde.publisher import Publisher

from commando.util import getLoggerWithNullHandler
@@ -47,8 +48,8 @@ class PyPI(Publisher):
pass
# Prompt for username on command-line
if self.username is None:
print "Username: ",
self.username = raw_input().strip()
print("Username: ",)
self.username = input().strip()
# Try to find password in .pypirc
if self.password is None:
if pypirc is not None:


+ 2
- 1
hyde/ext/publishers/ssh.py View File

@@ -30,6 +30,7 @@ within the ``deploy/`` directory:
rsync -r -e ssh ./ username@ssh.server.com:/www/username/mysite/

"""
from hyde._compat import str
from hyde.publisher import Publisher

from subprocess import Popen, PIPE
@@ -54,7 +55,7 @@ class SSH(Publisher):
target=self.target)
deploy_path = self.site.config.deploy_root_path.path

cmd = Popen(command, cwd=unicode(deploy_path), stdout=PIPE, shell=True)
cmd = Popen(command, cwd=str(deploy_path), stdout=PIPE, shell=True)
cmdresult = cmd.communicate()[0]
if cmd.returncode:
raise Exception(cmdresult)

+ 29
- 27
hyde/ext/templates/jinja.py View File

@@ -8,8 +8,8 @@ import itertools
import os
import re
import sys
from urllib import quote, unquote

from hyde._compat import PY3, quote, unquote, str, StringIO
from hyde.exceptions import HydeException
from hyde.model import Expando
from hyde.template import HtmlWrap, Template
@@ -79,7 +79,10 @@ def urlencode(ctx, url, safe=None):

@contextfilter
def urldecode(ctx, url):
return unquote(url).decode('utf8')
url = unquote(url)
if not PY3:
url = url.decode('utf8')
return url


@contextfilter
@@ -125,18 +128,17 @@ def asciidoc(env, value):
try:
from asciidocapi import AsciiDocAPI
except ImportError:
print u"Requires AsciiDoc library to use AsciiDoc tag."
print(u"Requires AsciiDoc library to use AsciiDoc tag.")
raise

import StringIO
output = value

asciidoc = AsciiDocAPI()
asciidoc.options('--no-header-footer')
result = StringIO.StringIO()
result = StringIO()
asciidoc.execute(
StringIO.StringIO(output.encode('utf-8')), result, backend='html4')
return unicode(result.getvalue(), "utf-8")
StringIO(output.encode('utf-8')), result, backend='html4')
return str(result.getvalue(), "utf-8")


@environmentfilter
@@ -238,7 +240,7 @@ class Spaceless(Extension):
"""
Parses the statements and calls back to strip spaces.
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endspaceless'],
drop_needle=True)
return nodes.CallBlock(
@@ -253,7 +255,7 @@ class Spaceless(Extension):
"""
if not caller:
return ''
return re.sub(r'>\s+<', '><', unicode(caller().strip()))
return re.sub(r'>\s+<', '><', str(caller().strip()))


class Asciidoc(Extension):
@@ -268,7 +270,7 @@ class Asciidoc(Extension):
Parses the statements and defers to the callback
for asciidoc processing.
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endasciidoc'], drop_needle=True)

return nodes.CallBlock(
@@ -297,7 +299,7 @@ class Markdown(Extension):
Parses the statements and defers to the callback
for markdown processing.
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)

return nodes.CallBlock(
@@ -325,7 +327,7 @@ class restructuredText(Extension):
"""
Simply extract our content
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
body = parser.parse_statements(
['name:endrestructuredtext'], drop_needle=True)

@@ -357,7 +359,7 @@ class YamlVar(Extension):
Parses the contained data and defers to the callback to load it as
yaml.
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
var = parser.stream.expect('name').value
body = parser.parse_statements(['name:endyaml'], drop_needle=True)
return [
@@ -396,7 +398,7 @@ def parse_kwargs(parser):
if parser.stream.current.test('string'):
value = parser.parse_expression()
else:
value = nodes.Const(parser.stream.next().value)
value = nodes.Const(next(parser.stream).value)
return (name, value)


@@ -413,7 +415,7 @@ class Syntax(Extension):
Parses the statements and defers to the callback for
pygments processing.
"""
lineno = parser.stream.next().lineno
lineno = next(parser.stream).lineno
lex = nodes.Const(None)
filename = nodes.Const(None)

@@ -428,7 +430,7 @@ class Syntax(Extension):
if name == 'lex' \
else (value1, value)
else:
lex = nodes.Const(parser.stream.next().value)
lex = nodes.Const(next(parser.stream).value)
if parser.stream.skip_if('comma'):
filename = parser.parse_expression()

@@ -496,10 +498,10 @@ class Reference(Extension):
"""
Parse the variable name that the content must be assigned to.
"""
token = parser.stream.next()
token = next(parser.stream)
lineno = token.lineno
tag = token.value
name = parser.stream.next().value
name = next(parser.stream).value
body = parser.parse_statements(['name:end%s' % tag], drop_needle=True)
return nodes.CallBlock(self.call_method('_render_output',
args=[
@@ -533,12 +535,12 @@ class Refer(Extension):
"""
Parse the referred template and the namespace.
"""
token = parser.stream.next()
token = next(parser.stream)
lineno = token.lineno
parser.stream.expect('name:to')
template = parser.parse_expression()
parser.stream.expect('name:as')
namespace = parser.stream.next().value
namespace = next(parser.stream).value
includeNode = nodes.Include(lineno=lineno)
includeNode.with_context = True
includeNode.ignore_missing = False
@@ -623,11 +625,11 @@ class HydeLoader(FileSystemLoader):
config = site.config if hasattr(site, 'config') else None
if config:
super(HydeLoader, self).__init__([
unicode(config.content_root_path),
unicode(config.layout_root_path),
str(config.content_root_path),
str(config.layout_root_path),
])
else:
super(HydeLoader, self).__init__(unicode(sitepath))
super(HydeLoader, self).__init__(str(sitepath))

self.site = site
self.preprocessor = preprocessor
@@ -650,10 +652,10 @@ class HydeLoader(FileSystemLoader):
except UnicodeDecodeError:
HydeException.reraise(
"Unicode error when processing %s" % template, sys.exc_info())
except TemplateError, exc:
except TemplateError as exc:
HydeException.reraise('Error when processing %s: %s' % (
template,
unicode(exc)
str(exc)
), sys.exc_info())

if self.preprocessor:
@@ -800,9 +802,9 @@ class Jinja2Template(Template):
from jinja2.meta import find_referenced_templates
try:
ast = self.env.parse(text)
except Exception, e:
except Exception as e:
HydeException.reraise(
"Error processing %s: \n%s" % (path, unicode(e)),
"Error processing %s: \n%s" % (path, str(e)),
sys.exc_info())

tpls = find_referenced_templates(ast)


+ 1
- 1
hyde/generator.py View File

@@ -336,7 +336,7 @@ class Generator(object):
try:
text = self.template.render_resource(resource,
context)
except Exception, e:
except Exception as e:
HydeException.reraise("Error occurred when processing"
"template: [%s]: %s" %
(resource, repr(e)),


+ 3
- 1
hyde/layout.py View File

@@ -6,6 +6,8 @@ import os

from fswrap import File, Folder

from hyde._compat import str

HYDE_DATA = "HYDE_DATA"
LAYOUTS = "layouts"

@@ -39,6 +41,6 @@ class Layout(object):
Finds the layout folder from the given root folder.
If it does not exist, return None
"""
layouts_folder = Folder(unicode(root)).child_folder(LAYOUTS)
layouts_folder = Folder(str(root)).child_folder(LAYOUTS)
layout_folder = layouts_folder.child_folder(layout_name)
return layout_folder if layout_folder.exists else None

+ 6
- 5
hyde/model.py View File

@@ -5,11 +5,12 @@ Contains data structures and utilities for hyde.
import codecs
import yaml
from datetime import datetime
from UserDict import IterableUserDict

from commando.util import getLoggerWithNullHandler
from fswrap import File, Folder

from hyde._compat import iteritems, str, UserDict

logger = getLoggerWithNullHandler('hyde.engine')

SEQS = (tuple, list, set, frozenset)
@@ -45,7 +46,7 @@ class Expando(object):
Returns an iterator for all the items in the
dictionary as key value pairs.
"""
return self.__dict__.iteritems()
return iteritems(self.__dict__)

def update(self, d):
"""
@@ -63,10 +64,10 @@ class Expando(object):
Sets the expando attribute after
transforming the value.
"""
setattr(self, unicode(key).encode('utf-8'), make_expando(value))
setattr(self, str(key), make_expando(value))

def __repr__(self):
return unicode(self.to_dict())
return str(self.to_dict())

def to_dict(self):
"""
@@ -128,7 +129,7 @@ class Context(object):
return context


class Dependents(IterableUserDict):
class Dependents(UserDict):

"""
Represents the dependency graph for hyde.


+ 7
- 7
hyde/plugin.py View File

@@ -2,6 +2,7 @@
"""
Contains definition for a plugin protocol and other utiltities.
"""
from hyde._compat import str
from hyde.exceptions import HydeException
from hyde.util import first_match, discover_executable
from hyde.model import Expando
@@ -17,6 +18,8 @@ import sys
from commando.util import getLoggerWithNullHandler, load_python_object
from fswrap import File

from hyde._compat import with_metaclass

logger = getLoggerWithNullHandler('hyde.engine')

# Plugins have been reorganized. Map old plugin paths to new.
@@ -106,12 +109,11 @@ class PluginProxy(object):
"Unknown plugin method [%s] called." % method_name)


class Plugin(object):
class Plugin(with_metaclass(abc.ABCMeta)):

"""
The plugin protocol
"""
__metaclass__ = abc.ABCMeta

def __init__(self, site):
super(Plugin, self).__init__()
@@ -440,14 +442,14 @@ class CLTransformer(Plugin):
try:
self.logger.debug(
"Calling executable [%s] with arguments %s" %
(args[0], unicode(args[1:])))
(args[0], str(args[1:])))
return subprocess.check_output(args)
except subprocess.CalledProcessError, error:
except subprocess.CalledProcessError as error:
self.logger.error(error.output)
raise


class TextyPlugin(Plugin):
class TextyPlugin(with_metaclass(abc.ABCMeta, Plugin)):

"""
Base class for text preprocessing plugins.
@@ -457,8 +459,6 @@ class TextyPlugin(Plugin):
can inherit from this class.
"""

__metaclass__ = abc.ABCMeta

def __init__(self, site):
super(TextyPlugin, self).__init__(site)
self.open_pattern = self.default_open_pattern


+ 3
- 3
hyde/publisher.py View File

@@ -3,20 +3,20 @@ from operator import attrgetter

from commando.util import getLoggerWithNullHandler, load_python_object

from hyde._compat import with_metaclass

"""
Contains abstract classes and utilities that help publishing a website to a
server.
"""


class Publisher(object):
class Publisher(with_metaclass(abc.ABCMeta)):

"""
The abstract base class for publishers.
"""

__metaclass__ = abc.ABCMeta

def __init__(self, site, settings, message):
super(Publisher, self).__init__()
self.logger = getLoggerWithNullHandler(


+ 14
- 11
hyde/server.py View File

@@ -4,13 +4,13 @@ Contains classes and utilities for serving a site
generated from hyde.
"""
import threading
import urlparse
import urllib
import traceback
from datetime import datetime
from SimpleHTTPServer import SimpleHTTPRequestHandler
from BaseHTTPServer import HTTPServer


from hyde._compat import (HTTPServer, iteritems, parse, PY3,
SimpleHTTPRequestHandler, unquote)
from hyde.generator import Generator

from fswrap import File, Folder
@@ -35,8 +35,8 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
"""
self.server.request_time = datetime.now()
logger.debug("Processing request: [%s]" % self.path)
result = urlparse.urlparse(self.path)
query = urlparse.parse_qs(result.query)
result = parse.urlparse(self.path)
query = parse.parse_qs(result.query)
if 'refresh' in query or result.query == 'refresh':
self.server.regenerate()
if 'refresh' in query:
@@ -44,7 +44,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
parts = list(tuple(result))
parts[4] = urllib.urlencode(query)
parts = tuple(parts)
new_url = urlparse.urlunparse(parts)
new_url = parse.urlunparse(parts)
logger.info('Redirecting... [%s]' % new_url)
self.redirect(new_url)
else:
@@ -56,7 +56,10 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
referring to the `site` variable in the server.
"""
site = self.server.site
result = urlparse.urlparse(urllib.unquote(self.path).decode('utf-8'))
path = unquote(self.path)
if not PY3:
path = path.decode('utf-8')
result = parse.urlparse(path)
logger.debug(
"Trying to load file based on request: [%s]" % result.path)
path = result.path.lstrip('/')
@@ -150,7 +153,7 @@ class HydeWebServer(HTTPServer):
except AttributeError:
extensions = {}

for extension, type in extensions.iteritems():
for extension, type in iteritems(extensions):
ext = "." + extension if not extension == 'default' else ''
HydeRequestHandler.extensions_map[ext] = type

@@ -165,7 +168,7 @@ class HydeWebServer(HTTPServer):
self.site.config.reload()
self.site.load()
self.generator.generate_all(incremental=False)
except Exception, exception:
except Exception as exception:
logger.error('Error occured when regenerating the site [%s]'
% exception.message)
logger.debug(traceback.format_exc())
@@ -182,7 +185,7 @@ class HydeWebServer(HTTPServer):
try:
logger.debug('Serving node [%s]' % node)
self.generator.generate_node(node, incremental=True)
except Exception, exception:
except Exception as exception:
logger.error(
'Error [%s] occured when generating the node [%s]'
% (repr(exception), node))
@@ -201,7 +204,7 @@ class HydeWebServer(HTTPServer):
try:
logger.debug('Serving resource [%s]' % resource)
self.generator.generate_resource(resource, incremental=True)
except Exception, exception:
except Exception as exception:
logger.error(
'Error [%s] occured when serving the resource [%s]'
% (repr(exception), resource))


+ 14
- 14
hyde/site.py View File

@@ -5,10 +5,9 @@ Parses & holds information about the site to be generated.
import os
import fnmatch
import sys
import urlparse
from functools import wraps
from urllib import quote

from hyde._compat import parse, quote, str
from hyde.exceptions import HydeException
from hyde.model import Config

@@ -19,7 +18,7 @@ from fswrap import FS, File, Folder
def path_normalized(f):
@wraps(f)
def wrapper(self, path):
return f(self, unicode(path).replace('/', os.sep))
return f(self, str(path).replace('/', os.sep))
return wrapper

logger = getLoggerWithNullHandler('hyde.engine')
@@ -138,7 +137,7 @@ class Node(Processable):
self.root = self
self.module = None
self.site = None
self.source_folder = Folder(unicode(source_folder))
self.source_folder = Folder(str(source_folder))
self.parent = parent
if parent:
self.root = self.parent.root
@@ -249,7 +248,7 @@ class RootNode(Node):
"""
if Folder(path) == self.source_folder:
return self
return self.node_map.get(unicode(Folder(path)), None)
return self.node_map.get(str(Folder(path)), None)

@path_normalized
def node_from_relative_path(self, relative_path):
@@ -258,7 +257,7 @@ class RootNode(Node):
If no match is found it returns None.
"""
return self.node_from_path(
self.source_folder.child(unicode(relative_path)))
self.source_folder.child(str(relative_path)))

@path_normalized
def resource_from_path(self, path):
@@ -266,7 +265,7 @@ class RootNode(Node):
Gets the resource that maps to the given path.
If no match is found it returns None.
"""
return self.resource_map.get(unicode(File(path)), None)
return self.resource_map.get(str(File(path)), None)

@path_normalized
def resource_from_relative_path(self, relative_path):
@@ -282,7 +281,7 @@ class RootNode(Node):
Handles the case where the relative deploy path of a
resource has changed.
"""
self.resource_deploy_map[unicode(item.relative_deploy_path)] = item
self.resource_deploy_map[str(item.relative_deploy_path)] = item

@path_normalized
def resource_from_relative_deploy_path(self, relative_deploy_path):
@@ -323,7 +322,7 @@ class RootNode(Node):
node = parent if parent else self
for h_folder in hierarchy:
node = node.add_child_node(h_folder)
self.node_map[unicode(h_folder)] = node
self.node_map[str(h_folder)] = node
logger.debug("Added node [%s] to [%s]" % (
node.relative_path, self.source_folder))

@@ -352,7 +351,7 @@ class RootNode(Node):
if not node:
node = self.add_node(afile.parent)
resource = node.add_child_resource(afile)
self.resource_map[unicode(afile)] = resource
self.resource_map[str(afile)] = resource
relative_path = resource.relative_path
resource.simple_copy = any(fnmatch.fnmatch(relative_path, pattern)
for pattern in self.site.config.simple_copy)
@@ -395,10 +394,11 @@ class RootNode(Node):


def _encode_path(base, path, safe):
base = base.strip().replace(os.sep, '/').encode('utf-8')
path = path.strip().replace(os.sep, '/').encode('utf-8')
base = base.strip().replace(os.sep, '/')
path = path.strip().replace(os.sep, '/')
path = quote(path, safe) if safe is not None else quote(path)
return base.rstrip('/') + '/' + path.lstrip('/')
full_path = base.rstrip('/') + '/' + path.lstrip('/')
return full_path


class Site(object):
@@ -471,7 +471,7 @@ class Site(object):
configuration and returns the appropriate url. The return value
is url encoded.
"""
if urlparse.urlparse(path)[:2] != ("", ""):
if parse.urlparse(path)[:2] != ("", ""):
return path

if self.is_media(path):


+ 6
- 4
hyde/template.py View File

@@ -3,6 +3,7 @@
"""
Abstract classes and utilities for template engines
"""
from hyde._compat import with_metaclass
from hyde.exceptions import HydeException

import abc
@@ -30,24 +31,25 @@ class HtmlWrap(object):
PyQuery = None
self.q = PyQuery(html) if PyQuery else None

def __unicode__(self):
def __str__(self):
return self.raw

# Support __unicode__ as well as __str__ for backward compatibility.
__unicode__ = __str__

def __call__(self, selector=None):
if not self.q:
return self.raw
return self.q(selector).html()


class Template(object):
class Template(with_metaclass(abc.ABCMeta)):

"""
Interface for hyde template engines. To use a different template engine,
the following interface must be implemented.
"""

__metaclass__ = abc.ABCMeta

def __init__(self, sitepath):
self.sitepath = sitepath
self.logger = getLoggerWithNullHandler(self.__class__.__name__)


+ 5
- 3
hyde/util.py View File

@@ -3,7 +3,9 @@ Module for python 2.6 compatibility.
"""
import os
from functools import partial
from itertools import izip, tee
from itertools import tee

from hyde._compat import str, zip


def make_method(method_name, method_):
@@ -26,7 +28,7 @@ def add_method(obj, method_name, method_, *args, **kwargs):
def pairwalk(iterable):
a, b = tee(iterable)
next(b, None)
return izip(a, b)
return zip(a, b)


def first_match(predicate, iterable):
@@ -49,7 +51,7 @@ def discover_executable(name, sitepath):
# Check if an executable can be found in the site path first.
# If not check the os $PATH for its presence.

paths = [unicode(sitepath)] + os.environ['PATH'].split(os.pathsep)
paths = [str(sitepath)] + os.environ['PATH'].split(os.pathsep)
for path in paths:
full_name = os.path.join(path, name)
if os.path.exists(full_name):


+ 10
- 6
setup.py View File

@@ -72,9 +72,8 @@ def find_package_data(

bad_name = True
if show_ignored:
print >> sys.stderr, (
"Directory %s ignored by pattern %s"
% (fn, pattern))
msg = "Directory {} ignored by pattern {}"
sys.stderr.write(msg.format(fn, pattern))
break
if bad_name:
continue
@@ -96,9 +95,8 @@ def find_package_data(

bad_name = True
if show_ignored:
print >> sys.stderr, (
"File %s ignored by pattern %s"
% (fn, pattern))
msg = "File {} ignored by pattern {}"
sys.stderr.write(msg.format(fn, pattern))
break
if bad_name:
continue
@@ -157,6 +155,12 @@ setup(name=PROJECT,
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Software Development',
'Topic :: Software Development :: Build Tools',
'Topic :: Software Development :: Code Generators',


+ 4
- 4
tests/ext/test_sass.py View File

@@ -41,8 +41,8 @@ class TestSass(object):
assert target.exists
text = target.read_all()
expected_text = File(SCSS_SOURCE.child('expected-sass.css')).read_all()
print "TEXT" + "-" * 80
print text
print "-" * 80
print expected_text
print("TEXT" + "-" * 80)
print(text)
print("-" * 80)
print(expected_text)
assert_no_diff(expected_text, text)

+ 2
- 2
tests/ext/test_textlinks.py View File

@@ -3,9 +3,9 @@ Use nose
`$ pip install nose`
`$ nosetests`
"""
from hyde._compat import quote
from hyde.generator import Generator
from hyde.site import Site
from urllib import quote

from fswrap import File

@@ -46,7 +46,7 @@ class TestTextlinks(object):
site.config.media_url = '/media'
tlink = File(site.content.source_folder.child('tlink.html'))
tlink.write(text % d)
print tlink.read_all()
print(tlink.read_all())
gen = Generator(site)
gen.generate_all()
f = File(site.config.deploy_root_path.child(tlink.name))


+ 15
- 14
tests/test_initialize.py View File

@@ -5,7 +5,7 @@ Use nose
`$ nosetests`
"""

from hyde._compat import str
from hyde.engine import Engine
from hyde.exceptions import HydeException
from hyde.layout import Layout
@@ -42,7 +42,7 @@ def delete_test_site_at_user():
def test_ensure_exception_when_site_yaml_exists():
e = Engine(raise_exceptions=True)
File(TEST_SITE.child('site.yaml')).write("Hey")
e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
e.run(e.parse(['-s', str(TEST_SITE), 'create']))


@raises(HydeException)
@@ -50,7 +50,7 @@ def test_ensure_exception_when_site_yaml_exists():
def test_ensure_exception_when_content_folder_exists():
e = Engine(raise_exceptions=True)
TEST_SITE.child_folder('content').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
e.run(e.parse(['-s', str(TEST_SITE), 'create']))


@raises(HydeException)
@@ -58,13 +58,13 @@ def test_ensure_exception_when_content_folder_exists():
def test_ensure_exception_when_layout_folder_exists():
e = Engine(raise_exceptions=True)
TEST_SITE.child_folder('layout').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
e.run(e.parse(['-s', str(TEST_SITE), 'create']))


@with_setup(create_test_site, delete_test_site)
def test_ensure_no_exception_when_empty_site_exists():
e = Engine(raise_exceptions=True)
e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))
e.run(e.parse(['-s', str(TEST_SITE), 'create']))
verify_site_contents(TEST_SITE, Layout.find_layout())


@@ -72,16 +72,16 @@ def test_ensure_no_exception_when_empty_site_exists():
def test_ensure_no_exception_when_forced():
e = Engine(raise_exceptions=True)
TEST_SITE.child_folder('layout').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
e.run(e.parse(['-s', str(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout())
TEST_SITE.delete()
TEST_SITE.child_folder('content').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
e.run(e.parse(['-s', str(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout())
TEST_SITE.delete()
TEST_SITE.make()
File(TEST_SITE.child('site.yaml')).write("Hey")
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
e.run(e.parse(['-s', str(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout())


@@ -89,7 +89,7 @@ def test_ensure_no_exception_when_forced():
def test_ensure_no_exception_when_sitepath_does_not_exist():
e = Engine(raise_exceptions=True)
TEST_SITE.delete()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
e.run(e.parse(['-s', str(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout())


@@ -97,7 +97,7 @@ def test_ensure_no_exception_when_sitepath_does_not_exist():
def test_ensure_can_create_site_at_user():
e = Engine(raise_exceptions=True)
TEST_SITE_AT_USER.delete()
e.run(e.parse(['-s', unicode(TEST_SITE_AT_USER), 'create', '-f']))
e.run(e.parse(['-s', str(TEST_SITE_AT_USER), 'create', '-f']))
verify_site_contents(TEST_SITE_AT_USER, Layout.find_layout())


@@ -107,9 +107,10 @@ def verify_site_contents(site, layout):
assert site.child_folder('layout').exists
assert File(site.child('info.yaml')).exists

expected = map(
lambda f: f.get_relative_path(layout), layout.walker.walk_all())
actual = map(lambda f: f.get_relative_path(site), site.walker.walk_all())
expected = list(map(
lambda f: f.get_relative_path(layout), layout.walker.walk_all()))
actual = list(map(
lambda f: f.get_relative_path(site), site.walker.walk_all()))
assert actual
assert expected

@@ -122,4 +123,4 @@ def verify_site_contents(site, layout):
@with_setup(create_test_site, delete_test_site)
def test_ensure_exception_when_layout_is_invalid():
e = Engine(raise_exceptions=True)
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-l', 'junk']))
e.run(e.parse(['-s', str(TEST_SITE), 'create', '-l', 'junk']))

+ 8
- 1
tests/test_jinja2template.py View File

@@ -9,6 +9,7 @@ Some code borrowed from rwbench.py from the jinja2 examples
from datetime import datetime
from random import choice, randrange

from hyde._compat import PY3
from hyde.ext.templates.jinja import Jinja2Template
from hyde.site import Site
from hyde.generator import Generator
@@ -16,6 +17,7 @@ from hyde.model import Config

from fswrap import File
from jinja2.utils import generate_lorem_ipsum
from nose.plugins.skip import SkipTest
from nose.tools import nottest
from pyquery import PyQuery

@@ -49,7 +51,7 @@ class User(object):
self.username = username


users = map(User, [u'John Doe', u'Jane Doe', u'Peter Somewhat'])
users = list(map(User, [u'John Doe', u'Jane Doe', u'Peter Somewhat']))
articles = map(Article, range(20))
navigation = [
('index', 'Index'),
@@ -132,6 +134,11 @@ def test_spaceless():


def test_asciidoc():
if PY3:
# asciidoc is not supported under Python 3. Supporting it is out
# of the scope of this project, so its tests are simply skipped
# when run under Python 3.
raise SkipTest
source = """
{%asciidoc%}
== Heading 2 ==


+ 2
- 1
tests/test_layout.py View File

@@ -6,6 +6,7 @@ Use nose
"""
import os

from hyde._compat import str
from hyde.layout import Layout, HYDE_DATA, LAYOUTS

from fswrap import File
@@ -36,7 +37,7 @@ def test_find_layout_from_env_var():
f = Layout.find_layout()
LAYOUT_ROOT.make()
f.copy_to(LAYOUT_ROOT)
os.environ[HYDE_DATA] = unicode(DATA_ROOT)
os.environ[HYDE_DATA] = str(DATA_ROOT)
f = Layout.find_layout()
assert f.parent == LAYOUT_ROOT
assert f.name == 'basic'


+ 2
- 2
tests/test_plugin.py View File

@@ -24,14 +24,14 @@ class PluginLoaderStub(Plugin):
class NoReturnPlugin(Plugin):

def begin_text_resource(self, resource, text):
print "NoReturnPlugin"
print("NoReturnPlugin")
return None


class ConstantReturnPlugin(Plugin):

def begin_text_resource(self, resource, text):
print "ConstantReturnPlugin"
print("ConstantReturnPlugin")
return "Jam"




+ 3
- 3
tests/test_site.py View File

@@ -5,8 +5,8 @@ Use nose
`$ nosetests`
"""
import yaml
from urllib import quote

from hyde._compat import quote
from hyde.model import Config
from hyde.site import Node, RootNode, Site

@@ -242,8 +242,8 @@ class TestSiteWithConfig(object):
s = Site(self.SITE_PATH, config=self.config)
s.load()
path = '".jpg/abc'
print s.content_url(path, "")
print "/" + quote(path, "")
print(s.content_url(path, ""))
print("/" + quote(path, ""))
assert s.content_url(path, "") == "/" + quote(path, "")

def test_media_url(self):


+ 3
- 1
tests/util.py View File

@@ -1,13 +1,15 @@
import re
import difflib

from hyde._compat import str


def strip_spaces_between_tags(value):
"""
Stolen from `django.util.html`
Returns the given HTML with spaces between tags removed.
"""
return re.sub(r'>\s+<', '><', unicode(value))
return re.sub(r'>\s+<', '><', str(value))


def assert_no_diff(expected, out):


+ 3
- 3
tox.ini View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27,pep8
envlist = py{27,33,34,35},pep8

[testenv]
usedevelop = True
@@ -8,11 +8,11 @@ sitepackages = True
# Needed for asciidoc
passenv = PYTHONPATH
deps = -r{toxinidir}/dev-req.txt
commands = nosetests
commands = nosetests {posargs}

[testenv:pep8]
deps = flake8
commands = flake8
commands = flake8 {posargs}

[flake8]
exclude = .tox


Loading…
Cancel
Save