Browse Source

pep8

main
Jordi Llonch 9 years ago
parent
commit
089cf7af9d
62 changed files with 1187 additions and 1692 deletions
  1. +25
    -24
      hyde/engine.py
  2. +1
    -2
      hyde/exceptions.py
  3. +3
    -4
      hyde/ext/plugins/blog.py
  4. +30
    -24
      hyde/ext/plugins/css.py
  5. +12
    -11
      hyde/ext/plugins/depends.py
  6. +86
    -54
      hyde/ext/plugins/images.py
  7. +11
    -5
      hyde/ext/plugins/js.py
  8. +10
    -6
      hyde/ext/plugins/languages.py
  9. +61
    -42
      hyde/ext/plugins/meta.py
  10. +32
    -26
      hyde/ext/plugins/sphinx.py
  11. +32
    -18
      hyde/ext/plugins/structure.py
  12. +15
    -5
      hyde/ext/plugins/text.py
  13. +5
    -3
      hyde/ext/plugins/urls.py
  14. +14
    -7
      hyde/ext/plugins/vcs.py
  15. +16
    -11
      hyde/ext/publishers/dvcs.py
  16. +22
    -24
      hyde/ext/publishers/pyfs.py
  17. +23
    -24
      hyde/ext/publishers/pypi.py
  18. +3
    -1
      hyde/ext/publishers/ssh.py
  19. +160
    -117
      hyde/ext/templates/jinja.py
  20. +25
    -24
      hyde/generator.py
  21. +3
    -2
      hyde/layout.py
  22. +4
    -1
      hyde/lib/pygments/rst_directive.py
  23. +2
    -1
      hyde/main.py
  24. +18
    -13
      hyde/model.py
  25. +76
    -47
      hyde/plugin.py
  26. +8
    -4
      hyde/publisher.py
  27. +19
    -13
      hyde/server.py
  28. +23
    -19
      hyde/site.py
  29. +3
    -1
      hyde/template.py
  30. +7
    -6
      hyde/tests/ext/test_auto_extend.py
  31. +5
    -3
      hyde/tests/ext/test_blockdown.py
  32. +8
    -4
      hyde/tests/ext/test_combine.py
  33. +5
    -4
      hyde/tests/ext/test_depends.py
  34. +4
    -5
      hyde/tests/ext/test_drafts.py
  35. +3
    -4
      hyde/tests/ext/test_flattener.py
  36. +29
    -19
      hyde/tests/ext/test_grouper.py
  37. +0
    -286
      hyde/tests/ext/test_images.py
  38. +3
    -3
      hyde/tests/ext/test_less.py
  39. +3
    -3
      hyde/tests/ext/test_markings.py
  40. +10
    -10
      hyde/tests/ext/test_meta.py
  41. +5
    -4
      hyde/tests/ext/test_optipng.py
  42. +3
    -10
      hyde/tests/ext/test_paginator.py
  43. +6
    -2
      hyde/tests/ext/test_requirejs.py
  44. +3
    -5
      hyde/tests/ext/test_scss.py
  45. +0
    -366
      hyde/tests/ext/test_sorter.py
  46. +10
    -6
      hyde/tests/ext/test_stylus.py
  47. +1
    -3
      hyde/tests/ext/test_syntext.py
  48. +0
    -226
      hyde/tests/ext/test_tagger.py
  49. +4
    -6
      hyde/tests/ext/test_textlinks.py
  50. +9
    -8
      hyde/tests/ext/test_uglify.py
  51. +19
    -17
      hyde/tests/ext/test_urlcleaner.py
  52. +9
    -8
      hyde/tests/ssp/ext/banner.py
  53. +20
    -11
      hyde/tests/test_generate.py
  54. +15
    -2
      hyde/tests/test_initialize.py
  55. +56
    -32
      hyde/tests/test_jinja2template.py
  56. +4
    -0
      hyde/tests/test_layout.py
  57. +11
    -2
      hyde/tests/test_model.py
  58. +128
    -78
      hyde/tests/test_plugin.py
  59. +17
    -8
      hyde/tests/test_simple_copy.py
  60. +40
    -14
      hyde/tests/test_site.py
  61. +7
    -3
      hyde/tests/util.py
  62. +1
    -1
      hyde/util.py

+ 25
- 24
hyde/engine.py View File

@@ -32,10 +32,11 @@ class Engine(Application):
) )


@command(description='hyde - a python static website generator', @command(description='hyde - a python static website generator',
epilog='Use %(prog)s {command} -h to get help on individual commands')
epilog='Use %(prog)s {command} -h to get help'
'on individual commands')
@true('-v', '--verbose', help="Show detailed information in console") @true('-v', '--verbose', help="Show detailed information in console")
@true('-x', '--raise-exceptions', default=None, @true('-x', '--raise-exceptions', default=None,
help="Don't handle exceptions.")
help="Don't handle exceptions.")
@version('--version', version='%(prog)s ' + __version__) @version('--version', version='%(prog)s ' + __version__)
@store('-s', '--sitepath', default='.', help="Location of the hyde site") @store('-s', '--sitepath', default='.', help="Location of the hyde site")
def main(self, args): def main(self, args):
@@ -52,7 +53,7 @@ class Engine(Application):
@subcommand('create', help='Create a new hyde site.') @subcommand('create', help='Create a new hyde site.')
@store('-l', '--layout', default='basic', help='Layout for the new site') @store('-l', '--layout', default='basic', help='Layout for the new site')
@true('-f', '--force', default=False, dest='overwrite', @true('-f', '--force', default=False, dest='overwrite',
help='Overwrite the current site if it exists')
help='Overwrite the current site if it exists')
def create(self, args): def create(self, args):
""" """
The create command. Creates a new site from the template at the given The create command. Creates a new site from the template at the given
@@ -64,27 +65,27 @@ class Engine(Application):


if exists and not args.overwrite: if exists and not args.overwrite:
raise HydeException( raise HydeException(
"The given site path [%s] already contains a hyde site."
" Use -f to overwrite." % sitepath)
"The given site path [%s] already contains a hyde site."
" Use -f to overwrite." % sitepath)
layout = Layout.find_layout(args.layout) layout = Layout.find_layout(args.layout)
self.logger.info( self.logger.info(
"Creating site at [%s] with layout [%s]" % (sitepath, layout)) "Creating site at [%s] with layout [%s]" % (sitepath, layout))
if not layout or not layout.exists: if not layout or not layout.exists:
raise HydeException( raise HydeException(
"The given layout is invalid. Please check if you have the"
" `layout` in the right place and the environment variable(%s)"
" has been setup properly if you are using custom path for"
" layouts" % HYDE_DATA)
"The given layout is invalid. Please check if you have the"
" `layout` in the right place and the environment variable(%s)"
" has been setup properly if you are using custom path for"
" layouts" % HYDE_DATA)
layout.copy_contents_to(args.sitepath) layout.copy_contents_to(args.sitepath)
self.logger.info("Site creation complete") self.logger.info("Site creation complete")


@subcommand('gen', help='Generate the site') @subcommand('gen', help='Generate the site')
@store('-c', '--config-path', default='site.yaml', dest='config', @store('-c', '--config-path', default='site.yaml', dest='config',
help='The configuration used to generate the site')
help='The configuration used to generate the site')
@store('-d', '--deploy-path', dest='deploy', default=None, @store('-d', '--deploy-path', dest='deploy', default=None,
help='Where should the site be generated?')
help='Where should the site be generated?')
@true('-r', '--regen', dest='regen', default=False, @true('-r', '--regen', dest='regen', default=False,
help='Regenerate the whole site, including unchanged files')
help='Regenerate the whole site, including unchanged files')
def gen(self, args): def gen(self, args):
""" """
The generate command. Generates the site at the given The generate command. Generates the site at the given
@@ -103,13 +104,13 @@ class Engine(Application):


@subcommand('serve', help='Serve the website') @subcommand('serve', help='Serve the website')
@store('-a', '--address', default='localhost', dest='address', @store('-a', '--address', default='localhost', dest='address',
help='The address where the website must be served from.')
help='The address where the website must be served from.')
@store('-p', '--port', type=int, default=8080, dest='port', @store('-p', '--port', type=int, default=8080, dest='port',
help='The port where the website must be served from.')
help='The port where the website must be served from.')
@store('-c', '--config-path', default='site.yaml', dest='config', @store('-c', '--config-path', default='site.yaml', dest='config',
help='The configuration used to generate the site')
help='The configuration used to generate the site')
@store('-d', '--deploy-path', dest='deploy', default=None, @store('-d', '--deploy-path', dest='deploy', default=None,
help='Where should the site be generated?')
help='Where should the site be generated?')
def serve(self, args): def serve(self, args):
""" """
The serve command. Serves the site at the given The serve command. Serves the site at the given
@@ -120,7 +121,8 @@ class Engine(Application):
site = self.make_site(sitepath, args.config, args.deploy) site = self.make_site(sitepath, args.config, args.deploy)
from hyde.server import HydeWebServer from hyde.server import HydeWebServer
server = HydeWebServer(site, args.address, args.port) server = HydeWebServer(site, args.address, args.port)
self.logger.info("Starting webserver at [%s]:[%d]", args.address, args.port)
self.logger.info(
"Starting webserver at [%s]:[%d]", args.address, args.port)
try: try:
server.serve_forever() server.serve_forever()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
@@ -131,11 +133,11 @@ class Engine(Application):


@subcommand('publish', help='Publish the website') @subcommand('publish', help='Publish the website')
@store('-c', '--config-path', default='site.yaml', dest='config', @store('-c', '--config-path', default='site.yaml', dest='config',
help='The configuration used to generate the site')
help='The configuration used to generate the site')
@store('-p', '--publisher', dest='publisher', default='default', @store('-p', '--publisher', dest='publisher', default='default',
help='Points to the publisher configuration.')
help='Points to the publisher configuration.')
@store('-m', '--message', dest='message', @store('-m', '--message', dest='message',
help='Optional message.')
help='Optional message.')
def publish(self, args): def publish(self, args):
""" """
Publishes the site based on the configuration from the `target` Publishes the site based on the configuration from the `target`
@@ -145,11 +147,10 @@ class Engine(Application):
site = self.make_site(sitepath, args.config) site = self.make_site(sitepath, args.config)
from hyde.publisher import Publisher from hyde.publisher import Publisher
publisher = Publisher.load_publisher(site, publisher = Publisher.load_publisher(site,
args.publisher,
args.message)
args.publisher,
args.message)
publisher.publish() publisher.publish()



def make_site(self, sitepath, config, deploy=None): def make_site(self, sitepath, config, deploy=None):
""" """
Creates a site object from the given sitepath and the config file. Creates a site object from the given sitepath and the config file.
@@ -157,4 +158,4 @@ class Engine(Application):
config = Config(sitepath, config_file=config) config = Config(sitepath, config_file=config)
if deploy: if deploy:
config.deploy_root = deploy config.deploy_root = deploy
return Site(sitepath, config)
return Site(sitepath, config)

+ 1
- 2
hyde/exceptions.py View File

@@ -1,4 +1,5 @@
class HydeException(Exception): class HydeException(Exception):

""" """
Base class for exceptions from hyde Base class for exceptions from hyde
""" """
@@ -7,5 +8,3 @@ class HydeException(Exception):
def reraise(message, exc_info): def reraise(message, exc_info):
_, _, tb = exc_info _, _, tb = exc_info
raise HydeException(message), None, tb raise HydeException(message), None, tb



+ 3
- 4
hyde/ext/plugins/blog.py View File

@@ -10,13 +10,12 @@ from hyde.plugin import Plugin


class DraftsPlugin(Plugin): class DraftsPlugin(Plugin):



def begin_site(self): def begin_site(self):


in_production = self.site.config.mode.startswith('prod') in_production = self.site.config.mode.startswith('prod')
if not in_production: if not in_production:
self.logger.info(
'Generating draft posts as the site is not in production mode.')
self.logger.info('Generating draft posts as the site is'
'not in production mode.')
return return


for resource in self.site.content.walk_resources(): for resource in self.site.content.walk_resources():
@@ -33,4 +32,4 @@ class DraftsPlugin(Plugin):


self.logger.info( self.logger.info(
'%s is%s draft' % (resource, '%s is%s draft' % (resource,
'' if is_draft else ' not'))
'' if is_draft else ' not'))

+ 30
- 24
hyde/ext/plugins/css.py View File

@@ -18,7 +18,9 @@ from fswrap import File
# Less CSS # Less CSS
# #



class LessCSSPlugin(CLTransformer): class LessCSSPlugin(CLTransformer):

""" """
The plugin class for less css The plugin class for less css
""" """
@@ -29,7 +31,6 @@ class LessCSSPlugin(CLTransformer):
re.compile('^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$', re.compile('^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
re.MULTILINE) re.MULTILINE)



@property @property
def executable_name(self): def executable_name(self):
return "lessc" return "lessc"
@@ -39,7 +40,7 @@ class LessCSSPlugin(CLTransformer):
Check user defined Check user defined
""" """
return resource.source_file.kind == 'less' and \ return resource.source_file.kind == 'less' and \
getattr(resource, 'meta', {}).get('parse', True)
getattr(resource, 'meta', {}).get('parse', True)


def _should_replace_imports(self, resource): def _should_replace_imports(self, resource):
return getattr(resource, 'meta', {}).get('uses_template', True) return getattr(resource, 'meta', {}).get('uses_template', True)
@@ -72,13 +73,13 @@ class LessCSSPlugin(CLTransformer):
afile = File(afile.path + '.less') afile = File(afile.path + '.less')
ref = self.site.content.resource_from_path(afile.path) ref = self.site.content.resource_from_path(afile.path)
if not ref: if not ref:
raise HydeException("Cannot import from path [%s]" % afile.path)
raise HydeException(
"Cannot import from path [%s]" % afile.path)
ref.is_processable = False ref.is_processable = False
return self.template.get_include_statement(ref.relative_path) return self.template.get_include_statement(ref.relative_path)
text = self.import_finder.sub(import_to_include, text) text = self.import_finder.sub(import_to_include, text)
return text return text



@property @property
def plugin_name(self): def plugin_name(self):
""" """
@@ -114,10 +115,10 @@ class LessCSSPlugin(CLTransformer):
try: try:
self.call_app(args) self.call_app(args)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
HydeException.reraise(
"Cannot process %s. Error occurred when "
"processing [%s]" % (self.app.name, resource.source_file),
sys.exc_info())
HydeException.reraise(
"Cannot process %s. Error occurred when "
"processing [%s]" % (self.app.name, resource.source_file),
sys.exc_info())


return target.read_all() return target.read_all()


@@ -127,6 +128,7 @@ class LessCSSPlugin(CLTransformer):
# #


class StylusPlugin(CLTransformer): class StylusPlugin(CLTransformer):

""" """
The plugin class for stylus css The plugin class for stylus css
""" """
@@ -162,7 +164,8 @@ class StylusPlugin(CLTransformer):
if not match.lastindex: if not match.lastindex:
return '' return ''
path = match.groups(1)[0] path = match.groups(1)[0]
afile = File(File(resource.source_file.parent.child(path)).fully_expanded_path)
first_child = resource.source_file.parent.child(path)
afile = File(File(first_child).fully_expanded_path)
if len(afile.kind.strip()) == 0: if len(afile.kind.strip()) == 0:
afile = File(afile.path + '.styl') afile = File(afile.path + '.styl')


@@ -179,8 +182,8 @@ class StylusPlugin(CLTransformer):
else: else:
ref.is_processable = False ref.is_processable = False
return "\n" + \ return "\n" + \
self.template.get_include_statement(ref.relative_path) + \
"\n"
self.template.get_include_statement(ref.relative_path) + \
"\n"
return '@import "' + path + '"\n' return '@import "' + path + '"\n'


text = self.import_finder.sub(import_to_include, text) text = self.import_finder.sub(import_to_include, text)
@@ -196,7 +199,7 @@ class StylusPlugin(CLTransformer):
except AttributeError: except AttributeError:
mode = "production" mode = "production"


defaults = {"compress":""}
defaults = {"compress": ""}
if mode.startswith('dev'): if mode.startswith('dev'):
defaults = {} defaults = {}
return defaults return defaults
@@ -227,9 +230,9 @@ class StylusPlugin(CLTransformer):
self.call_app(args) self.call_app(args)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
HydeException.reraise( HydeException.reraise(
"Cannot process %s. Error occurred when "
"processing [%s]" % (stylus.name, resource.source_file),
sys.exc_info())
"Cannot process %s. Error occurred when "
"processing [%s]" % (stylus.name, resource.source_file),
sys.exc_info())
target = File(source.path + '.css') target = File(source.path + '.css')
return target.read_all() return target.read_all()


@@ -239,6 +242,7 @@ class StylusPlugin(CLTransformer):
# #


class CleverCSSPlugin(Plugin): class CleverCSSPlugin(Plugin):

""" """
The plugin class for CleverCSS The plugin class for CleverCSS
""" """
@@ -257,7 +261,7 @@ class CleverCSSPlugin(Plugin):
Check user defined Check user defined
""" """
return resource.source_file.kind == 'ccss' and \ return resource.source_file.kind == 'ccss' and \
getattr(resource, 'meta', {}).get('parse', True)
getattr(resource, 'meta', {}).get('parse', True)


def _should_replace_imports(self, resource): def _should_replace_imports(self, resource):
return getattr(resource, 'meta', {}).get('uses_template', True) return getattr(resource, 'meta', {}).get('uses_template', True)
@@ -282,8 +286,8 @@ class CleverCSSPlugin(Plugin):
return text return text


import_finder = re.compile( import_finder = re.compile(
'^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
re.MULTILINE)
'^\\s*@import\s+(?:\'|\")([^\'\"]*)(?:\'|\")\s*\;\s*$',
re.MULTILINE)


def import_to_include(match): def import_to_include(match):
if not match.lastindex: if not match.lastindex:
@@ -294,7 +298,8 @@ class CleverCSSPlugin(Plugin):
afile = File(afile.path + '.ccss') afile = File(afile.path + '.ccss')
ref = self.site.content.resource_from_path(afile.path) ref = self.site.content.resource_from_path(afile.path)
if not ref: if not ref:
raise HydeException("Cannot import from path [%s]" % afile.path)
raise HydeException(
"Cannot import from path [%s]" % afile.path)
ref.is_processable = False ref.is_processable = False
return self.template.get_include_statement(ref.relative_path) return self.template.get_include_statement(ref.relative_path)
text = import_finder.sub(import_to_include, text) text = import_finder.sub(import_to_include, text)
@@ -313,7 +318,9 @@ class CleverCSSPlugin(Plugin):
# Sassy CSS # Sassy CSS
# #



class SassyCSSPlugin(Plugin): class SassyCSSPlugin(Plugin):

""" """
The plugin class for SassyCSS The plugin class for SassyCSS
""" """
@@ -332,7 +339,7 @@ class SassyCSSPlugin(Plugin):
Check user defined Check user defined
""" """
return resource.source_file.kind == 'scss' and \ return resource.source_file.kind == 'scss' and \
getattr(resource, 'meta', {}).get('parse', True)
getattr(resource, 'meta', {}).get('parse', True)


@property @property
def options(self): def options(self):
@@ -364,7 +371,6 @@ class SassyCSSPlugin(Plugin):
""" """
return self.settings.get('includes', []) return self.settings.get('includes', [])



def begin_site(self): def begin_site(self):
""" """
Find all the sassycss files and set their relative deploy path. Find all the sassycss files and set their relative deploy path.
@@ -373,7 +379,7 @@ class SassyCSSPlugin(Plugin):
self.scss.STATIC_ROOT = self.site.config.content_root_path.path self.scss.STATIC_ROOT = self.site.config.content_root_path.path
self.scss.ASSETS_URL = self.site.media_url('/') self.scss.ASSETS_URL = self.site.media_url('/')
self.scss.ASSETS_ROOT = self.site.config.deploy_root_path.child( self.scss.ASSETS_ROOT = self.site.config.deploy_root_path.child(
self.site.config.media_root)
self.site.config.media_root)


for resource in self.site.content.walk_resources(): for resource in self.site.content.walk_resources():
if self._should_parse_resource(resource): if self._should_parse_resource(resource):
@@ -391,8 +397,8 @@ class SassyCSSPlugin(Plugin):
includes = [resource.node.path] + self.includes includes = [resource.node.path] + self.includes
includes = [path.rstrip(os.sep) + os.sep for path in includes] includes = [path.rstrip(os.sep) + os.sep for path in includes]
options = self.options options = self.options
if not 'load_paths' in options:
if 'load_paths' not in options:
options['load_paths'] = [] options['load_paths'] = []
options['load_paths'].extend(includes) options['load_paths'].extend(includes)
scss = self.scss.Scss(scss_opts=options, scss_vars=self.vars )
scss = self.scss.Scss(scss_opts=options, scss_vars=self.vars)
return scss.compile(text) return scss.compile(text)

+ 12
- 11
hyde/ext/plugins/depends.py View File

@@ -7,7 +7,9 @@ Depends plugin


from hyde.plugin import Plugin from hyde.plugin import Plugin



class DependsPlugin(Plugin): class DependsPlugin(Plugin):

""" """
The plugin class setting explicit dependencies. The plugin class setting explicit dependencies.
""" """
@@ -16,15 +18,14 @@ class DependsPlugin(Plugin):
super(DependsPlugin, self).__init__(site) super(DependsPlugin, self).__init__(site)


def begin_site(self): def begin_site(self):
"""
Initialize dependencies.

Go through all the nodes and resources to initialize
dependencies at each level.
"""
for resource in self.site.content.walk_resources():
self._update_resource(resource)
"""
Initialize dependencies.


Go through all the nodes and resources to initialize
dependencies at each level.
"""
for resource in self.site.content.walk_resources():
self._update_resource(resource)


def _update_resource(self, resource): def _update_resource(self, resource):
""" """
@@ -53,7 +54,7 @@ class DependsPlugin(Plugin):


for dep in depends: for dep in depends:
resource.depends.append(dep.format(node=resource.node, resource.depends.append(dep.format(node=resource.node,
resource=resource,
site=self.site,
context=self.site.context))
resource=resource,
site=self.site,
context=self.site.context))
resource.depends = list(set(resource.depends)) resource.depends = list(set(resource.depends))

+ 86
- 54
hyde/ext/plugins/images.py View File

@@ -39,6 +39,7 @@ class PILPlugin(Plugin):




class ImageSizerPlugin(PILPlugin): class ImageSizerPlugin(PILPlugin):

""" """
Each HTML page is modified to add width and height for images if Each HTML page is modified to add width and height for images if
they are not already specified. they are not already specified.
@@ -50,26 +51,28 @@ class ImageSizerPlugin(PILPlugin):
super(ImageSizerPlugin, self).__init__(site) super(ImageSizerPlugin, self).__init__(site)
self.cache = {} self.cache = {}



def _handle_img(self, resource, src, width, height): def _handle_img(self, resource, src, width, height):
"""Determine what should be added to an img tag""" """Determine what should be added to an img tag"""
if height is not None and width is not None: if height is not None and width is not None:
return "" # Nothing return "" # Nothing
if src is None: if src is None:
self.logger.warn("[%s] has an img tag without src attribute" % resource)
self.logger.warn(
"[%s] has an img tag without src attribute" % resource)
return "" # Nothing return "" # Nothing
if src not in self.cache: if src not in self.cache:
if src.startswith(self.site.config.media_url): if src.startswith(self.site.config.media_url):
path = src[len(self.site.config.media_url):].lstrip("/") path = src[len(self.site.config.media_url):].lstrip("/")
path = self.site.config.media_root_path.child(path) path = self.site.config.media_root_path.child(path)
image = self.site.content.resource_from_relative_deploy_path(path)
image = self.site.content.resource_from_relative_deploy_path(
path)
elif re.match(r'([a-z]+://|//).*', src): elif re.match(r'([a-z]+://|//).*', src):
# Not a local link # Not a local link
return "" # Nothing return "" # Nothing
elif src.startswith("/"): elif src.startswith("/"):
# Absolute resource # Absolute resource
path = src.lstrip("/") path = src.lstrip("/")
image = self.site.content.resource_from_relative_deploy_path(path)
image = self.site.content.resource_from_relative_deploy_path(
path)
else: else:
# Relative resource # Relative resource
path = resource.node.source_folder.child(src) path = resource.node.source_folder.child(src)
@@ -80,7 +83,7 @@ class ImageSizerPlugin(PILPlugin):
return "" # Nothing return "" # Nothing
if image.source_file.kind not in ['png', 'jpg', 'jpeg', 'gif']: if image.source_file.kind not in ['png', 'jpg', 'jpeg', 'gif']:
self.logger.warn( self.logger.warn(
"[%s] has an img tag not linking to an image" % resource)
"[%s] has an img tag not linking to an image" % resource)
return "" # Nothing return "" # Nothing
# Now, get the size of the image # Now, get the size of the image
try: try:
@@ -96,9 +99,9 @@ class ImageSizerPlugin(PILPlugin):
if new_width is None or new_height is None: if new_width is None or new_height is None:
return "" # Nothing return "" # Nothing
if width is not None: if width is not None:
return 'height="%d" ' % (int(width)*new_height/new_width)
return 'height="%d" ' % (int(width) * new_height / new_width)
elif height is not None: elif height is not None:
return 'width="%d" ' % (int(height)*new_width/new_height)
return 'width="%d" ' % (int(height) * new_width / new_height)
return 'height="%d" width="%d" ' % (new_height, new_width) return 'height="%d" width="%d" ' % (new_height, new_width)


def text_resource_complete(self, resource, text): def text_resource_complete(self, resource, text):
@@ -151,7 +154,7 @@ class ImageSizerPlugin(PILPlugin):
continue continue
attr = None attr = None
for tag in tags: for tag in tags:
if text[pos:(pos+len(tag)+1)] == ("%s=" % tag):
if text[pos:(pos + len(tag) + 1)] == ("%s=" % tag):
attr = tag attr = tag
pos = pos + len(tag) + 1 pos = pos + len(tag) + 1
break break
@@ -177,12 +180,13 @@ class ImageSizerPlugin(PILPlugin):


return text return text



def scale_aspect(a, b1, b2): def scale_aspect(a, b1, b2):
from math import ceil
"""
from math import ceil
"""
Scales a by b2/b1 rounding up to nearest integer Scales a by b2/b1 rounding up to nearest integer
""" """
return int(ceil(a * b2 / float(b1)))
return int(ceil(a * b2 / float(b1)))




def thumb_scale_size(orig_width, orig_height, width, height): def thumb_scale_size(orig_width, orig_height, width, height):
@@ -197,7 +201,7 @@ def thumb_scale_size(orig_width, orig_height, width, height):
width = scale_aspect(orig_width, orig_height, height) width = scale_aspect(orig_width, orig_height, height)
elif height is None: elif height is None:
height = scale_aspect(orig_height, orig_width, width) height = scale_aspect(orig_height, orig_width, width)
elif orig_width*height >= orig_height*width:
elif orig_width * height >= orig_height * width:
width = scale_aspect(orig_width, orig_height, height) width = scale_aspect(orig_width, orig_height, height)
else: else:
height = scale_aspect(orig_height, orig_width, width) height = scale_aspect(orig_height, orig_width, width)
@@ -208,7 +212,9 @@ def thumb_scale_size(orig_width, orig_height, width, height):
# Image Thumbnails # Image Thumbnails
# #



class ImageThumbnailsPlugin(PILPlugin): class ImageThumbnailsPlugin(PILPlugin):

""" """
Provide a function to get thumbnail for any image resource. Provide a function to get thumbnail for any image resource.


@@ -239,11 +245,11 @@ class ImageThumbnailsPlugin(PILPlugin):
prefix: thumbs4_ prefix: thumbs4_
include: include:
- '*.jpg' - '*.jpg'
which means - make four thumbnails from every picture with different prefixes
and sizes
which means - make four thumbnails from every picture with different
prefixes and sizes


It is only valid to specify either width/height or larger/smaller, but not to
mix the two types.
It is only valid to specify either width/height or larger/smaller, but
not to mix the two types.


If larger/smaller are specified, then the orientation (i.e., landscape or If larger/smaller are specified, then the orientation (i.e., landscape or
portrait) is preserved while thumbnailing. portrait) is preserved while thumbnailing.
@@ -256,7 +262,8 @@ class ImageThumbnailsPlugin(PILPlugin):
def __init__(self, site): def __init__(self, site):
super(ImageThumbnailsPlugin, self).__init__(site) super(ImageThumbnailsPlugin, self).__init__(site)


def thumb(self, resource, width, height, prefix, crop_type, preserve_orientation=False):
def thumb(self, resource, width, height, prefix, crop_type,
preserve_orientation=False):
""" """
Generate a thumbnail for the given image Generate a thumbnail for the given image
""" """
@@ -268,14 +275,17 @@ class ImageThumbnailsPlugin(PILPlugin):
# for simple maintenance but keep original deploy path to preserve # for simple maintenance but keep original deploy path to preserve
# naming logic in generated site # naming logic in generated site
path = os.path.join(".thumbnails", path = os.path.join(".thumbnails",
os.path.dirname(resource.get_relative_deploy_path()),
os.path.dirname(
resource.get_relative_deploy_path()),
"%s%s" % (prefix, name)) "%s%s" % (prefix, name))
target = resource.site.config.content_root_path.child_file(path) target = resource.site.config.content_root_path.child_file(path)
res = self.site.content.add_resource(target) res = self.site.content.add_resource(target)
res.set_relative_deploy_path(res.get_relative_deploy_path().replace('.thumbnails/', '', 1))
res.set_relative_deploy_path(
res.get_relative_deploy_path().replace('.thumbnails/', '', 1))


target.parent.make() target.parent.make()
if os.path.exists(target.path) and os.path.getmtime(resource.path) <= os.path.getmtime(target.path):
if (os.path.exists(target.path) and os.path.getmtime(resource.path) <=
os.path.getmtime(target.path)):
return return
self.logger.debug("Making thumbnail for [%s]" % resource) self.logger.debug("Making thumbnail for [%s]" % resource)


@@ -285,17 +295,18 @@ class ImageThumbnailsPlugin(PILPlugin):
format = im.format format = im.format


if preserve_orientation and im.size[1] > im.size[0]: if preserve_orientation and im.size[1] > im.size[0]:
width, height = height, width
width, height = height, width


resize_width, resize_height = thumb_scale_size(im.size[0], im.size[1], width, height)
resize_width, resize_height = thumb_scale_size(
im.size[0], im.size[1], width, height)


self.logger.debug("Resize to: %d,%d" % (resize_width, resize_height)) self.logger.debug("Resize to: %d,%d" % (resize_width, resize_height))
im = im.resize((resize_width, resize_height), self.Image.ANTIALIAS) im = im.resize((resize_width, resize_height), self.Image.ANTIALIAS)
if width is not None and height is not None: if width is not None and height is not None:
shiftx = shifty = 0 shiftx = shifty = 0
if crop_type == "center": if crop_type == "center":
shiftx = (im.size[0] - width)/2
shifty = (im.size[1] - height)/2
shiftx = (im.size[0] - width) / 2
shifty = (im.size[1] - height) / 2
elif crop_type == "bottomright": elif crop_type == "bottomright":
shiftx = (im.size[0] - width) shiftx = (im.size[0] - width)
shifty = (im.size[1] - height) shifty = (im.size[1] - height)
@@ -304,22 +315,23 @@ class ImageThumbnailsPlugin(PILPlugin):


options = dict(optimize=True) options = dict(optimize=True)
if format == "JPEG": if format == "JPEG":
options['quality'] = 75
options['quality'] = 75


im.save(target.path, **options) im.save(target.path, **options)


def begin_site(self): def begin_site(self):
""" """
Find any image resource that should be thumbnailed and call thumb on it.
Find any image resource that should be thumbnailed and call thumb
on it.
""" """
# Grab default values from config # Grab default values from config
config = self.site.config config = self.site.config
defaults = { "width": None,
"height": None,
"larger": None,
"smaller": None,
"crop_type": "topleft",
"prefix": 'thumb_'}
defaults = {"width": None,
"height": None,
"larger": None,
"smaller": None,
"crop_type": "topleft",
"prefix": 'thumb_'}
if hasattr(config, 'thumbnails'): if hasattr(config, 'thumbnails'):
defaults.update(config.thumbnails) defaults.update(config.thumbnails)


@@ -327,45 +339,64 @@ class ImageThumbnailsPlugin(PILPlugin):
if hasattr(node, 'meta') and hasattr(node.meta, 'thumbnails'): if hasattr(node, 'meta') and hasattr(node.meta, 'thumbnails'):
for th in node.meta.thumbnails: for th in node.meta.thumbnails:
if not hasattr(th, 'include'): if not hasattr(th, 'include'):
self.logger.error("Include is not set for node [%s]" % node)
self.logger.error(
"Include is not set for node [%s]" % node)
continue continue
include = th.include include = th.include
prefix = th.prefix if hasattr(th, 'prefix') else defaults['prefix']
height = th.height if hasattr(th, 'height') else defaults['height']
width = th.width if hasattr(th, 'width') else defaults['width']
larger = th.larger if hasattr(th, 'larger') else defaults['larger']
smaller = th.smaller if hasattr(th, 'smaller') else defaults['smaller']
crop_type = th.crop_type if hasattr(th, 'crop_type') else defaults['crop_type']
prefix = th.prefix if hasattr(
th, 'prefix') else defaults['prefix']
height = th.height if hasattr(
th, 'height') else defaults['height']
width = th.width if hasattr(
th, 'width') else defaults['width']
larger = th.larger if hasattr(
th, 'larger') else defaults['larger']
smaller = th.smaller if hasattr(
th, 'smaller') else defaults['smaller']
crop_type = th.crop_type if hasattr(
th, 'crop_type') else defaults['crop_type']
if crop_type not in ["topleft", "center", "bottomright"]: if crop_type not in ["topleft", "center", "bottomright"]:
self.logger.error("Unknown crop_type defined for node [%s]" % node)
self.logger.error(
"Unknown crop_type defined for node [%s]" % node)
continue continue
if width is None and height is None and larger is None and smaller is None:
self.logger.error("At least one of width, height, larger, or smaller must be set for node [%s]" % node)
if (width is None and height is None and larger is None and
smaller is None):
self.logger.error(
"At least one of width, height, larger, or smaller"
"must be set for node [%s]" % node)
continue continue


