diff --git a/.travis.yml b/.travis.yml index fc6abe0..9bae6e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f2a25df..3f03ba7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +Version TBA +=========== + +* Experimental Python 3 support + + Version 0.8.9 (2015-11-09) =========================================================== diff --git a/hyde/_compat.py b/hyde/_compat.py new file mode 100644 index 0000000..60d7f62 --- /dev/null +++ b/hyde/_compat.py @@ -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, {}) diff --git a/hyde/exceptions.py b/hyde/exceptions.py index 70a991e..a6aca51 100644 --- a/hyde/exceptions.py +++ b/hyde/exceptions.py @@ -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) diff --git a/hyde/ext/plugins/css.py b/hyde/ext/plugins/css.py index 74cf0f9..828e5e9 100644 --- a/hyde/ext/plugins/css.py +++ b/hyde/ext/plugins/css.py @@ -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 diff --git a/hyde/ext/plugins/depends.py b/hyde/ext/plugins/depends.py index 304d403..7ee9a4a 100644 --- a/hyde/ext/plugins/depends.py +++ b/hyde/ext/plugins/depends.py @@ -5,6 +5,7 @@ Depends plugin /// Experimental: Not working yet. """ +from hyde._compat import basestring from hyde.plugin import Plugin diff --git a/hyde/ext/plugins/images.py b/hyde/ext/plugins/images.py index ee24ade..7bac9d5 100644 --- a/hyde/ext/plugins/images.py +++ b/hyde/ext/plugins/images.py @@ -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) diff --git a/hyde/ext/plugins/js.py b/hyde/ext/plugins/js.py index 5c849ab..e156eb8 100644 --- a/hyde/ext/plugins/js.py +++ b/hyde/ext/plugins/js.py @@ -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) diff --git a/hyde/ext/plugins/meta.py b/hyde/ext/plugins/meta.py index 50874a7..ccd9968 100644 --- a/hyde/ext/plugins/meta.py +++ b/hyde/ext/plugins/meta.py @@ -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) diff --git a/hyde/ext/plugins/sphinx.py b/hyde/ext/plugins/sphinx.py index cf2ebef..e7ff646 100644 --- a/hyde/ext/plugins/sphinx.py +++ b/hyde/ext/plugins/sphinx.py @@ -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: diff --git a/hyde/ext/plugins/structure.py b/hyde/ext/plugins/structure.py index f818fd4..ba8ba81 100644 --- a/hyde/ext/plugins/structure.py +++ b/hyde/ext/plugins/structure.py @@ -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 diff --git a/hyde/ext/publishers/dvcs.py b/hyde/ext/publishers/dvcs.py index cb10e92..02f2787 100644 --- a/hyde/ext/publishers/dvcs.py +++ b/hyde/ext/publishers/dvcs.py @@ -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) diff --git a/hyde/ext/publishers/pyfs.py b/hyde/ext/publishers/pyfs.py index 669afac..b463270 100644 --- a/hyde/ext/publishers/pyfs.py +++ b/hyde/ext/publishers/pyfs.py @@ -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: diff --git a/hyde/ext/publishers/pypi.py b/hyde/ext/publishers/pypi.py index 22b1153..d3bb2f8 100644 --- a/hyde/ext/publishers/pypi.py +++ b/hyde/ext/publishers/pypi.py @@ -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: diff --git a/hyde/ext/publishers/ssh.py b/hyde/ext/publishers/ssh.py index b41a167..b22514a 100644 --- a/hyde/ext/publishers/ssh.py +++ b/hyde/ext/publishers/ssh.py @@ -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) diff --git a/hyde/ext/templates/jinja.py b/hyde/ext/templates/jinja.py index 8f7f2bc..9077415 100644 --- a/hyde/ext/templates/jinja.py +++ b/hyde/ext/templates/jinja.py @@ -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) diff --git a/hyde/generator.py b/hyde/generator.py index 8eaa16d..d08f444 100644 --- a/hyde/generator.py +++ b/hyde/generator.py @@ -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)), diff --git a/hyde/layout.py b/hyde/layout.py index 31defe1..929cc17 100644 --- a/hyde/layout.py +++ b/hyde/layout.py @@ -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 diff --git a/hyde/model.py b/hyde/model.py index c1d91ac..e4bf2c8 100644 --- a/hyde/model.py +++ b/hyde/model.py @@ -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. diff --git a/hyde/plugin.py b/hyde/plugin.py index 37bc7ff..6356454 100644 --- a/hyde/plugin.py +++ b/hyde/plugin.py @@ -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 diff --git a/hyde/publisher.py b/hyde/publisher.py index 1eb0b20..9d082be 100644 --- a/hyde/publisher.py +++ b/hyde/publisher.py @@ -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( diff --git a/hyde/server.py b/hyde/server.py index 79be549..9858165 100644 --- a/hyde/server.py +++ b/hyde/server.py @@ -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)) diff --git a/hyde/site.py b/hyde/site.py index 29ccd08..b594d05 100644 --- a/hyde/site.py +++ b/hyde/site.py @@ -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): diff --git a/hyde/template.py b/hyde/template.py index e369fc8..4cb193b 100644 --- a/hyde/template.py +++ b/hyde/template.py @@ -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__) diff --git a/hyde/util.py b/hyde/util.py index c9ea268..a238387 100644 --- a/hyde/util.py +++ b/hyde/util.py @@ -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): diff --git a/setup.py b/setup.py index f2fc207..9b9872e 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tests/ext/test_sass.py b/tests/ext/test_sass.py index 488344e..07a9111 100644 --- a/tests/ext/test_sass.py +++ b/tests/ext/test_sass.py @@ -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) diff --git a/tests/ext/test_textlinks.py b/tests/ext/test_textlinks.py index 5c8582a..b6158c6 100644 --- a/tests/ext/test_textlinks.py +++ b/tests/ext/test_textlinks.py @@ -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)) diff --git a/tests/test_initialize.py b/tests/test_initialize.py index 85620e5..6fc1dbd 100644 --- a/tests/test_initialize.py +++ b/tests/test_initialize.py @@ -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'])) diff --git a/tests/test_jinja2template.py b/tests/test_jinja2template.py index 8bfa729..2e413c1 100644 --- a/tests/test_jinja2template.py +++ b/tests/test_jinja2template.py @@ -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 == diff --git a/tests/test_layout.py b/tests/test_layout.py index ad92fa2..32bb614 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -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' diff --git a/tests/test_plugin.py b/tests/test_plugin.py index ff7525e..4f4bd11 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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" diff --git a/tests/test_site.py b/tests/test_site.py index 1aef6f3..43b2f35 100644 --- a/tests/test_site.py +++ b/tests/test_site.py @@ -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): diff --git a/tests/util.py b/tests/util.py index 3443a57..2c0d9d5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -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): diff --git a/tox.ini b/tox.ini index ecede01..71d358d 100644 --- a/tox.ini +++ b/tox.ini @@ -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