Browse Source

Added plugin lifecycle hooks and tests. Moved media into the content folder

main
Lakshmi Vyasarajan 14 years ago
parent
commit
db2879035e
14 changed files with 432 additions and 51 deletions
  1. +2
    -4
      hyde/ext/templates/jinja.py
  2. +16
    -1
      hyde/fs.py
  3. +79
    -18
      hyde/generator.py
  4. +9
    -1
      hyde/loader.py
  5. +2
    -1
      hyde/model.py
  6. +60
    -13
      hyde/plugin.py
  7. +1
    -0
      hyde/site.py
  8. +0
    -0
      hyde/tests/__init__.py
  9. +11
    -11
      hyde/tests/sites/test_jinja/content/crossdomain.xml
  10. +0
    -0
      hyde/tests/sites/test_jinja/content/media/css/site.css
  11. +2
    -0
      hyde/tests/test_fs.py
  12. +2
    -1
      hyde/tests/test_model.py
  13. +246
    -0
      hyde/tests/test_plugin.py
  14. +2
    -1
      hyde/tests/test_site.py

+ 2
- 4
hyde/ext/templates/jinja.py View File

@@ -6,8 +6,7 @@ from hyde.fs import File, Folder
from hyde.template import Template
from jinja2 import contextfunction, Environment, FileSystemLoader, Undefined


class LoyalUndefined(Undefined):
class SilentUndefined(Undefined):
def __getattr__(self, name):
return self

@@ -45,12 +44,11 @@ class Jinja2Template(Template):
if config:
loader = FileSystemLoader([
str(config.content_root_path),
str(config.media_root_path),
str(config.layout_root_path),
])
else:
loader = FileSystemLoader(str(self.sitepath))
self.env = Environment(loader=loader, undefined=LoyalUndefined)
self.env = Environment(loader=loader, undefined=SilentUndefined)
self.env.globals['media_url'] = media_url
self.env.globals['content_url'] = content_url



+ 16
- 1
hyde/fs.py View File

@@ -171,12 +171,27 @@ class File(FS):
(mime, encoding) = mimetypes.guess_type(self.path)
return mime

@property
def is_binary(self):
"""Return true if this is a binary file."""
with open(self.path, 'rb') as fin:
CHUNKSIZE = 1024
while 1:
chunk = fin.read(CHUNKSIZE)
if '\0' in chunk:
return True
if len(chunk) < CHUNKSIZE:
break
return False

@property
def is_text(self):
return self.mimetype.split("/")[0] == "text"
"""Return true if this is a text file."""
return (not self.is_binary)

@property
def is_image(self):
"""Return true if this is an image file."""
return self.mimetype.split("/")[0] == "image"

def read_all(self, encoding='utf-8'):


+ 79
- 18
hyde/generator.py View File

@@ -3,6 +3,7 @@ The generator class and related utility functions.
"""
from hyde.exceptions import HydeException
from hyde.fs import File
from hyde.plugin import Plugin
from hyde.template import Template

from contextlib import contextmanager
@@ -24,6 +25,30 @@ class Generator(object):
self.site = site
self.__context__ = dict(site=site)
self.template = None
Plugin.load_all(site)

class PluginProxy(object):
"""
A proxy class to raise events in registered plugins
"""

def __init__(self, site):
super(PluginProxy, self).__init__()
self.site = site

def __getattr__(self, method_name):
if hasattr(Plugin, method_name):

def __call_plugins__(*args, **kwargs):
if self.site.plugins:
for plugin in self.site.plugins:
if hasattr(plugin, method_name):
function = getattr(plugin, method_name)
function(*args, **kwargs)
return __call_plugins__
raise HydeException(
"Unknown plugin method [%s] called." % method_name)
self.events = PluginProxy(self.site)

@contextmanager
def context_for_resource(self, resource):
@@ -37,7 +62,7 @@ class Generator(object):
yield self.__context__
self.__context__.update(resource=None)

def initialize_template_if_needed(self):
def load_template_if_needed(self):
"""
Loads and configures the template environement from the site
configuration if its not done already.
@@ -50,7 +75,16 @@ class Generator(object):
logger.info("Configuring the template environment")
self.template.configure(self.site.config)