if ((larger is not None or smaller is not None) and if ((larger is not None or smaller is not None) and
(width is not None or height is not None)):
self.logger.error("It is not valid to specify both one of width/height and one of larger/smaller for node [%s]" % node)
(width is not None or height is not None)):
self.logger.error(
"It is not valid to specify both one of"
"width/height and one of larger/smaller"
"for node [%s]" % node)
continue continue


if larger is None and smaller is None: if larger is None and smaller is None:
preserve_orientation = False
dim1, dim2 = width, height
preserve_orientation = False
dim1, dim2 = width, height
else: else:
preserve_orientation = True
dim1, dim2 = larger, smaller
preserve_orientation = True
dim1, dim2 = larger, smaller


match_includes = lambda s: any([glob.fnmatch.fnmatch(s, inc) for inc in include])
match_includes = lambda s: any(
[glob.fnmatch.fnmatch(s, inc) for inc in include])


for resource in node.resources: for resource in node.resources:
if match_includes(resource.path): if match_includes(resource.path):
self.thumb(resource, dim1, dim2, prefix, crop_type, preserve_orientation)
self.thumb(
resource, dim1, dim2, prefix, crop_type,
preserve_orientation)


# #
# JPEG Optimization # JPEG Optimization
# #



class JPEGOptimPlugin(CLTransformer): class JPEGOptimPlugin(CLTransformer):

""" """
The plugin class for JPEGOptim The plugin class for JPEGOptim
""" """
@@ -408,7 +439,7 @@ class JPEGOptimPlugin(CLTransformer):
"strip-icc", "strip-icc",
] ]
target = File(self.site.config.deploy_root_path.child( target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.relative_deploy_path))
jpegoptim = self.app jpegoptim = self.app
args = [unicode(jpegoptim)] args = [unicode(jpegoptim)]
args.extend(self.process_args(supported)) args.extend(self.process_args(supported))
@@ -417,6 +448,7 @@ class JPEGOptimPlugin(CLTransformer):




class JPEGTranPlugin(CLTransformer): class JPEGTranPlugin(CLTransformer):

""" """
Almost like jpegoptim except it uses jpegtran. jpegtran allows to make Almost like jpegoptim except it uses jpegtran. jpegtran allows to make
progressive JPEG. Unfortunately, it only does lossless compression. If progressive JPEG. Unfortunately, it only does lossless compression. If
@@ -463,7 +495,7 @@ class JPEGTranPlugin(CLTransformer):
"copy", "copy",
] ]
source = File(self.site.config.deploy_root_path.child( source = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.relative_deploy_path))
target = File.make_temp('') target = File.make_temp('')
jpegtran = self.app jpegtran = self.app
args = [unicode(jpegtran)] args = [unicode(jpegtran)]
@@ -474,12 +506,12 @@ class JPEGTranPlugin(CLTransformer):
target.delete() target.delete()





# #
# PNG Optimization # PNG Optimization
# #


class OptiPNGPlugin(CLTransformer): class OptiPNGPlugin(CLTransformer):

""" """
The plugin class for OptiPNG The plugin class for OptiPNG
""" """
@@ -535,7 +567,7 @@ class OptiPNGPlugin(CLTransformer):
"nz" "nz"
] ]
target = File(self.site.config.deploy_root_path.child( target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.relative_deploy_path))
optipng = self.app optipng = self.app
args = [unicode(optipng)] args = [unicode(optipng)]
args.extend(self.process_args(supported)) args.extend(self.process_args(supported))


+ 11
- 5
hyde/ext/plugins/js.py View File

@@ -16,6 +16,7 @@ from fswrap import File
# #


class UglifyPlugin(CLTransformer): class UglifyPlugin(CLTransformer):

""" """
The plugin class for Uglify JS The plugin class for Uglify JS
""" """
@@ -85,7 +86,9 @@ class UglifyPlugin(CLTransformer):
out = target.read_all() out = target.read_all()
return out return out



class RequireJSPlugin(CLTransformer): class RequireJSPlugin(CLTransformer):

""" """
requirejs plugin requirejs plugin


@@ -103,6 +106,7 @@ class RequireJSPlugin(CLTransformer):


Please see the homepage of requirejs for usage details. Please see the homepage of requirejs for usage details.
""" """

def __init__(self, site): def __init__(self, site):
super(RequireJSPlugin, self).__init__(site) super(RequireJSPlugin, self).__init__(site)


@@ -124,20 +128,22 @@ class RequireJSPlugin(CLTransformer):
rjs = self.app rjs = self.app
target = File.make_temp('') target = File.make_temp('')
args = [unicode(rjs)] args = [unicode(rjs)]
args.extend(['-o', unicode(resource), ("out=" + target.fully_expanded_path)])
args.extend(
['-o', unicode(resource), ("out=" + target.fully_expanded_path)])


try: try:
self.call_app(args) self.call_app(args)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
HydeException.reraise(
"Cannot process %s. Error occurred when "
"processing [%s]" % (self.app.name, resource.source_file),
sys.exc_info())
HydeException.reraise(
"Cannot process %s. Error occurred when "
"processing [%s]" % (self.app.name, resource.source_file),
sys.exc_info())


return target.read_all() return target.read_all()




class CoffeePlugin(CLTransformer): class CoffeePlugin(CLTransformer):

""" """
The plugin class for Coffeescript The plugin class for Coffeescript
""" """


+ 10
- 6
hyde/ext/plugins/languages.py View File

@@ -5,7 +5,9 @@ Contains classes to help manage multi-language pages.


from hyde.plugin import Plugin from hyde.plugin import Plugin



class LanguagePlugin(Plugin): class LanguagePlugin(Plugin):

""" """
Each page should be tagged with a language using `language` meta Each page should be tagged with a language using `language` meta
data. Each page should also have an UUID stored in `uuid` meta data. Each page should also have an UUID stored in `uuid` meta
@@ -36,7 +38,8 @@ class LanguagePlugin(Plugin):


def __init__(self, site): def __init__(self, site):
super(LanguagePlugin, self).__init__(site) super(LanguagePlugin, self).__init__(site)
self.languages = {} # Associate a UUID to the list of resources available
# Associate a UUID to the list of resources available
self.languages = {}


def begin_site(self): def begin_site(self):
""" """
@@ -60,8 +63,9 @@ class LanguagePlugin(Plugin):
resource.translations = \ resource.translations = \
[r for r in resources [r for r in resources
if r.meta.language != language] if r.meta.language != language]
translations = ",".join([t.meta.language for t in resource.translations])
self.logger.debug(
"Adding translations for resource [%s] from %s to %s" % (resource,
language,
translations))
translations = ",".join(
[t.meta.language for t in resource.translations])
self.logger.debug("Adding translations for resource"
"[%s] from %s to %s" % (resource,
language,
translations))

+ 61
- 42
hyde/ext/plugins/meta.py View File

@@ -26,6 +26,7 @@ import yaml
# #


class Metadata(Expando): class Metadata(Expando):

""" """
Container class for yaml meta data. Container class for yaml meta data.
""" """
@@ -49,6 +50,7 @@ class Metadata(Expando):




class MetaPlugin(Plugin): class MetaPlugin(Plugin):

""" """
Metadata plugin for hyde. Loads meta data in the following order: Metadata plugin for hyde. Loads meta data in the following order:


@@ -66,8 +68,8 @@ class MetaPlugin(Plugin):
def __init__(self, site): def __init__(self, site):
super(MetaPlugin, self).__init__(site) super(MetaPlugin, self).__init__(site)
self.yaml_finder = re.compile( self.yaml_finder = re.compile(
r"^\s*(?:---|===)\s*\n((?:.|\n)+?)\n\s*(?:---|===)\s*\n*",
re.MULTILINE)
r"^\s*(?:---|===)\s*\n((?:.|\n)+?)\n\s*(?:---|===)\s*\n*",
re.MULTILINE)


def begin_site(self): def begin_site(self):
""" """
@@ -88,7 +90,8 @@ class MetaPlugin(Plugin):
if not hasattr(resource, 'meta'): if not hasattr(resource, 'meta'):
resource.meta = Metadata({}, node.meta) resource.meta = Metadata({}, node.meta)
if resource.source_file.is_text and not resource.simple_copy: if resource.source_file.is_text and not resource.simple_copy:
self.__read_resource__(resource, resource.source_file.read_all())
self.__read_resource__(
resource, resource.source_file.read_all())


def __read_resource__(self, resource, text): def __read_resource__(self, resource, text):
""" """
@@ -96,7 +99,8 @@ class MetaPlugin(Plugin):
the resource. Load meta data by looking for the marker. the resource. Load meta data by looking for the marker.
Once loaded, remove the meta area from the text. Once loaded, remove the meta area from the text.
""" """
self.logger.debug("Trying to load metadata from resource [%s]" % resource)
self.logger.debug(
"Trying to load metadata from resource [%s]" % resource)
match = re.match(self.yaml_finder, text) match = re.match(self.yaml_finder, text)
if not match: if not match:
self.logger.debug("No metadata found in resource [%s]" % resource) self.logger.debug("No metadata found in resource [%s]" % resource)
@@ -113,7 +117,7 @@ class MetaPlugin(Plugin):
resource.meta.update(data) resource.meta.update(data)
self.__update_standard_attributes__(resource) self.__update_standard_attributes__(resource)
self.logger.debug("Successfully loaded metadata from resource [%s]" self.logger.debug("Successfully loaded metadata from resource [%s]"
% resource)
% resource)
return text or ' ' return text or ' '


def __update_standard_attributes__(self, obj): def __update_standard_attributes__(self, obj):
@@ -165,6 +169,7 @@ class MetaPlugin(Plugin):
# #


class AutoExtendPlugin(Plugin): class AutoExtendPlugin(Plugin):

""" """
The plugin class for extending templates using metadata. The plugin class for extending templates using metadata.
""" """
@@ -204,9 +209,9 @@ class AutoExtendPlugin(Plugin):
extended_text += '\n' extended_text += '\n'
if block: if block:
extended_text += ('%s\n%s\n%s' % extended_text += ('%s\n%s\n%s' %
(self.t_block_open_tag(block),
text,
self.t_block_close_tag(block)))
(self.t_block_open_tag(block),
text,
self.t_block_close_tag(block)))
else: else:
extended_text += text extended_text += text
return extended_text return extended_text
@@ -218,6 +223,7 @@ class AutoExtendPlugin(Plugin):
# #


class Tag(Expando): class Tag(Expando):

""" """
A simple object that represents a tag. A simple object that represents a tag.
""" """
@@ -255,6 +261,7 @@ def get_tagger_sort_method(site):
sys.exc_info()) sys.exc_info())
return walker return walker



def walk_resources_tagged_with(node, tag): def walk_resources_tagged_with(node, tag):
tags = set(unicode(tag).split('+')) tags = set(unicode(tag).split('+'))
walker = get_tagger_sort_method(node.site) walker = get_tagger_sort_method(node.site)
@@ -266,7 +273,9 @@ def walk_resources_tagged_with(node, tag):
if tags <= taglist: if tags <= taglist:
yield resource yield resource



class TaggerPlugin(Plugin): class TaggerPlugin(Plugin):

""" """
Tagger plugin for hyde. Adds the ability to do tag resources and search Tagger plugin for hyde. Adds the ability to do tag resources and search
based on the tags. based on the tags.
@@ -286,6 +295,7 @@ class TaggerPlugin(Plugin):
target: blog/tags target: blog/tags
archive_extension: html archive_extension: html
""" """

def __init__(self, site): def __init__(self, site):
super(TaggerPlugin, self).__init__(site) super(TaggerPlugin, self).__init__(site)


@@ -295,11 +305,13 @@ class TaggerPlugin(Plugin):
and methods for walking tagged resources. and methods for walking tagged resources.
""" """
self.logger.debug("Adding tags from metadata") self.logger.debug("Adding tags from metadata")
config = self.site.config
content = self.site.content
# *F841 local variable 'config' is assigned to but never used
# config = self.site.config
# *F841 local variable 'content' is assigned to but never used
# content = self.site.content
tags = {} tags = {}
add_method(Node, add_method(Node,
'walk_resources_tagged_with', walk_resources_tagged_with)
'walk_resources_tagged_with', walk_resources_tagged_with)
walker = get_tagger_sort_method(self.site) walker = get_tagger_sort_method(self.site)
for resource in walker(): for resource in walker():
self._process_tags_in_resource(resource, tags) self._process_tags_in_resource(resource, tags)
@@ -337,14 +349,14 @@ class TaggerPlugin(Plugin):
return return


for tagname in taglist: for tagname in taglist:
if not tagname in tags:
if tagname not in tags:
tag = Tag(tagname) tag = Tag(tagname)
tags[tagname] = tag tags[tagname] = tag
tag.resources.append(resource) tag.resources.append(resource)
add_method(Node, add_method(Node,
'walk_resources_tagged_with_%s' % tagname,
walk_resources_tagged_with,
tag=tag)
'walk_resources_tagged_with_%s' % tagname,
walk_resources_tagged_with,
tag=tag)
else: else:
tags[tagname].resources.append(resource) tags[tagname].resources.append(resource)
if not hasattr(resource, 'tags'): if not hasattr(resource, 'tags'):
@@ -367,13 +379,13 @@ class TaggerPlugin(Plugin):
for name, config in archive_config.to_dict().iteritems(): for name, config in archive_config.to_dict().iteritems():
self._create_tag_archive(config) self._create_tag_archive(config)



def _create_tag_archive(self, config): def _create_tag_archive(self, config):
""" """
Generates archives for each tag based on the given configuration. Generates archives for each tag based on the given configuration.
""" """
if not 'template' in config:
raise HydeException("No Template specified in tagger configuration.")
if 'template' not in config:
raise HydeException(
"No Template specified in tagger configuration.")
content = self.site.content.source_folder content = self.site.content.source_folder
source = Folder(config.get('source', '')) source = Folder(config.get('source', ''))
target = content.child_folder(config.get('target', 'tags')) target = content.child_folder(config.get('target', 'tags'))
@@ -441,15 +453,17 @@ def filter_method(item, settings=None):
break break
return all_match return all_match



def attributes_checker(item, attributes=None): def attributes_checker(item, attributes=None):
""" """
Checks if the given list of attributes exist. Checks if the given list of attributes exist.
""" """
try: try:
attrgetter(*attributes)(item)
return True
attrgetter(*attributes)(item)
return True
except AttributeError: except AttributeError:
return False
return False



def sort_method(node, settings=None): def sort_method(node, settings=None):
""" """
@@ -471,11 +485,12 @@ def sort_method(node, settings=None):
resources = ifilter(lambda x: excluder_(x) and filter_(x), resources = ifilter(lambda x: excluder_(x) and filter_(x),
node.walk_resources()) node.walk_resources())
return sorted(resources, return sorted(resources,
key=attrgetter(*attr),
reverse=reverse)
key=attrgetter(*attr),
reverse=reverse)




class SorterPlugin(Plugin): class SorterPlugin(Plugin):

""" """
Sorter plugin for hyde. Adds the ability to do Sorter plugin for hyde. Adds the ability to do
sophisticated sorting by expanding the site objects sophisticated sorting by expanding the site objects
@@ -529,8 +544,8 @@ class SorterPlugin(Plugin):
setattr(Resource, next_att, None) setattr(Resource, next_att, None)


walker = getattr(self.site.content, walker = getattr(self.site.content,
sort_method_name,
self.site.content.walk_resources)
sort_method_name,
self.site.content.walk_resources)
first, last = None, None first, last = None, None
for prev, next in pairwalk(walker()): for prev, next in pairwalk(walker()):
if not first: if not first:
@@ -555,7 +570,9 @@ class SorterPlugin(Plugin):


Grouper = namedtuple('Grouper', 'group resources') Grouper = namedtuple('Grouper', 'group resources')



class Group(Expando): class Group(Expando):

""" """
A wrapper class for groups. Adds methods for A wrapper class for groups. Adds methods for
grouping resources. grouping resources.
@@ -573,21 +590,21 @@ class Group(Expando):
super(Group, self).__init__(grouping) super(Group, self).__init__(grouping)


add_method(Node, add_method(Node,
'walk_%s_groups' % self.name,
Group.walk_groups_in_node,
group=self)
'walk_%s_groups' % self.name,
Group.walk_groups_in_node,
group=self)
add_method(Node, add_method(Node,
'walk_resources_grouped_by_%s' % self.name,
Group.walk_resources,
group=self)
'walk_resources_grouped_by_%s' % self.name,
Group.walk_resources,
group=self)
add_property(Resource, add_property(Resource,
'%s_group' % self.name,
Group.get_resource_group,
group=self)
'%s_group' % self.name,
Group.get_resource_group,
group=self)
add_method(Resource, add_method(Resource,
'walk_%s_groups' % self.name,
Group.walk_resource_groups,
group=self)
'walk_%s_groups' % self.name,
Group.walk_resource_groups,
group=self)


def set_expando(self, key, value): def set_expando(self, key, value):
""" """
@@ -612,9 +629,9 @@ class Group(Expando):
group_name = None group_name = None


return next((g for g in group.walk_groups() return next((g for g in group.walk_groups()
if g.name == group_name), None) \
if group_name \
else None
if g.name == group_name), None) \
if group_name \
else None


@staticmethod @staticmethod
def walk_resource_groups(resource, group): def walk_resource_groups(resource, group):
@@ -693,7 +710,9 @@ class Group(Expando):
if group_value == self.name: if group_value == self.name:
yield resource yield resource



class GrouperPlugin(Plugin): class GrouperPlugin(Plugin):

""" """
Grouper plugin for hyde. Adds the ability to do Grouper plugin for hyde. Adds the ability to do
group resources and nodes in an arbitrary group resources and nodes in an arbitrary
@@ -726,6 +745,7 @@ class GrouperPlugin(Plugin):
Helpful snippets and tweaks to Helpful snippets and tweaks to
make hyde more awesome. make hyde more awesome.
""" """

def __init__(self, site): def __init__(self, site):
super(GrouperPlugin, self).__init__(site) super(GrouperPlugin, self).__init__(site)


@@ -748,9 +768,8 @@ class GrouperPlugin(Plugin):
setattr(Resource, next_att, None) setattr(Resource, next_att, None)
self.site.grouper[name] = Group(grouping) self.site.grouper[name] = Group(grouping)
walker = Group.walk_resources( walker = Group.walk_resources(
self.site.content, self.site.grouper[name])
self.site.content, self.site.grouper[name])


for prev, next in pairwalk(walker): for prev, next in pairwalk(walker):
setattr(next, prev_att, prev) setattr(next, prev_att, prev)
setattr(prev, next_att, next) setattr(prev, next_att, next)


+ 32
- 26
hyde/ext/plugins/sphinx.py View File

@@ -63,6 +63,7 @@ except ImportError:




class SphinxPlugin(Plugin): class SphinxPlugin(Plugin):

"""The plugin class for rendering sphinx-generated documentation.""" """The plugin class for rendering sphinx-generated documentation."""


def __init__(self, site): def __init__(self, site):
@@ -93,7 +94,7 @@ class SphinxPlugin(Plugin):
else: else:
for name in dir(user_settings): for name in dir(user_settings):
if not name.startswith("_"): if not name.startswith("_"):
setattr(settings,name,getattr(user_settings,name))
setattr(settings, name, getattr(user_settings, name))
return settings return settings


@property @property
@@ -109,11 +110,11 @@ class SphinxPlugin(Plugin):
conf_path = self.site.sitepath.child_folder(conf_path) conf_path = self.site.sitepath.child_folder(conf_path)
# Sphinx always execs the config file in its parent dir. # Sphinx always execs the config file in its parent dir.
conf_file = conf_path.child("conf.py") conf_file = conf_path.child("conf.py")
self._sphinx_config = {"__file__":conf_file}
self._sphinx_config = {"__file__": conf_file}
curdir = os.getcwd() curdir = os.getcwd()
os.chdir(conf_path.path) os.chdir(conf_path.path)
try: try:
execfile(conf_file,self._sphinx_config)
execfile(conf_file, self._sphinx_config)
finally: finally:
os.chdir(curdir) os.chdir(curdir)
return self._sphinx_config return self._sphinx_config
@@ -132,16 +133,17 @@ class SphinxPlugin(Plugin):
# We need to: # We need to:
# * change the deploy name from .rst to .html # * change the deploy name from .rst to .html
# * if a block_map is given, switch off default_block # * if a block_map is given, switch off default_block
suffix = self.sphinx_config.get("source_suffix",".rst")
suffix = self.sphinx_config.get("source_suffix", ".rst")
for resource in self.site.content.walk_resources(): for resource in self.site.content.walk_resources():
if resource.source_file.path.endswith(suffix): if resource.source_file.path.endswith(suffix):
new_name = resource.source_file.name_without_extension + ".html"
new_name = resource.source_file.name_without_extension + \
".html"
target_folder = File(resource.relative_deploy_path).parent target_folder = File(resource.relative_deploy_path).parent
resource.relative_deploy_path = target_folder.child(new_name) resource.relative_deploy_path = target_folder.child(new_name)
if settings.block_map: if settings.block_map:
resource.meta.default_block = None resource.meta.default_block = None


def begin_text_resource(self,resource,text):
def begin_text_resource(self, resource, text):
"""Event hook for processing an individual resource. """Event hook for processing an individual resource.


If the input resource is a sphinx input file, this method will replace If the input resource is a sphinx input file, this method will replace
@@ -151,7 +153,7 @@ class SphinxPlugin(Plugin):
This means that if no sphinx-related resources need updating, then This means that if no sphinx-related resources need updating, then
we entirely avoid running sphinx. we entirely avoid running sphinx.
""" """
suffix = self.sphinx_config.get("source_suffix",".rst")
suffix = self.sphinx_config.get("source_suffix", ".rst")
if not resource.source_file.path.endswith(suffix): if not resource.source_file.path.endswith(suffix):
return text return text
if self.sphinx_build_dir is None: if self.sphinx_build_dir is None:
@@ -164,9 +166,9 @@ class SphinxPlugin(Plugin):
if not settings.block_map: if not settings.block_map:
output.append(sphinx_output["body"]) output.append(sphinx_output["body"])
else: else:
for (nm,content) in sphinx_output.iteritems():
for (nm, content) in sphinx_output.iteritems():
try: try:
block = getattr(settings.block_map,nm)
block = getattr(settings.block_map, nm)
except AttributeError: except AttributeError:
pass pass
else: else:
@@ -198,41 +200,45 @@ class SphinxPlugin(Plugin):
conf_path = self.settings.conf_path conf_path = self.settings.conf_path
conf_path = self.site.sitepath.child_folder(conf_path) conf_path = self.site.sitepath.child_folder(conf_path)
conf_file = conf_path.child("conf.py") conf_file = conf_path.child("conf.py")
logger.error("Please ensure %s is a valid sphinx config",conf_file)
logger.error(
"Please ensure %s is a valid sphinx config", conf_file)
logger.error("or set sphinx.conf_path to the directory") logger.error("or set sphinx.conf_path to the directory")
logger.error("containing your sphinx conf.py") logger.error("containing your sphinx conf.py")
raise raise
# Check that the hyde_json extension is loaded # Check that the hyde_json extension is loaded
extensions = sphinx_config.get("extensions",[])
extensions = sphinx_config.get("extensions", [])
if "hyde.ext.plugins.sphinx" not in extensions: if "hyde.ext.plugins.sphinx" not in extensions:
logger.error("The hyde_json sphinx extension is not configured.") logger.error("The hyde_json sphinx extension is not configured.")
logger.error("Please add 'hyde.ext.plugins.sphinx' to the list") logger.error("Please add 'hyde.ext.plugins.sphinx' to the list")
logger.error("of extensions in your sphinx conf.py file.") logger.error("of extensions in your sphinx conf.py file.")
logger.info("(set sphinx.sanity_check=false to disable this check)")
logger.info(
"(set sphinx.sanity_check=false to disable this check)")
raise RuntimeError("sphinx is not configured correctly") raise RuntimeError("sphinx is not configured correctly")
# Check that the master doc exists in the source tree. # Check that the master doc exists in the source tree.
master_doc = sphinx_config.get("master_doc","index")
master_doc += sphinx_config.get("source_suffix",".rst")
master_doc = os.path.join(self.site.content.path,master_doc)
master_doc = sphinx_config.get("master_doc", "index")
master_doc += sphinx_config.get("source_suffix", ".rst")
master_doc = os.path.join(self.site.content.path, master_doc)
if not os.path.exists(master_doc): if not os.path.exists(master_doc):
logger.error("The sphinx master document doesn't exist.") logger.error("The sphinx master document doesn't exist.")
logger.error("Please create the file %s",master_doc)
logger.error("Please create the file %s", master_doc)
logger.error("or change the 'master_doc' setting in your") logger.error("or change the 'master_doc' setting in your")
logger.error("sphinx conf.py file.") logger.error("sphinx conf.py file.")
logger.info("(set sphinx.sanity_check=false to disable this check)")
logger.info(
"(set sphinx.sanity_check=false to disable this check)")
raise RuntimeError("sphinx is not configured correctly") raise RuntimeError("sphinx is not configured correctly")
# Check that I am *before* the other plugins, # Check that I am *before* the other plugins,
# with the possible exception of MetaPlugin # with the possible exception of MetaPlugin
for plugin in self.site.plugins: for plugin in self.site.plugins:
if plugin is self: if plugin is self:
break break
if not isinstance(plugin,_MetaPlugin):
if not isinstance(plugin, _MetaPlugin):
logger.error("The sphinx plugin is installed after the") logger.error("The sphinx plugin is installed after the")
logger.error("plugin %r.",plugin.__class__.__name__)
logger.error("plugin %r.", plugin.__class__.__name__)
logger.error("It's quite likely that this will break things.") logger.error("It's quite likely that this will break things.")
logger.error("Please move the sphinx plugin to the top") logger.error("Please move the sphinx plugin to the top")
logger.error("of the plugins list.") logger.error("of the plugins list.")
logger.info("(sphinx.sanity_check=false to disable this check)")
logger.info(
"(sphinx.sanity_check=false to disable this check)")
raise RuntimeError("sphinx is not configured correctly") raise RuntimeError("sphinx is not configured correctly")


def _run_sphinx(self): def _run_sphinx(self):
@@ -254,7 +260,7 @@ class SphinxPlugin(Plugin):
if sphinx.main(sphinx_args) != 0: if sphinx.main(sphinx_args) != 0:
raise RuntimeError("sphinx build failed") raise RuntimeError("sphinx build failed")


def _get_sphinx_output(self,resource):
def _get_sphinx_output(self, resource):
"""Get the sphinx output for a given resource. """Get the sphinx output for a given resource.


This returns a dict mapping block names to HTML text fragments. This returns a dict mapping block names to HTML text fragments.
@@ -263,13 +269,14 @@ class SphinxPlugin(Plugin):
related pages and so-on. related pages and so-on.
""" """
relpath = File(resource.relative_path) relpath = File(resource.relative_path)
relpath = relpath.parent.child(relpath.name_without_extension+".fjson")
with open(self.sphinx_build_dir.child(relpath),"rb") as f:
relpath = relpath.parent.child(
relpath.name_without_extension + ".fjson")
with open(self.sphinx_build_dir.child(relpath), "rb") as f:
return json.load(f) return json.load(f)





class HydeJSONHTMLBuilder(JSONHTMLBuilder): class HydeJSONHTMLBuilder(JSONHTMLBuilder):

"""A slightly-customised JSONHTMLBuilder, for use by Hyde. """A slightly-customised JSONHTMLBuilder, for use by Hyde.


This is a Sphinx builder that serilises the generated HTML fragments into This is a Sphinx builder that serilises the generated HTML fragments into
@@ -280,6 +287,7 @@ class HydeJSONHTMLBuilder(JSONHTMLBuilder):
work correctly once things have been processed by Hyde. work correctly once things have been processed by Hyde.
""" """
name = "hyde_json" name = "hyde_json"

def get_target_uri(self, docname, typ=None): def get_target_uri(self, docname, typ=None):
return docname + ".html" return docname + ".html"


@@ -291,5 +299,3 @@ def setup(app):
Hyde plugin. It simply registers the HydeJSONHTMLBuilder class. Hyde plugin. It simply registers the HydeJSONHTMLBuilder class.
""" """
app.add_builder(HydeJSONHTMLBuilder) app.add_builder(HydeJSONHTMLBuilder)



+ 32
- 18
hyde/ext/plugins/structure.py View File

@@ -20,9 +20,11 @@ import operator
# #


class FlattenerPlugin(Plugin): class FlattenerPlugin(Plugin):

""" """
The plugin class for flattening nested folders. The plugin class for flattening nested folders.
""" """

def __init__(self, site): def __init__(self, site):
super(FlattenerPlugin, self).__init__(site) super(FlattenerPlugin, self).__init__(site)


@@ -50,7 +52,7 @@ class FlattenerPlugin(Plugin):
target_path = target.child(resource.name) target_path = target.child(resource.name)
self.logger.debug( self.logger.debug(
'Flattening resource path [%s] to [%s]' % 'Flattening resource path [%s] to [%s]' %
(resource, target_path))
(resource, target_path))
resource.relative_deploy_path = target_path resource.relative_deploy_path = target_path
for child in node.walk(): for child in node.walk():
child.relative_deploy_path = target.path child.relative_deploy_path = target.path
@@ -61,12 +63,14 @@ class FlattenerPlugin(Plugin):
# #


class CombinePlugin(Plugin): class CombinePlugin(Plugin):

""" """
To use this combine, the following configuration should be added To use this combine, the following configuration should be added
to meta data:: to meta data::
combine: combine:
sort: false #Optional. Defaults to true. sort: false #Optional. Defaults to true.
root: content/media #Optional. Path must be relative to content folder - default current folder
root: content/media #Optional. Path must be relative to content
folder - default current folder
recurse: true #Optional. Default false. recurse: true #Optional. Default false.
files: files:
- ns1.*.js - ns1.*.js
@@ -97,13 +101,14 @@ class CombinePlugin(Plugin):
except AttributeError: except AttributeError:
raise AttributeError("No resources to combine for [%s]" % resource) raise AttributeError("No resources to combine for [%s]" % resource)
if type(files) is str: if type(files) is str:
files = [ files ]
files = [files]


# Grab resources to combine # Grab resources to combine


# select site root # select site root
try: try:
root = self.site.content.node_from_relative_path(resource.meta.combine.root)
root = self.site.content.node_from_relative_path(
resource.meta.combine.root)
except AttributeError: except AttributeError:
root = resource.node root = resource.node


@@ -122,10 +127,12 @@ class CombinePlugin(Plugin):
sort = True sort = True


if sort: if sort:
resources = sorted([r for r in walker if any(fnmatch(r.name, f) for f in files)],
key=operator.attrgetter('name'))
resources = sorted([r for r in walker
if any(fnmatch(r.name, f) for f in files)],
key=operator.attrgetter('name'))
else: else:
resources = [(f, r) for r in walker for f in files if fnmatch(r.name, f)]
resources = [(f, r)
for r in walker for f in files if fnmatch(r.name, f)]
resources = [r[1] for f in files for r in resources if f in r] resources = [r[1] for f in files for r in resources if f in r]


if not resources: if not resources:
@@ -173,7 +180,7 @@ class CombinePlugin(Plugin):
except AttributeError: except AttributeError:
pass pass


if where not in [ "top", "bottom" ]:
if where not in ["top", "bottom"]:
raise ValueError("%r should be either `top` or `bottom`" % where) raise ValueError("%r should be either `top` or `bottom`" % where)


self.logger.debug( self.logger.debug(
@@ -190,11 +197,14 @@ class CombinePlugin(Plugin):
# #


class Page: class Page:

def __init__(self, posts, number): def __init__(self, posts, number):
self.posts = posts self.posts = posts
self.number = number self.number = number



class Paginator: class Paginator:

""" """
Iterates resources which have pages associated with them. Iterates resources which have pages associated with them.
""" """
@@ -204,7 +214,8 @@ class Paginator:
def __init__(self, settings): def __init__(self, settings):
self.sorter = getattr(settings, 'sorter', None) self.sorter = getattr(settings, 'sorter', None)
self.size = getattr(settings, 'size', 10) self.size = getattr(settings, 'size', 10)
self.file_pattern = getattr(settings, 'file_pattern', self.file_pattern)
self.file_pattern = getattr(
settings, 'file_pattern', self.file_pattern)


def _relative_url(self, source_path, number, basename, ext): def _relative_url(self, source_path, number, basename, ext):
""" """
@@ -214,8 +225,8 @@ class Paginator:
path = File(source_path) path = File(source_path)
if number != 1: if number != 1:
filename = self.file_pattern.replace('$PAGE', str(number)) \ filename = self.file_pattern.replace('$PAGE', str(number)) \
.replace('$FILE', basename) \
.replace('$EXT', ext)
.replace('$FILE', basename) \
.replace('$EXT', ext)
path = path.parent.child(os.path.normpath(filename)) path = path.parent.child(os.path.normpath(filename))
return path return path


@@ -227,10 +238,11 @@ class Paginator:
res = Resource(base_resource.source_file, node) res = Resource(base_resource.source_file, node)
res.node.meta = Metadata(node.meta) res.node.meta = Metadata(node.meta)
res.meta = Metadata(base_resource.meta, res.node.meta) res.meta = Metadata(base_resource.meta, res.node.meta)
brs = base_resource.source_file
path = self._relative_url(base_resource.relative_path, path = self._relative_url(base_resource.relative_path,
page_number,
base_resource.source_file.name_without_extension,
base_resource.source_file.extension)
page_number,
brs.name_without_extension,
brs.extension)
res.set_relative_deploy_path(path) res.set_relative_deploy_path(path)
return res return res


@@ -250,7 +262,7 @@ class Paginator:
if not hasattr(resource, 'depends'): if not hasattr(resource, 'depends'):
resource.depends = [] resource.depends = []
resource.depends.extend([dep.relative_path for dep in dependencies resource.depends.extend([dep.relative_path for dep in dependencies
if dep.relative_path not in resource.depends])
if dep.relative_path not in resource.depends])


def _walk_pages_in_node(self, node): def _walk_pages_in_node(self, node):
""" """
@@ -294,6 +306,7 @@ class Paginator:




class PaginatorPlugin(Plugin): class PaginatorPlugin(Plugin):

""" """
Paginator plugin. Paginator plugin.


@@ -315,6 +328,7 @@ class PaginatorPlugin(Plugin):
{{ resource.page.next }} {{ resource.page.next }}


""" """

def __init__(self, site): def __init__(self, site):
super(PaginatorPlugin, self).__init__(site) super(PaginatorPlugin, self).__init__(site)


@@ -322,10 +336,10 @@ class PaginatorPlugin(Plugin):
for node in self.site.content.walk(): for node in self.site.content.walk():
added_resources = [] added_resources = []
paged_resources = (res for res in node.resources paged_resources = (res for res in node.resources
if hasattr(res.meta, 'paginator'))
if hasattr(res.meta, 'paginator'))
for resource in paged_resources: for resource in paged_resources:
paginator = Paginator(resource.meta.paginator) paginator = Paginator(resource.meta.paginator)
added_resources += paginator.walk_paged_resources(node, resource)
added_resources += paginator.walk_paged_resources(
node, resource)


node.resources += added_resources node.resources += added_resources


+ 15
- 5
hyde/ext/plugins/text.py View File

@@ -3,7 +3,7 @@
Text processing plugins Text processing plugins
""" """


from hyde.plugin import Plugin,TextyPlugin
from hyde.plugin import Plugin, TextyPlugin




# #
@@ -11,9 +11,11 @@ from hyde.plugin import Plugin,TextyPlugin
# #


class BlockdownPlugin(TextyPlugin): class BlockdownPlugin(TextyPlugin):

""" """
The plugin class for block text replacement. The plugin class for block text replacement.
""" """

def __init__(self, site): def __init__(self, site):
super(BlockdownPlugin, self).__init__(site) super(BlockdownPlugin, self).__init__(site)


@@ -55,9 +57,11 @@ class BlockdownPlugin(TextyPlugin):
# #


class MarkingsPlugin(TextyPlugin): class MarkingsPlugin(TextyPlugin):

""" """
The plugin class for mark text replacement. The plugin class for mark text replacement.
""" """

def __init__(self, site): def __init__(self, site):
super(MarkingsPlugin, self).__init__(site) super(MarkingsPlugin, self).__init__(site)


@@ -99,9 +103,11 @@ class MarkingsPlugin(TextyPlugin):
# #


class ReferencePlugin(TextyPlugin): class ReferencePlugin(TextyPlugin):

""" """
The plugin class for reference text replacement. The plugin class for reference text replacement.
""" """

def __init__(self, site): def __init__(self, site):
super(ReferencePlugin, self).__init__(site) super(ReferencePlugin, self).__init__(site)


@@ -143,9 +149,11 @@ class ReferencePlugin(TextyPlugin):
# #


class SyntextPlugin(TextyPlugin): class SyntextPlugin(TextyPlugin):

""" """
The plugin class for syntax text replacement. The plugin class for syntax text replacement.
""" """

def __init__(self, site): def __init__(self, site):
super(SyntextPlugin, self).__init__(site) super(SyntextPlugin, self).__init__(site)


@@ -170,7 +178,6 @@ class SyntextPlugin(TextyPlugin):
""" """
return '^\s*~~~+\s*$' return '^\s*~~~+\s*$'



def get_params(self, match, start=True): def get_params(self, match, start=True):
""" """
~~~css~~~ will return css ~~~css~~~ will return css
@@ -199,16 +206,18 @@ class SyntextPlugin(TextyPlugin):
# #


class TextlinksPlugin(Plugin): class TextlinksPlugin(Plugin):

""" """
The plugin class for text link replacement. The plugin class for text link replacement.
""" """

def __init__(self, site): def __init__(self, site):
super(TextlinksPlugin, self).__init__(site) super(TextlinksPlugin, self).__init__(site)
import re import re
self.content_link = re.compile('\[\[([^\]^!][^\]]*)\]\]', self.content_link = re.compile('\[\[([^\]^!][^\]]*)\]\]',
re.UNICODE|re.MULTILINE)
re.UNICODE | re.MULTILINE)
self.media_link = re.compile('\[\[\!\!([^\]]*)\]\]', self.media_link = re.compile('\[\[\!\!([^\]]*)\]\]',
re.UNICODE|re.MULTILINE)
re.UNICODE | re.MULTILINE)


def begin_text_resource(self, resource, text): def begin_text_resource(self, resource, text):
""" """
@@ -221,11 +230,12 @@ class TextlinksPlugin(Plugin):
""" """
if not resource.uses_template: if not resource.uses_template:
return text return text

def replace_content(match): def replace_content(match):
return self.template.get_content_url_statement(match.groups(1)[0]) return self.template.get_content_url_statement(match.groups(1)[0])

def replace_media(match): def replace_media(match):
return self.template.get_media_url_statement(match.groups(1)[0]) return self.template.get_media_url_statement(match.groups(1)[0])
text = self.content_link.sub(replace_content, text) text = self.content_link.sub(replace_content, text)
text = self.media_link.sub(replace_media, text) text = self.media_link.sub(replace_media, text)
return text return text


+ 5
- 3
hyde/ext/plugins/urls.py View File

@@ -8,7 +8,9 @@ from hyde.site import Site
from functools import wraps from functools import wraps
from fswrap import File from fswrap import File



class UrlCleanerPlugin(Plugin): class UrlCleanerPlugin(Plugin):

""" """
Url Cleaner plugin for hyde. Adds to hyde the ability to generate clean Url Cleaner plugin for hyde. Adds to hyde the ability to generate clean
urls. urls.
@@ -53,13 +55,13 @@ class UrlCleanerPlugin(Plugin):
def wrapper(site, path, safe=None): def wrapper(site, path, safe=None):
url = urlgetter(site, path, safe) url = urlgetter(site, path, safe)
index_file_names = getattr(settings, index_file_names = getattr(settings,
'index_file_names',
['index.html'])
'index_file_names',
['index.html'])
rep = File(url) rep = File(url)
if rep.name in index_file_names: if rep.name in index_file_names:
url = rep.parent.path.rstrip('/') url = rep.parent.path.rstrip('/')
if hasattr(settings, 'append_slash') and \ if hasattr(settings, 'append_slash') and \
settings.append_slash:
settings.append_slash:
url += '/' url += '/'
elif hasattr(settings, 'strip_extensions'): elif hasattr(settings, 'strip_extensions'):
if rep.kind in settings.strip_extensions: if rep.kind in settings.strip_extensions:


+ 14
- 7
hyde/ext/plugins/vcs.py View File

@@ -12,9 +12,11 @@ import subprocess




class VCSDatesPlugin(Plugin): class VCSDatesPlugin(Plugin):

""" """
Base class for getting resource timestamps from VCS. Base class for getting resource timestamps from VCS.
""" """

def __init__(self, site, vcs_name='vcs'): def __init__(self, site, vcs_name='vcs'):
super(VCSDatesPlugin, self).__init__(site) super(VCSDatesPlugin, self).__init__(site)
self.vcs_name = vcs_name self.vcs_name = vcs_name
@@ -38,8 +40,8 @@ class VCSDatesPlugin(Plugin):


if created == "git": if created == "git":
created = date_created or \ created = date_created or \
datetime.utcfromtimestamp(
os.path.getctime(resource.path))
datetime.utcfromtimestamp(
os.path.getctime(resource.path))
created = created.replace(tzinfo=None) created = created.replace(tzinfo=None)
resource.meta.created = created resource.meta.created = created


@@ -48,7 +50,6 @@ class VCSDatesPlugin(Plugin):
modified = modified.replace(tzinfo=None) modified = modified.replace(tzinfo=None)
resource.meta.modified = modified resource.meta.modified = modified



def get_dates(self): def get_dates(self):
""" """
Extract creation and last modification date from the vcs and include Extract creation and last modification date from the vcs and include
@@ -60,7 +61,10 @@ class VCSDatesPlugin(Plugin):
# #
# Git Dates # Git Dates
# #


class GitDatesPlugin(VCSDatesPlugin): class GitDatesPlugin(VCSDatesPlugin):

def __init__(self, site): def __init__(self, site):
super(GitDatesPlugin, self).__init__(site, 'git') super(GitDatesPlugin, self).__init__(site, 'git')


@@ -78,7 +82,8 @@ class GitDatesPlugin(VCSDatesPlugin):
]).split("\n") ]).split("\n")
commits = commits[:-1] commits = commits[:-1]
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
self.logger.warning("Unable to get git history for [%s]" % resource)
self.logger.warning(
"Unable to get git history for [%s]" % resource)
commits = None commits = None


if commits: if commits:
@@ -93,6 +98,8 @@ class GitDatesPlugin(VCSDatesPlugin):
# #
# Mercurial Dates # Mercurial Dates
# #


class MercurialDatesPlugin(VCSDatesPlugin): class MercurialDatesPlugin(VCSDatesPlugin):


def __init__(self, site): def __init__(self, site):
@@ -105,12 +112,12 @@ class MercurialDatesPlugin(VCSDatesPlugin):
# Run hg log --template={date|isodatesec} # Run hg log --template={date|isodatesec}
try: try:
commits = subprocess.check_output([ commits = subprocess.check_output([
"hg", "log", "--template={date|isodatesec}\n",
resource.path]).split('\n')
"hg", "log", "--template={date|isodatesec}\n",
resource.path]).split('\n')
commits = commits[:-1] commits = commits[:-1]
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
self.logger.warning("Unable to get mercurial history for [%s]" self.logger.warning("Unable to get mercurial history for [%s]"
% resource)
% resource)
commits = None commits = None


if not commits: if not commits:


+ 16
- 11
hyde/ext/publishers/dvcs.py View File

@@ -8,6 +8,7 @@ from hyde.publisher import Publisher
import abc import abc
from subprocess import Popen, PIPE from subprocess import Popen, PIPE



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


@@ -19,23 +20,28 @@ class DVCS(Publisher):
self.switch(self.branch) self.switch(self.branch)


@abc.abstractmethod @abc.abstractmethod
def pull(self): pass
def pull(self):
pass


@abc.abstractmethod @abc.abstractmethod
def push(self): pass
def push(self):
pass


@abc.abstractmethod @abc.abstractmethod
def commit(self, message): pass
def commit(self, message):
pass


@abc.abstractmethod @abc.abstractmethod
def switch(self, branch): pass
def switch(self, branch):
pass


@abc.abstractmethod @abc.abstractmethod
def add(self, path="."): pass
def add(self, path="."):
pass


@abc.abstractmethod @abc.abstractmethod
def merge(self, branch): pass
def merge(self, branch):
pass


def publish(self): def publish(self):
super(DVCS, self).publish() super(DVCS, self).publish()
@@ -47,8 +53,8 @@ class DVCS(Publisher):
self.push() self.push()





class Git(DVCS): class Git(DVCS):

""" """
Acts as a publisher to a git repository. Can be used to publish to Acts as a publisher to a git repository. Can be used to publish to
github pages. github pages.
@@ -56,7 +62,7 @@ class Git(DVCS):


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



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

+ 22
- 24
hyde/ext/publishers/pyfs.py View File

@@ -32,15 +32,14 @@ except ImportError:
raise raise





class PyFS(Publisher): class PyFS(Publisher):


def initialize(self, settings): def initialize(self, settings):
self.settings = settings self.settings = settings
self.url = settings.url self.url = settings.url
self.check_mtime = getattr(settings,"check_mtime",False)
self.check_etag = getattr(settings,"check_etag",False)
if self.check_etag and not isinstance(self.check_etag,basestring):
self.check_mtime = getattr(settings, "check_mtime", False)
self.check_etag = getattr(settings, "check_etag", False)
if self.check_etag and not isinstance(self.check_etag, basestring):
raise ValueError("check_etag must name the etag algorithm") raise ValueError("check_etag must name the etag algorithm")
self.prompt_for_credentials() self.prompt_for_credentials()
self.fs = fsopendir(self.url) self.fs = fsopendir(self.url)
@@ -58,48 +57,47 @@ class PyFS(Publisher):
def publish(self): def publish(self):
super(PyFS, self).publish() super(PyFS, self).publish()
deploy_fs = OSFS(self.site.config.deploy_root_path.path) deploy_fs = OSFS(self.site.config.deploy_root_path.path)
for (dirnm,local_filenms) in deploy_fs.walk():
logger.info("Making directory: %s",dirnm)
self.fs.makedir(dirnm,allow_recreate=True)
remote_fileinfos = self.fs.listdirinfo(dirnm,files_only=True)
for (dirnm, local_filenms) in deploy_fs.walk():
logger.info("Making directory: %s", dirnm)
self.fs.makedir(dirnm, allow_recreate=True)
remote_fileinfos = self.fs.listdirinfo(dirnm, files_only=True)
# Process each local file, to see if it needs updating. # Process each local file, to see if it needs updating.
for filenm in local_filenms: for filenm in local_filenms:
filepath = pathjoin(dirnm,filenm)
filepath = pathjoin(dirnm, filenm)
# Try to find an existing remote file, to compare metadata. # Try to find an existing remote file, to compare metadata.
for (nm,info) in remote_fileinfos:
for (nm, info) in remote_fileinfos:
if nm == filenm: if nm == filenm:
break break
else: else:
info = {} info = {}
# Skip it if the etags match # Skip it if the etags match
if self.check_etag and "etag" in info: if self.check_etag and "etag" in info:
with deploy_fs.open(filepath,"rb") as f:
with deploy_fs.open(filepath, "rb") as f:
local_etag = self._calculate_etag(f) local_etag = self._calculate_etag(f)
if info["etag"] == local_etag: if info["etag"] == local_etag:
logger.info("Skipping file [etag]: %s",filepath)
logger.info("Skipping file [etag]: %s", filepath)
continue continue
# Skip it if the mtime is more recent remotely. # Skip it if the mtime is more recent remotely.
if self.check_mtime and "modified_time" in info: if self.check_mtime and "modified_time" in info:
local_mtime = deploy_fs.getinfo(filepath)["modified_time"] local_mtime = deploy_fs.getinfo(filepath)["modified_time"]
if info["modified_time"] > local_mtime: if info["modified_time"] > local_mtime:
logger.info("Skipping file [mtime]: %s",filepath)
logger.info("Skipping file [mtime]: %s", filepath)
continue continue
# Upload it to the remote filesystem. # Upload it to the remote filesystem.
logger.info("Uploading file: %s",filepath)
with deploy_fs.open(filepath,"rb") as f:
self.fs.setcontents(filepath,f)
logger.info("Uploading file: %s", filepath)
with deploy_fs.open(filepath, "rb") as f:
self.fs.setcontents(filepath, f)
# Process each remote file, to see if it needs deleting. # Process each remote file, to see if it needs deleting.
for (filenm,info) in remote_fileinfos:
filepath = pathjoin(dirnm,filenm)
for (filenm, info) in remote_fileinfos:
filepath = pathjoin(dirnm, filenm)
if filenm not in local_filenms: if filenm not in local_filenms:
logger.info("Removing file: %s",filepath)
logger.info("Removing file: %s", filepath)
self.fs.remove(filepath) self.fs.remove(filepath)


def _calculate_etag(self,f):
hasher = getattr(hashlib,self.check_etag.lower())()
data = f.read(1024*64)
def _calculate_etag(self, f):
hasher = getattr(hashlib, self.check_etag.lower())()
data = f.read(1024 * 64)
while data: while data:
hasher.update(data) hasher.update(data)
data = f.read(1024*64)
data = f.read(1024 * 64)
return hasher.hexdigest() return hasher.hexdigest()


+ 23
- 24
hyde/ext/publishers/pypi.py View File

@@ -19,16 +19,14 @@ from commando.util import getLoggerWithNullHandler
logger = getLoggerWithNullHandler('hyde.ext.publishers.pypi') logger = getLoggerWithNullHandler('hyde.ext.publishers.pypi')






class PyPI(Publisher): class PyPI(Publisher):


def initialize(self, settings): def initialize(self, settings):
self.settings = settings self.settings = settings
self.project = settings.project self.project = settings.project
self.url = getattr(settings,"url","https://pypi.python.org/pypi/")
self.username = getattr(settings,"username",None)
self.password = getattr(settings,"password",None)
self.url = getattr(settings, "url", "https://pypi.python.org/pypi/")
self.username = getattr(settings, "username", None)
self.password = getattr(settings, "password", None)
self.prompt_for_credentials() self.prompt_for_credentials()


def prompt_for_credentials(self): def prompt_for_credentials(self):
@@ -38,12 +36,13 @@ class PyPI(Publisher):
else: else:
pypirc = ConfigParser.RawConfigParser() pypirc = ConfigParser.RawConfigParser()
pypirc.read([pypirc_file]) pypirc.read([pypirc_file])
missing_errs = (ConfigParser.NoSectionError,ConfigParser.NoOptionError)
missing_errs = (
ConfigParser.NoSectionError, ConfigParser.NoOptionError)
# Try to find username in .pypirc # Try to find username in .pypirc
if self.username is None: if self.username is None:
if pypirc is not None: if pypirc is not None:
try: try:
self.username = pypirc.get("server-login","username")
self.username = pypirc.get("server-login", "username")
except missing_errs: except missing_errs:
pass pass
# Prompt for username on command-line # Prompt for username on command-line
@@ -54,7 +53,7 @@ class PyPI(Publisher):
if self.password is None: if self.password is None:
if pypirc is not None: if pypirc is not None:
try: try:
self.password = pypirc.get("server-login","password")
self.password = pypirc.get("server-login", "password")
except missing_errs: except missing_errs:
pass pass
# Prompt for username on command-line # Prompt for username on command-line
@@ -73,11 +72,11 @@ class PyPI(Publisher):
# Bundle it up into a zipfile # Bundle it up into a zipfile
logger.info("building the zipfile") logger.info("building the zipfile")
root = self.site.config.deploy_root_path root = self.site.config.deploy_root_path
zf = zipfile.ZipFile(tf,"w",zipfile.ZIP_DEFLATED)
zf = zipfile.ZipFile(tf, "w", zipfile.ZIP_DEFLATED)
try: try:
for item in root.walker.walk_files(): for item in root.walker.walk_files():
logger.info(" adding file: %s",item.path)
zf.write(item.path,item.get_relative_path(root))
logger.info(" adding file: %s", item.path)
zf.write(item.path, item.get_relative_path(root))
finally: finally:
zf.close() zf.close()
# Formulate the necessary bits for the HTTP POST. # Formulate the necessary bits for the HTTP POST.
@@ -85,12 +84,14 @@ class PyPI(Publisher):
authz = self.username + ":" + self.password authz = self.username + ":" + self.password
authz = "Basic " + standard_b64encode(authz) authz = "Basic " + standard_b64encode(authz)
boundary = "-----------" + os.urandom(20).encode("hex") boundary = "-----------" + os.urandom(20).encode("hex")
sep_boundary = "\r\n--" + boundary
end_boundary = "\r\n--" + boundary + "--\r\n"
# *F841 local variable 'sep_boundary' is assigned to but never used
# sep_boundary = "\r\n--" + boundary
# *F841 local variable 'end_boundary' is assigned to but never used
# end_boundary = "\r\n--" + boundary + "--\r\n"
content_type = "multipart/form-data; boundary=%s" % (boundary,) content_type = "multipart/form-data; boundary=%s" % (boundary,)
items = ((":action","doc_upload"),("name",self.project))
items = ((":action", "doc_upload"), ("name", self.project))
body_prefix = "" body_prefix = ""
for (name,value) in items:
for (name, value) in items:
body_prefix += "--" + boundary + "\r\n" body_prefix += "--" + boundary + "\r\n"
body_prefix += "Content-Disposition: form-data; name=\"" body_prefix += "Content-Disposition: form-data; name=\""
body_prefix += name + "\"\r\n\r\n" body_prefix += name + "\"\r\n\r\n"
@@ -110,24 +111,24 @@ class PyPI(Publisher):
con.connect() con.connect()
try: try:
con.putrequest("POST", self.url) con.putrequest("POST", self.url)
con.putheader("Content-Type",content_type)
con.putheader("Content-Length",str(content_length))
con.putheader("Authorization",authz)
con.putheader("Content-Type", content_type)
con.putheader("Content-Length", str(content_length))
con.putheader("Authorization", authz)
con.endheaders() con.endheaders()
con.send(body_prefix) con.send(body_prefix)
tf.seek(0) tf.seek(0)
data = tf.read(1024*32)
data = tf.read(1024 * 32)
while data: while data:
con.send(data) con.send(data)
data = tf.read(1024*32)
data = tf.read(1024 * 32)
con.send(body_suffix) con.send(body_suffix)
r = con.getresponse() r = con.getresponse()
try: try:
# PyPI tries to redirect to the page on success. # PyPI tries to redirect to the page on success.
if r.status in (200,301,):
if r.status in (200, 301,):
logger.info("success!") logger.info("success!")
else: else:
msg = "Upload failed: %s %s" % (r.status,r.reason,)
msg = "Upload failed: %s %s" % (r.status, r.reason,)
raise Exception(msg) raise Exception(msg)
finally: finally:
r.close() r.close()
@@ -135,5 +136,3 @@ class PyPI(Publisher):
con.close() con.close()
finally: finally:
tf.close() tf.close()



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

@@ -34,7 +34,9 @@ from hyde.publisher import Publisher


from subprocess import Popen, PIPE from subprocess import Popen, PIPE



class SSH(Publisher): class SSH(Publisher):

def initialize(self, settings): def initialize(self, settings):
self.settings = settings self.settings = settings
self.username = settings.username self.username = settings.username
@@ -47,7 +49,7 @@ class SSH(Publisher):
command = "{command} {opts} ./ {username}{server}:{target}".format( command = "{command} {opts} ./ {username}{server}:{target}".format(
command=self.command, command=self.command,
opts=self.opts, opts=self.opts,
username=self.username+'@' if self.username else '',
username=self.username + '@' if self.username else '',
server=self.server, server=self.server,
target=self.target) target=self.target)
deploy_path = self.site.config.deploy_root_path.path deploy_path = self.site.config.deploy_root_path.path


+ 160
- 117
hyde/ext/templates/jinja.py View File

@@ -29,10 +29,13 @@ from commando.util import getLoggerWithNullHandler


logger = getLoggerWithNullHandler('hyde.engine.Jinja2') logger = getLoggerWithNullHandler('hyde.engine.Jinja2')



class SilentUndefined(Undefined): class SilentUndefined(Undefined):

""" """
A redefinition of undefined that eats errors. A redefinition of undefined that eats errors.
""" """

def __getattr__(self, name): def __getattr__(self, name):
return self return self


@@ -41,6 +44,7 @@ class SilentUndefined(Undefined):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self return self



@contextfunction @contextfunction
def media_url(context, path, safe=None): def media_url(context, path, safe=None):
""" """
@@ -48,6 +52,7 @@ def media_url(context, path, safe=None):
""" """
return context['site'].media_url(path, safe) return context['site'].media_url(path, safe)



@contextfunction @contextfunction
def content_url(context, path, safe=None): def content_url(context, path, safe=None):
""" """
@@ -55,6 +60,7 @@ def content_url(context, path, safe=None):
""" """
return context['site'].content_url(path, safe) return context['site'].content_url(path, safe)



@contextfunction @contextfunction
def full_url(context, path, safe=None): def full_url(context, path, safe=None):
""" """
@@ -62,6 +68,7 @@ def full_url(context, path, safe=None):
""" """
return context['site'].full_url(path, safe) return context['site'].full_url(path, safe)



@contextfilter @contextfilter
def urlencode(ctx, url, safe=None): def urlencode(ctx, url, safe=None):
if safe is not None: if safe is not None:
@@ -69,16 +76,18 @@ def urlencode(ctx, url, safe=None):
else: else:
return quote(url.encode('utf8')) return quote(url.encode('utf8'))



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



@contextfilter @contextfilter
def date_format(ctx, dt, fmt=None): def date_format(ctx, dt, fmt=None):
if not dt: if not dt:
dt = datetime.now() dt = datetime.now()
if not isinstance(dt, datetime) or \ if not isinstance(dt, datetime) or \
not isinstance(dt, date):
not isinstance(dt, date):
logger.error("Date format called on a non date object") logger.error("Date format called on a non date object")
return dt return dt


@@ -93,9 +102,11 @@ def date_format(ctx, dt, fmt=None):
def islice(iterable, start=0, stop=3, step=1): def islice(iterable, start=0, stop=3, step=1):
return itertools.islice(iterable, start, stop, step) return itertools.islice(iterable, start, stop, step)



def top(iterable, count=3): def top(iterable, count=3):
return islice(iterable, stop=count) return islice(iterable, stop=count)



def xmldatetime(dt): def xmldatetime(dt):
if not dt: if not dt:
dt = datetime.now() dt = datetime.now()
@@ -105,6 +116,7 @@ def xmldatetime(dt):
zprefix = tz[:3] + ":" + tz[3:] zprefix = tz[:3] + ":" + tz[3:]
return dt.strftime("%Y-%m-%dT%H:%M:%S") + zprefix return dt.strftime("%Y-%m-%dT%H:%M:%S") + zprefix



@environmentfilter @environmentfilter
def asciidoc(env, value): def asciidoc(env, value):
""" """
@@ -122,9 +134,11 @@ def asciidoc(env, value):
asciidoc = AsciiDocAPI() asciidoc = AsciiDocAPI()
asciidoc.options('--no-header-footer') asciidoc.options('--no-header-footer')
result = StringIO.StringIO() result = StringIO.StringIO()
asciidoc.execute(StringIO.StringIO(output.encode('utf-8')), result, backend='html4')
asciidoc.execute(
StringIO.StringIO(output.encode('utf-8')), result, backend='html4')
return unicode(result.getvalue(), "utf-8") return unicode(result.getvalue(), "utf-8")



@environmentfilter @environmentfilter
def markdown(env, value): def markdown(env, value):
""" """
@@ -140,14 +154,15 @@ def markdown(env, value):
if hasattr(env.config, 'markdown'): if hasattr(env.config, 'markdown'):
d['extensions'] = getattr(env.config.markdown, 'extensions', []) d['extensions'] = getattr(env.config.markdown, 'extensions', [])
d['extension_configs'] = getattr(env.config.markdown, d['extension_configs'] = getattr(env.config.markdown,
'extension_configs',
Expando({})).to_dict()
'extension_configs',
Expando({})).to_dict()
if hasattr(env.config.markdown, 'output_format'): if hasattr(env.config.markdown, 'output_format'):
d['output_format'] = env.config.markdown.output_format d['output_format'] = env.config.markdown.output_format
marked = md.Markdown(**d) marked = md.Markdown(**d)


return marked.convert(output) return marked.convert(output)



@environmentfilter @environmentfilter
def restructuredtext(env, value): def restructuredtext(env, value):
""" """
@@ -161,18 +176,20 @@ def restructuredtext(env, value):


highlight_source = False highlight_source = False
if hasattr(env.config, 'restructuredtext'): if hasattr(env.config, 'restructuredtext'):
highlight_source = getattr(env.config.restructuredtext, 'highlight_source', False)
highlight_source = getattr(
env.config.restructuredtext, 'highlight_source', False)
extensions = getattr(env.config.restructuredtext, 'extensions', []) extensions = getattr(env.config.restructuredtext, 'extensions', [])
import imp import imp
for extension in extensions: for extension in extensions:
imp.load_module(extension, *imp.find_module(extension)) imp.load_module(extension, *imp.find_module(extension))


if highlight_source: if highlight_source:
import hyde.lib.pygments.rst_directive
import hyde.lib.pygments.rst_directive # noqa


parts = publish_parts(source=value, writer_name="html") parts = publish_parts(source=value, writer_name="html")
return parts['html_body'] return parts['html_body']



@environmentfilter @environmentfilter
def syntax(env, value, lexer=None, filename=None): def syntax(env, value, lexer=None, filename=None):
""" """
@@ -184,17 +201,17 @@ def syntax(env, value, lexer=None, filename=None):
from pygments import formatters from pygments import formatters
except ImportError: except ImportError:
logger.error(u"pygments library is required to" logger.error(u"pygments library is required to"
" use syntax highlighting tags.")
" use syntax highlighting tags.")
raise TemplateError("Cannot load pygments") raise TemplateError("Cannot load pygments")


pyg = (lexers.get_lexer_by_name(lexer) pyg = (lexers.get_lexer_by_name(lexer)
if lexer else
lexers.guess_lexer(value))
if lexer else
lexers.guess_lexer(value))
settings = {} settings = {}
if hasattr(env.config, 'syntax'): if hasattr(env.config, 'syntax'):
settings = getattr(env.config.syntax, settings = getattr(env.config.syntax,
'options',
Expando({})).to_dict()
'options',
Expando({})).to_dict()


formatter = formatters.HtmlFormatter(**settings) formatter = formatters.HtmlFormatter(**settings)
code = pygments.highlight(value, pyg, formatter) code = pygments.highlight(value, pyg, formatter)
@@ -204,10 +221,13 @@ def syntax(env, value, lexer=None, filename=None):
if not getattr(env.config.syntax, 'use_figure', True): if not getattr(env.config.syntax, 'use_figure', True):
return Markup(code) return Markup(code)
return Markup( return Markup(
'<div class="codebox"><figure class="code">%s<figcaption>%s</figcaption></figure></div>\n\n'
% (code, caption))
'<div class="codebox"><figure class="code">%s<figcaption>'
'%s</figcaption></figure></div>\n\n'
% (code, caption))



class Spaceless(Extension): class Spaceless(Extension):

""" """
Emulates the django spaceless template tag. Emulates the django spaceless template tag.
""" """
@@ -220,10 +240,10 @@ class Spaceless(Extension):
""" """
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
body = parser.parse_statements(['name:endspaceless'], body = parser.parse_statements(['name:endspaceless'],
drop_needle=True)
drop_needle=True)
return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_spaceless'),
[], [], body).set_lineno(lineno)
self.call_method('_render_spaceless'),
[], [], body).set_lineno(lineno)


def _render_spaceless(self, caller=None): def _render_spaceless(self, caller=None):
""" """
@@ -235,7 +255,9 @@ class Spaceless(Extension):
return '' return ''
return re.sub(r'>\s+<', '><', unicode(caller().strip())) return re.sub(r'>\s+<', '><', unicode(caller().strip()))



class Asciidoc(Extension): class Asciidoc(Extension):

""" """
A wrapper around the asciidoc filter for syntactic sugar. A wrapper around the asciidoc filter for syntactic sugar.
""" """
@@ -243,14 +265,15 @@ class Asciidoc(Extension):


def parse(self, parser): def parse(self, parser):
""" """
Parses the statements and defers to the callback for asciidoc processing.
Parses the statements and defers to the callback
for asciidoc processing.
""" """
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
body = parser.parse_statements(['name:endasciidoc'], drop_needle=True) body = parser.parse_statements(['name:endasciidoc'], drop_needle=True)


return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_asciidoc'),
[], [], body).set_lineno(lineno)
self.call_method('_render_asciidoc'),
[], [], body).set_lineno(lineno)


def _render_asciidoc(self, caller=None): def _render_asciidoc(self, caller=None):
""" """
@@ -261,7 +284,9 @@ class Asciidoc(Extension):
output = caller().strip() output = caller().strip()
return asciidoc(self.environment, output) return asciidoc(self.environment, output)



class Markdown(Extension): class Markdown(Extension):

""" """
A wrapper around the markdown filter for syntactic sugar. A wrapper around the markdown filter for syntactic sugar.
""" """
@@ -269,14 +294,15 @@ class Markdown(Extension):


def parse(self, parser): def parse(self, parser):
""" """
Parses the statements and defers to the callback for markdown processing.
Parses the statements and defers to the callback
for markdown processing.
""" """
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
body = parser.parse_statements(['name:endmarkdown'], drop_needle=True) body = parser.parse_statements(['name:endmarkdown'], drop_needle=True)


return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_markdown'),
[], [], body).set_lineno(lineno)
self.call_method('_render_markdown'),
[], [], body).set_lineno(lineno)


def _render_markdown(self, caller=None): def _render_markdown(self, caller=None):
""" """
@@ -287,7 +313,9 @@ class Markdown(Extension):
output = caller().strip() output = caller().strip()
return markdown(self.environment, output) return markdown(self.environment, output)



class restructuredText(Extension): class restructuredText(Extension):

""" """
A wrapper around the restructuredtext filter for syntactic sugar A wrapper around the restructuredtext filter for syntactic sugar
""" """
@@ -298,10 +326,11 @@ class restructuredText(Extension):
Simply extract our content Simply extract our content
""" """
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
body = parser.parse_statements(['name:endrestructuredtext'], drop_needle=True)
body = parser.parse_statements(
['name:endrestructuredtext'], drop_needle=True)


return nodes.CallBlock(self.call_method('_render_rst'), [], [], body return nodes.CallBlock(self.call_method('_render_rst'), [], [], body
).set_lineno(lineno)
).set_lineno(lineno)


def _render_rst(self, caller=None): def _render_rst(self, caller=None):
""" """
@@ -312,7 +341,9 @@ class restructuredText(Extension):
output = caller().strip() output = caller().strip()
return restructuredtext(self.environment, output) return restructuredtext(self.environment, output)



class YamlVar(Extension): class YamlVar(Extension):

""" """
An extension that converts the content between the tags An extension that converts the content between the tags
into an yaml object and sets the value in the given into an yaml object and sets the value in the given
@@ -330,16 +361,15 @@ class YamlVar(Extension):
var = parser.stream.expect('name').value var = parser.stream.expect('name').value
body = parser.parse_statements(['name:endyaml'], drop_needle=True) body = parser.parse_statements(['name:endyaml'], drop_needle=True)
return [ return [
nodes.Assign(
nodes.Name(var, 'store'),
nodes.Const({})
).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_set_yaml',
args=[nodes.Name(var, 'load')]),
[], [], body).set_lineno(lineno)
]