def reload_if_needed(self):
self.events.template_loaded(self.template)

def initialize(self):
"""
Start Generation. Perform setup tasks and inform plugins.
"""
logger.info("Begin Generation")
self.events.begin_generation()

def load_site_if_needed(self):
"""
Checks if the site requries a reload and loads if
necessary.
@@ -60,25 +94,35 @@ class Generator(object):
logger.info("Reading site contents")
self.site.load()

def finalize(self):
"""
Generation complete. Inform plugins and cleanup.
"""
logger.info("Generation Complete")
self.events.generation_complete()

def generate_all(self):
"""
Generates the entire website
"""
logger.info("Reading site contents")
self.initialize_template_if_needed()
self.reload_if_needed()

self.load_template_if_needed()
self.initialize()
self.load_site_if_needed()
self.events.begin_site()
logger.info("Generating site to [%s]" %
self.site.config.deploy_root_path)
self.__generate_node__(self.site.content)
self.events.site_complete()
self.finalize()

def generate_node_at_path(self, node_path=None):
"""
Generates a single node. If node_path is non-existent or empty,
generates the entire site.
"""
self.initialize_template_if_needed()
self.reload_if_needed()
self.load_template_if_needed()
self.load_site_if_needed()
node = None
if node_path:
node = self.site.content.node_from_path(node_path)
@@ -89,12 +133,16 @@ class Generator(object):
Generates the given node. If node is invalid, empty or
non-existent, generates the entire website.
"""
self.initialize_template_if_needed()
self.reload_if_needed()
if not node:
return self.generate_all()

self.load_template_if_needed()
self.initialize()
self.load_site_if_needed()

try:
self.__generate_node__(node)
self.finalize()
except HydeException:
self.generate_all()

@@ -103,31 +151,39 @@ class Generator(object):
Generates a single resource. If resource_path is non-existent or empty,
generats the entire website.
"""
self.initialize_template_if_needed()
self.reload_if_needed()
self.load_template_if_needed()
self.load_site_if_needed()
resource = None
if resource_path:
resource = self.site.content.resource_from_path(resource_path)
return self.generate_resource(resource)
self.generate_resource(resource)

def generate_resource(self, resource=None):
"""
Generates the given resource. If resource is invalid, empty or
non-existent, generates the entire website.
"""
self.initialize_template_if_needed()
self.reload_if_needed()
if not resource:
return self.generate_all()

self.load_template_if_needed()
self.initialize()
self.load_site_if_needed()

try:
self.__generate_resource__(resource)
self.finalize()
except HydeException:
self.generate_all()


def __generate_node__(self, node):
logger.info("Generating [%s]", node)
for resource in node.walk_resources():
self.__generate_resource__(resource)
for node in node.walk():
self.events.begin_node(node)
for resource in node.resources:
self.__generate_resource__(resource)
self.events.node_complete(node)

def __generate_resource__(self, resource):
logger.info("Processing [%s]", resource)
@@ -137,10 +193,15 @@ class Generator(object):
self.site.content.source_folder))
target.parent.make()
if resource.source_file.is_text:
text = resource.source_file.read_all()
text = self.events.begin_text_resource(resource, text) or text
logger.info("Rendering [%s]", resource)
text = self.template.render(resource.source_file.read_all(),
context)
text = self.template.render(text, context)
text = self.events.text_resource_complete(
resource, text) or text
target.write(text)
else:
logger.info("Copying binary file [%s]", resource)
self.events.begin_binary_resource(resource)
resource.source_file.copy_to(target)
self.events.binary_resource_complete(resource)

+ 9
- 1
hyde/loader.py View File

@@ -5,6 +5,11 @@ import sys

from hyde.exceptions import HydeException

import logging
from logging import NullHandler
logger = logging.getLogger('hyde.engine')
logger.addHandler(NullHandler())

plugins = {}
templates = {}

@@ -17,6 +22,7 @@ def load_python_object(name):
if module_name == '':
(module_name, object_name) = (object_name, module_name)
try:
logger.info('Loading module [%s]' % module_name)
module = __import__(module_name)
except ImportError:
raise HydeException("The given module name [%s] is invalid." %
@@ -32,11 +38,13 @@ def load_python_object(name):
module_name)

try:
logger.info('Getting object [%s] from module [%s]' %
(object_name, module_name))
return getattr(module, object_name)
except AttributeError:
raise HydeException("Cannot load the specified plugin [%s]. "
"The given module [%s] does not contain the "
"desired object [%s]. Please fix the"
"desired object [%s]. Please fix the "
"configuration or ensure that the module is "
"installed properly" %
(name, module_name, object_name))

+ 2
- 1
hyde/model.py View File

@@ -44,7 +44,8 @@ class Config(Expando):
media_root='media',
layout_root='layout',
media_url='/media',
site_url='/'
site_url='/',
plugins = []
)
conf = dict(**default_config)
if config_dict:


+ 60
- 13
hyde/plugin.py View File

@@ -2,12 +2,14 @@
"""
Contains definition for a plugin protocol and other utiltities.
"""

import abc
from hyde import loader

class Plugin(object):
"""
The plugin protocol
"""
__metaclass__ = abc.ABCMeta

def __init__(self, site):
super(Plugin, self).__init__()
@@ -19,60 +21,105 @@ class Plugin(object):
"""
pass