nodes.Assign(
nodes.Name(var, 'store'),
nodes.Const({})
).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_set_yaml',
args=[nodes.Name(var, 'load')]),
[], [], body).set_lineno(lineno)
]


def _set_yaml(self, var, caller=None): def _set_yaml(self, var, caller=None):
""" """
@@ -356,6 +386,7 @@ class YamlVar(Extension):
var.update(yaml.load(out)) var.update(yaml.load(out))
return '' return ''



def parse_kwargs(parser): def parse_kwargs(parser):
""" """
Parses keyword arguments in tags. Parses keyword arguments in tags.
@@ -368,17 +399,19 @@ def parse_kwargs(parser):
value = nodes.Const(parser.stream.next().value) value = nodes.Const(parser.stream.next().value)
return (name, value) return (name, value)



class Syntax(Extension): class Syntax(Extension):

""" """
A wrapper around the syntax filter for syntactic sugar. A wrapper around the syntax filter for syntactic sugar.
""" """


tags = set(['syntax']) tags = set(['syntax'])



def parse(self, parser): def parse(self, parser):
""" """
Parses the statements and defers to the callback for pygments processing.
Parses the statements and defers to the callback for
pygments processing.
""" """
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
lex = nodes.Const(None) lex = nodes.Const(None)
@@ -392,8 +425,8 @@ class Syntax(Extension):
(_, value1) = parse_kwargs(parser) (_, value1) = parse_kwargs(parser)


(lex, filename) = (value, value1) \ (lex, filename) = (value, value1) \
if name == 'lex' \
else (value1, value)
if name == 'lex' \
else (value1, value)
else: else:
lex = nodes.Const(parser.stream.next().value) lex = nodes.Const(parser.stream.next().value)
if parser.stream.skip_if('comma'): if parser.stream.skip_if('comma'):
@@ -401,10 +434,9 @@ class Syntax(Extension):


body = parser.parse_statements(['name:endsyntax'], drop_needle=True) body = parser.parse_statements(['name:endsyntax'], drop_needle=True)
return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_syntax',
args=[lex, filename]),
[], [], body).set_lineno(lineno)

self.call_method('_render_syntax',
args=[lex, filename]),
[], [], body).set_lineno(lineno)


def _render_syntax(self, lex, filename, caller=None): def _render_syntax(self, lex, filename, caller=None):
""" """
@@ -415,7 +447,9 @@ class Syntax(Extension):
output = caller().strip() output = caller().strip()
return syntax(self.environment, output, lex, filename) return syntax(self.environment, output, lex, filename)



class IncludeText(Extension): class IncludeText(Extension):

""" """
Automatically runs `markdown` and `typogrify` on included Automatically runs `markdown` and `typogrify` on included
files. files.
@@ -429,8 +463,8 @@ class IncludeText(Extension):
""" """
node = parser.parse_include() node = parser.parse_include()
return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_include_text'),
[], [], [node]).set_lineno(node.lineno)
self.call_method('_render_include_text'),
[], [], [node]).set_lineno(node.lineno)


def _render_include_text(self, caller=None): def _render_include_text(self, caller=None):
""" """
@@ -448,7 +482,9 @@ class IncludeText(Extension):


MARKINGS = '_markings_' MARKINGS = '_markings_'



class Reference(Extension): class Reference(Extension):

""" """
Marks a block in a template such that its available for use Marks a block in a template such that its available for use
when referenced using a `refer` tag. when referenced using a `refer` tag.
@@ -465,11 +501,13 @@ class Reference(Extension):
tag = token.value tag = token.value
name = parser.stream.next().value name = parser.stream.next().value
body = parser.parse_statements(['name:end%s' % tag], drop_needle=True) body = parser.parse_statements(['name:end%s' % tag], drop_needle=True)
return nodes.CallBlock(
self.call_method('_render_output',
args=[nodes.Name(MARKINGS, 'load'), nodes.Const(name)]),
[], [], body).set_lineno(lineno)

return nodes.CallBlock(self.call_method('_render_output',
args=[
nodes.Name(MARKINGS,
'load'),
nodes.Const(name)
]), [], [],
body).set_lineno(lineno)


def _render_output(self, markings, name, caller=None): def _render_output(self, markings, name, caller=None):
""" """
@@ -482,7 +520,9 @@ class Reference(Extension):
markings[name] = out markings[name] = out
return out return out



class Refer(Extension): class Refer(Extension):

""" """
Imports content blocks specified in the referred template as Imports content blocks specified in the referred template as
variables in a given namespace. variables in a given namespace.
@@ -507,43 +547,43 @@ class Refer(Extension):
temp = parser.free_identifier(lineno) temp = parser.free_identifier(lineno)


return [ return [
nodes.Assign(
nodes.Name(temp.name, 'store'),
nodes.Name(MARKINGS, 'load')
).set_lineno(lineno),
nodes.Assign(
nodes.Name(MARKINGS, 'store'),
nodes.Const({})).set_lineno(lineno),
nodes.Assign(
nodes.Name(namespace, 'store'),
nodes.Const({})).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_push_resource',
args=[
nodes.Name(namespace, 'load'),
nodes.Name('site', 'load'),
nodes.Name('resource', 'load'),
template]),
[], [], []).set_lineno(lineno),
nodes.Assign(
nodes.Name('resource', 'store'),
nodes.Getitem(nodes.Name(namespace, 'load'),
nodes.Const('resource'), 'load')
).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_assign_reference',
args=[
nodes.Name(MARKINGS, 'load'),
nodes.Name(namespace, 'load')]),
[], [], [includeNode]).set_lineno(lineno),
nodes.Assign(nodes.Name('resource', 'store'),
nodes.Getitem(nodes.Name(namespace, 'load'),
nodes.Const('parent_resource'), 'load')
).set_lineno(lineno),
nodes.Assign(
nodes.Name(MARKINGS, 'store'),
nodes.Name(temp.name, 'load')
).set_lineno(lineno),
nodes.Assign(
nodes.Name(temp.name, 'store'),
nodes.Name(MARKINGS, 'load')
).set_lineno(lineno),
nodes.Assign(
nodes.Name(MARKINGS, 'store'),
nodes.Const({})).set_lineno(lineno),
nodes.Assign(
nodes.Name(namespace, 'store'),
nodes.Const({})).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_push_resource',
args=[
nodes.Name(namespace, 'load'),
nodes.Name('site', 'load'),
nodes.Name('resource', 'load'),
template]),
[], [], []).set_lineno(lineno),
nodes.Assign(
nodes.Name('resource', 'store'),
nodes.Getitem(nodes.Name(namespace, 'load'),
nodes.Const('resource'), 'load')
).set_lineno(lineno),
nodes.CallBlock(
self.call_method('_assign_reference',
args=[
nodes.Name(MARKINGS, 'load'),
nodes.Name(namespace, 'load')]),
[], [], [includeNode]).set_lineno(lineno),
nodes.Assign(nodes.Name('resource', 'store'),
nodes.Getitem(nodes.Name(namespace, 'load'),
nodes.Const('parent_resource'), 'load')
).set_lineno(lineno),
nodes.Assign(
nodes.Name(MARKINGS, 'store'),
nodes.Name(temp.name, 'load')
).set_lineno(lineno),
] ]


def _push_resource(self, namespace, site, resource, template, caller): def _push_resource(self, namespace, site, resource, template, caller):
@@ -553,10 +593,10 @@ class Refer(Extension):
namespace['parent_resource'] = resource namespace['parent_resource'] = resource
if not hasattr(resource, 'depends'): if not hasattr(resource, 'depends'):
resource.depends = [] resource.depends = []
if not template in resource.depends:
if template not in resource.depends:
resource.depends.append(template) resource.depends.append(template)
namespace['resource'] = site.content.resource_from_relative_path( namespace['resource'] = site.content.resource_from_relative_path(
template)
template)
return '' return ''


def _assign_reference(self, markings, namespace, caller): def _assign_reference(self, markings, namespace, caller):
@@ -573,6 +613,7 @@ class Refer(Extension):




class HydeLoader(FileSystemLoader): class HydeLoader(FileSystemLoader):

""" """
A wrapper around the file system loader that performs A wrapper around the file system loader that performs
hyde specific tweaks. hyde specific tweaks.
@@ -582,9 +623,9 @@ class HydeLoader(FileSystemLoader):
config = site.config if hasattr(site, 'config') else None config = site.config if hasattr(site, 'config') else None
if config: if config:
super(HydeLoader, self).__init__([ super(HydeLoader, self).__init__([
unicode(config.content_root_path),
unicode(config.layout_root_path),
])
unicode(config.content_root_path),
unicode(config.layout_root_path),
])
else: else:
super(HydeLoader, self).__init__(unicode(sitepath)) super(HydeLoader, self).__init__(unicode(sitepath))


@@ -604,8 +645,8 @@ class HydeLoader(FileSystemLoader):
try: try:
(contents, (contents,
filename, filename,
date) = super(HydeLoader, self).get_source(
environment, template)
date) = super(HydeLoader, self).get_source(
environment, template)
except UnicodeDecodeError: except UnicodeDecodeError:
HydeException.reraise( HydeException.reraise(
"Unicode error when processing %s" % template, sys.exc_info()) "Unicode error when processing %s" % template, sys.exc_info())
@@ -624,6 +665,7 @@ class HydeLoader(FileSystemLoader):


# pylint: disable-msg=W0104,E0602,W0613,R0201 # pylint: disable-msg=W0104,E0602,W0613,R0201
class Jinja2Template(Template): class Jinja2Template(Template):

""" """
The Jinja2 Template implementation The Jinja2 Template implementation
""" """
@@ -638,23 +680,23 @@ class Jinja2Template(Template):
self.site = site self.site = site
self.engine = engine self.engine = engine
self.preprocessor = (engine.preprocessor self.preprocessor = (engine.preprocessor
if hasattr(engine, 'preprocessor') else None)
if hasattr(engine, 'preprocessor') else None)


self.loader = HydeLoader(self.sitepath, site, self.preprocessor) self.loader = HydeLoader(self.sitepath, site, self.preprocessor)


default_extensions = [ default_extensions = [
IncludeText,
Spaceless,
Asciidoc,
Markdown,
restructuredText,
Syntax,
Reference,
Refer,
YamlVar,
'jinja2.ext.do',
'jinja2.ext.loopcontrols',
'jinja2.ext.with_'
IncludeText,
Spaceless,
Asciidoc,
Markdown,
restructuredText,
Syntax,
Reference,
Refer,
YamlVar,
'jinja2.ext.do',
'jinja2.ext.loopcontrols',
'jinja2.ext.with_'
] ]


defaults = { defaults = {
@@ -694,12 +736,12 @@ class Jinja2Template(Template):
settings['filters'][name] = getattr(module, function_name) settings['filters'][name] = getattr(module, function_name)


self.env = Environment( self.env = Environment(
loader=self.loader,
undefined=SilentUndefined,
line_statement_prefix=settings['line_statement_prefix'],
trim_blocks=True,
bytecode_cache=FileSystemBytecodeCache(),
extensions=settings['extensions'])
loader=self.loader,
undefined=SilentUndefined,
line_statement_prefix=settings['line_statement_prefix'],
trim_blocks=True,
bytecode_cache=FileSystemBytecodeCache(),
extensions=settings['extensions'])
self.env.globals['media_url'] = media_url self.env.globals['media_url'] = media_url
self.env.globals['content_url'] = content_url self.env.globals['content_url'] = content_url
self.env.globals['full_url'] = full_url self.env.globals['full_url'] = full_url
@@ -738,7 +780,6 @@ class Jinja2Template(Template):
if self.env.bytecode_cache: if self.env.bytecode_cache:
self.env.bytecode_cache.clear() self.env.bytecode_cache.clear()



def get_dependencies(self, path): def get_dependencies(self, path):
""" """
Finds dependencies hierarchically based on the included Finds dependencies hierarchically based on the included
@@ -774,10 +815,12 @@ class Jinja2Template(Template):
The pattern for matching selected template statements. The pattern for matching selected template statements.
""" """
return { return {
"block_open": '\s*\{\%\s*block\s*([^\s]+)\s*\%\}',
"block_close": '\s*\{\%\s*endblock\s*([^\s]*)\s*\%\}',
"include": '\s*\{\%\s*include\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}',
"extends": '\s*\{\%\s*extends\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}'
"block_open": '\s*\{\%\s*block\s*([^\s]+)\s*\%\}',
"block_close": '\s*\{\%\s*endblock\s*([^\s]*)\s*\%\}',
"include":
'\s*\{\%\s*include\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}',
"extends":
'\s*\{\%\s*extends\s*(?:\'|\")(.+?\.[^.]*)(?:\'|\")\s*\%\}'
} }


def get_include_statement(self, path_to_include): def get_include_statement(self, path_to_include):


+ 25
- 24
hyde/generator.py View File

@@ -43,7 +43,6 @@ class Generator(object):
site.context = Context.load(site.sitepath, site.config.context) site.context = Context.load(site.sitepath, site.config.context)
self.__context__.update(site.context) self.__context__.update(site.context)



@contextmanager @contextmanager
def context_for_resource(self, resource): def context_for_resource(self, resource):
""" """
@@ -77,7 +76,8 @@ class Generator(object):
providing restricted access to the methods. providing restricted access to the methods.
""" """


def __init__(self, preprocessor=None, postprocessor=None, context_for_path=None):
def __init__(self, preprocessor=None, postprocessor=None,
context_for_path=None):
self.preprocessor = preprocessor self.preprocessor = preprocessor
self.postprocessor = postprocessor self.postprocessor = postprocessor
self.context_for_path = context_for_path self.context_for_path = context_for_path
@@ -86,14 +86,16 @@ class Generator(object):
logger.info("Generating site at [%s]" % self.site.sitepath) logger.info("Generating site at [%s]" % self.site.sitepath)
self.template = Template.find_template(self.site) self.template = Template.find_template(self.site)
logger.debug("Using [%s] as the template", logger.debug("Using [%s] as the template",
self.template.__class__.__name__)
self.template.__class__.__name__)


logger.info("Configuring the template environment") logger.info("Configuring the template environment")
preprocessor = self.events.begin_text_resource
postprocessor = self.events.text_resource_complete
proxy = GeneratorProxy(context_for_path=self.context_for_path,
preprocessor=preprocessor,
postprocessor=postprocessor)
self.template.configure(self.site, self.template.configure(self.site,
engine=GeneratorProxy(
context_for_path=self.context_for_path,
preprocessor=self.events.begin_text_resource,
postprocessor=self.events.text_resource_complete))
engine=proxy)
self.events.template_loaded(self.template) self.events.template_loaded(self.template)


def initialize(self): def initialize(self):
@@ -124,7 +126,7 @@ class Generator(object):


rel_path = resource.relative_path rel_path = resource.relative_path
deps = self.deps[rel_path] if rel_path in self.deps \ deps = self.deps[rel_path] if rel_path in self.deps \
else self.update_deps(resource)
else self.update_deps(resource)
return deps return deps


def update_deps(self, resource): def update_deps(self, resource):
@@ -143,7 +145,8 @@ class Generator(object):
dep_res = self.site.content.resource_from_relative_path(dep) dep_res = self.site.content.resource_from_relative_path(dep)
if dep_res: if dep_res:
if dep_res.relative_path in self.waiting_deps.keys(): if dep_res.relative_path in self.waiting_deps.keys():
self.waiting_deps[dep_res.relative_path].append(rel_path)
self.waiting_deps[
dep_res.relative_path].append(rel_path)
else: else:
deps.extend(self.get_dependencies(dep_res)) deps.extend(self.get_dependencies(dep_res))
if resource.uses_template: if resource.uses_template:
@@ -166,7 +169,7 @@ class Generator(object):
self.load_site_if_needed() self.load_site_if_needed()


target = File(self.site.config.deploy_root_path.child( target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.relative_deploy_path))
if not target.exists or target.older_than(resource.source_file): if not target.exists or target.older_than(resource.source_file):
logger.debug("Found changes in %s" % resource) logger.debug("Found changes in %s" % resource)
return True return True
@@ -209,7 +212,7 @@ class Generator(object):
self.load_site_if_needed() self.load_site_if_needed()
self.events.begin_site() self.events.begin_site()
logger.info("Generating site to [%s]" % logger.info("Generating site to [%s]" %
self.site.config.deploy_root_path)
self.site.config.deploy_root_path)
self.__generate_node__(self.site.content, incremental) self.__generate_node__(self.site.content, incremental)
self.events.site_complete() self.events.site_complete()
self.finalize() self.finalize()
@@ -261,9 +264,8 @@ class Generator(object):
except HydeException: except HydeException:
self.generate_all() self.generate_all()


def generate_resource_at_path(self,
resource_path=None,
incremental=False):
def generate_resource_at_path(self, resource_path=None,
incremental=False):
""" """
Generates a single resource. If resource_path is non-existent or empty, Generates a single resource. If resource_path is non-existent or empty,
generates the entire website. generates the entire website.
@@ -311,7 +313,6 @@ class Generator(object):
self.__generate_resource__(resource, incremental) self.__generate_resource__(resource, incremental)
self.events.node_complete(node) self.events.node_complete(node)



def __generate_resource__(self, resource, incremental=False): def __generate_resource__(self, resource, incremental=False):
self.refresh_config() self.refresh_config()
if not resource.is_processable: if not resource.is_processable:
@@ -323,7 +324,7 @@ class Generator(object):
logger.debug("Processing [%s]", resource) logger.debug("Processing [%s]", resource)
with self.context_for_resource(resource) as context: with self.context_for_resource(resource) as context:
target = File(self.site.config.deploy_root_path.child( target = File(self.site.config.deploy_root_path.child(
resource.relative_deploy_path))
resource.relative_deploy_path))
target.parent.make() target.parent.make()
if resource.simple_copy: if resource.simple_copy:
logger.debug("Simply Copying [%s]", resource) logger.debug("Simply Copying [%s]", resource)
@@ -334,19 +335,19 @@ class Generator(object):
logger.debug("Rendering [%s]", resource) logger.debug("Rendering [%s]", resource)
try: try:
text = self.template.render_resource(resource, text = self.template.render_resource(resource,
context)
context)
except Exception, e: except Exception, e:
HydeException.reraise("Error occurred when"
" processing template: [%s]: %s" %
(resource, repr(e)),
sys.exc_info()
)
HydeException.reraise("Error occurred when processing"
"template: [%s]: %s" %
(resource, repr(e)),
sys.exc_info())
else: else:
text = resource.source_file.read_all() text = resource.source_file.read_all()
text = self.events.begin_text_resource(resource, text) or text
text = self.events.begin_text_resource(
resource, text) or text


text = self.events.text_resource_complete( text = self.events.text_resource_complete(
resource, text) or text
resource, text) or text
target.write(text) target.write(text)
copymode(resource.source_file.path, target.path) copymode(resource.source_file.path, target.path)
else: else:


+ 3
- 2
hyde/layout.py View File

@@ -11,6 +11,7 @@ LAYOUTS = "layouts"




class Layout(object): class Layout(object):

""" """
Represents a layout package Represents a layout package
""" """
@@ -26,10 +27,10 @@ class Layout(object):
layout_folder = None layout_folder = None
if HYDE_DATA in os.environ: if HYDE_DATA in os.environ:
layout_folder = Layout._get_layout_folder( layout_folder = Layout._get_layout_folder(
os.environ[HYDE_DATA], layout_name)
os.environ[HYDE_DATA], layout_name)
if not layout_folder: if not layout_folder:
layout_folder = Layout._get_layout_folder( layout_folder = Layout._get_layout_folder(
File(__file__).parent, layout_name)
File(__file__).parent, layout_name)
return layout_folder return layout_folder


@staticmethod @staticmethod


+ 4
- 1
hyde/lib/pygments/rst_directive.py View File

@@ -58,7 +58,9 @@ from docutils.parsers.rst import directives, Directive
from pygments import highlight from pygments import highlight
from pygments.lexers import get_lexer_by_name, TextLexer from pygments.lexers import get_lexer_by_name, TextLexer



class Pygments(Directive): class Pygments(Directive):

""" Source code syntax hightlighting. """ Source code syntax hightlighting.
""" """
required_arguments = 1 required_arguments = 1
@@ -75,7 +77,8 @@ class Pygments(Directive):
# no lexer found - use the text one instead of an exception # no lexer found - use the text one instead of an exception
lexer = TextLexer() lexer = TextLexer()
# take an arbitrary option if more than one is given # take an arbitrary option if more than one is given
formatter = self.options and VARIANTS[self.options.keys()[0]] or DEFAULT
formatter = self.options and VARIANTS[
self.options.keys()[0]] or DEFAULT
parsed = highlight(u'\n'.join(self.content), lexer, formatter) parsed = highlight(u'\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')] return [nodes.raw('', parsed, format='html')]




+ 2
- 1
hyde/main.py View File

@@ -5,9 +5,10 @@ The hyde executable
""" """
from hyde.engine import Engine from hyde.engine import Engine



def main(): def main():
"""Main""" """Main"""
Engine().run() Engine().run()


if __name__ == "__main__": if __name__ == "__main__":
main()
main()

+ 18
- 13
hyde/model.py View File

@@ -14,6 +14,7 @@ logger = getLoggerWithNullHandler('hyde.engine')


SEQS = (tuple, list, set, frozenset) SEQS = (tuple, list, set, frozenset)



def make_expando(primitive): def make_expando(primitive):
""" """
Creates an expando object, a sequence of expando objects or just Creates an expando object, a sequence of expando objects or just
@@ -29,6 +30,7 @@ def make_expando(primitive):




class Expando(object): class Expando(object):

""" """
A generic expando class that creates attributes from A generic expando class that creates attributes from
the passed in dictionary. the passed in dictionary.
@@ -63,7 +65,6 @@ class Expando(object):
""" """
setattr(self, unicode(key).encode('utf-8'), make_expando(value)) setattr(self, unicode(key).encode('utf-8'), make_expando(value))



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


@@ -79,9 +80,9 @@ class Expando(object):
elif isinstance(v, SEQS): elif isinstance(v, SEQS):
seq = type(v) seq = type(v)
result[k] = seq(item.to_dict() result[k] = seq(item.to_dict()
if isinstance(item, Expando)
else item for item in v
)
if isinstance(item, Expando)
else item for item in v
)
else: else:
result[k] = v result[k] = v
return result return result
@@ -94,6 +95,7 @@ class Expando(object):




class Context(object): class Context(object):

""" """
Wraps the context related functions and utilities. Wraps the context related functions and utilities.
""" """
@@ -125,7 +127,9 @@ class Context(object):


return context return context



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

""" """
Represents the dependency graph for hyde. Represents the dependency graph for hyde.
""" """
@@ -146,11 +150,14 @@ class Dependents(IterableUserDict):
if self.deps_file.parent.exists: if self.deps_file.parent.exists:
self.deps_file.write(yaml.dump(self.data)) self.deps_file.write(yaml.dump(self.data))



def _expand_path(sitepath, path): def _expand_path(sitepath, path):
child = sitepath.child_folder(path) child = sitepath.child_folder(path)
return Folder(child.fully_expanded_path) return Folder(child.fully_expanded_path)



class Config(Expando): class Config(Expando):

""" """
Represents the hyde configuration file Represents the hyde configuration file
""" """
@@ -158,7 +165,7 @@ class Config(Expando):
def __init__(self, sitepath, config_file=None, config_dict=None): def __init__(self, sitepath, config_file=None, config_dict=None):
self.default_config = dict( self.default_config = dict(
mode='production', mode='production',
simple_copy = [],
simple_copy=[],
content_root='content', content_root='content',
deploy_root='deploy', deploy_root='deploy',
media_root='media', media_root='media',
@@ -167,9 +174,9 @@ class Config(Expando):
base_url="/", base_url="/",
encode_safe=None, encode_safe=None,
not_found='404.html', not_found='404.html',
plugins = [],
ignore = [ "*~", "*.bak", ".hg", ".git", ".svn"],
meta = {
plugins=[],
ignore=["*~", "*.bak", ".hg", ".git", ".svn"],
meta={
"nodemeta": 'meta.yaml' "nodemeta": 'meta.yaml'
} }
) )
@@ -188,7 +195,7 @@ class Config(Expando):
if not self.config_files: if not self.config_files:
return True return True
return any((conf.has_changed_since(self.load_time) return any((conf.has_changed_since(self.load_time)
for conf in self.config_files))
for conf in self.config_files))


def load(self): def load(self):
conf = dict(**self.default_config) conf = dict(**self.default_config)
@@ -202,15 +209,14 @@ class Config(Expando):
return return
self.update(self.load()) self.update(self.load())



def read_config(self, config_file): def read_config(self, config_file):
""" """
Reads the configuration file and updates this Reads the configuration file and updates this
object while allowing for inherited configurations. object while allowing for inherited configurations.
""" """
conf_file = self.sitepath.child( conf_file = self.sitepath.child(
config_file if
config_file else 'site.yaml')
config_file if
config_file else 'site.yaml')
conf = {} conf = {}
if File(conf_file).exists: if File(conf_file).exists:
self.config_files.append(File(conf_file)) self.config_files.append(File(conf_file))
@@ -224,7 +230,6 @@ class Config(Expando):
self.load_time = datetime.now() self.load_time = datetime.now()
return conf return conf



@property @property
def deploy_root_path(self): def deploy_root_path(self):
""" """


+ 76
- 47
hyde/plugin.py View File

@@ -13,7 +13,6 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
import traceback


from commando.util import getLoggerWithNullHandler, load_python_object from commando.util import getLoggerWithNullHandler, load_python_object
from fswrap import File from fswrap import File
@@ -22,30 +21,53 @@ logger = getLoggerWithNullHandler('hyde.engine')


# Plugins have been reorganized. Map old plugin paths to new. # Plugins have been reorganized. Map old plugin paths to new.
PLUGINS_OLD_AND_NEW = { PLUGINS_OLD_AND_NEW = {
"hyde.ext.plugins.less.LessCSSPlugin" : "hyde.ext.plugins.css.LessCSSPlugin",
"hyde.ext.plugins.stylus.StylusPlugin" : "hyde.ext.plugins.css.StylusPlugin",
"hyde.ext.plugins.jpegoptim.JPEGOptimPlugin" : "hyde.ext.plugins.images.JPEGOptimPlugin",
"hyde.ext.plugins.optipng.OptiPNGPlugin" : "hyde.ext.plugins.images.OptiPNGPlugin",
"hyde.ext.plugins.jpegtran.JPEGTranPlugin" : "hyde.ext.plugins.images.JPEGTranPlugin",
"hyde.ext.plugins.uglify.UglifyPlugin": "hyde.ext.plugins.js.UglifyPlugin",
"hyde.ext.plugins.requirejs.RequireJSPlugin": "hyde.ext.plugins.js.RequireJSPlugin",
"hyde.ext.plugins.coffee.CoffeePlugin": "hyde.ext.plugins.js.CoffeePlugin",
"hyde.ext.plugins.sorter.SorterPlugin": "hyde.ext.plugins.meta.SorterPlugin",
"hyde.ext.plugins.grouper.GrouperPlugin": "hyde.ext.plugins.meta.GrouperPlugin",
"hyde.ext.plugins.tagger.TaggerPlugin": "hyde.ext.plugins.meta.TaggerPlugin",
"hyde.ext.plugins.auto_extend.AutoExtendPlugin": "hyde.ext.plugins.meta.AutoExtendPlugin",
"hyde.ext.plugins.folders.FlattenerPlugin": "hyde.ext.plugins.structure.FlattenerPlugin",
"hyde.ext.plugins.combine.CombinePlugin": "hyde.ext.plugins.structure.CombinePlugin",
"hyde.ext.plugins.paginator.PaginatorPlugin": "hyde.ext.plugins.structure.PaginatorPlugin",
"hyde.ext.plugins.blockdown.BlockdownPlugin": "hyde.ext.plugins.text.BlockdownPlugin",
"hyde.ext.plugins.markings.MarkingsPlugin": "hyde.ext.plugins.text.MarkingsPlugin",
"hyde.ext.plugins.markings.ReferencePlugin": "hyde.ext.plugins.text.ReferencePlugin",
"hyde.ext.plugins.syntext.SyntextPlugin": "hyde.ext.plugins.text.SyntextPlugin",
"hyde.ext.plugins.textlinks.TextlinksPlugin": "hyde.ext.plugins.text.TextlinksPlugin",
"hyde.ext.plugins.git.GitDatesPlugin": "hyde.ext.plugins.vcs.GitDatesPlugin"
"hyde.ext.plugins.less.LessCSSPlugin":
"hyde.ext.plugins.css.LessCSSPlugin",
"hyde.ext.plugins.stylus.StylusPlugin":
"hyde.ext.plugins.css.StylusPlugin",
"hyde.ext.plugins.jpegoptim.JPEGOptimPlugin":
"hyde.ext.plugins.images.JPEGOptimPlugin",
"hyde.ext.plugins.optipng.OptiPNGPlugin":
"hyde.ext.plugins.images.OptiPNGPlugin",
"hyde.ext.plugins.jpegtran.JPEGTranPlugin":
"hyde.ext.plugins.images.JPEGTranPlugin",
"hyde.ext.plugins.uglify.UglifyPlugin":
"hyde.ext.plugins.js.UglifyPlugin",
"hyde.ext.plugins.requirejs.RequireJSPlugin":
"hyde.ext.plugins.js.RequireJSPlugin",
"hyde.ext.plugins.coffee.CoffeePlugin":
"hyde.ext.plugins.js.CoffeePlugin",
"hyde.ext.plugins.sorter.SorterPlugin":
"hyde.ext.plugins.meta.SorterPlugin",
"hyde.ext.plugins.grouper.GrouperPlugin":
"hyde.ext.plugins.meta.GrouperPlugin",
"hyde.ext.plugins.tagger.TaggerPlugin":
"hyde.ext.plugins.meta.TaggerPlugin",
"hyde.ext.plugins.auto_extend.AutoExtendPlugin":
"hyde.ext.plugins.meta.AutoExtendPlugin",
"hyde.ext.plugins.folders.FlattenerPlugin":
"hyde.ext.plugins.structure.FlattenerPlugin",
"hyde.ext.plugins.combine.CombinePlugin":
"hyde.ext.plugins.structure.CombinePlugin",
"hyde.ext.plugins.paginator.PaginatorPlugin":
"hyde.ext.plugins.structure.PaginatorPlugin",
"hyde.ext.plugins.blockdown.BlockdownPlugin":
"hyde.ext.plugins.text.BlockdownPlugin",
"hyde.ext.plugins.markings.MarkingsPlugin":
"hyde.ext.plugins.text.MarkingsPlugin",
"hyde.ext.plugins.markings.ReferencePlugin":
"hyde.ext.plugins.text.ReferencePlugin",
"hyde.ext.plugins.syntext.SyntextPlugin":
"hyde.ext.plugins.text.SyntextPlugin",
"hyde.ext.plugins.textlinks.TextlinksPlugin":
"hyde.ext.plugins.text.TextlinksPlugin",
"hyde.ext.plugins.git.GitDatesPlugin":
"hyde.ext.plugins.vcs.GitDatesPlugin"
} }



class PluginProxy(object): class PluginProxy(object):

""" """
A proxy class to raise events in registered plugins A proxy class to raise events in registered plugins
""" """
@@ -61,7 +83,8 @@ class PluginProxy(object):
if self.site.plugins: if self.site.plugins:
for plugin in self.site.plugins: for plugin in self.site.plugins:
if hasattr(plugin, method_name): if hasattr(plugin, method_name):
checker = getattr(plugin, 'should_call__' + method_name)
checker = getattr(
plugin, 'should_call__' + method_name)
if checker(*args): if checker(*args):
function = getattr(plugin, method_name) function = getattr(plugin, method_name)
try: try:
@@ -80,9 +103,11 @@ class PluginProxy(object):


return __call_plugins__ return __call_plugins__
raise HydeException( raise HydeException(
"Unknown plugin method [%s] called." % method_name)
"Unknown plugin method [%s] called." % method_name)



class Plugin(object): class Plugin(object):

""" """
The plugin protocol The plugin protocol
""" """
@@ -92,10 +117,9 @@ class Plugin(object):
super(Plugin, self).__init__() super(Plugin, self).__init__()
self.site = site self.site = site
self.logger = getLoggerWithNullHandler( self.logger = getLoggerWithNullHandler(
'hyde.engine.%s' % self.__class__.__name__)
'hyde.engine.%s' % self.__class__.__name__)
self.template = None self.template = None



def template_loaded(self, template): def template_loaded(self, template):
""" """
Called when the template for the site has been identified. Called when the template for the site has been identified.
@@ -123,7 +147,8 @@ class Plugin(object):
elif name.startswith('should_call__'): elif name.startswith('should_call__'):
(_, _, method) = name.rpartition('__') (_, _, method) = name.rpartition('__')
if (method in ('begin_text_resource', 'text_resource_complete', if (method in ('begin_text_resource', 'text_resource_complete',
'begin_binary_resource', 'binary_resource_complete')):
'begin_binary_resource',
'binary_resource_complete')):
result = self._file_filter result = self._file_filter
elif (method in ('begin_node', 'node_complete')): elif (method in ('begin_node', 'node_complete')):
result = self._dir_filter result = self._dir_filter
@@ -132,7 +157,7 @@ class Plugin(object):
return True return True
result = always_true result = always_true


return result if result else super(Plugin, self).__getattribute__(name)
return result if result else super(Plugin, self).__getattribute__(name)


@property @property
def settings(self): def settings(self):
@@ -147,7 +172,6 @@ class Plugin(object):
pass pass
return opts return opts



@property @property
def plugin_name(self): def plugin_name(self):
""" """
@@ -195,26 +219,26 @@ class Plugin(object):
except AttributeError: except AttributeError:
filters = None filters = None
result = any(fnmatch.fnmatch(resource.path, f) result = any(fnmatch.fnmatch(resource.path, f)
for f in filters) if filters else True
for f in filters) if filters else True
return result return result


def _dir_filter(self, node, *args, **kwargs): def _dir_filter(self, node, *args, **kwargs):
""" """
Returns True if the node path is a descendant of the include_paths property in
plugin settings.
Returns True if the node path is a descendant of the
include_paths property in plugin settings.
""" """
try: try:
node_filters = self.settings.include_paths node_filters = self.settings.include_paths
if not isinstance(node_filters, list): if not isinstance(node_filters, list):
node_filters = [node_filters] node_filters = [node_filters]
node_filters = [self.site.content.node_from_relative_path(f) node_filters = [self.site.content.node_from_relative_path(f)
for f in node_filters]
for f in node_filters]
except AttributeError: except AttributeError:
node_filters = None node_filters = None
result = any(node.source == f.source or result = any(node.source == f.source or
node.source.is_descendant_of(f.source)
for f in node_filters if f) \
if node_filters else True
node.source.is_descendant_of(f.source)
for f in node_filters if f) \
if node_filters else True
return result return result


def begin_text_resource(self, resource, text): def begin_text_resource(self, resource, text):
@@ -293,7 +317,7 @@ class Plugin(object):
return load_python_object(plugin_name)(site) return load_python_object(plugin_name)(site)


site.plugins = [load_plugin(name) site.plugins = [load_plugin(name)
for name in site.config.plugins]
for name in site.config.plugins]


@staticmethod @staticmethod
def get_proxy(site): def get_proxy(site):
@@ -302,7 +326,9 @@ class Plugin(object):
""" """
return PluginProxy(site) return PluginProxy(site)



class CLTransformer(Plugin): class CLTransformer(Plugin):

""" """
Handy class for plugins that simply call a command line app to Handy class for plugins that simply call a command line app to
transform resources. transform resources.
@@ -333,11 +359,11 @@ class CLTransformer(Plugin):
""" """


return ("%(name)s executable path not configured properly. " return ("%(name)s executable path not configured properly. "
"This plugin expects `%(name)s.app` to point "
"to the full path of the `%(exec)s` executable." %
{
"name":self.plugin_name, "exec": self.executable_name
})
"This plugin expects `%(name)s.app` to point "
"to the full path of the `%(exec)s` executable." %
{
"name": self.plugin_name, "exec": self.executable_name
})


@property @property
def app(self): def app(self):
@@ -398,7 +424,7 @@ class CLTransformer(Plugin):
if match: if match:
val = args[match] val = args[match]
param = "%s%s" % (self.option_prefix(descriptive), param = "%s%s" % (self.option_prefix(descriptive),
descriptive)
descriptive)
if descriptive.endswith("="): if descriptive.endswith("="):
param += val param += val
val = None val = None
@@ -414,13 +440,15 @@ class CLTransformer(Plugin):
try: try:
self.logger.debug( self.logger.debug(
"Calling executable [%s] with arguments %s" % "Calling executable [%s] with arguments %s" %
(args[0], unicode(args[1:])))
(args[0], unicode(args[1:])))
return subprocess.check_output(args) return subprocess.check_output(args)
except subprocess.CalledProcessError, error: except subprocess.CalledProcessError, error:
self.logger.error(error.output) self.logger.error(error.output)
raise raise



class TextyPlugin(Plugin): class TextyPlugin(Plugin):

""" """
Base class for text preprocessing plugins. Base class for text preprocessing plugins.


@@ -493,10 +521,11 @@ class TextyPlugin(Plugin):
""" """
Replace a text base pattern with a template statement. Replace a text base pattern with a template statement.
""" """
text_open = re.compile(self.open_pattern, re.UNICODE|re.MULTILINE)
text_open = re.compile(self.open_pattern, re.UNICODE | re.MULTILINE)
text = text_open.sub(self.text_to_tag, text) text = text_open.sub(self.text_to_tag, text)
if self.close_pattern: if self.close_pattern:
text_close = re.compile(self.close_pattern, re.UNICODE|re.MULTILINE)
text_close = re.compile(
self.close_pattern, re.UNICODE | re.MULTILINE)
text = text_close.sub( text = text_close.sub(
partial(self.text_to_tag, start=False), text)
partial(self.text_to_tag, start=False), text)
return text return text

+ 8
- 4
hyde/publisher.py View File

@@ -8,7 +8,9 @@ Contains abstract classes and utilities that help publishing a website to a
server. server.
""" """



class Publisher(object): class Publisher(object):

""" """
The abstract base class for publishers. The abstract base class for publishers.
""" """
@@ -18,13 +20,14 @@ class Publisher(object):
def __init__(self, site, settings, message): def __init__(self, site, settings, message):
super(Publisher, self).__init__() super(Publisher, self).__init__()
self.logger = getLoggerWithNullHandler( self.logger = getLoggerWithNullHandler(
'hyde.engine.%s' % self.__class__.__name__)
'hyde.engine.%s' % self.__class__.__name__)
self.site = site self.site = site
self.message = message self.message = message
self.initialize(settings) self.initialize(settings)


@abc.abstractmethod @abc.abstractmethod
def initialize(self, settings): pass
def initialize(self, settings):
pass


@abc.abstractmethod @abc.abstractmethod
def publish(self): def publish(self):
@@ -43,7 +46,8 @@ class Publisher(object):
# Find the first configured publisher # Find the first configured publisher
try: try:
publisher = site.config.publisher.__dict__.iterkeys().next() publisher = site.config.publisher.__dict__.iterkeys().next()
logger.warning("No default publisher configured. Using: %s" % publisher)
logger.warning(
"No default publisher configured. Using: %s" % publisher)
settings = attrgetter("publisher.%s" % publisher)(site.config) settings = attrgetter("publisher.%s" % publisher)(site.config)
except (AttributeError, StopIteration): except (AttributeError, StopIteration):
logger.error( logger.error(
@@ -56,4 +60,4 @@ class Publisher(object):
raise Exception("Please specify the publisher type in config.") raise Exception("Please specify the publisher type in config.")


pub_class = load_python_object(settings.type) pub_class = load_python_object(settings.type)
return pub_class(site, settings, message)
return pub_class(site, settings, message)

+ 19
- 13
hyde/server.py View File

@@ -18,7 +18,9 @@ from fswrap import File, Folder
from commando.util import getLoggerWithNullHandler from commando.util import getLoggerWithNullHandler
logger = getLoggerWithNullHandler('hyde.server') logger = getLoggerWithNullHandler('hyde.server')



class HydeRequestHandler(SimpleHTTPRequestHandler): class HydeRequestHandler(SimpleHTTPRequestHandler):

""" """
Serves files by regenerating the resource (or) Serves files by regenerating the resource (or)
everything when a request is issued. everything when a request is issued.
@@ -35,7 +37,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
logger.debug("Processing request: [%s]" % self.path) logger.debug("Processing request: [%s]" % self.path)
result = urlparse.urlparse(self.path) result = urlparse.urlparse(self.path)
query = urlparse.parse_qs(result.query) query = urlparse.parse_qs(result.query)
if 'refresh' in query or result.query=='refresh':
if 'refresh' in query or result.query == 'refresh':
self.server.regenerate() self.server.regenerate()
if 'refresh' in query: if 'refresh' in query:
del query['refresh'] del query['refresh']
@@ -48,7 +50,6 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
else: else:
SimpleHTTPRequestHandler.do_GET(self) SimpleHTTPRequestHandler.do_GET(self)



def translate_path(self, path): def translate_path(self, path):
""" """
Finds the absolute path of the requested file by Finds the absolute path of the requested file by
@@ -56,7 +57,8 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
""" """
site = self.server.site site = self.server.site
result = urlparse.urlparse(urllib.unquote(self.path).decode('utf-8')) result = urlparse.urlparse(urllib.unquote(self.path).decode('utf-8'))
logger.debug("Trying to load file based on request: [%s]" % result.path)
logger.debug(
"Trying to load file based on request: [%s]" % result.path)
path = result.path.lstrip('/') path = result.path.lstrip('/')
res = None res = None
if path.strip() == "" or File(path).kind.strip() == "": if path.strip() == "" or File(path).kind.strip() == "":
@@ -65,13 +67,16 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
if isinstance(deployed, Folder): if isinstance(deployed, Folder):
node = site.content.node_from_relative_path(path) node = site.content.node_from_relative_path(path)
res = node.get_resource('index.html') res = node.get_resource('index.html')
elif hasattr(site.config, 'urlcleaner') and hasattr(site.config.urlcleaner, 'strip_extensions'):
elif hasattr(site.config, 'urlcleaner') and hasattr(
site.config.urlcleaner, 'strip_extensions'):
for ext in site.config.urlcleaner.strip_extensions: for ext in site.config.urlcleaner.strip_extensions:
res = site.content.resource_from_relative_deploy_path(path + '.' + ext)
res = site.content.resource_from_relative_deploy_path(
path + '.' + ext)
if res: if res:
break break
for ext in site.config.urlcleaner.strip_extensions: for ext in site.config.urlcleaner.strip_extensions:
new_path = site.config.deploy_root_path.child(path + '.' + ext)
new_path = site.config.deploy_root_path.child(
path + '.' + ext)
if File(new_path).exists: if File(new_path).exists:
return new_path return new_path
else: else:
@@ -83,7 +88,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
else: else:
self.server.generate_resource(res) self.server.generate_resource(res)
new_path = site.config.deploy_root_path.child( new_path = site.config.deploy_root_path.child(
res.relative_deploy_path)
res.relative_deploy_path)
return new_path return new_path


def do_404(self): def do_404(self):
@@ -95,13 +100,13 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):
self.redirect(site.config.not_found) self.redirect(site.config.not_found)
else: else:
res = site.content.resource_from_relative_deploy_path( res = site.content.resource_from_relative_deploy_path(
site.config.not_found)
site.config.not_found)


message = "Requested resource not found" message = "Requested resource not found"
if not res: if not res:
logger.error( logger.error(
"Cannot find the 404 template [%s]." "Cannot find the 404 template [%s]."
% site.config.not_found)
% site.config.not_found)
else: else:
f404 = File(self.translate_path(site.config.not_found)) f404 = File(self.translate_path(site.config.not_found))
if f404.exists: if f404.exists:
@@ -118,6 +123,7 @@ class HydeRequestHandler(SimpleHTTPRequestHandler):




class HydeWebServer(HTTPServer): class HydeWebServer(HTTPServer):

""" """
The hyde web server that regenerates the resource, node or site when The hyde web server that regenerates the resource, node or site when
a request is issued. a request is issued.
@@ -133,7 +139,7 @@ class HydeWebServer(HTTPServer):
self.__shutdown_request = False self.__shutdown_request = False
self.map_extensions() self.map_extensions()
HTTPServer.__init__(self, (address, port), HTTPServer.__init__(self, (address, port),
HydeRequestHandler)
HydeRequestHandler)


def map_extensions(self): def map_extensions(self):
""" """
@@ -161,7 +167,7 @@ class HydeWebServer(HTTPServer):
self.generator.generate_all(incremental=False) self.generator.generate_all(incremental=False)
except Exception, exception: except Exception, exception:
logger.error('Error occured when regenerating the site [%s]' logger.error('Error occured when regenerating the site [%s]'
% exception.message)
% exception.message)
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())


def generate_node(self, node): def generate_node(self, node):
@@ -179,7 +185,7 @@ class HydeWebServer(HTTPServer):
except Exception, exception: except Exception, exception:
logger.error( logger.error(
'Error [%s] occured when generating the node [%s]' 'Error [%s] occured when generating the node [%s]'
% (repr(exception), node))
% (repr(exception), node))
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())


def generate_resource(self, resource): def generate_resource(self, resource):
@@ -198,5 +204,5 @@ class HydeWebServer(HTTPServer):
except Exception, exception: except Exception, exception:
logger.error( logger.error(
'Error [%s] occured when serving the resource [%s]' 'Error [%s] occured when serving the resource [%s]'
% (repr(exception), resource))
% (repr(exception), resource))
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())

+ 23
- 19
hyde/site.py View File

@@ -15,6 +15,7 @@ from hyde.model import Config
from commando.util import getLoggerWithNullHandler from commando.util import getLoggerWithNullHandler
from fswrap import FS, File, Folder from fswrap import FS, File, Folder



def path_normalized(f): def path_normalized(f):
@wraps(f) @wraps(f)
def wrapper(self, path): def wrapper(self, path):
@@ -23,6 +24,7 @@ def path_normalized(f):


logger = getLoggerWithNullHandler('hyde.engine') logger = getLoggerWithNullHandler('hyde.engine')



class Processable(object): class Processable(object):
""" """
A node or resource. A node or resource.
@@ -64,8 +66,8 @@ class Processable(object):
after its been processed. after its been processed.
""" """
return self._relative_deploy_path \ return self._relative_deploy_path \
if self._relative_deploy_path is not None \
else self.relative_path
if self._relative_deploy_path is not None \
else self.relative_path


def set_relative_deploy_path(self, path): def set_relative_deploy_path(self, path):
""" """
@@ -75,7 +77,8 @@ class Processable(object):
self._relative_deploy_path = path self._relative_deploy_path = path
self.site.content.deploy_path_changed(self) self.site.content.deploy_path_changed(self)


relative_deploy_path = property(get_relative_deploy_path, set_relative_deploy_path)
relative_deploy_path = property(get_relative_deploy_path,
set_relative_deploy_path)


@property @property
def url(self): def url(self):
@@ -118,9 +121,10 @@ class Resource(Processable):


@property @property
def slug(self): def slug(self):
#TODO: Add a more sophisticated slugify method
# TODO: Add a more sophisticated slugify method
return self.source.name_without_extension return self.source.name_without_extension



class Node(Processable): class Node(Processable):
""" """
Represents any folder that is processed by hyde Represents any folder that is processed by hyde
@@ -159,7 +163,7 @@ class Node(Processable):


if self.contains_resource(resource_name): if self.contains_resource(resource_name):
return self.root.resource_from_path( return self.root.resource_from_path(
self.source_folder.child(resource_name))
self.source_folder.child(resource_name))
return None return None


def add_child_node(self, folder): def add_child_node(self, folder):
@@ -223,6 +227,7 @@ class Node(Processable):
""" """
return self.source_folder.get_relative_path(self.root.source_folder) return self.source_folder.get_relative_path(self.root.source_folder)



class RootNode(Node): class RootNode(Node):
""" """
Represents one of the roots of site: Content, Media or Layout Represents one of the roots of site: Content, Media or Layout
@@ -253,7 +258,7 @@ class RootNode(Node):
If no match is found it returns None. If no match is found it returns None.
""" """
return self.node_from_path( return self.node_from_path(
self.source_folder.child(unicode(relative_path)))
self.source_folder.child(unicode(relative_path)))


@path_normalized @path_normalized
def resource_from_path(self, path): def resource_from_path(self, path):
@@ -270,7 +275,7 @@ class RootNode(Node):
If no match is found it returns None. If no match is found it returns None.
""" """
return self.resource_from_path( return self.resource_from_path(
self.source_folder.child(relative_path))
self.source_folder.child(relative_path))


def deploy_path_changed(self, item): def deploy_path_changed(self, item):
""" """
@@ -320,7 +325,7 @@ class RootNode(Node):
node = node.add_child_node(h_folder) node = node.add_child_node(h_folder)
self.node_map[unicode(h_folder)] = node self.node_map[unicode(h_folder)] = node
logger.debug("Added node [%s] to [%s]" % ( logger.debug("Added node [%s] to [%s]" % (
node.relative_path, self.source_folder))
node.relative_path, self.source_folder))


return node return node


@@ -350,11 +355,10 @@ class RootNode(Node):
self.resource_map[unicode(afile)] = resource self.resource_map[unicode(afile)] = resource
relative_path = resource.relative_path relative_path = resource.relative_path
resource.simple_copy = any(fnmatch.fnmatch(relative_path, pattern) resource.simple_copy = any(fnmatch.fnmatch(relative_path, pattern)
for pattern
in self.site.config.simple_copy)
for pattern in self.site.config.simple_copy)


logger.debug("Added resource [%s] to [%s]" % logger.debug("Added resource [%s] to [%s]" %
(resource.relative_path, self.source_folder))
(resource.relative_path, self.source_folder))
return resource return resource


def load(self): def load(self):
@@ -396,6 +400,7 @@ def _encode_path(base, path, safe):
path = quote(path, safe) if safe is not None else quote(path) path = quote(path, safe) if safe is not None else quote(path)
return base.rstrip('/') + '/' + path.lstrip('/') return base.rstrip('/') + '/' + path.lstrip('/')



class Site(object): class Site(object):
""" """
Represents the site to be generated. Represents the site to be generated.
@@ -420,9 +425,8 @@ class Site(object):
""" """
if self.config.needs_refresh(): if self.config.needs_refresh():
logger.debug("Refreshing config data") logger.debug("Refreshing config data")
self.config = Config(self.sitepath,
self.config.config_file,
self.config.config_dict)
self.config = Config(self.sitepath, self.config.config_file,
self.config.config_dict)


def reload_if_needed(self): def reload_if_needed(self):
""" """
@@ -453,13 +457,13 @@ class Site(object):
""" """
return _encode_path(self.config.base_url, path, self._safe_chars(safe)) return _encode_path(self.config.base_url, path, self._safe_chars(safe))



def media_url(self, path, safe=None): def media_url(self, path, safe=None):
""" """
Returns the media url by appending the media base url from the config Returns the media url by appending the media base url from the config
with the given path. The return value is url encoded. with the given path. The return value is url encoded.
""" """
return _encode_path(self.config.media_url, path, self._safe_chars(safe))
return _encode_path(self.config.media_url, path,
self._safe_chars(safe))


def full_url(self, path, safe=None): def full_url(self, path, safe=None):
""" """
@@ -467,12 +471,12 @@ class Site(object):
configuration and returns the appropriate url. The return value configuration and returns the appropriate url. The return value
is url encoded. is url encoded.
""" """
if urlparse.urlparse(path)[:2] != ("",""):
if urlparse.urlparse(path)[:2] != ("", ""):
return path return path


if self.is_media(path): if self.is_media(path):
relative_path = File(path).get_relative_path( relative_path = File(path).get_relative_path(
Folder(self.config.media_root))
Folder(self.config.media_root))
return self.media_url(relative_path, safe) return self.media_url(relative_path, safe)
else: else:
return self.content_url(path, safe) return self.content_url(path, safe)
@@ -482,4 +486,4 @@ class Site(object):
Given the relative path, determines if it is content or media. Given the relative path, determines if it is content or media.
""" """
folder = self.content.source.child_folder(path) folder = self.content.source.child_folder(path)
return folder.is_descendant_of(self.config.media_root_path)
return folder.is_descendant_of(self.config.media_root_path)

+ 3
- 1
hyde/template.py View File

@@ -11,6 +11,7 @@ from commando.util import getLoggerWithNullHandler




class HtmlWrap(object): class HtmlWrap(object):

""" """
A wrapper class for raw html. A wrapper class for raw html.


@@ -36,7 +37,9 @@ class HtmlWrap(object):
return self.raw return self.raw
return self.q(selector).html() return self.q(selector).html()



class Template(object): class Template(object):

""" """
Interface for hyde template engines. To use a different template engine, Interface for hyde template engines. To use a different template engine,
the following interface must be implemented. the following interface must be implemented.
@@ -50,7 +53,6 @@ class Template(object):


@abc.abstractmethod @abc.abstractmethod
def configure(self, site, engine): def configure(self, site, engine):

""" """
The site object should contain a config attribute. The config object The site object should contain a config attribute. The config object
is a simple YAML object with required settings. The template is a simple YAML object with required settings. The template


+ 7
- 6
hyde/tests/ext/test_auto_extend.py View File

@@ -20,7 +20,7 @@ class TestAutoExtend(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -33,7 +33,8 @@ class TestAutoExtend(object):
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(bd.path) gen.generate_resource_at_path(bd.path)
res = s.content.resource_from_path(bd.path) res = s.content.resource_from_path(bd.path)
target = File(s.config.deploy_root_path.child(res.relative_deploy_path))
target = File(
s.config.deploy_root_path.child(res.relative_deploy_path))
assert target.exists assert target.exists
text = target.read_all() text = target.read_all()
q = PyQuery(text) q = PyQuery(text)
@@ -44,7 +45,8 @@ class TestAutoExtend(object):
s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin', s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin',
'hyde.ext.plugins.meta.AutoExtendPlugin', 'hyde.ext.plugins.meta.AutoExtendPlugin',
'hyde.ext.plugins.text.BlockdownPlugin'] 'hyde.ext.plugins.text.BlockdownPlugin']
txt ="This template tests to make sure blocks can be replaced with markdownish syntax."
txt = ("This template tests to make sure blocks can be replaced with"
"markdownish syntax.")
templ = """ templ = """
--- ---
extends: base.html extends: base.html
@@ -54,14 +56,13 @@ extends: base.html
====/title========""" ====/title========"""
self.assert_extended(s, txt, templ) self.assert_extended(s, txt, templ)




def test_can_auto_extend_with_default_blocks(self): def test_can_auto_extend_with_default_blocks(self):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin', s.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin',
'hyde.ext.plugins.meta.AutoExtendPlugin', 'hyde.ext.plugins.meta.AutoExtendPlugin',
'hyde.ext.plugins.text.BlockdownPlugin'] 'hyde.ext.plugins.text.BlockdownPlugin']
txt ="This template tests to make sure blocks can be replaced with markdownish syntax."
txt = ("This template tests to make sure blocks can be replaced with"
"markdownish syntax.")
templ = """ templ = """
--- ---
extends: base.html extends: base.html


+ 5
- 3
hyde/tests/ext/test_blockdown.py View File

@@ -18,7 +18,7 @@ class TestBlockdown(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -26,7 +26,8 @@ class TestBlockdown(object):
def test_can_parse_blockdown(self): def test_can_parse_blockdown(self):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.text.BlockdownPlugin'] s.config.plugins = ['hyde.ext.plugins.text.BlockdownPlugin']
txt ="This template tests to make sure blocks can be replaced with markdownish syntax."
txt = ("This template tests to make sure blocks can be replaced"
"with markdownish syntax.")
templ = """ templ = """
{%% extends "base.html" %%} {%% extends "base.html" %%}
=====title======== =====title========
@@ -39,7 +40,8 @@ class TestBlockdown(object):
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(bd.path) gen.generate_resource_at_path(bd.path)
res = s.content.resource_from_path(bd.path) res = s.content.resource_from_path(bd.path)
target = File(s.config.deploy_root_path.child(res.relative_deploy_path))
target = File(
s.config.deploy_root_path.child(res.relative_deploy_path))
assert target.exists assert target.exists
text = target.read_all() text = target.read_all()
q = PyQuery(text) q = PyQuery(text)


+ 8
- 4
hyde/tests/ext/test_combine.py View File

@@ -12,14 +12,17 @@ from fswrap import File, Folder
COMBINE_SOURCE = File(__file__).parent.child_folder('combine') COMBINE_SOURCE = File(__file__).parent.child_folder('combine')
TEST_SITE = File(__file__).parent.parent.child_folder('_test') TEST_SITE = File(__file__).parent.parent.child_folder('_test')



class CombineTester(object): class CombineTester(object):

def _test_combine(self, content): def _test_combine(self, content):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = [ s.config.plugins = [
'hyde.ext.plugins.meta.MetaPlugin', 'hyde.ext.plugins.meta.MetaPlugin',
'hyde.ext.plugins.structure.CombinePlugin'] 'hyde.ext.plugins.structure.CombinePlugin']
source = TEST_SITE.child('content/media/js/script.js') source = TEST_SITE.child('content/media/js/script.js')
target = File(Folder(s.config.deploy_root_path).child('media/js/script.js'))
target = File(
Folder(s.config.deploy_root_path).child('media/js/script.js'))
File(source).write(content) File(source).write(content)


gen = Generator(s) gen = Generator(s)
@@ -29,12 +32,13 @@ class CombineTester(object):
text = target.read_all() text = target.read_all()
return text, s return text, s



class TestCombine(CombineTester): class TestCombine(CombineTester):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.child_folder('content/media/js').make() TEST_SITE.child_folder('content/media/js').make()
COMBINE_SOURCE.copy_contents_to(TEST_SITE.child('content/media/js')) COMBINE_SOURCE.copy_contents_to(TEST_SITE.child('content/media/js'))


@@ -107,7 +111,7 @@ combine:
--- ---


First line""") First line""")
for i in range(1,4):
for i in range(1, 4):
assert not File(Folder(s.config.deploy_root_path). assert not File(Folder(s.config.deploy_root_path).
child('media/js/script.%d.js' % i)).exists child('media/js/script.%d.js' % i)).exists


@@ -117,7 +121,7 @@ class TestCombinePaths(CombineTester):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.child_folder('content/media/js').make() TEST_SITE.child_folder('content/media/js').make()
JS = TEST_SITE.child_folder('content/scripts').make() JS = TEST_SITE.child_folder('content/scripts').make()
S1 = JS.child_folder('s1').make() S1 = JS.child_folder('s1').make()


+ 5
- 4
hyde/tests/ext/test_depends.py View File

@@ -17,10 +17,10 @@ class TestDepends(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'templates/jinja2').copy_contents_to(
TEST_SITE.child_folder('content'))
'templates/jinja2').copy_contents_to(
TEST_SITE.child_folder('content'))


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -40,6 +40,7 @@ depends: index.html
gen = Generator(s) gen = Generator(s)
gen.load_site_if_needed() gen.load_site_if_needed()
gen.load_template_if_needed() gen.load_template_if_needed()

def dateformat(x): def dateformat(x):
return x.strftime('%Y-%m-%d') return x.strftime('%Y-%m-%d')
gen.template.env.filters['dateformat'] = dateformat gen.template.env.filters['dateformat'] = dateformat
@@ -52,4 +53,4 @@ depends: index.html


assert 'helpers.html' in deps assert 'helpers.html' in deps
assert 'layout.html' in deps assert 'layout.html' in deps
assert 'index.html' in deps
assert 'index.html' in deps

+ 4
- 5
hyde/tests/ext/test_drafts.py View File

@@ -23,12 +23,13 @@ A draft post.


""" """