def prepare_site(self):
def begin_generation(self):
"""
Called when generation is about to take place.
"""
pass

def site_load_complete(self):
def begin_site(self):
"""
Called when the site is built complete. This implies that all the
Called when the site is loaded completely. This implies that all the
nodes and resources have been identified and are accessible in the
site variable.
"""
pass

def prepare_node(self, node):
def begin_node(self, node):
"""
Called when a node is about to be processed for generation.
This method is called only when the entire node is generated.
"""
pass

def prepare_resource(self, resource, text):
def begin_text_resource(self, resource, text):
"""
Called when a resource is about to be processed for generation.
The `text` parameter contains the, resource text at this point
Called when a text resource is about to be processed for generation.
The `text` parameter contains the resource text at this point
in its lifecycle. It is the text that has been loaded and any
plugins that are higher in the order may have tampered with it.
But the text has not been processed by the template yet.
But the text has not been processed by the template yet. Note that
the source file associated with the text resource may not be modifed
by any plugins.

If this function returns a value, it is used as the text for further
processing.
"""
return text

def process_resource(self, resource, text):
def begin_binary_resource(self, resource):
"""
Called when a binary resource is about to be processed for generation.

Plugins are free to modify the contents of the file.
"""
pass

def text_resource_complete(self, resource, text):
"""
Called when a resource has been processed by the template.
The `text` parameter contains the, resource text at this point
The `text` parameter contains the resource text at this point
in its lifecycle. It is the text that has been processed by the
template and any plugins that are higher in the order may have
tampered with it.
tampered with it. Note that the source file associated with the
text resource may not be modifed by any plugins.

If this function returns a value, it is used as the text for further
processing.
"""
return text

def binary_resource_complete(self, resource):
"""
Called when a binary resource has already been processed.

Plugins are free to modify the contents of the file.
"""
pass

def node_complete(self, node):
"""
Called when all the resources in the node have been processed.
This method is called only when the entire node is generated.
"""
pass

def site_complete(self):
"""
Called when the entire site has been processed. This method is called
only when the entire site is generated.
"""
pass

def site_complete(self):
"""
Called when the entire site has been processed.
Called when the generation process is complete. This method is called
only when the entire site is generated.
"""
pass

def generation_complete(self):
"""
Called when generation is completed.
"""
pass

@staticmethod
def load_all(site):
"""
Loads plugins based on the configuration. Assigns the plugins to
'site.plugins'
"""

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

+ 1
- 0
hyde/site.py View File

@@ -275,6 +275,7 @@ class Site(object):
self.sitepath = Folder(str(sitepath))
self.config = config if config else Config(self.sitepath)
self.content = RootNode(self.config.content_root_path, self)
self.plugins = []