class TestDrafts(object): class TestDrafts(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
draft = TEST_SITE.child_file('content/blog/2013/may/draft-post.html') draft = TEST_SITE.child_file('content/blog/2013/may/draft-post.html')
draft.parent.make() draft.parent.make()
draft.write(DRAFT_POST) draft.write(DRAFT_POST)
@@ -50,7 +51,7 @@ class TestDrafts(object):
gen = Generator(s) gen = Generator(s)
gen.generate_all() gen.generate_all()
assert not s.config.deploy_root_path.child_file( assert not s.config.deploy_root_path.child_file(
'blog/2013/may/draft-post.html').exists
'blog/2013/may/draft-post.html').exists


def test_drafts_are_published_in_development(self): def test_drafts_are_published_in_development(self):
s = Site(TEST_SITE) s = Site(TEST_SITE)
@@ -66,6 +67,4 @@ class TestDrafts(object):
gen = Generator(s) gen = Generator(s)
gen.generate_all() gen.generate_all()
assert s.config.deploy_root_path.child_file( assert s.config.deploy_root_path.child_file(
'blog/2013/may/draft-post.html').exists


'blog/2013/may/draft-post.html').exists

+ 3
- 4
hyde/tests/ext/test_flattener.py View File

@@ -19,7 +19,7 @@ class TestFlattner(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -42,7 +42,8 @@ class TestFlattner(object):
gen.generate_all() gen.generate_all()


assert not s.config.deploy_root_path.child_folder('blog').exists assert not s.config.deploy_root_path.child_folder('blog').exists
assert File(s.config.deploy_root_path.child('merry-christmas.html')).exists
assert File(
s.config.deploy_root_path.child('merry-christmas.html')).exists


def test_flattener_fixes_nodes(self): def test_flattener_fixes_nodes(self):
s = Site(TEST_SITE) s = Site(TEST_SITE)
@@ -64,5 +65,3 @@ class TestFlattner(object):


assert blog_node assert blog_node
assert blog_node.url == '/' assert blog_node.url == '/'



+ 29
- 19
hyde/tests/ext/test_grouper.py View File

@@ -21,7 +21,7 @@ class TestGrouperSingleLevel(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_grouper').copy_contents_to(TEST_SITE)
'sites/test_grouper').copy_contents_to(TEST_SITE)


self.s = Site(TEST_SITE) self.s = Site(TEST_SITE)
cfg = """ cfg = """
@@ -55,7 +55,8 @@ class TestGrouperSingleLevel(object):
SorterPlugin(self.s).begin_site() SorterPlugin(self.s).begin_site()
GrouperPlugin(self.s).begin_site() GrouperPlugin(self.s).begin_site()


self.all = ['installation.html', 'overview.html', 'templating.html', 'plugins.html', 'tags.html']
self.all = ['installation.html', 'overview.html',
'templating.html', 'plugins.html', 'tags.html']
self.start = ['installation.html', 'overview.html', 'templating.html'] self.start = ['installation.html', 'overview.html', 'templating.html']
self.plugins = ['plugins.html', 'tags.html'] self.plugins = ['plugins.html', 'tags.html']
self.section = self.all self.section = self.all
@@ -72,7 +73,8 @@ class TestGrouperSingleLevel(object):


def test_site_grouper_walk_groups(self): def test_site_grouper_walk_groups(self):


groups = dict([(g.name, g) for g in self.s.grouper['section'].walk_groups()])
groups = dict([(g.name, g)
for g in self.s.grouper['section'].walk_groups()])
assert len(groups) == 3 assert len(groups) == 3
assert 'section' in groups assert 'section' in groups
assert 'start' in groups assert 'start' in groups
@@ -81,7 +83,8 @@ class TestGrouperSingleLevel(object):
def test_walk_section_groups(self): def test_walk_section_groups(self):


assert hasattr(self.s.content, 'walk_section_groups') assert hasattr(self.s.content, 'walk_section_groups')
groups = dict([(grouper.group.name, grouper) for grouper in self.s.content.walk_section_groups()])
groups = dict([(grouper.group.name, grouper)
for grouper in self.s.content.walk_section_groups()])
assert len(groups) == 3 assert len(groups) == 3
assert 'section' in groups assert 'section' in groups
assert 'start' in groups assert 'start' in groups
@@ -93,15 +96,16 @@ class TestGrouperSingleLevel(object):
def test_walk_start_groups(self): def test_walk_start_groups(self):


assert hasattr(self.s.content, 'walk_start_groups') assert hasattr(self.s.content, 'walk_start_groups')
groups = dict([(g.name, g) for g, resources in self.s.content.walk_start_groups()])
groups = dict([(g.name, g)
for g, resources in self.s.content.walk_start_groups()])
assert len(groups) == 1 assert len(groups) == 1
assert 'start' in groups assert 'start' in groups



def test_walk_plugins_groups(self): def test_walk_plugins_groups(self):


assert hasattr(self.s.content, 'walk_plugins_groups') assert hasattr(self.s.content, 'walk_plugins_groups')
groups = dict([(g.name, g) for g, resources in self.s.content.walk_plugins_groups()])
groups = dict([(g.name, g) for g, resources in
self.s.content.walk_plugins_groups()])
assert len(groups) == 1 assert len(groups) == 1
assert 'plugins' in groups assert 'plugins' in groups


@@ -109,22 +113,24 @@ class TestGrouperSingleLevel(object):


assert hasattr(self.s.content, 'walk_resources_grouped_by_section') assert hasattr(self.s.content, 'walk_resources_grouped_by_section')


resources = [resource.name for resource in self.s.content.walk_resources_grouped_by_section()]
resources = [resource.name for resource in
self.s.content.walk_resources_grouped_by_section()]
assert resources == self.all assert resources == self.all



def test_walk_start_resources(self): def test_walk_start_resources(self):


assert hasattr(self.s.content, 'walk_resources_grouped_by_start') assert hasattr(self.s.content, 'walk_resources_grouped_by_start')


start_resources = [resource.name for resource in self.s.content.walk_resources_grouped_by_start()]
start_resources = [resource.name for resource in
self.s.content.walk_resources_grouped_by_start()]
assert start_resources == self.start assert start_resources == self.start


def test_walk_plugins_resources(self): def test_walk_plugins_resources(self):


assert hasattr(self.s.content, 'walk_resources_grouped_by_plugins') assert hasattr(self.s.content, 'walk_resources_grouped_by_plugins')


plugin_resources = [resource.name for resource in self.s.content.walk_resources_grouped_by_plugins()]
plugin_resources = [resource.name for resource in
self.s.content.walk_resources_grouped_by_plugins()]
assert plugin_resources == self.plugins assert plugin_resources == self.plugins


def test_resource_group(self): def test_resource_group(self):
@@ -134,7 +140,8 @@ class TestGrouperSingleLevel(object):
for name, group in groups.items(): for name, group in groups.items():
pages = getattr(self, name) pages = getattr(self, name)
for page in pages: for page in pages:
res = self.s.content.resource_from_relative_path('blog/' + page)
res = self.s.content.resource_from_relative_path(
'blog/' + page)
assert hasattr(res, 'section_group') assert hasattr(res, 'section_group')
res_group = getattr(res, 'section_group') res_group = getattr(res, 'section_group')
assert res_group == group assert res_group == group
@@ -146,7 +153,8 @@ class TestGrouperSingleLevel(object):
for name, group in groups.items(): for name, group in groups.items():
pages = getattr(self, name) pages = getattr(self, name)
for page in pages: for page in pages:
res = self.s.content.resource_from_relative_path('blog/' + page)
res = self.s.content.resource_from_relative_path(
'blog/' + page)
res_groups = getattr(res, 'walk_%s_groups' % name)() res_groups = getattr(res, 'walk_%s_groups' % name)()
assert group in res_groups assert group in res_groups


@@ -154,7 +162,8 @@ class TestGrouperSingleLevel(object):


resources = [] resources = []
for page in self.all: for page in self.all:
resources.append(self.s.content.resource_from_relative_path('blog/' + page))
resources.append(
self.s.content.resource_from_relative_path('blog/' + page))


index = 0 index = 0
for res in resources: for res in resources:
@@ -174,7 +183,7 @@ class TestGrouperSingleLevel(object):


def test_nav_with_grouper(self): def test_nav_with_grouper(self):


text ="""
text = """
{% for group, resources in site.content.walk_section_groups() %} {% for group, resources in site.content.walk_section_groups() %}
<ul> <ul>
<li> <li>
@@ -225,7 +234,7 @@ class TestGrouperSingleLevel(object):
gen = Generator(self.s) gen = Generator(self.s)
gen.load_site_if_needed() gen.load_site_if_needed()
gen.load_template_if_needed() gen.load_template_if_needed()
out = gen.template.render(text, {'site':self.s})
out = gen.template.render(text, {'site': self.s})
assert_html_equals(out, expected) assert_html_equals(out, expected)


def test_nav_with_grouper_sorted(self): def test_nav_with_grouper_sorted(self):
@@ -264,7 +273,7 @@ class TestGrouperSingleLevel(object):
SorterPlugin(self.s).begin_site() SorterPlugin(self.s).begin_site()
GrouperPlugin(self.s).begin_site() GrouperPlugin(self.s).begin_site()


text ="""
text = """
{% set sorted = site.grouper['section'].groups|sort(attribute='name') %} {% set sorted = site.grouper['section'].groups|sort(attribute='name') %}
{% for group in sorted %} {% for group in sorted %}
<ul> <ul>
@@ -314,9 +323,10 @@ class TestGrouperSingleLevel(object):




""" """
self.s.config.grouper.section.groups.append(Expando({"name": "awesome", "description": "Aweesoome"}));
self.s.config.grouper.section.groups.append(
Expando({"name": "awesome", "description": "Aweesoome"}))
gen = Generator(self.s) gen = Generator(self.s)
gen.load_site_if_needed() gen.load_site_if_needed()
gen.load_template_if_needed() gen.load_template_if_needed()
out = gen.template.render(text, {'site':self.s})
out = gen.template.render(text, {'site': self.s})
assert_html_equals(out, expected) assert_html_equals(out, expected)

+ 0
- 286
hyde/tests/ext/test_images.py View File

@@ -1,286 +0,0 @@
# -*- coding: utf-8 -*-
"""
Use nose
`$ pip install nose`
`$ nosetests`

Requires PIL
"""
from hyde.generator import Generator
from hyde.site import Site
from hyde.ext.plugins.images import thumb_scale_size


from fswrap import File
import yaml

TEST_SITE = File(__file__).parent.parent.child_folder('_test')
IMAGE_SOURCE = File(__file__).parent.child_folder('images')

PORTRAIT_IMAGE = "portrait.jpg"
PORTRAIT_SIZE = (90, 120)
LANDSCAPE_IMAGE = "landscape.jpg"
LANDSCAPE_SIZE = (120, 90)

IMAGES = [PORTRAIT_IMAGE, LANDSCAPE_IMAGE]
SIZES = [PORTRAIT_SIZE, LANDSCAPE_SIZE]

# PIL requirement
try:
from PIL import Image
except ImportError:
# No pillow
import Image

class TestImageSizer(object):

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
IMAGES = TEST_SITE.child_folder('content/media/img')
IMAGES.make()
IMAGE_SOURCE.copy_contents_to(IMAGES)
self.site = Site(TEST_SITE)

def tearDown(self):
TEST_SITE.delete()

def _generic_test_image(self, text):
self.site.config.mode = "production"
self.site.config.plugins = ['hyde.ext.plugins.images.ImageSizerPlugin']
tlink = File(self.site.content.source_folder.child('timg.html'))
tlink.write(text)
gen = Generator(self.site)
gen.generate_all()
f = File(self.site.config.deploy_root_path.child(tlink.name))
assert f.exists
html = f.read_all()
assert html
return html

def test_size_image(self):
text = u"""
<img src="/media/img/%s">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

def test_size_image_relative(self):
text = u"""
<img src="media/img/%s">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

def test_size_image_no_resize(self):
text = u"""
<img src="/media/img/%s" width="2000" height="150">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] not in html
assert ' height="%d"' % PORTRAIT_SIZE[1] not in html

def test_size_image_size_proportional(self):
text = u"""
<img src="/media/img/%s" width="%d">
""" % (PORTRAIT_IMAGE, PORTRAIT_SIZE[0]*2)
html = self._generic_test_image(text)
assert ' width="%d"' % (PORTRAIT_SIZE[0]*2) in html
assert ' height="%d"' % (PORTRAIT_SIZE[1]*2) in html

def test_size_image_not_exists(self):
text = u"""
<img src="/media/img/hyde-logo-no.png">
"""
self._generic_test_image(text)

def test_size_image_multiline(self):
text = u"""
<img src="/media/img/%s">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

def test_size_multiple_images(self):
text = u"""
<img src="/media/img/%s">
<img src="/media/img/%s">Hello <img src="/media/img/%s">
<img src="/media/img/%s">Bye
""" % ((PORTRAIT_IMAGE,)*4)
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html
assert 'Hello ' in html
assert 'Bye' in html
assert len([f for f in html.split("<img")
if ' width=' in f]) == 4
assert len([f for f in html.split("<img")
if ' height=' in f]) == 4

def test_size_malformed1(self):
text = u"""
<img src="/media/img/%s>
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

def test_size_malformed2(self):
text = u"""
<img src="/media/img/%s alt="hello">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

def test_outside_media_url(self):
self.site.config.media_url = "http://media.example.com/"
text = u"""
<img src="http://media.example.com/img/%s" alt="hello">
""" % PORTRAIT_IMAGE
html = self._generic_test_image(text)
assert ' width="%d"' % PORTRAIT_SIZE[0] in html
assert ' height="%d"' % PORTRAIT_SIZE[1] in html

class TestImageThumbSize(object):

def test_width_only(self):
ow, oh = 100, 200
nw, nh = thumb_scale_size(ow, oh, 50, None)
assert nw == 50
assert nh == 100

def test_width_only_nonintegral(self):
ow, oh = 100, 205
nw, nh = thumb_scale_size(ow, oh, 50, None)
assert nw == 50
assert nh == 103

def test_height_only(self):
ow, oh = 100, 200
nw, nh = thumb_scale_size(ow, oh, None, 100)
assert nw == 50
assert nh == 100

def test_height_only_nonintegral(self):
ow, oh = 105, 200
nw, nh = thumb_scale_size(ow, oh, None, 100)
assert nw == 53
assert nh == 100

def test_height_and_width_portrait(self):
ow, oh = 100, 200
nw, nh = thumb_scale_size(ow, oh, 50, 50)
assert nw == 50
assert nh == 100

def test_height_and_width_landscape(self):
ow, oh = 200, 100
nw, nh = thumb_scale_size(ow, oh, 50, 50)
assert nw == 100
assert nh == 50

class TestImageThumbnails(object):
# TODO: add tests for cropping? (not easy currently)

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
IMAGES = TEST_SITE.child_folder('content/media/img')
IMAGES.make()
IMAGE_SOURCE.copy_contents_to(IMAGES)
self.image_folder = IMAGES
self.site = Site(TEST_SITE)

def tearDown(self):
TEST_SITE.delete()

def _generate_site_with_meta(self, meta):
self.site.config.mode = "production"
self.site.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin', 'hyde.ext.plugins.images.ImageThumbnailsPlugin']

mlink = File(self.image_folder.child('meta.yaml'))
meta_text = yaml.dump(meta, default_flow_style=False)
mlink.write(meta_text)
gen = Generator(self.site)
gen.generate_all()

def _test_generic_thumbnails(self, meta):
self._generate_site_with_meta(meta)
thumb_meta = meta.get('thumbnails', [])
for th in thumb_meta:
prefix = th.get('prefix')
if prefix is None:
continue

for fn in [PORTRAIT_IMAGE, LANDSCAPE_IMAGE]:
f = File(self._deployed_image(prefix, fn))
assert f.exists

def _deployed_image(self, prefix, filename):
return self.site.config.deploy_root_path.child('media/img/%s%s'%(prefix,filename))

def test_width(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(width=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)
for fn in IMAGES:
im = Image.open(self._deployed_image(prefix, fn))
assert im.size[0] == 50

def test_height(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(height=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)
for fn in IMAGES:
im = Image.open(self._deployed_image(prefix, fn))
assert im.size[1] == 50

def test_width_and_height(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(width=50, height=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)
for fn in IMAGES:
im = Image.open(self._deployed_image(prefix, fn))
assert im.size[0] == 50
assert im.size[1] == 50

def test_larger(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(larger=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)

im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE))
assert im.size[1] == 50

im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE))
assert im.size[0] == 50

def test_smaller(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(smaller=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)

im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE))
assert im.size[0] == 50

im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE))
assert im.size[1] == 50

def test_larger_and_smaller(self):
prefix='thumb_'
meta = dict(thumbnails=[dict(larger=100, smaller=50, prefix=prefix, include=['*.jpg'])])
self._test_generic_thumbnails(meta)

im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE))
assert im.size[0] == 50
assert im.size[1] == 100

im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE))
assert im.size[0] == 100
assert im.size[1] == 50

+ 3
- 3
hyde/tests/ext/test_less.py View File

@@ -18,11 +18,10 @@ class TestLess(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
LESS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css')) LESS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css'))
File(TEST_SITE.child('content/media/css/site.css')).delete() File(TEST_SITE.child('content/media/css/site.css')).delete()



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


@@ -30,7 +29,8 @@ class TestLess(object):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.css.LessCSSPlugin'] s.config.plugins = ['hyde.ext.plugins.css.LessCSSPlugin']
source = TEST_SITE.child('content/media/css/site.less') source = TEST_SITE.child('content/media/css/site.less')
target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
target = File(
Folder(s.config.deploy_root_path).child('media/css/site.css'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)




+ 3
- 3
hyde/tests/ext/test_markings.py View File

@@ -12,6 +12,7 @@ from pyquery import PyQuery


TEST_SITE = File(__file__).parent.parent.child_folder('_test') TEST_SITE = File(__file__).parent.parent.child_folder('_test')



def assert_valid_conversion(html): def assert_valid_conversion(html):
assert html assert html
q = PyQuery(html) q = PyQuery(html)
@@ -25,18 +26,17 @@ def assert_valid_conversion(html):
assert '.' not in q.text() assert '.' not in q.text()
assert '/' not in q.text() assert '/' not in q.text()



class TestMarkings(object): class TestMarkings(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()




def test_mark(self): def test_mark(self):
text = u""" text = u"""
=== ===


+ 10
- 10
hyde/tests/ext/test_meta.py View File

@@ -20,7 +20,7 @@ class TestMeta(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -76,8 +76,8 @@ Heading 2


def test_can_load_front_matter(self): def test_can_load_front_matter(self):
d = {'title': 'A nice title', d = {'title': 'A nice title',
'author': 'Lakshmi Vyas',
'twitter': 'lakshmivyas'}
'author': 'Lakshmi Vyas',
'twitter': 'lakshmivyas'}
text = """ text = """
--- ---
title: %(title)s title: %(title)s
@@ -118,8 +118,8 @@ twitter: %(twitter)s


def test_can_load_from_node_meta(self): def test_can_load_from_node_meta(self):
d = {'title': 'A nice title', d = {'title': 'A nice title',
'author': 'Lakshmi Vyas',
'twitter': 'lakshmivyas'}
'author': 'Lakshmi Vyas',
'twitter': 'lakshmivyas'}
text = """ text = """
=== ===
title: Even nicer title title: Even nicer title
@@ -161,7 +161,7 @@ title: Even nicer title


def test_can_load_from_site_meta(self): def test_can_load_from_site_meta(self):
d = {'title': 'A nice title', d = {'title': 'A nice title',
'author': 'Lakshmi Vyas'}
'author': 'Lakshmi Vyas'}
text = """ text = """
--- ---
title: Even nicer title title: Even nicer title
@@ -205,15 +205,14 @@ title: Even nicer title
assert v in q("span." + k).text() assert v in q("span." + k).text()
assert q("span.title").text() == "Even nicer title" assert q("span.title").text() == "Even nicer title"



def test_multiple_levels(self): def test_multiple_levels(self):


page_d = {'title': 'An even nicer title'} page_d = {'title': 'An even nicer title'}


blog_d = {'author': 'Laks'} blog_d = {'author': 'Laks'}


content_d = {'title': 'A nice title',
'author': 'Lakshmi Vyas'}
content_d = {'title': 'A nice title',
'author': 'Lakshmi Vyas'}


site_d = {'author': 'Lakshmi', site_d = {'author': 'Lakshmi',
'twitter': 'lakshmivyas', 'twitter': 'lakshmivyas',
@@ -256,7 +255,8 @@ title: %(title)s
for k, v in expected.items(): for k, v in expected.items():
assert hasattr(res.meta, k) assert hasattr(res.meta, k)
assert getattr(res.meta, k) == v assert getattr(res.meta, k) == v
target = File(Folder(s.config.deploy_root_path).child('blog/about2.html'))
target = File(
Folder(s.config.deploy_root_path).child('blog/about2.html'))
text = target.read_all() text = target.read_all()
q = PyQuery(text) q = PyQuery(text)
for k, v in expected.items(): for k, v in expected.items():


+ 5
- 4
hyde/tests/ext/test_optipng.py View File

@@ -19,12 +19,11 @@ class TestOptipng(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
IMAGES = TEST_SITE.child_folder('content/media/images') IMAGES = TEST_SITE.child_folder('content/media/images')
IMAGES.make() IMAGES.make()
OPTIPNG_SOURCE.copy_contents_to(IMAGES) OPTIPNG_SOURCE.copy_contents_to(IMAGES)



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


@@ -33,8 +32,10 @@ class TestOptipng(object):
s.config.mode = "production" s.config.mode = "production"
s.config.plugins = ['hyde.ext.plugins.images.OptiPNGPlugin'] s.config.plugins = ['hyde.ext.plugins.images.OptiPNGPlugin']
s.config.optipng = Expando(dict(args=dict(quiet=""))) s.config.optipng = Expando(dict(args=dict(quiet="")))
source =File(TEST_SITE.child('content/media/images/hyde-lt-b.png'))
target = File(Folder(s.config.deploy_root_path).child('media/images/hyde-lt-b.png'))
source = File(TEST_SITE.child('content/media/images/hyde-lt-b.png'))
target = File(
Folder(s.config.deploy_root_path).child(
'media/images/hyde-lt-b.png'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)
assert target.exists assert target.exists


+ 3
- 10
hyde/tests/ext/test_paginator.py View File

@@ -13,12 +13,13 @@ from fswrap import File


TEST_SITE = File(__file__).parent.parent.child_folder('_test') TEST_SITE = File(__file__).parent.parent.child_folder('_test')



class TestPaginator(object): class TestPaginator(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_paginator').copy_contents_to(TEST_SITE)
'sites/test_paginator').copy_contents_to(TEST_SITE)
self.s = Site(TEST_SITE) self.s = Site(TEST_SITE)
self.deploy = TEST_SITE.child_folder('deploy') self.deploy = TEST_SITE.child_folder('deploy')


@@ -27,14 +28,12 @@ class TestPaginator(object):
self.gen.load_template_if_needed() self.gen.load_template_if_needed()
self.gen.generate_all() self.gen.generate_all()



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()



def test_pages_of_one(self): def test_pages_of_one(self):
pages = ['pages_of_one.txt', 'page2/pages_of_one.txt', pages = ['pages_of_one.txt', 'page2/pages_of_one.txt',
'page3/pages_of_one.txt', 'page4/pages_of_one.txt']
'page3/pages_of_one.txt', 'page4/pages_of_one.txt']
files = [File(self.deploy.child(p)) for p in pages] files = [File(self.deploy.child(p)) for p in pages]
for f in files: for f in files:
assert f.exists assert f.exists
@@ -42,7 +41,6 @@ class TestPaginator(object):
page5 = File(self.deploy.child('page5/pages_of_one.txt')) page5 = File(self.deploy.child('page5/pages_of_one.txt'))
assert not page5.exists assert not page5.exists



def test_pages_of_one_content(self): def test_pages_of_one_content(self):
expected_page1_content = dedent('''\ expected_page1_content = dedent('''\
Another Sad Post Another Sad Post
@@ -77,7 +75,6 @@ class TestPaginator(object):
content = File(page4).read_all() content = File(page4).read_all()
assert expected_page4_content == content assert expected_page4_content == content



def test_pages_of_ten(self): def test_pages_of_ten(self):
page1 = self.deploy.child('pages_of_ten.txt') page1 = self.deploy.child('pages_of_ten.txt')
page2 = self.deploy.child('page2/pages_of_ten.txt') page2 = self.deploy.child('page2/pages_of_ten.txt')
@@ -85,7 +82,6 @@ class TestPaginator(object):
assert File(page1).exists assert File(page1).exists
assert not File(page2).exists assert not File(page2).exists



def test_pages_of_ten_depends(self): def test_pages_of_ten_depends(self):
depends = self.gen.deps['pages_of_ten.txt'] depends = self.gen.deps['pages_of_ten.txt']


@@ -96,7 +92,6 @@ class TestPaginator(object):
assert 'blog/angry-post.html' in depends assert 'blog/angry-post.html' in depends
assert 'blog/happy-post.html' in depends assert 'blog/happy-post.html' in depends



def test_pages_of_ten_content(self): def test_pages_of_ten_content(self):
expected_content = dedent('''\ expected_content = dedent('''\
Another Sad Post Another Sad Post
@@ -109,7 +104,6 @@ class TestPaginator(object):
content = File(page).read_all() content = File(page).read_all()
assert expected_content == content assert expected_content == content



def test_pages_of_one_depends(self): def test_pages_of_one_depends(self):
depends = self.gen.deps['pages_of_one.txt'] depends = self.gen.deps['pages_of_one.txt']


@@ -120,7 +114,6 @@ class TestPaginator(object):
assert 'blog/angry-post.html' in depends assert 'blog/angry-post.html' in depends
assert 'blog/happy-post.html' in depends assert 'blog/happy-post.html' in depends



def test_custom_file_pattern(self): def test_custom_file_pattern(self):
page1 = self.deploy.child('custom_file_pattern.txt') page1 = self.deploy.child('custom_file_pattern.txt')
page2 = self.deploy.child('custom_file_pattern-2.txt') page2 = self.deploy.child('custom_file_pattern-2.txt')


+ 6
- 2
hyde/tests/ext/test_requirejs.py View File

@@ -12,10 +12,13 @@ from fswrap import File, Folder
RJS_SOURCE = File(__file__).parent.child_folder('requirejs') RJS_SOURCE = File(__file__).parent.child_folder('requirejs')
TEST_SITE = File(__file__).parent.parent.child_folder('_test') TEST_SITE = File(__file__).parent.parent.child_folder('_test')



class TestRequireJS(object): class TestRequireJS(object):

def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
RJS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/js')) RJS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/js'))
File(TEST_SITE.child('content/media/js/app.js')).delete() File(TEST_SITE.child('content/media/js/app.js')).delete()


@@ -26,7 +29,8 @@ class TestRequireJS(object):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.js.RequireJSPlugin'] s.config.plugins = ['hyde.ext.plugins.js.RequireJSPlugin']
source = TEST_SITE.child('content/media/js/rjs.conf') source = TEST_SITE.child('content/media/js/rjs.conf')
target = File(Folder(s.config.deploy_root_path).child('media/js/app.js'))
target = File(
Folder(s.config.deploy_root_path).child('media/js/app.js'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)




+ 3
- 5
hyde/tests/ext/test_scss.py View File

@@ -19,21 +19,20 @@ class TestSassyCSS(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
SCSS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css')) SCSS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css'))
File(TEST_SITE.child('content/media/css/site.css')).delete() File(TEST_SITE.child('content/media/css/site.css')).delete()



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()



def test_scss(self): def test_scss(self):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.mode = 'prod' s.config.mode = 'prod'
s.config.plugins = ['hyde.ext.plugins.css.SassyCSSPlugin'] s.config.plugins = ['hyde.ext.plugins.css.SassyCSSPlugin']
source = TEST_SITE.child('content/media/css/site.scss') source = TEST_SITE.child('content/media/css/site.scss')
target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
target = File(
Folder(s.config.deploy_root_path).child('media/css/site.css'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


@@ -41,4 +40,3 @@ class TestSassyCSS(object):
text = target.read_all() text = target.read_all()
expected_text = File(SCSS_SOURCE.child('expected-site.css')).read_all() expected_text = File(SCSS_SOURCE.child('expected-site.css')).read_all()
assert_no_diff(expected_text, text) assert_no_diff(expected_text, text)


+ 0
- 366
hyde/tests/ext/test_sorter.py View File

@@ -1,366 +0,0 @@
# -*- coding: utf-8 -*-
"""
Use nose
`$ pip install nose`
`$ nosetests`
"""
from hyde.ext.plugins.meta import MetaPlugin, SorterPlugin
from hyde.generator import Generator
from hyde.site import Site
from hyde.model import Config, Expando

from fswrap import File, Folder
import yaml


TEST_SITE = File(__file__).parent.parent.child_folder('_test')


class TestSorter(object):

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)

def tearDown(self):
TEST_SITE.delete()

def test_walk_resources_sorted(self):
s = Site(TEST_SITE)
s.load()
s.config.plugins = ['hyde.ext.meta.SorterPlugin']
s.config.sorter = Expando(dict(kind=dict(attr=['source_file.kind', 'name'])))

SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_kind')
expected = ["404.html",
"about.html",
"apple-touch-icon.png",
"merry-christmas.html",
"crossdomain.xml",
"favicon.ico",
"robots.txt",
"site.css"
]

pages = [page.name for page in
s.content.walk_resources_sorted_by_kind()]

assert pages == sorted(expected, key=lambda f: (File(f).kind, f))

def test_walk_resources_sorted_reverse(self):
s = Site(TEST_SITE)
s.load()
s.config.plugins = ['hyde.ext.meta.SorterPlugin']
s.config.sorter = Expando(dict(kind=dict(attr=['source_file.kind', 'name'], reverse=True)))

SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_kind')
expected = ["404.html",
"about.html",
"apple-touch-icon.png",
"merry-christmas.html",
"crossdomain.xml",
"favicon.ico",
"robots.txt",
"site.css"
]

pages = [page.name for page in
s.content.walk_resources_sorted_by_kind()]


assert pages == sorted(expected, key=lambda f: (File(f).kind, f), reverse=True)

def test_walk_resources_sorted_with_filters(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
kind2:
filters:
source_file.kind: html
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_kind2')
expected = ["404.html",
"about.html",
"merry-christmas.html"
]

pages = [page.name for page in s.content.walk_resources_sorted_by_kind2()]

assert pages == sorted(expected)

def test_walk_resources_sorted_with_multiple_attributes(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
multi:
attr:
- source_file.kind
- node.name
- name

"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_multi')
expected = ["content/404.html",
"content/about.html",
"content/apple-touch-icon.png",
"content/blog/2010/december/merry-christmas.html",
"content/crossdomain.xml",
"content/favicon.ico",
"content/robots.txt",
"content/site.css"
]

pages = [page.name for page in s.content.walk_resources_sorted_by_multi()]

expected_sorted = [File(page).name
for page in
sorted(expected,
key=lambda p: tuple(
[File(p).kind,
File(p).parent.name, p]))]
assert pages == expected_sorted

def test_walk_resources_sorted_no_default_is_processable(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
kind2:
filters:
source_file.kind: html
attr:
- name
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
p_404 = s.content.resource_from_relative_path('404.html')
p_404.is_processable = False
SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_kind2')
expected = ["404.html", "about.html", "merry-christmas.html"]

pages = [page.name for page in s.content.walk_resources_sorted_by_kind2()]

assert pages == sorted(expected)

def test_prev_next(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
kind2:
filters:
source_file.kind: html
attr:
- name
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
SorterPlugin(s).begin_site()

p_404 = s.content.resource_from_relative_path('404.html')
p_about = s.content.resource_from_relative_path('about.html')
p_mc = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')

assert hasattr(p_404, 'prev_by_kind2')
assert not p_404.prev_by_kind2
assert hasattr(p_404, 'next_by_kind2')
assert p_404.next_by_kind2 == p_about

assert hasattr(p_about, 'prev_by_kind2')
assert p_about.prev_by_kind2 == p_404
assert hasattr(p_about, 'next_by_kind2')
assert p_about.next_by_kind2 == p_mc

assert hasattr(p_mc, 'prev_by_kind2')
assert p_mc.prev_by_kind2 == p_about
assert hasattr(p_mc, 'next_by_kind2')
assert not p_mc.next_by_kind2

def test_prev_next_looped(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
kind2:
circular: true
filters:
source_file.kind: html
attr:
- name
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
SorterPlugin(s).begin_site()

p_404 = s.content.resource_from_relative_path('404.html')
p_about = s.content.resource_from_relative_path('about.html')
p_mc = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')

assert hasattr(p_404, 'prev_by_kind2')
assert p_404.prev_by_kind2 == p_mc
assert hasattr(p_404, 'next_by_kind2')
assert p_404.next_by_kind2 == p_about

assert hasattr(p_about, 'prev_by_kind2')
assert p_about.prev_by_kind2 == p_404
assert hasattr(p_about, 'next_by_kind2')
assert p_about.next_by_kind2 == p_mc

assert hasattr(p_mc, 'prev_by_kind2')
assert p_mc.prev_by_kind2 == p_about
assert hasattr(p_mc, 'next_by_kind2')
assert p_mc.next_by_kind2 == p_404

def test_prev_next_reversed(self):
s = Site(TEST_SITE)
cfg = """
plugins:
- hyde.ext.meta.SorterPlugin
sorter:
folder_name:
attr:
- node.name
reverse: True
filters:
source_file.kind: html
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
s.load()
SorterPlugin(s).begin_site()

p_404 = s.content.resource_from_relative_path('404.html')
p_about = s.content.resource_from_relative_path('about.html')
p_mc = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')

assert hasattr(p_mc, 'prev_by_folder_name')
assert not p_mc.prev_by_folder_name
assert hasattr(p_mc, 'next_by_folder_name')
assert p_mc.next_by_folder_name == p_404

assert hasattr(p_404, 'prev_by_folder_name')
assert p_404.prev_by_folder_name == p_mc
assert hasattr(p_404, 'next_by_folder_name')
assert p_404.next_by_folder_name == p_about

assert hasattr(p_about, 'prev_by_folder_name')
assert p_about.prev_by_folder_name == p_404
assert hasattr(p_about, 'next_by_folder_name')
assert not p_about.next_by_folder_name

def test_walk_resources_sorted_using_generator(self):
s = Site(TEST_SITE)
cfg = """
meta:
time: !!timestamp 2010-10-23
title: NahNahNah
plugins:
- hyde.ext.plugins.meta.MetaPlugin
- hyde.ext.plugins.meta.SorterPlugin
sorter:
time:
attr: meta.time
filters:
source_file.kind: html
"""
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
text = """
---
time: !!timestamp 2010-12-31
title: YayYayYay
---
{% extends "base.html" %}

{% block main %}
{% set latest = site.content.walk_resources_sorted_by_time()|reverse|first %}
<span class="latest">{{ latest.meta.title }}</span>
{% endblock %}
"""

about2 = File(TEST_SITE.child('content/about2.html'))
about2.write(text)
gen = Generator(s)
gen.generate_all()

from pyquery import PyQuery
target = File(Folder(s.config.deploy_root_path).child('about2.html'))
text = target.read_all()
q = PyQuery(text)

assert q('span.latest').text() == 'YayYayYay'

class TestSorterMeta(object):

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_sorter').copy_contents_to(TEST_SITE)

def tearDown(self):
TEST_SITE.delete()

def test_attribute_checker_no_meta(self):
s = Site(TEST_SITE)
s.load()
from hyde.ext.plugins.meta import attributes_checker
for r in s.content.walk_resources():
assert not attributes_checker(r, ['meta.index'])

def test_attribute_checker_with_meta(self):
s = Site(TEST_SITE)
s.load()
MetaPlugin(s).begin_site()
from hyde.ext.plugins.meta import attributes_checker
have_index = ["angry-post.html",
"another-sad-post.html",
"happy-post.html"]
for r in s.content.walk_resources():
expected = r.name in have_index
assert attributes_checker(r, ['meta.index']) == expected


def test_walk_resources_sorted_by_index(self):
s = Site(TEST_SITE)
s.load()
config = {
"index": {
"attr": ['meta.index', 'name']
}
}
s.config.sorter = Expando(config)
MetaPlugin(s).begin_site()
SorterPlugin(s).begin_site()

assert hasattr(s.content, 'walk_resources_sorted_by_index')
expected = ["angry-post.html",
"another-sad-post.html",
"happy-post.html"]

pages = [page.name for page in
s.content.walk_resources_sorted_by_index()]

assert pages == sorted(expected, key=lambda f: (File(f).kind, f))

+ 10
- 6
hyde/tests/ext/test_stylus.py View File

@@ -13,16 +13,16 @@ from fswrap import File, Folder
STYLUS_SOURCE = File(__file__).parent.child_folder('stylus') STYLUS_SOURCE = File(__file__).parent.child_folder('stylus')
TEST_SITE = File(__file__).parent.parent.child_folder('_test') TEST_SITE = File(__file__).parent.parent.child_folder('_test')



class TestStylus(object): class TestStylus(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
STYLUS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css')) STYLUS_SOURCE.copy_contents_to(TEST_SITE.child('content/media/css'))
File(TEST_SITE.child('content/media/css/site.css')).delete() File(TEST_SITE.child('content/media/css/site.css')).delete()



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


@@ -34,13 +34,15 @@ class TestStylus(object):
if File(path).exists: if File(path).exists:
s.config.stylus = Expando(dict(app=path)) s.config.stylus = Expando(dict(app=path))
source = TEST_SITE.child('content/media/css/site.styl') source = TEST_SITE.child('content/media/css/site.styl')
target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
target = File(
Folder(s.config.deploy_root_path).child('media/css/site.css'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


assert target.exists assert target.exists
text = target.read_all() text = target.read_all()
expected_text = File(STYLUS_SOURCE.child('expected-site.css')).read_all()
expected_text = File(
STYLUS_SOURCE.child('expected-site.css')).read_all()
assert text.strip() == expected_text.strip() assert text.strip() == expected_text.strip()


def test_can_compress_with_stylus(self): def test_can_compress_with_stylus(self):
@@ -52,11 +54,13 @@ class TestStylus(object):
if File(path).exists: if File(path).exists:
s.config.stylus = Expando(dict(app=path)) s.config.stylus = Expando(dict(app=path))
source = TEST_SITE.child('content/media/css/site.styl') source = TEST_SITE.child('content/media/css/site.styl')
target = File(Folder(s.config.deploy_root_path).child('media/css/site.css'))
target = File(
Folder(s.config.deploy_root_path).child('media/css/site.css'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


assert target.exists assert target.exists
text = target.read_all() text = target.read_all()
expected_text = File(STYLUS_SOURCE.child('expected-site-compressed.css')).read_all()
expected_text = File(
STYLUS_SOURCE.child('expected-site-compressed.css')).read_all()
assert text.strip() == expected_text.strip() assert text.strip() == expected_text.strip()

+ 1
- 3
hyde/tests/ext/test_syntext.py View File

@@ -18,13 +18,11 @@ class TestSyntext(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()




def test_syntext(self): def test_syntext(self):
text = u""" text = u"""
~~~~~~~~css~~~~~~~ ~~~~~~~~css~~~~~~~


+ 0
- 226
hyde/tests/ext/test_tagger.py View File

@@ -1,226 +0,0 @@
# -*- coding: utf-8 -*-
"""
Use nose
`$ pip install nose`
`$ nosetests`
"""
from hyde.generator import Generator
from hyde.site import Site

from fswrap import File

TEST_SITE = File(__file__).parent.parent.child_folder('_test')

class TestTagger(object):

def setUp(self):
TEST_SITE.make()
TEST_SITE.parent.child_folder(
'sites/test_tagger').copy_contents_to(TEST_SITE)
self.s = Site(TEST_SITE)
self.deploy = TEST_SITE.child_folder('deploy')


def tearDown(self):
TEST_SITE.delete()

def test_tagger_walker(self):
gen = Generator(self.s)
gen.load_site_if_needed()
gen.generate_all()

assert hasattr(self.s, 'tagger')
assert hasattr(self.s.tagger, 'tags')
assert self.s.tagger.tags
tags = self.s.tagger.tags.to_dict()

assert len(tags) == 6

for tag in ['sad', 'happy', 'angry', 'thoughts', 'events']:
assert tag in tags

sad_posts = [post.name for post in tags['sad']['resources']]
assert len(sad_posts) == 2
assert "sad-post.html" in sad_posts
assert "another-sad-post.html" in sad_posts
sad_posts == [post.name for post in
self.s.content.walk_resources_tagged_with('sad')]


happy_posts = [post.name for post in
self.s.content.walk_resources_tagged_with('happy')]
assert len(happy_posts) == 1
assert "happy-post.html" in happy_posts

angry_posts = [post.name for post in
self.s.content.walk_resources_tagged_with('angry')]
assert len(angry_posts) == 1
assert "angry-post.html" in angry_posts

sad_thought_posts = [post.name for post in
self.s.content.walk_resources_tagged_with('sad+thoughts')]
assert len(sad_thought_posts) == 1
assert "sad-post.html" in sad_thought_posts

def test_tagger_archives_generated(self):
gen = Generator(self.s)
gen.load_site_if_needed()
gen.load_template_if_needed()
gen.generate_all()
tags_folder = self.deploy.child_folder('blog/tags')

assert tags_folder.exists
tags = ['sad', 'happy', 'angry', 'thoughts', 'events']

archives = (File(tags_folder.child("%s.html" % tag)) for tag in tags)

for archive in archives:
assert archive.exists

from pyquery import PyQuery

q = PyQuery(File(tags_folder.child('sad.html')).read_all())
assert q

assert q('li').length == 2
assert q('li:nth-child(1) a').attr('href') == '/blog/another-sad-post.html'
assert q('li:nth-child(2) a').attr('href') == '/blog/sad-post.html'

q = PyQuery(File(tags_folder.child('happy.html')).read_all())
assert q

assert q('li').length == 1
assert q('li a:first-child').attr('href') == '/blog/happy-post.html'

q = PyQuery(File(tags_folder.child('angry.html')).read_all())
assert q

assert q('li').length == 1
assert q('li a:first-child').attr('href') == '/blog/angry-post.html'

q = PyQuery(File(tags_folder.child('thoughts.html')).read_all())
assert q

assert q('li').length == 3
assert q('li:nth-child(1) a').attr('href') == '/blog/happy-post.html'
assert q('li:nth-child(2) a').attr('href') == '/blog/angry-post.html'
assert q('li:nth-child(3) a').attr('href') == '/blog/sad-post.html'

q = PyQuery(File(tags_folder.child('events.html')).read_all())
assert q

assert q('li').length == 1
assert q('li a:first-child').attr('href') == '/blog/another-sad-post.html'

def test_tagger_metadata(self):
conf = {
"tagger":{
"tags": {
"sad" : {
"emotions": ["Dissappointed", "Lost"]
},
"angry": {
"emotions": ["Irritated", "Annoyed", "Disgusted"]
}
}
}
}
s = Site(TEST_SITE)
s.config.update(conf)
gen = Generator(s)
gen.load_site_if_needed()
gen.generate_all()

assert hasattr(s, 'tagger')
assert hasattr(s.tagger, 'tags')
assert s.tagger.tags
tags = s.tagger.tags
sad_tag = tags.sad
assert hasattr(sad_tag, "emotions")

assert sad_tag.emotions == s.config.tagger.tags.sad.emotions

assert hasattr(tags, "angry")
angry_tag = tags.angry
assert angry_tag
assert hasattr(angry_tag, "emotions")
assert angry_tag.emotions == s.config.tagger.tags.angry.emotions

for tagname in ['happy', 'thoughts', 'events']:
tag = getattr(tags, tagname)
assert tag
assert not hasattr(tag, "emotions")

def test_tagger_sorted(self):
conf = {
"tagger":{
"sorter": "time",
"archives": {
"blog": {
"template": "emotions.j2",
"source": "blog",
"target": "blog/tags",
"extension": "html",
"meta": {
"author": "Tagger Plugin"
}
}
},
"tags": {
"sad" : {
"emotions": ["Dissappointed", "Lost"]
},
"angry": {
"emotions": ["Irritated", "Annoyed", "Disgusted"]
}
}
}
}

text = """
<div id="author">{{ resource.meta.author }}</div>
<h1>Posts tagged: {{ tag }} in {{ node.name|title }}</h1>
Emotions:
<ul>
{% for emotion in tag.emotions %}
<li class="emotion">
{{ emotion }}
</li>
{% endfor %}
<ul>
{% for resource in walker() -%}
<li>
<a href="{{ content_url(resource.url) }}">{{ resource.meta.title }}</a>
</li>
{%- endfor %}
</ul>
"""
template = File(TEST_SITE.child('layout/emotions.j2'))
template.write(text)
s = Site(TEST_SITE)
s.config.update(conf)
gen = Generator(s)
gen.load_site_if_needed()
gen.generate_all()

tags_folder = self.deploy.child_folder('blog/tags')
assert tags_folder.exists
tags = ['sad', 'happy', 'angry', 'thoughts', 'events']
archives = dict((tag, File(tags_folder.child("%s.html" % tag))) for tag in tags)

for tag, archive in archives.items():
assert archive.exists

from pyquery import PyQuery

q = PyQuery(archives['sad'].read_all())
assert len(q("li.emotion")) == 2
assert q("#author").text() == "Tagger Plugin"

q = PyQuery(archives['angry'].read_all())
assert len(q("li.emotion")) == 3

for tag, archive in archives.items():
if tag not in ["sad", "angry"]:
q = PyQuery(archives[tag].read_all())
assert not len(q("li.emotion"))

+ 4
- 6
hyde/tests/ext/test_textlinks.py View File

@@ -17,13 +17,11 @@ class TestTextlinks(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()




def test_textlinks(self): def test_textlinks(self):
d = { d = {
'objects': 'template/variables', 'objects': 'template/variables',
@@ -34,8 +32,8 @@ class TestTextlinks(object):
{%% markdown %%} {%% markdown %%}
[[!!img/hyde-logo.png]] [[!!img/hyde-logo.png]]
* [Rich object model][hyde objects] and * [Rich object model][hyde objects] and
[overridable hierarchical metadata]([[ %(plugins)s ]]) thats available for use in
templates.
[overridable hierarchical metadata]([[ %(plugins)s ]]) thats available
for use in templates.
* Configurable [sorting][], filtering and grouping support. * Configurable [sorting][], filtering and grouping support.


[hyde objects]: [[ %(objects)s ]] [hyde objects]: [[ %(objects)s ]]
@@ -57,5 +55,5 @@ class TestTextlinks(object):
assert html assert html
for name, path in d.items(): for name, path in d.items():


assert site.config.base_url + quote(path) in html
assert site.config.base_url + quote(path) in html
assert '/media/img/hyde-logo.png' in html assert '/media/img/hyde-logo.png' in html

+ 9
- 8
hyde/tests/ext/test_uglify.py View File

@@ -20,12 +20,11 @@ class TestUglify(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)
JS = TEST_SITE.child_folder('content/media/js') JS = TEST_SITE.child_folder('content/media/js')
JS.make() JS.make()
UGLIFY_SOURCE.copy_contents_to(JS) UGLIFY_SOURCE.copy_contents_to(JS)



def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


@@ -34,7 +33,8 @@ class TestUglify(object):
s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin'] s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
s.config.mode = "production" s.config.mode = "production"
source = TEST_SITE.child('content/media/js/jquery.js') source = TEST_SITE.child('content/media/js/jquery.js')
target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
target = File(
Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


@@ -48,9 +48,11 @@ class TestUglify(object):
s = Site(TEST_SITE) s = Site(TEST_SITE)
s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin'] s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
s.config.mode = "production" s.config.mode = "production"
s.config.uglify = Expando(dict(args={"comments":"/http\:\/\/jquery.org\/license/"}))
s.config.uglify = Expando(
dict(args={"comments": "/http\:\/\/jquery.org\/license/"}))
source = TEST_SITE.child('content/media/js/jquery.js') source = TEST_SITE.child('content/media/js/jquery.js')
target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
target = File(
Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


@@ -65,7 +67,8 @@ class TestUglify(object):
s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin'] s.config.plugins = ['hyde.ext.plugins.js.UglifyPlugin']
s.config.mode = "dev" s.config.mode = "dev"
source = TEST_SITE.child('content/media/js/jquery.js') source = TEST_SITE.child('content/media/js/jquery.js')
target = File(Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
target = File(
Folder(s.config.deploy_root_path).child('media/js/jquery.js'))
gen = Generator(s) gen = Generator(s)
gen.generate_resource_at_path(source) gen.generate_resource_at_path(source)


@@ -74,5 +77,3 @@ class TestUglify(object):
# TODO: Very fragile. Better comparison needed. # TODO: Very fragile. Better comparison needed.
text = target.read_all() text = target.read_all()
assert_no_diff(expected, text) assert_no_diff(expected, text)



+ 19
- 17
hyde/tests/ext/test_urlcleaner.py View File

@@ -19,14 +19,14 @@ class TestUrlCleaner(object):
def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder( TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


def test_url_cleaner(self): def test_url_cleaner(self):
s = Site(TEST_SITE)
cfg = """
s = Site(TEST_SITE)
cfg = """
plugins: plugins:
- hyde.ext.plugins.urls.UrlCleanerPlugin - hyde.ext.plugins.urls.UrlCleanerPlugin
urlcleaner: urlcleaner:
@@ -36,24 +36,26 @@ class TestUrlCleaner(object):
- html - html
append_slash: true append_slash: true
""" """
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
text = """
s.config = Config(TEST_SITE, config_dict=yaml.load(cfg))
text = """
{% extends "base.html" %} {% extends "base.html" %}


{% block main %} {% block main %}
<a id="index" href="{{ content_url('about.html') }}"></a> <a id="index" href="{{ content_url('about.html') }}"></a>
<a id="blog" href="{{ content_url('blog/2010/december/merry-christmas.html') }}"></a>
<a id="blog" href=
"{{ content_url('blog/2010/december/merry-christmas.html') }}"></a>
{% endblock %} {% endblock %}
""" """


about2 = File(TEST_SITE.child('content/test.html'))
about2.write(text)
gen = Generator(s)
gen.generate_all()

from pyquery import PyQuery
target = File(Folder(s.config.deploy_root_path).child('test.html'))
text = target.read_all()
q = PyQuery(text)
assert q('a#index').attr("href") == '/'
assert q('a#blog').attr("href") == '/blog/2010/december/merry-christmas'
about2 = File(TEST_SITE.child('content/test.html'))
about2.write(text)
gen = Generator(s)
gen.generate_all()

from pyquery import PyQuery
target = File(Folder(s.config.deploy_root_path).child('test.html'))
text = target.read_all()
q = PyQuery(text)
assert q('a#index').attr("href") == '/'
assert q('a#blog').attr(
"href") == '/blog/2010/december/merry-christmas'

+ 9
- 8
hyde/tests/ssp/ext/banner.py View File

@@ -2,17 +2,18 @@ from hyde.plugin import Plugin




class BannerPlugin(Plugin): class BannerPlugin(Plugin):
"""
Adds a comment banner to all generated html files
"""


def text_resource_complete(self, resource, text):
banner = """
"""
Adds a comment banner to all generated html files
"""

def text_resource_complete(self, resource, text):
banner = """
<!-- <!--
This file was produced with infinite love, care & sweat. This file was produced with infinite love, care & sweat.
Please dont copy. If you have to, please drop me a note. Please dont copy. If you have to, please drop me a note.
--> -->
""" """
if resource.source.kind == "html":
text = banner + text
return text
if resource.source.kind == "html":
text = banner + text
return text

+ 20
- 11
hyde/tests/test_generate.py View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
""" """
Use nose Use nose
`$ pip install nose` `$ pip install nose`
@@ -15,11 +15,13 @@ from fswrap import File, Folder


TEST_SITE = File(__file__).parent.child_folder('_test') TEST_SITE = File(__file__).parent.child_folder('_test')



class TestGenerator(object): class TestGenerator(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -38,7 +40,8 @@ class TestGenerator(object):
def test_generate_resource_from_path_with_is_processable_false(self): def test_generate_resource_from_path_with_is_processable_false(self):
site = Site(TEST_SITE) site = Site(TEST_SITE)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
resource.is_processable = False resource.is_processable = False
gen = Generator(site) gen = Generator(site)
gen.generate_resource_at_path(TEST_SITE.child('content/about.html')) gen.generate_resource_at_path(TEST_SITE.child('content/about.html'))
@@ -48,7 +51,8 @@ class TestGenerator(object):
def test_generate_resource_from_path_with_uses_template_false(self): def test_generate_resource_from_path_with_uses_template_false(self):
site = Site(TEST_SITE) site = Site(TEST_SITE)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
resource.uses_template = False resource.uses_template = False
gen = Generator(site) gen = Generator(site)
gen.generate_resource_at_path(TEST_SITE.child('content/about.html')) gen.generate_resource_at_path(TEST_SITE.child('content/about.html'))
@@ -61,11 +65,13 @@ class TestGenerator(object):
def test_generate_resource_from_path_with_deploy_override(self): def test_generate_resource_from_path_with_deploy_override(self):
site = Site(TEST_SITE) site = Site(TEST_SITE)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
resource.relative_deploy_path = 'about/index.html' resource.relative_deploy_path = 'about/index.html'
gen = Generator(site) gen = Generator(site)
gen.generate_resource_at_path(TEST_SITE.child('content/about.html')) gen.generate_resource_at_path(TEST_SITE.child('content/about.html'))
about = File(Folder(site.config.deploy_root_path).child('about/index.html'))
about = File(
Folder(site.config.deploy_root_path).child('about/index.html'))
assert about.exists assert about.exists
text = about.read_all() text = about.read_all()
q = PyQuery(text) q = PyQuery(text)
@@ -74,7 +80,8 @@ class TestGenerator(object):
def test_has_resource_changed(self): def test_has_resource_changed(self):
site = Site(TEST_SITE) site = Site(TEST_SITE)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
gen = Generator(site) gen = Generator(site)
gen.generate_all() gen.generate_all()
import time import time
@@ -110,7 +117,8 @@ class TestGenerator(object):
{% endblock %} {% endblock %}
""" """
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
gen = Generator(site) gen = Generator(site)
resource.source_file.write(text) resource.source_file.write(text)
gen.generate_all() gen.generate_all()
@@ -150,7 +158,8 @@ class TestGenerator(object):
""" """
File(TEST_SITE.child('nav.yaml')).write(nav) File(TEST_SITE.child('nav.yaml')).write(nav)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
gen = Generator(site) gen = Generator(site)
resource.source_file.write(text) resource.source_file.write(text)
gen.generate_all() gen.generate_all()
@@ -192,7 +201,8 @@ main:
""" """
File(TEST_SITE.child('nav.yaml')).write(nav) File(TEST_SITE.child('nav.yaml')).write(nav)
site.load() site.load()
resource = site.content.resource_from_path(TEST_SITE.child('content/about.html'))
resource = site.content.resource_from_path(
TEST_SITE.child('content/about.html'))
gen = Generator(site) gen = Generator(site)
resource.source_file.write(text) resource.source_file.write(text)
gen.generate_all() gen.generate_all()
@@ -257,4 +267,3 @@ main:
left = File(site.config.deploy_root_path.child(f1.name)).read_all() left = File(site.config.deploy_root_path.child(f1.name)).read_all()
right = File(site.config.deploy_root_path.child(f2.name)).read_all() right = File(site.config.deploy_root_path.child(f2.name)).read_all()
assert left == right assert left == right


+ 15
- 2
hyde/tests/test_initialize.py View File

@@ -16,22 +16,27 @@ from nose.tools import raises, with_setup, nottest
TEST_SITE = File(__file__).parent.child_folder('_test') TEST_SITE = File(__file__).parent.child_folder('_test')
TEST_SITE_AT_USER = Folder('~/_test') TEST_SITE_AT_USER = Folder('~/_test')



@nottest @nottest
def create_test_site(): def create_test_site():
TEST_SITE.make() TEST_SITE.make()



@nottest @nottest
def delete_test_site(): def delete_test_site():
TEST_SITE.delete() TEST_SITE.delete()



@nottest @nottest
def create_test_site_at_user(): def create_test_site_at_user():
TEST_SITE_AT_USER.make() TEST_SITE_AT_USER.make()



@nottest @nottest
def delete_test_site_at_user(): def delete_test_site_at_user():
TEST_SITE_AT_USER.delete() TEST_SITE_AT_USER.delete()



@raises(HydeException) @raises(HydeException)
@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_exception_when_site_yaml_exists(): def test_ensure_exception_when_site_yaml_exists():
@@ -39,6 +44,7 @@ def test_ensure_exception_when_site_yaml_exists():
File(TEST_SITE.child('site.yaml')).write("Hey") File(TEST_SITE.child('site.yaml')).write("Hey")
e.run(e.parse(['-s', unicode(TEST_SITE), 'create'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))



@raises(HydeException) @raises(HydeException)
@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_exception_when_content_folder_exists(): def test_ensure_exception_when_content_folder_exists():
@@ -46,6 +52,7 @@ def test_ensure_exception_when_content_folder_exists():
TEST_SITE.child_folder('content').make() TEST_SITE.child_folder('content').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))



@raises(HydeException) @raises(HydeException)
@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_exception_when_layout_folder_exists(): def test_ensure_exception_when_layout_folder_exists():
@@ -53,12 +60,14 @@ def test_ensure_exception_when_layout_folder_exists():
TEST_SITE.child_folder('layout').make() TEST_SITE.child_folder('layout').make()
e.run(e.parse(['-s', unicode(TEST_SITE), 'create'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create']))



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



@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_no_exception_when_forced(): def test_ensure_no_exception_when_forced():
e = Engine(raise_exceptions=True) e = Engine(raise_exceptions=True)
@@ -75,6 +84,7 @@ def test_ensure_no_exception_when_forced():
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout()) verify_site_contents(TEST_SITE, Layout.find_layout())



@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_no_exception_when_sitepath_does_not_exist(): def test_ensure_no_exception_when_sitepath_does_not_exist():
e = Engine(raise_exceptions=True) e = Engine(raise_exceptions=True)
@@ -82,6 +92,7 @@ def test_ensure_no_exception_when_sitepath_does_not_exist():
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-f']))
verify_site_contents(TEST_SITE, Layout.find_layout()) verify_site_contents(TEST_SITE, Layout.find_layout())



@with_setup(create_test_site_at_user, delete_test_site_at_user) @with_setup(create_test_site_at_user, delete_test_site_at_user)
def test_ensure_can_create_site_at_user(): def test_ensure_can_create_site_at_user():
e = Engine(raise_exceptions=True) e = Engine(raise_exceptions=True)
@@ -89,13 +100,15 @@ def test_ensure_can_create_site_at_user():
e.run(e.parse(['-s', unicode(TEST_SITE_AT_USER), 'create', '-f'])) e.run(e.parse(['-s', unicode(TEST_SITE_AT_USER), 'create', '-f']))
verify_site_contents(TEST_SITE_AT_USER, Layout.find_layout()) verify_site_contents(TEST_SITE_AT_USER, Layout.find_layout())



@nottest @nottest
def verify_site_contents(site, layout): def verify_site_contents(site, layout):
assert site.exists assert site.exists
assert site.child_folder('layout').exists assert site.child_folder('layout').exists
assert File(site.child('info.yaml')).exists assert File(site.child('info.yaml')).exists


expected = map(lambda f: f.get_relative_path(layout), layout.walker.walk_all())
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()) actual = map(lambda f: f.get_relative_path(site), site.walker.walk_all())
assert actual assert actual
assert expected assert expected
@@ -104,9 +117,9 @@ def verify_site_contents(site, layout):
actual.sort() actual.sort()
assert actual == expected assert actual == expected



@raises(HydeException) @raises(HydeException)
@with_setup(create_test_site, delete_test_site) @with_setup(create_test_site, delete_test_site)
def test_ensure_exception_when_layout_is_invalid(): def test_ensure_exception_when_layout_is_invalid():
e = Engine(raise_exceptions=True) e = Engine(raise_exceptions=True)
e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-l', 'junk'])) e.run(e.parse(['-s', unicode(TEST_SITE), 'create', '-l', 'junk']))


+ 56
- 32
hyde/tests/test_jinja2template.py View File

@@ -24,6 +24,7 @@ import yaml
ROOT = File(__file__).parent ROOT = File(__file__).parent
JINJA2 = ROOT.child_folder('templates/jinja2') JINJA2 = ROOT.child_folder('templates/jinja2')



class Article(object): class Article(object):


def __init__(self, id): def __init__(self, id):
@@ -33,12 +34,14 @@ class Article(object):
self.user = choice(users) self.user = choice(users)
self.body = generate_lorem_ipsum() self.body = generate_lorem_ipsum()
self.pub_date = datetime.utcfromtimestamp( self.pub_date = datetime.utcfromtimestamp(
randrange(10 ** 9, 2 * 10 ** 9))
randrange(10 ** 9, 2 * 10 ** 9))
self.published = True self.published = True



def dateformat(x): def dateformat(x):
return x.strftime('%Y-%m-%d') return x.strftime('%Y-%m-%d')



class User(object): class User(object):


def __init__(self, username): def __init__(self, username):
@@ -59,6 +62,7 @@ navigation = [


context = dict(users=users, articles=articles, page_navigation=navigation) context = dict(users=users, articles=articles, page_navigation=navigation)



def test_render(): def test_render():
""" """
Uses pyquery to test the html structure for validity Uses pyquery to test the html structure for validity
@@ -77,6 +81,7 @@ def test_render():
assert actual("div.article p.meta").length == 20 assert actual("div.article p.meta").length == 20
assert actual("div.article div.text").length == 20 assert actual("div.article div.text").length == 20



def test_typogrify(): def test_typogrify():
source = """ source = """
{%filter typogrify%} {%filter typogrify%}
@@ -89,6 +94,7 @@ def test_typogrify():
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
assert html == u'One <span class="amp">&amp;</span>&nbsp;two' assert html == u'One <span class="amp">&amp;</span>&nbsp;two'



def test_spaceless(): def test_spaceless():
source = """ source = """
{%spaceless%} {%spaceless%}
@@ -124,6 +130,7 @@ def test_spaceless():
""" """
assert html.strip() == expected.strip() assert html.strip() == expected.strip()



def test_asciidoc(): def test_asciidoc():
source = """ source = """
{%asciidoc%} {%asciidoc%}
@@ -146,6 +153,7 @@ def test_asciidoc():
assert q("li:nth-child(2)").text().strip() == "test2" assert q("li:nth-child(2)").text().strip() == "test2"
assert q("li:nth-child(3)").text().strip() == "test3" assert q("li:nth-child(3)").text().strip() == "test3"



def test_markdown(): def test_markdown():
source = """ source = """
{%markdown%} {%markdown%}
@@ -157,6 +165,7 @@ def test_markdown():
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
assert html == u'<h3>Heading 3</h3>' assert html == u'<h3>Heading 3</h3>'



def test_restructuredtext(): def test_restructuredtext():
source = """ source = """
{% restructuredtext %} {% restructuredtext %}
@@ -171,6 +180,7 @@ Hello
<h1 class="title">Hello</h1> <h1 class="title">Hello</h1>
</div>""", html </div>""", html



def test_restructuredtext_with_sourcecode(): def test_restructuredtext_with_sourcecode():
source = """ source = """
{% restructuredtext %} {% restructuredtext %}
@@ -188,24 +198,30 @@ See `Example`_
{% endrestructuredtext %} {% endrestructuredtext %}
""" """


expected = """
expected = ("""
<div class="document" id="code"> <div class="document" id="code">
<h1 class="title">Code</h1> <h1 class="title">Code</h1>
<div class="highlight"><pre><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
<span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
<div class="highlight"><pre><span class="k">def</span> """
"""<span class="nf">add</span><span class="p">(</span>"""
"""<span class="n">a"""
"""</span><span class="p">,</span> <span class="n">b</span>"""
"""<span class="p">):</span>
<span class="k">return</span> <span class="n">a</span> """
"""<span class="o">+</span> <span class="n">b</span>
</pre></div> </pre></div>
<p>See <a class="reference external" href="example.html">Example</a></p> <p>See <a class="reference external" href="example.html">Example</a></p>
</div> </div>
"""
""")
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
s = Site(JINJA2.path) s = Site(JINJA2.path)
c = Config(JINJA2.path, config_dict=dict( c = Config(JINJA2.path, config_dict=dict(
restructuredtext=dict(highlight_source=True)))
restructuredtext=dict(highlight_source=True)))
s.config = c s.config = c
t.configure(s) t.configure(s)
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
assert html.strip() == expected.strip() assert html.strip() == expected.strip()



def test_markdown_with_extensions(): def test_markdown_with_extensions():
source = """ source = """
{%markdown%} {%markdown%}
@@ -215,13 +231,15 @@ def test_markdown_with_extensions():
""" """
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
s = Site(JINJA2.path) s = Site(JINJA2.path)
c = Config(JINJA2.path, config_dict=dict(markdown=dict(extensions=['headerid'])))
c = Config(JINJA2.path, config_dict=dict(
markdown=dict(extensions=['headerid'])))
s.config = c s.config = c
t.configure(s) t.configure(s)
t.env.filters['dateformat'] = dateformat t.env.filters['dateformat'] = dateformat
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
assert html == u'<h3 id="heading-3">Heading 3</h3>' assert html == u'<h3 id="heading-3">Heading 3</h3>'



def test_markdown_with_sourcecode(): def test_markdown_with_sourcecode():
source = """ source = """
{%markdown%} {%markdown%}
@@ -237,19 +255,23 @@ See [Example][]
{%endmarkdown%} {%endmarkdown%}
""" """


expected = """
expected = ("""
<h1>Code</h1> <h1>Code</h1>
<div class="codehilite"><pre><span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
<span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
<div class="codehilite"><pre><span class="k">def</span> """
"""<span class="nf">add</span><span class="p">(</span>"""
"""<span class="n">a</span><span class="p">,</span> """
"""<span class="n">b</span><span class="p">):</span>
<span class="k">return</span> <span class="n">a</span> <span class="o">+"""
"""</span> <span class="n">b</span>
</pre></div> </pre></div>




<p>See <a href="example.html">Example</a></p> <p>See <a href="example.html">Example</a></p>
"""
""")
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
s = Site(JINJA2.path) s = Site(JINJA2.path)
c = Config(JINJA2.path, config_dict=dict( c = Config(JINJA2.path, config_dict=dict(
markdown=dict(extensions=['codehilite'])))
markdown=dict(extensions=['codehilite'])))
s.config = c s.config = c
t.configure(s) t.configure(s)
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
@@ -265,13 +287,15 @@ def test_line_statements():
""" """
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
s = Site(JINJA2.path) s = Site(JINJA2.path)
c = Config(JINJA2.path, config_dict=dict(markdown=dict(extensions=['headerid'])))
c = Config(JINJA2.path, config_dict=dict(
markdown=dict(extensions=['headerid'])))
s.config = c s.config = c
t.configure(s) t.configure(s)
t.env.filters['dateformat'] = dateformat t.env.filters['dateformat'] = dateformat
html = t.render(source, {}).strip() html = t.render(source, {}).strip()
assert html == u'<h3 id="heading-3">Heading 3</h3>' assert html == u'<h3 id="heading-3">Heading 3</h3>'



def test_line_statements_with_config(): def test_line_statements_with_config():
source = """ source = """
%% markdown %% markdown
@@ -298,6 +322,7 @@ def test_line_statements_with_config():


TEST_SITE = File(__file__).parent.child_folder('_test') TEST_SITE = File(__file__).parent.child_folder('_test')



@nottest @nottest
def assert_markdown_typogrify_processed_well(include_text, includer_text): def assert_markdown_typogrify_processed_well(include_text, includer_text):
site = Site(TEST_SITE) site = Site(TEST_SITE)
@@ -317,11 +342,13 @@ def assert_markdown_typogrify_processed_well(include_text, includer_text):
assert q(".amp").length == 1 assert q(".amp").length == 1
return html return html



class TestJinjaTemplate(object): class TestJinjaTemplate(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()
@@ -375,7 +402,6 @@ class TestJinjaTemplate(object):
assert article.length == 1 assert article.length == 1
assert article.text() == "Heya" assert article.text() == "Heya"



def test_depends_with_reference_tag(self): def test_depends_with_reference_tag(self):
site = Site(TEST_SITE) site = Site(TEST_SITE)
JINJA2.copy_contents_to(site.content.source) JINJA2.copy_contents_to(site.content.source)
@@ -408,7 +434,6 @@ class TestJinjaTemplate(object):


assert not deps[0] assert not deps[0]



def test_can_include_templates_with_processing(self): def test_can_include_templates_with_processing(self):
text = """ text = """
=== ===
@@ -424,11 +449,9 @@ Hyde & Jinja.
{% endmarkdown %}{% endfilter %} {% endmarkdown %}{% endfilter %}
""" """



text2 = """{% include "inc.md" %}""" text2 = """{% include "inc.md" %}"""
assert_markdown_typogrify_processed_well(text, text2) assert_markdown_typogrify_processed_well(text, text2)



def test_includetext(self): def test_includetext(self):
text = """ text = """
=== ===
@@ -595,7 +618,6 @@ Hyde & Jinja.
assert "mark" not in html assert "mark" not in html
assert "reference" not in html assert "reference" not in html



def test_refer_with_var(self): def test_refer_with_var(self):
text = """ text = """
=== ===
@@ -623,7 +645,6 @@ Hyde & Jinja.
assert "mark" not in html assert "mark" not in html
assert "reference" not in html assert "reference" not in html



def test_yaml_tag(self): def test_yaml_tag(self):


text = """ text = """
@@ -728,20 +749,22 @@ item_list:
</ul> </ul>
</aside> </aside>
""" """
text = "{%% filter markdown|typogrify %%}{%% raw %%}%s{%% endraw %%}{%% endfilter %%}" % expected
text = """{%% filter markdown|typogrify %%}{%% raw
%%}%s{%% endraw %%}{%% endfilter %%}""" % expected
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
t.configure(None) t.configure(None)
html = t.render(text, {}).strip() html = t.render(text, {}).strip()
assert html.strip() == expected.strip() assert html.strip() == expected.strip()


def test_urlencode_filter(self): def test_urlencode_filter(self):
text= u"""
<a href="{{ 'фотография.jpg'|urlencode }}">фотография</a>
<a href="{{ 'http://localhost:8080/"abc.jpg'|urlencode }}">quoted</a>
text = u"""
<a href="{{ 'фотография.jpg'|urlencode }}"
>фотография</a><a href="{{ 'http://localhost:8080/"abc.jpg'|urlencode
}}">quoted</a>
""" """
expected = u""" expected = u"""
<a href="%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F.jpg">фотография</a>
<a href="http%3A//localhost%3A8080/%22abc.jpg">quoted</a>
<a href="%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F.jpg"
>фотография</a><a href="http%3A//localhost%3A8080/%22abc.jpg">quoted</a>
""" """
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
t.configure(None) t.configure(None)
@@ -749,13 +772,14 @@ item_list:
assert html.strip() == expected.strip() assert html.strip() == expected.strip()


def test_urldecode_filter(self): def test_urldecode_filter(self):
text= u"""
<a href="{{ 'фотография.jpg'|urlencode }}">{{ "%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F.jpg"|urldecode }}</a>
"""
expected = u"""
<a href="%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F.jpg">фотография.jpg</a>
text = u"""
<a href="{{ 'фотография.jpg'|urlencode }}">{{
"%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F.jpg"|urldecode
}}</a>
""" """
expected = (u'<a href="%D1%84%D0%BE%D1%82%D0%BE%D0%B3%D1'
u'%80%D0%B0%D1%84%D0%B8%D1%8F.jpg">фотография.jpg</a>')
t = Jinja2Template(JINJA2.path) t = Jinja2Template(JINJA2.path)
t.configure(None) t.configure(None)
html = t.render(text, {}).strip() html = t.render(text, {}).strip()
assert html.strip() == expected.strip()
assert html.strip() == expected.strip()

+ 4
- 0
hyde/tests/test_layout.py View File

@@ -14,19 +14,23 @@ from nose.tools import nottest, with_setup
DATA_ROOT = File(__file__).parent.child_folder('data') DATA_ROOT = File(__file__).parent.child_folder('data')
LAYOUT_ROOT = DATA_ROOT.child_folder(LAYOUTS) LAYOUT_ROOT = DATA_ROOT.child_folder(LAYOUTS)



@nottest @nottest
def setup_data(): def setup_data():
DATA_ROOT.make() DATA_ROOT.make()



@nottest @nottest
def cleanup_data(): def cleanup_data():
DATA_ROOT.delete() DATA_ROOT.delete()



def test_find_layout_from_package_dir(): def test_find_layout_from_package_dir():
f = Layout.find_layout() f = Layout.find_layout()
assert f.name == 'basic' assert f.name == 'basic'
assert f.child_folder('layout').exists assert f.child_folder('layout').exists



@with_setup(setup_data, cleanup_data) @with_setup(setup_data, cleanup_data)
def test_find_layout_from_env_var(): def test_find_layout_from_env_var():
f = Layout.find_layout() f = Layout.find_layout()


+ 11
- 2
hyde/tests/test_model.py View File

@@ -8,18 +8,21 @@ from hyde.model import Config, Expando


from fswrap import File, Folder from fswrap import File, Folder



def test_expando_one_level(): def test_expando_one_level():
d = {"a": 123, "b": "abc"} d = {"a": 123, "b": "abc"}
x = Expando(d) x = Expando(d)
assert x.a == d['a'] assert x.a == d['a']
assert x.b == d['b'] assert x.b == d['b']



def test_expando_two_levels(): def test_expando_two_levels():
d = {"a": 123, "b": {"c": 456}} d = {"a": 123, "b": {"c": 456}}
x = Expando(d) x = Expando(d)
assert x.a == d['a'] assert x.a == d['a']
assert x.b.c == d['b']['c'] assert x.b.c == d['b']['c']



def test_expando_three_levels(): def test_expando_three_levels():
d = {"a": 123, "b": {"c": 456, "d": {"e": "abc"}}} d = {"a": 123, "b": {"c": 456, "d": {"e": "abc"}}}
x = Expando(d) x = Expando(d)
@@ -27,6 +30,7 @@ def test_expando_three_levels():
assert x.b.c == d['b']['c'] assert x.b.c == d['b']['c']
assert x.b.d.e == d['b']['d']['e'] assert x.b.d.e == d['b']['d']['e']



def test_expando_update(): def test_expando_update():
d1 = {"a": 123, "b": "abc"} d1 = {"a": 123, "b": "abc"}
x = Expando(d1) x = Expando(d1)
@@ -34,7 +38,7 @@ def test_expando_update():
assert x.b == d1['b'] assert x.b == d1['b']
d = {"b": {"c": 456, "d": {"e": "abc"}}, "f": "lmn"} d = {"b": {"c": 456, "d": {"e": "abc"}}, "f": "lmn"}
x.update(d) x.update(d)
assert x.a == d1['a']
assert x.a == d1['a']
assert x.b.c == d['b']['c'] assert x.b.c == d['b']['c']
assert x.b.d.e == d['b']['d']['e'] assert x.b.d.e == d['b']['d']['e']
assert x.f == d["f"] assert x.f == d["f"]
@@ -44,11 +48,13 @@ def test_expando_update():
assert x.a == 789 assert x.a == 789
assert x.f == "opq" assert x.f == "opq"



def test_expando_to_dict(): def test_expando_to_dict():
d = {"a": 123, "b": {"c": 456, "d": {"e": "abc"}}} d = {"a": 123, "b": {"c": 456, "d": {"e": "abc"}}}
x = Expando(d) x = Expando(d)
assert d == x.to_dict() assert d == x.to_dict()



def test_expando_to_dict_with_update(): def test_expando_to_dict_with_update():
d1 = {"a": 123, "b": "abc"} d1 = {"a": 123, "b": "abc"}
x = Expando(d1) x = Expando(d1)
@@ -67,6 +73,8 @@ def test_expando_to_dict_with_update():
TEST_SITE = File(__file__).parent.child_folder('_test') TEST_SITE = File(__file__).parent.child_folder('_test')


import yaml import yaml


class TestConfig(object): class TestConfig(object):


@classmethod @classmethod
@@ -94,7 +102,8 @@ class TestConfig(object):


def setUp(self): def setUp(self):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)


def tearDown(self): def tearDown(self):
TEST_SITE.delete() TEST_SITE.delete()


+ 128
- 78
hyde/tests/test_plugin.py View File

@@ -16,15 +16,18 @@ from fswrap import File, Folder


TEST_SITE = File(__file__).parent.child_folder('_test') TEST_SITE = File(__file__).parent.child_folder('_test')



class PluginLoaderStub(Plugin): class PluginLoaderStub(Plugin):
pass pass



class NoReturnPlugin(Plugin): class NoReturnPlugin(Plugin):


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



class ConstantReturnPlugin(Plugin): class ConstantReturnPlugin(Plugin):


def begin_text_resource(self, resource, text): def begin_text_resource(self, resource, text):
@@ -37,7 +40,8 @@ class TestPlugins(object):
@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
TEST_SITE.make() TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
TEST_SITE.parent.child_folder(
'sites/test_jinja').copy_contents_to(TEST_SITE)
folders = [] folders = []
text_files = [] text_files = []
binary_files = [] binary_files = []
@@ -58,14 +62,13 @@ class TestPlugins(object):
cls.content_text_resources = sorted(text_files) cls.content_text_resources = sorted(text_files)
cls.content_binary_resources = sorted(binary_files) cls.content_binary_resources = sorted(binary_files)



@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls):
TEST_SITE.delete() TEST_SITE.delete()


def setUp(self): def setUp(self):
self.site = Site(TEST_SITE)
self.site.config.plugins = ['hyde.tests.test_plugin.PluginLoaderStub']
self.site = Site(TEST_SITE)
self.site.config.plugins = ['hyde.tests.test_plugin.PluginLoaderStub']


def test_can_load_plugin_modules(self): def test_can_load_plugin_modules(self):
assert not len(self.site.plugins) assert not len(self.site.plugins)
@@ -74,25 +77,27 @@ class TestPlugins(object):
assert len(self.site.plugins) == 1 assert len(self.site.plugins) == 1
assert self.site.plugins[0].__class__.__name__ == 'PluginLoaderStub' assert self.site.plugins[0].__class__.__name__ == 'PluginLoaderStub'



def test_generator_loads_plugins(self): def test_generator_loads_plugins(self):
gen = Generator(self.site)
Generator(self.site)
assert len(self.site.plugins) == 1 assert len(self.site.plugins) == 1


def test_generator_template_registered_called(self): def test_generator_template_registered_called(self):
with patch.object(PluginLoaderStub, 'template_loaded') as template_loaded_stub:
with patch.object(PluginLoaderStub,
'template_loaded') as template_loaded_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert template_loaded_stub.call_count == 1 assert template_loaded_stub.call_count == 1


def test_generator_template_begin_generation_called(self): def test_generator_template_begin_generation_called(self):
with patch.object(PluginLoaderStub, 'begin_generation') as begin_generation_stub:
with patch.object(PluginLoaderStub,
'begin_generation') as begin_generation_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert begin_generation_stub.call_count == 1 assert begin_generation_stub.call_count == 1


def test_generator_template_begin_generation_called_for_single_resource(self):
with patch.object(PluginLoaderStub, 'begin_generation') as begin_generation_stub:
def test_generator_template_begin_generation_called_for_single_res(self):
with patch.object(PluginLoaderStub,
'begin_generation') as begin_generation_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html') path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path) gen.generate_resource_at_path(path)
@@ -100,29 +105,32 @@ class TestPlugins(object):
assert begin_generation_stub.call_count == 1 assert begin_generation_stub.call_count == 1


def test_generator_template_begin_generation_called_for_single_node(self): def test_generator_template_begin_generation_called_for_single_node(self):
with patch.object(PluginLoaderStub, 'begin_generation') as begin_generation_stub:
with patch.object(PluginLoaderStub,
'begin_generation') as begin_generation_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder path = self.site.content.source_folder
gen.generate_node_at_path(path) gen.generate_node_at_path(path)
assert begin_generation_stub.call_count == 1 assert begin_generation_stub.call_count == 1



def test_generator_template_generation_complete_called(self): def test_generator_template_generation_complete_called(self):
with patch.object(PluginLoaderStub, 'generation_complete') as generation_complete_stub:
with patch.object(PluginLoaderStub,
'generation_complete') as generation_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert generation_complete_stub.call_count == 1 assert generation_complete_stub.call_count == 1


def test_generator_template_generation_complete_called_for_single_resource(self):
with patch.object(PluginLoaderStub, 'generation_complete') as generation_complete_stub:
def test_generator_template_generation_complete_called_for_single_rs(self):
with patch.object(PluginLoaderStub,
'generation_complete') as generation_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html') path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path) gen.generate_resource_at_path(path)


assert generation_complete_stub.call_count == 1 assert generation_complete_stub.call_count == 1


def test_generator_template_generation_complete_called_for_single_node(self):
with patch.object(PluginLoaderStub, 'generation_complete') as generation_complete_stub:
def test_generator_template_generation_complete_called_for_single_nd(self):
with patch.object(PluginLoaderStub,
'generation_complete') as generation_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder path = self.site.content.source_folder
gen.generate_node_at_path(path) gen.generate_node_at_path(path)
@@ -141,7 +149,7 @@ class TestPlugins(object):
gen.generate_resource_at_path(path) gen.generate_resource_at_path(path)
assert begin_site_stub.call_count == 1 assert begin_site_stub.call_count == 1


def test_generator_template_begin_site_not_called_for_single_resource_second_time(self):
def test_generator_template_begin_site_not_called_sngle_res_scnd_tm(self):
with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub: with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
@@ -158,7 +166,7 @@ class TestPlugins(object):


assert begin_site_stub.call_count == 1 assert begin_site_stub.call_count == 1


def test_generator_template_begin_site_not_called_for_single_node_second_time(self):
def test_generator_template_begin_site_not_call_for_sngl_nd_scnd_tm(self):
with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub: with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
@@ -169,24 +177,26 @@ class TestPlugins(object):
assert begin_site_stub.call_count == 1 assert begin_site_stub.call_count == 1


def test_generator_template_site_complete_called(self): def test_generator_template_site_complete_called(self):
with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
with patch.object(PluginLoaderStub,
'site_complete') as site_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert site_complete_stub.call_count == 1 assert site_complete_stub.call_count == 1



def test_generator_template_site_complete_called_for_single_resource(self): def test_generator_template_site_complete_called_for_single_resource(self):


with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
with patch.object(PluginLoaderStub,
'site_complete') as site_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html') path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path) gen.generate_resource_at_path(path)


assert site_complete_stub.call_count == 1 assert site_complete_stub.call_count == 1


def test_generator_template_site_complete_not_called_for_single_resource_second_time(self):
def test_generator_template_site_complt_not_call_4_sngl_res_scnd_tm(self):


with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
with patch.object(PluginLoaderStub,
'site_complete') as site_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert site_complete_stub.call_count == 1 assert site_complete_stub.call_count == 1
@@ -197,16 +207,18 @@ class TestPlugins(object):


def test_generator_template_site_complete_called_for_single_node(self): def test_generator_template_site_complete_called_for_single_node(self):


with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
with patch.object(PluginLoaderStub,
'site_complete') as site_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
path = self.site.content.source_folder path = self.site.content.source_folder
gen.generate_node_at_path(path) gen.generate_node_at_path(path)


assert site_complete_stub.call_count == 1 assert site_complete_stub.call_count == 1


def test_generator_template_site_complete_not_called_for_single_node_second_time(self):
def test_generator_template_site_complete_not_call_4_sngl_nd_scnd_tm(self):


with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
with patch.object(PluginLoaderStub,
'site_complete') as site_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
path = self.site.content.source_folder path = self.site.content.source_folder
@@ -221,67 +233,81 @@ class TestPlugins(object):
gen.generate_all() gen.generate_all()


assert begin_node_stub.call_count == len(self.content_nodes) assert begin_node_stub.call_count == len(self.content_nodes)
called_with_nodes = sorted([arg[0][0].path for arg in begin_node_stub.call_args_list])
called_with_nodes = sorted(
[arg[0][0].path for arg in begin_node_stub.call_args_list])
assert called_with_nodes == self.content_nodes assert called_with_nodes == self.content_nodes


def test_generator_template_begin_node_called_for_single_resource(self): def test_generator_template_begin_node_called_for_single_resource(self):


with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub: with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
gen.generate_resource_at_path(
self.site.content.source_folder.child('about.html'))
assert begin_node_stub.call_count == len(self.content_nodes) assert begin_node_stub.call_count == len(self.content_nodes)



def test_generator_template_begin_node_not_called_for_single_resource_second_time(self):
def test_generator_template_begin_node_not_called_4_sngl_res_scnd_tm(self):


with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub: with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert begin_node_stub.call_count == len(self.content_nodes) assert begin_node_stub.call_count == len(self.content_nodes)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
assert begin_node_stub.call_count == len(self.content_nodes) # No extra calls

gen.generate_resource_at_path(
self.site.content.source_folder.child('about.html'))
assert begin_node_stub.call_count == len(
self.content_nodes) # No extra calls


def test_generator_template_node_complete_called(self): def test_generator_template_node_complete_called(self):


with patch.object(PluginLoaderStub, 'node_complete') as node_complete_stub:
with patch.object(PluginLoaderStub,
'node_complete') as node_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()


assert node_complete_stub.call_count == len(self.content_nodes) assert node_complete_stub.call_count == len(self.content_nodes)
called_with_nodes = sorted([arg[0][0].path for arg in node_complete_stub.call_args_list])
called_with_nodes = sorted(
[arg[0][0].path for arg in node_complete_stub.call_args_list])
assert called_with_nodes == self.content_nodes assert called_with_nodes == self.content_nodes


def test_generator_template_node_complete_called_for_single_resource(self): def test_generator_template_node_complete_called_for_single_resource(self):


with patch.object(PluginLoaderStub, 'node_complete') as node_complete_stub:
with patch.object(PluginLoaderStub,
'node_complete') as node_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
gen.generate_resource_at_path(
self.site.content.source_folder.child('about.html'))
assert node_complete_stub.call_count == len(self.content_nodes) assert node_complete_stub.call_count == len(self.content_nodes)


def test_generator_template_node_complete_not_called_for_single_resource_second_time(self):
def test_generator_template_node_complete_not_cal_4_sngl_res_scnd_tm(self):


with patch.object(PluginLoaderStub, 'node_complete') as node_complete_stub:
with patch.object(PluginLoaderStub,
'node_complete') as node_complete_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
assert node_complete_stub.call_count == len(self.content_nodes) assert node_complete_stub.call_count == len(self.content_nodes)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
assert node_complete_stub.call_count == len(self.content_nodes) # No extra calls
gen.generate_resource_at_path(
self.site.content.source_folder.child('about.html'))
assert node_complete_stub.call_count == len(
self.content_nodes) # No extra calls


def test_generator_template_begin_text_resource_called(self): def test_generator_template_begin_text_resource_called(self):


with patch.object(PluginLoaderStub, 'begin_text_resource') as begin_text_resource_stub:
with patch.object(PluginLoaderStub,
'begin_text_resource') as begin_text_resource_stub:
begin_text_resource_stub.reset_mock() begin_text_resource_stub.reset_mock()
begin_text_resource_stub.return_value = '' begin_text_resource_stub.return_value = ''
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()


called_with_resources = sorted([arg[0][0].path for arg in begin_text_resource_stub.call_args_list])
assert set(called_with_resources) == set(self.content_text_resources)
called_with_resources = sorted(
[arg[0][0].path for arg in
begin_text_resource_stub.call_args_list])
assert set(called_with_resources) == set(
self.content_text_resources)


def test_generator_template_begin_text_resource_called_for_single_resource(self):
def test_generator_template_begin_text_resource_called_for_sngl_res(self):


with patch.object(PluginLoaderStub, 'begin_text_resource') as begin_text_resource_stub:
with patch.object(PluginLoaderStub,
'begin_text_resource') as begin_text_resource_stub:
begin_text_resource_stub.return_value = '' begin_text_resource_stub.return_value = ''
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
@@ -290,81 +316,105 @@ class TestPlugins(object):
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_resource_at_path(path, incremental=True) gen.generate_resource_at_path(path, incremental=True)


called_with_resources = sorted([arg[0][0].path for arg in begin_text_resource_stub.call_args_list])
called_with_resources = sorted(
[arg[0][0].path for arg in
begin_text_resource_stub.call_args_list])
assert begin_text_resource_stub.call_count == 1 assert begin_text_resource_stub.call_count == 1
assert called_with_resources[0] == path assert called_with_resources[0] == path


def test_generator_template_begin_binary_resource_called(self): def test_generator_template_begin_binary_resource_called(self):


with patch.object(PluginLoaderStub, 'begin_binary_resource') as begin_binary_resource_stub:
with patch.object(PluginLoaderStub, 'begin_binary_resource') as \
begin_binary_resource_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()


called_with_resources = sorted([arg[0][0].path for arg in begin_binary_resource_stub.call_args_list])
assert begin_binary_resource_stub.call_count == len(self.content_binary_resources)
called_with_resources = sorted(
[arg[0][0].path for arg in
begin_binary_resource_stub.call_args_list])
assert begin_binary_resource_stub.call_count == len(
self.content_binary_resources)
assert called_with_resources == self.content_binary_resources assert called_with_resources == self.content_binary_resources


def test_generator_template_begin_binary_resource_called_for_single_resource(self):
def test_generator_template_begin_binary_resource_called_4_sngl_res(self):


with patch.object(PluginLoaderStub, 'begin_binary_resource') as begin_binary_resource_stub:
with patch.object(PluginLoaderStub, 'begin_binary_resource') as \
begin_binary_resource_stub:
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
begin_binary_resource_stub.reset_mock() begin_binary_resource_stub.reset_mock()
path = self.site.content.source_folder.child('favicon.ico') path = self.site.content.source_folder.child('favicon.ico')
gen.generate_resource_at_path(path) gen.generate_resource_at_path(path)


called_with_resources = sorted([arg[0][0].path for arg in begin_binary_resource_stub.call_args_list])
called_with_resources = sorted(
[arg[0][0].path for arg in
begin_binary_resource_stub.call_args_list])
assert begin_binary_resource_stub.call_count == 1 assert begin_binary_resource_stub.call_count == 1
assert called_with_resources[0] == path assert called_with_resources[0] == path


def test_plugin_chaining(self): def test_plugin_chaining(self):
self.site.config.plugins = [
self.site.config.plugins = [
'hyde.tests.test_plugin.ConstantReturnPlugin', 'hyde.tests.test_plugin.ConstantReturnPlugin',
'hyde.tests.test_plugin.NoReturnPlugin' 'hyde.tests.test_plugin.NoReturnPlugin'
]
path = self.site.content.source_folder.child('about.html')
gen = Generator(self.site)
gen.generate_resource_at_path(path)
about = File(Folder(
self.site.config.deploy_root_path).child('about.html'))
assert about.read_all() == "Jam"
]
path = self.site.content.source_folder.child('about.html')
gen = Generator(self.site)
gen.generate_resource_at_path(path)
about = File(Folder(
self.site.config.deploy_root_path).child('about.html'))
assert about.read_all() == "Jam"


def test_plugin_filters_begin_text_resource(self): def test_plugin_filters_begin_text_resource(self):
def empty_return(self, resource, text=''): def empty_return(self, resource, text=''):
return text return text
with patch.object(ConstantReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock1:
with patch.object(NoReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock2:
with patch.object(ConstantReturnPlugin, 'begin_text_resource',
new=Mock(wraps=empty_return)) as mock1:
with patch.object(NoReturnPlugin, 'begin_text_resource',
new=Mock(wraps=empty_return)) as mock2:
self.site.config.plugins = [ self.site.config.plugins = [
'hyde.tests.test_plugin.ConstantReturnPlugin', 'hyde.tests.test_plugin.ConstantReturnPlugin',
'hyde.tests.test_plugin.NoReturnPlugin' 'hyde.tests.test_plugin.NoReturnPlugin'
]
self.site.config.constantreturn = Expando(dict(include_file_pattern="*.css"))
self.site.config.noreturn = Expando(dict(include_file_pattern=["*.html", "*.txt"]))
]
self.site.config.constantreturn = Expando(
dict(include_file_pattern="*.css"))
self.site.config.noreturn = Expando(
dict(include_file_pattern=["*.html", "*.txt"]))
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
mock1_args = sorted(set([arg[0][0].name for arg in mock1.call_args_list]))
mock2_args = sorted(set([arg[0][0].name for arg in mock2.call_args_list]))
mock1_args = sorted(
set([arg[0][0].name for arg in mock1.call_args_list]))
mock2_args = sorted(
set([arg[0][0].name for arg in mock2.call_args_list]))
assert len(mock1_args) == 1 assert len(mock1_args) == 1
assert len(mock2_args) == 4 assert len(mock2_args) == 4
assert mock1_args == ["site.css"] assert mock1_args == ["site.css"]
assert mock2_args == ["404.html", "about.html", "merry-christmas.html", "robots.txt"]
assert mock2_args == [
"404.html", "about.html",
"merry-christmas.html", "robots.txt"]


def test_plugin_node_filters_begin_text_resource(self): def test_plugin_node_filters_begin_text_resource(self):
def empty_return(*args, **kwargs): def empty_return(*args, **kwargs):
return None return None
with patch.object(ConstantReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock1:
with patch.object(NoReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock2:
with patch.object(ConstantReturnPlugin, 'begin_text_resource',
new=Mock(wraps=empty_return)) as mock1:
with patch.object(NoReturnPlugin, 'begin_text_resource',
new=Mock(wraps=empty_return)) as mock2:
self.site.config.plugins = [ self.site.config.plugins = [
'hyde.tests.test_plugin.ConstantReturnPlugin', 'hyde.tests.test_plugin.ConstantReturnPlugin',
'hyde.tests.test_plugin.NoReturnPlugin' 'hyde.tests.test_plugin.NoReturnPlugin'
]
self.site.config.constantreturn = Expando(dict(include_paths="media"))
self.site.config.noreturn = Expando(dict(include_file_pattern="*.html", include_paths=["blog"]))
]
self.site.config.constantreturn = Expando(
dict(include_paths="media"))
self.site.config.noreturn = Expando(
dict(include_file_pattern="*.html",
include_paths=["blog"]))
gen = Generator(self.site) gen = Generator(self.site)
gen.generate_all() gen.generate_all()
mock1_args = sorted(set([arg[0][0].name for arg in mock1.call_args_list]))
mock2_args = sorted(set([arg[0][0].name for arg in mock2.call_args_list]))
mock1_args = sorted(
set([arg[0][0].name for arg in mock1.call_args_list]))
mock2_args = sorted(
set([arg[0][0].name for arg in mock2.call_args_list]))
assert len(mock1_args) == 1 assert len(mock1_args) == 1
assert len(mock2_args) == 1 assert len(mock2_args) == 1
assert mock1_args == ["site.css"] assert mock1_args == ["site.css"]
assert mock2_args == ["merry-christmas.html"]
assert mock2_args == ["merry-christmas.html"]

+ 17
- 8
hyde/tests/test_simple_copy.py View File

@@ -29,10 +29,13 @@ from nose.tools import nottest


TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja') TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja')



class TestSimpleCopy(object): class TestSimpleCopy(object):

@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
cls.SITE_PATH = File(__file__).parent.child_folder('sites/test_jinja_with_config')
cls.SITE_PATH = File(__file__).parent.child_folder(
'sites/test_jinja_with_config')
cls.SITE_PATH.make() cls.SITE_PATH.make()
TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH) TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH)


@@ -67,7 +70,8 @@ class TestSimpleCopy(object):
res = s.content.resource_from_relative_path('about.html') res = s.content.resource_from_relative_path('about.html')
assert res assert res
assert not res.simple_copy assert not res.simple_copy
res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
res = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')
assert res assert res
assert res.simple_copy assert res.simple_copy


@@ -81,7 +85,8 @@ class TestSimpleCopy(object):
res = s.content.resource_from_relative_path('about.html') res = s.content.resource_from_relative_path('about.html')
assert res assert res
assert not res.simple_copy assert not res.simple_copy
res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
res = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')
assert res assert res
assert res.simple_copy assert res.simple_copy
res = s.content.resource_from_relative_path('media/css/site.css') res = s.content.resource_from_relative_path('media/css/site.css')
@@ -96,8 +101,10 @@ class TestSimpleCopy(object):
s = Site(self.SITE_PATH, self.config) s = Site(self.SITE_PATH, self.config)
g = Generator(s) g = Generator(s)
g.generate_all() g.generate_all()
source = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
target = File(s.config.deploy_root_path.child(source.relative_deploy_path))
source = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')
target = File(
s.config.deploy_root_path.child(source.relative_deploy_path))
left = source.source_file.read_all() left = source.source_file.read_all()
right = target.read_all() right = target.read_all()
assert left == right assert left == right
@@ -129,11 +136,13 @@ twitter: @me
]) ])
conf = {'plugins': ['hyde.ext.plugins.meta.MetaPlugin']} conf = {'plugins': ['hyde.ext.plugins.meta.MetaPlugin']}
conf.update(self.config.to_dict()) conf.update(self.config.to_dict())
s = Site(self.SITE_PATH, Config(sitepath=self.SITE_PATH, config_dict=conf))
s = Site(self.SITE_PATH, Config(
sitepath=self.SITE_PATH, config_dict=conf))
g = Generator(s) g = Generator(s)
g.generate_all() g.generate_all()
source = s.content.resource_from_relative_path('blog/index.html') source = s.content.resource_from_relative_path('blog/index.html')
target = File(s.config.deploy_root_path.child(source.relative_deploy_path))
target = File(
s.config.deploy_root_path.child(source.relative_deploy_path))
left = source.source_file.read_all() left = source.source_file.read_all()
right = target.read_all() right = target.read_all()
assert left == right
assert left == right

+ 40
- 14
hyde/tests/test_site.py View File

@@ -14,6 +14,7 @@ from fswrap import File, Folder


TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja') TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja')



def test_node_site(): def test_node_site():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
@@ -21,6 +22,7 @@ def test_node_site():
n = Node(r.source_folder.child_folder('blog'), r) n = Node(r.source_folder.child_folder('blog'), r)
assert n.site == s assert n.site == s



def test_node_root(): def test_node_root():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
@@ -28,12 +30,14 @@ def test_node_root():
n = Node(r.source_folder.child_folder('blog'), r) n = Node(r.source_folder.child_folder('blog'), r)
assert n.root == r assert n.root == r



def test_node_parent(): def test_node_parent():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december'))
assert c.parent == r.node_from_relative_path('blog/2010') assert c.parent == r.node_from_relative_path('blog/2010')



def test_node_module(): def test_node_module():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
@@ -43,6 +47,7 @@ def test_node_module():
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december'))
assert c.module == n assert c.module == n



def test_node_url(): def test_node_url():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
@@ -54,6 +59,7 @@ def test_node_url():
assert c.url == '/' + c.relative_path assert c.url == '/' + c.relative_path
assert c.url == '/blog/2010/december' assert c.url == '/blog/2010/december'



def test_node_full_url(): def test_node_full_url():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.config.base_url = 'http://localhost' s.config.base_url = 'http://localhost'
@@ -64,6 +70,7 @@ def test_node_full_url():
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december'))
assert c.full_url == 'http://localhost/blog/2010/december' assert c.full_url == 'http://localhost/blog/2010/december'



def test_node_full_url_quoted(): def test_node_full_url_quoted():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.config.base_url = 'http://localhost' s.config.base_url = 'http://localhost'
@@ -74,6 +81,7 @@ def test_node_full_url_quoted():
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blo~g/2010/december')) c = r.add_node(TEST_SITE_ROOT.child_folder('content/blo~g/2010/december'))
assert c.full_url == 'http://localhost/' + quote('blo~g/2010/december') assert c.full_url == 'http://localhost/' + quote('blo~g/2010/december')



def test_node_relative_path(): def test_node_relative_path():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) r = RootNode(TEST_SITE_ROOT.child_folder('content'), s)
@@ -83,6 +91,7 @@ def test_node_relative_path():
c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december'))
assert c.relative_path == 'blog/2010/december' assert c.relative_path == 'blog/2010/december'



def test_load(): def test_load():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
@@ -96,6 +105,7 @@ def test_load():
assert resource.relative_path == path assert resource.relative_path == path
assert not s.content.resource_from_relative_path('/happy-festivus.html') assert not s.content.resource_from_relative_path('/happy-festivus.html')



def test_walk_resources(): def test_walk_resources():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
@@ -113,6 +123,7 @@ def test_walk_resources():
expected.sort() expected.sort()
assert pages == expected assert pages == expected



def test_contains_resource(): def test_contains_resource():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
@@ -120,13 +131,16 @@ def test_contains_resource():
node = s.content.node_from_relative_path(path) node = s.content.node_from_relative_path(path)
assert node.contains_resource('merry-christmas.html') assert node.contains_resource('merry-christmas.html')



def test_get_resource(): def test_get_resource():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
path = 'blog/2010/december' path = 'blog/2010/december'
node = s.content.node_from_relative_path(path) node = s.content.node_from_relative_path(path)
resource = node.get_resource('merry-christmas.html') resource = node.get_resource('merry-christmas.html')
assert resource == s.content.resource_from_relative_path(Folder(path).child('merry-christmas.html'))
assert resource == s.content.resource_from_relative_path(
Folder(path).child('merry-christmas.html'))



def test_resource_slug(): def test_resource_slug():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
@@ -143,9 +157,12 @@ def test_get_resource_from_relative_deploy_path():
path = 'blog/2010/december' path = 'blog/2010/december'
node = s.content.node_from_relative_path(path) node = s.content.node_from_relative_path(path)
resource = node.get_resource('merry-christmas.html') resource = node.get_resource('merry-christmas.html')
assert resource == s.content.resource_from_relative_deploy_path(Folder(path).child('merry-christmas.html'))
assert resource == s.content.resource_from_relative_deploy_path(
Folder(path).child('merry-christmas.html'))
resource.relative_deploy_path = Folder(path).child('merry-christmas.php') resource.relative_deploy_path = Folder(path).child('merry-christmas.php')
assert resource == s.content.resource_from_relative_deploy_path(Folder(path).child('merry-christmas.php'))
assert resource == s.content.resource_from_relative_deploy_path(
Folder(path).child('merry-christmas.php'))



def test_is_processable_default_true(): def test_is_processable_default_true():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
@@ -153,6 +170,7 @@ def test_is_processable_default_true():
for page in s.content.walk_resources(): for page in s.content.walk_resources():
assert page.is_processable assert page.is_processable



def test_relative_deploy_path(): def test_relative_deploy_path():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
@@ -160,28 +178,35 @@ def test_relative_deploy_path():
assert page.relative_deploy_path == Folder(page.relative_path) assert page.relative_deploy_path == Folder(page.relative_path)
assert page.url == '/' + page.relative_deploy_path assert page.url == '/' + page.relative_deploy_path



def test_relative_deploy_path_override(): def test_relative_deploy_path_override():
s = Site(TEST_SITE_ROOT) s = Site(TEST_SITE_ROOT)
s.load() s.load()
res = s.content.resource_from_relative_path('blog/2010/december/merry-christmas.html')
res = s.content.resource_from_relative_path(
'blog/2010/december/merry-christmas.html')
res.relative_deploy_path = 'blog/2010/december/happy-holidays.html' res.relative_deploy_path = 'blog/2010/december/happy-holidays.html'
for page in s.content.walk_resources(): for page in s.content.walk_resources():
if res.source_file == page.source_file: if res.source_file == page.source_file:
assert page.relative_deploy_path == 'blog/2010/december/happy-holidays.html'
assert (page.relative_deploy_path ==
'blog/2010/december/happy-holidays.html')
else: else:
assert page.relative_deploy_path == Folder(page.relative_path) assert page.relative_deploy_path == Folder(page.relative_path)



class TestSiteWithConfig(object): class TestSiteWithConfig(object):


@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
cls.SITE_PATH = File(__file__).parent.child_folder('sites/test_jinja_with_config')
cls.SITE_PATH = File(__file__).parent.child_folder(
'sites/test_jinja_with_config')
cls.SITE_PATH.make() cls.SITE_PATH.make()
TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH) TEST_SITE_ROOT.copy_contents_to(cls.SITE_PATH)
cls.config_file = File(cls.SITE_PATH.child('alternate.yaml')) cls.config_file = File(cls.SITE_PATH.child('alternate.yaml'))
with open(cls.config_file.path) as config: with open(cls.config_file.path) as config:
cls.config = Config(sitepath=cls.SITE_PATH, config_dict=yaml.load(config))
cls.SITE_PATH.child_folder('content').rename_to(cls.config.content_root)
cls.config = Config(
sitepath=cls.SITE_PATH, config_dict=yaml.load(config))
cls.SITE_PATH.child_folder('content').rename_to(
cls.config.content_root)


@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls):
@@ -198,7 +223,8 @@ class TestSiteWithConfig(object):
resource = s.content.resource_from_relative_path(path) resource = s.content.resource_from_relative_path(path)
assert resource assert resource
assert resource.relative_path == path assert resource.relative_path == path
assert not s.content.resource_from_relative_path('/happy-festivus.html')
assert not s.content.resource_from_relative_path(
'/happy-festivus.html')


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


def test_media_url(self): def test_media_url(self):
@@ -254,7 +280,7 @@ class TestSiteWithConfig(object):
s.load() s.load()
path = 'css/site.css' path = 'css/site.css'
resource = s.content.resource_from_relative_path( resource = s.content.resource_from_relative_path(
Folder("media").child(path))
Folder("media").child(path))
assert resource assert resource
assert resource.full_url == "/media/" + path assert resource.full_url == "/media/" + path


@@ -265,7 +291,7 @@ class TestSiteWithConfig(object):
path = 'apple-touch-icon.png' path = 'apple-touch-icon.png'
resource = s.content.resource_from_relative_path(path) resource = s.content.resource_from_relative_path(path)
assert resource assert resource
assert resource.full_url == "/" + path
assert resource.full_url == "/" + path
s = Site(self.SITE_PATH, config=c) s = Site(self.SITE_PATH, config=c)
s.config.ignore.append('*.png') s.config.ignore.append('*.png')
resource = s.content.resource_from_relative_path(path) resource = s.content.resource_from_relative_path(path)
@@ -281,10 +307,10 @@ class TestSiteWithConfig(object):
assert not git_node assert not git_node
blog_node = s.content.node_from_relative_path('blog') blog_node = s.content.node_from_relative_path('blog')
assert blog_node assert blog_node
assert blog_node.full_url == "/blog"
assert blog_node.full_url == "/blog"
s = Site(self.SITE_PATH, config=c) s = Site(self.SITE_PATH, config=c)
s.config.ignore.append('blog') s.config.ignore.append('blog')
blog_node = s.content.node_from_relative_path('blog') blog_node = s.content.node_from_relative_path('blog')
assert not blog_node assert not blog_node
git_node = s.content.node_from_relative_path('.git') git_node = s.content.node_from_relative_path('.git')
assert not git_node
assert not git_node

+ 7
- 3
hyde/tests/util.py View File

@@ -1,6 +1,7 @@
import re import re
import difflib import difflib



def strip_spaces_between_tags(value): def strip_spaces_between_tags(value):
""" """
Stolen from `django.util.html` Stolen from `django.util.html`
@@ -8,10 +9,11 @@ def strip_spaces_between_tags(value):
""" """
return re.sub(r'>\s+<', '><', unicode(value)) return re.sub(r'>\s+<', '><', unicode(value))



def assert_no_diff(expected, out): def assert_no_diff(expected, out):
diff = [l for l in difflib.unified_diff(expected.splitlines(True), diff = [l for l in difflib.unified_diff(expected.splitlines(True),
out.splitlines(True),
n=3)]
out.splitlines(True),
n=3)]
assert not diff, ''.join(diff) assert not diff, ''.join(diff)




@@ -23,6 +25,7 @@ def assert_html_equals(expected, actual, sanitize=None):
actual = sanitize(actual) actual = sanitize(actual)
assert expected == actual assert expected == actual



def trap_exit_fail(f): def trap_exit_fail(f):
def test_wrapper(*args): def test_wrapper(*args):
try: try:
@@ -32,6 +35,7 @@ def trap_exit_fail(f):
test_wrapper.__name__ = f.__name__ test_wrapper.__name__ = f.__name__
return test_wrapper return test_wrapper



def trap_exit_pass(f): def trap_exit_pass(f):
def test_wrapper(*args): def test_wrapper(*args):
try: try:
@@ -39,4 +43,4 @@ def trap_exit_pass(f):
except SystemExit: except SystemExit:
pass pass
test_wrapper.__name__ = f.__name__ test_wrapper.__name__ = f.__name__
return test_wrapper
return test_wrapper

+ 1
- 1
hyde/util.py View File

@@ -54,4 +54,4 @@ def discover_executable(name, sitepath):
full_name = os.path.join(path, name) full_name = os.path.join(path, name)
if os.path.exists(full_name): if os.path.exists(full_name):
return full_name return full_name
return None
return None

Loading…
Cancel
Save