def load(self):
"""


+ 0
- 0
hyde/tests/__init__.py View File


+ 11
- 11
hyde/tests/sites/test_jinja/content/crossdomain.xml View File

@@ -1,24 +1,24 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->

<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
<!--
If you host a crossdomain.xml file with allow-access-from domain=“*”
and don’t understand all of the points described here, you probably
If you host a crossdomain.xml file with allow-access-from domain=“*”
and don’t understand all of the points described here, you probably
have a nasty security vulnerability. ~ simon willison
-->


hyde/tests/sites/test_jinja/media/css/site.css → hyde/tests/sites/test_jinja/content/media/css/site.css View File


+ 2
- 0
hyde/tests/test_fs.py View File

@@ -109,6 +109,7 @@ HELPERS = File(JINJA2.child('helpers.html'))
INDEX = File(JINJA2.child('index.html'))
LAYOUT = File(JINJA2.child('layout.html'))
LOGO = File(TEMPLATE_ROOT.child('../../../resources/hyde-logo.png'))
XML = File(TEMPLATE_ROOT.child('../sites/test_jinja/content/crossdomain.xml'))

def test_ancestors():
depth = 0
@@ -156,6 +157,7 @@ def test_mimetype():
def test_is_text():
assert HELPERS.is_text
assert not LOGO.is_text
assert XML.is_text

def test_is_image():
assert not HELPERS.is_image


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

@@ -62,7 +62,8 @@ class TestConfig(object):
assert getattr(c, name) == root
assert hasattr(c, path)
assert getattr(c, path) == TEST_SITE_ROOT.child_folder(root)

assert hasattr(c, 'plugins')
assert len(c.plugins) == 0
assert c.deploy_root_path == TEST_SITE_ROOT.child_folder('deploy')

def test_conf1(self):


+ 246
- 0
hyde/tests/test_plugin.py View File

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

from hyde.exceptions import HydeException
from hyde.fs import File, Folder
from hyde.generator import Generator
from hyde.plugin import Plugin
from hyde.site import Site

from mock import patch
from nose.tools import raises, nottest, with_setup


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

class PluginLoaderStub(Plugin):
pass


class TestPlugins(object):

@classmethod
def setup_class(cls):
TEST_SITE.make()
TEST_SITE.parent.child_folder('sites/test_jinja').copy_contents_to(TEST_SITE)
folders = []
text_files = []
binary_files = []

with TEST_SITE.child_folder('content').walker as walker:
@walker.folder_visitor
def visit_folder(folder):
folders.append(folder.path)

@walker.file_visitor
def visit_file(afile):
if not afile.is_text:
binary_files.append(afile.path)
else:
text_files.append(afile.path)

cls.content_nodes = sorted(folders)
cls.content_text_resources = sorted(text_files)
cls.content_binary_resources = sorted(binary_files)


@classmethod
def teardown_class(cls):
TEST_SITE.delete()

def setUp(self):
self.site = Site(TEST_SITE)
self.site.config.plugins = ['hyde.tests.test_plugin.PluginLoaderStub']

def test_can_load_plugin_modules(self):
assert not len(self.site.plugins)
Plugin.load_all(self.site)

assert len(self.site.plugins) == 1
assert self.site.plugins[0].__class__.__name__ == 'PluginLoaderStub'


def test_generator_loads_plugins(self):
gen = Generator(self.site)
assert len(self.site.plugins) == 1

def test_generator_template_registered_called(self):
with patch.object(PluginLoaderStub, 'template_loaded') as template_loaded_stub:
gen = Generator(self.site)
gen.generate_all()
assert template_loaded_stub.call_count == 1

def test_generator_template_begin_generation_called(self):
with patch.object(PluginLoaderStub, 'begin_generation') as begin_generation_stub:
gen = Generator(self.site)
gen.generate_all()
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:
gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path)

assert begin_generation_stub.call_count == 1

def test_generator_template_begin_generation_called_for_single_node(self):
with patch.object(PluginLoaderStub, 'begin_generation') as begin_generation_stub:
gen = Generator(self.site)
path = self.site.content.source_folder
gen.generate_node_at_path(path)
assert begin_generation_stub.call_count == 1


def test_generator_template_generation_complete_called(self):
with patch.object(PluginLoaderStub, 'generation_complete') as generation_complete_stub:
gen = Generator(self.site)
gen.generate_all()
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:
gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path)

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:
gen = Generator(self.site)
path = self.site.content.source_folder
gen.generate_node_at_path(path)
assert generation_complete_stub.call_count == 1

def test_generator_template_begin_site_called(self):
with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub:
gen = Generator(self.site)
gen.generate_all()
assert begin_site_stub.call_count == 1

def test_generator_template_begin_site_not_called_for_single_resource(self):
with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub:
gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path)
assert begin_site_stub.call_count == 0

def test_generator_template_begin_site_not_called_for_single_node(self):
with patch.object(PluginLoaderStub, 'begin_site') as begin_site_stub:
gen = Generator(self.site)
path = self.site.content.source_folder
gen.generate_node_at_path(path)

assert begin_site_stub.call_count == 0

def test_generator_template_site_complete_called(self):
with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
gen = Generator(self.site)
gen.generate_all()
assert site_complete_stub.call_count == 1


def test_generator_template_site_complete_not_called_for_single_resource(self):

with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path)

assert site_complete_stub.call_count == 0

def test_generator_template_site_complete_not_called_for_single_node(self):

with patch.object(PluginLoaderStub, 'site_complete') as site_complete_stub:
gen = Generator(self.site)
path = self.site.content.source_folder
gen.generate_node_at_path(path)

assert site_complete_stub.call_count == 0

def test_generator_template_begin_node_called(self):

with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub:
gen = Generator(self.site)
gen.generate_all()


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])
assert called_with_nodes == self.content_nodes

def test_generator_template_begin_node_not_called_for_single_resource(self):

with patch.object(PluginLoaderStub, 'begin_node') as begin_node_stub:
gen = Generator(self.site)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
assert begin_node_stub.call_count == 0


def test_generator_template_node_complete_called(self):

with patch.object(PluginLoaderStub, 'node_complete') as node_complete_stub:
gen = Generator(self.site)
gen.generate_all()


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])
assert called_with_nodes == self.content_nodes

def test_generator_template_node_complete_not_called_for_single_resource(self):

with patch.object(PluginLoaderStub, 'node_complete') as node_complete_stub:
gen = Generator(self.site)
gen.generate_resource_at_path(self.site.content.source_folder.child('about.html'))
assert node_complete_stub.call_count == 0

def test_generator_template_begin_text_resource_called(self):

with patch.object(PluginLoaderStub, 'begin_text_resource') as begin_text_resource_stub:
gen = Generator(self.site)
gen.generate_all()


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 == len(self.content_text_resources)
assert called_with_resources == self.content_text_resources

def test_generator_template_begin_text_resource_called_for_single_resource(self):

with patch.object(PluginLoaderStub, 'begin_text_resource') as begin_text_resource_stub:
gen = Generator(self.site)
path = self.site.content.source_folder.child('about.html')
gen.generate_resource_at_path(path)

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 called_with_resources[0] == path

def test_generator_template_begin_binary_resource_called(self):

with patch.object(PluginLoaderStub, 'begin_binary_resource') as begin_binary_resource_stub:
gen = Generator(self.site)
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)
assert called_with_resources == self.content_binary_resources

def test_generator_template_begin_binary_resource_called_for_single_resource(self):

with patch.object(PluginLoaderStub, 'begin_binary_resource') as begin_binary_resource_stub:
gen = Generator(self.site)
path = self.site.content.source_folder.child('favicon.ico')
gen.generate_resource_at_path(path)


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 called_with_resources[0] == path

+ 2
- 1
hyde/tests/test_site.py View File

@@ -75,7 +75,8 @@ def test_walk_resources():
"merry-christmas.html",
"crossdomain.xml",
"favicon.ico",
"robots.txt"
"robots.txt",
"site.css"
]
pages.sort()
expected.sort()


Loading…
Cancel
Save