# -*- coding: utf-8 -*-
"""
Contains classes and utilities related to meta data in hyde.
"""
from collections import namedtuple
from functools import partial
from operator import attrgetter
import re
import sys
from hyde._compat import basestring, filter, iteritems, str
from hyde.exceptions import HydeException
from hyde.model import Expando
from hyde.plugin import Plugin
from hyde.site import Node, Resource
from hyde.util import add_method, add_property, pairwalk
from fswrap import File, Folder
import yaml
#
# Metadata
#
class Metadata(Expando):
"""
Container class for yaml meta data.
"""
def __init__(self, data, parent=None):
super(Metadata, self).__init__({})
if parent:
self.update(parent.__dict__)
if data:
self.update(data)
def update(self, data):
"""
Updates the metadata with new stuff
"""
if isinstance(data, basestring):
super(Metadata, self).update(yaml.load(data))
else:
super(Metadata, self).update(data)
#
# Auto Extend
#
[docs]class AutoExtendPlugin(Plugin):
"""
The plugin class for extending templates using metadata.
"""
def __init__(self, site):
super(AutoExtendPlugin, self).__init__(site)
def begin_text_resource(self, resource, text):
"""
If the meta data for the resource contains a layout attribute,
and there is no extends statement, this plugin automatically adds
an extends statement to the top of the file.
"""
if not resource.uses_template:
return text
layout = None
block = None
try:
layout = resource.meta.extends
except AttributeError:
pass
try:
block = resource.meta.default_block
except AttributeError:
pass
if layout:
self.logger.debug("Autoextending %s with %s" % (
resource.relative_path, layout))
extends_pattern = self.template.patterns['extends']
if not re.search(extends_pattern, text):
extended_text = self.template.get_extends_statement(layout)
extended_text += '\n'
if block:
extended_text += ('%s\n%s\n%s' %
(self.t_block_open_tag(block),
text,
self.t_block_close_tag(block)))
else:
extended_text += text
return extended_text
return text
#
# Tagging
#
class Tag(Expando):
"""
A simple object that represents a tag.
"""
def __init__(self, name):
"""
Initialize the tag with a name.
"""
self.name = name
self.resources = []
def __repr__(self):
return self.name
def __str__(self):
return self.name
def get_tagger_sort_method(site):
config = site.config
content = site.content
walker = 'walk_resources'
sorter = None
try:
sorter = attrgetter('tagger.sorter')(config)
walker = walker + '_sorted_by_%s' % sorter
except AttributeError:
pass
try:
walker = getattr(content, walker)
except AttributeError:
HydeException.reraise(
"Cannot find the sorter: %s" % sorter,
sys.exc_info())
return walker
def walk_resources_tagged_with(node, tag):
tags = set(str(tag).split('+'))
walker = get_tagger_sort_method(node.site)
for resource in walker():
try:
taglist = set(attrgetter("meta.tags")(resource))
except AttributeError:
continue
if tags <= taglist:
yield resource
class TaggerPlugin(Plugin):
"""
Tagger plugin for hyde. Adds the ability to do tag resources and search
based on the tags.
Configuration example
---------------------
#yaml
sorter:
kind:
attr: source.kind
tagger:
sorter: kind # How to sort the resources in a tag
archives:
blog:
template: tagged_posts.j2
source: blog
target: blog/tags
archive_extension: html
"""
def __init__(self, site):
super(TaggerPlugin, self).__init__(site)
def begin_site(self):
"""
Initialize plugin. Add tag to the site context variable
and methods for walking tagged resources.
"""
self.logger.debug("Adding tags from metadata")
# *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 = {}
add_method(Node,
'walk_resources_tagged_with', walk_resources_tagged_with)
walker = get_tagger_sort_method(self.site)
for resource in walker():
self._process_tags_in_resource(resource, tags)
self._process_tag_metadata(tags)
self.site.tagger = Expando(dict(tags=tags))
self._generate_archives()
def _process_tag_metadata(self, tags):
"""
Parses and adds metadata to the tagger object, if the tagger
configuration contains metadata.
"""
try:
tag_meta = self.site.config.tagger.tags.to_dict()
except AttributeError:
tag_meta = {}
for tagname, meta in iteritems(tag_meta):
# Don't allow name and resources in meta
if 'resources' in meta:
del(meta['resources'])
if 'name' in meta:
del(meta['name'])
if tagname in tags:
tags[tagname].update(meta)
def _process_tags_in_resource(self, resource, tags):
"""
Reads the tags associated with this resource and
adds them to the tag list if needed.
"""
try:
taglist = attrgetter("meta.tags")(resource)
except AttributeError:
return
for tagname in taglist:
if tagname not in tags:
tag = Tag(tagname)
tags[tagname] = tag
tag.resources.append(resource)
add_method(Node,
'walk_resources_tagged_with_%s' % tagname,
walk_resources_tagged_with,
tag=tag)
else:
tags[tagname].resources.append(resource)
if not hasattr(resource, 'tags'):
setattr(resource, 'tags', [])
resource.tags.append(tags[tagname])
def _generate_archives(self):
"""
Generates archives if the configuration demands.
"""
archive_config = None
try:
archive_config = attrgetter("tagger.archives")(self.site.config)
except AttributeError:
return
self.logger.debug("Generating archives for tags")
for name, config in iteritems(archive_config.to_dict()):
self._create_tag_archive(config)
def _create_tag_archive(self, config):
"""
Generates archives for each tag based on the given configuration.
"""
if 'template' not in config:
raise HydeException(
"No Template specified in tagger configuration.")
content = self.site.content.source_folder
source = Folder(config.get('source', ''))
target = content.child_folder(config.get('target', 'tags'))
if not target.exists:
target.make()
# Write meta data for the configuration
meta = config.get('meta', {})
meta_text = u''
if meta:
import yaml
meta_text = yaml.dump(meta, default_flow_style=False)
extension = config.get('extension', 'html')
template = config['template']
archive_text = u"""
---
extends: false
%(meta)s
---
{%% set tag = site.tagger.tags['%(tag)s'] %%}
{%% set source = site.content.node_from_relative_path('%(node)s') %%}
{%% set walker = source['walk_resources_tagged_with_%(tag)s'] %%}
{%% extends "%(template)s" %%}
"""
for tagname, tag in iteritems(self.site.tagger.tags.to_dict()):
tag_data = {
"tag": tagname,
"node": source.name,
"template": template,
"meta": meta_text
}
text = archive_text % tag_data
archive_file = File(target.child("%s.%s" % (tagname, extension)))
archive_file.delete()
archive_file.write(text.strip())
self.site.content.add_resource(archive_file)
#
# Sorting
#
def filter_method(item, settings=None):
"""
Returns true if all the filters in the
given settings evaluate to True.
"""
all_match = True
default_filters = {}
filters = {}
if hasattr(settings, 'filters'):
filters.update(default_filters)
filters.update(settings.filters.__dict__)
for field, value in filters.items():
try:
res = attrgetter(field)(item)
except:
res = None
if res != value:
all_match = False
break
return all_match
def attributes_checker(item, attributes=None):
"""
Checks if the given list of attributes exist.
"""
try:
attrgetter(*attributes)(item)
return True
except AttributeError:
return False
def sort_method(node, settings=None):
"""
Sorts the resources in the given node based on the
given settings.
"""
attr = 'name'
if settings and hasattr(settings, 'attr') and settings.attr:
attr = settings.attr
reverse = False
if settings and hasattr(settings, 'reverse'):
reverse = settings.reverse
if not isinstance(attr, list):
attr = [attr]
filter_ = partial(filter_method, settings=settings)
excluder_ = partial(attributes_checker, attributes=attr)
resources = filter(lambda x: excluder_(x) and filter_(x),
node.walk_resources())
return sorted(resources,
key=attrgetter(*attr),
reverse=reverse)
[docs]class SorterPlugin(Plugin):
"""
Sorter plugin for hyde. Adds the ability to do
sophisticated sorting by expanding the site objects
to support prebuilt sorting methods. These methods
can be used in the templates directly.
Configuration example::
#yaml
sorter:
kind:
# Sorts by this attribute name
# Uses `attrgetter` on the resource object
attr: source_file.kind
# The filters to be used before sorting
# This can be used to remove all the items
# that do not apply. For example,
# filtering non html content
filters:
source_file.kind: html
is_processable: True
meta.is_listable: True
"""
def __init__(self, site):
super(SorterPlugin, self).__init__(site)
def begin_site(self):
"""
Initialize plugin. Add a sort and match method
for every configuration mentioned in site settings
"""
config = self.site.config
if not hasattr(config, 'sorter'):
return
for name, settings in config.sorter.__dict__.items():
sort_method_name = 'walk_resources_sorted_by_%s' % name
self.logger.debug("Adding sort methods for [%s]" % name)
add_method(Node, sort_method_name, sort_method, settings=settings)
match_method_name = 'is_%s' % name
add_method(Resource, match_method_name, filter_method, settings)
prev_att = 'prev_by_%s' % name
next_att = 'next_by_%s' % name
setattr(Resource, prev_att, None)
setattr(Resource, next_att, None)
walker = getattr(self.site.content,
sort_method_name,
self.site.content.walk_resources)
first, last = None, None
for prev, next in pairwalk(walker()):
if not first:
first = prev
last = next
setattr(prev, next_att, next)
setattr(next, prev_att, prev)
try:
circular = settings.circular
except AttributeError:
circular = False
if circular and first:
setattr(first, prev_att, last)
setattr(last, next_att, first)
#
# Grouping
#
Grouper = namedtuple('Grouper', 'group resources')
class Group(Expando):
"""
A wrapper class for groups. Adds methods for
grouping resources.
"""
def __init__(self, grouping, parent=None):
self.name = 'groups'
self.parent = parent
self.root = self
self.root = parent.root if parent else self
self.groups = []
self.sorter = getattr(grouping, 'sorter', None)
if hasattr(parent, 'sorter'):
self.sorter = parent.sorter
super(Group, self).__init__(grouping)
add_method(Node,
'walk_%s_groups' % self.name,
Group.walk_groups_in_node,
group=self)
add_method(Node,
'walk_resources_grouped_by_%s' % self.name,
Group.walk_resources,
group=self)
add_property(Resource,
'%s_group' % self.name,
Group.get_resource_group,
group=self)
add_method(Resource,
'walk_%s_groups' % self.name,
Group.walk_resource_groups,
group=self)
def set_expando(self, key, value):
"""
If the key is groups, creates group objects instead of
regular expando objects.
"""
if key == "groups":
self.groups = [Group(group, parent=self) for group in value]
else:
return super(Group, self).set_expando(key, value)
@staticmethod
def get_resource_group(resource, group):
"""
This method gets attached to the resource object.
Returns group and its ancestors that the resource
belongs to, in that order.
"""
try:
group_name = getattr(resource.meta, group.root.name)
except AttributeError:
group_name = None
return next((g for g in group.walk_groups()
if g.name == group_name), None) \
if group_name \
else None
@staticmethod
def walk_resource_groups(resource, group):
"""
This method gets attached to the resource object.
Returns group and its ancestors that the resource
belongs to, in that order.
"""
try:
group_name = getattr(resource.meta, group.root.name)
except AttributeError:
group_name = None
if group_name:
for g in group.walk_groups():
if g.name == group_name:
return reversed(list(g.walk_hierarchy()))
return []
@staticmethod
def walk_resources(node, group):
"""
The method that gets attached to the node
object for walking the resources in the node
that belong to this group.
"""
for group in group.walk_groups():
for resource in group.walk_resources_in_node(node):
yield resource
@staticmethod
def walk_groups_in_node(node, group):
"""
The method that gets attached to the node
object for walking the groups in the node.
"""
walker = group.walk_groups()
for g in walker:
lister = g.walk_resources_in_node(node)
yield Grouper(group=g, resources=lister)
def walk_hierarchy(self):
"""
Walks the group hierarchy starting from
this group.
"""
g = self
yield g
while g.parent:
yield g.parent
g = g.parent
def walk_groups(self):
"""
Walks the groups in the current group
"""
yield self
for group in self.groups:
for child in group.walk_groups():
yield child
def walk_resources_in_node(self, node):
"""
Walks the resources in the given node
sorted based on sorter configuration in this
group.
"""
walker = 'walk_resources'
if hasattr(self, 'sorter') and self.sorter:
walker = 'walk_resources_sorted_by_' + self.sorter
walker = getattr(node, walker, getattr(node, 'walk_resources'))
for resource in walker():
try:
group_value = getattr(resource.meta, self.root.name)
except AttributeError:
continue
if group_value == self.name:
yield resource
class GrouperPlugin(Plugin):
"""
Grouper plugin for hyde. Adds the ability to do
group resources and nodes in an arbitrary
hierarchy.
Configuration example
---------------------
#yaml
sorter:
kind:
attr: source.kind
grouper:
hyde:
# Categorizes the nodes and resources
# based on the groups specified here.
# The node and resource should be tagged
# with the categories in their metadata
sorter: kind # A reference to the sorter
description: Articles about hyde
groups:
-
name: announcements
description: Hyde release announcements
-
name: making of
description: Articles about hyde design decisions
-
name: tips and tricks
description: >
Helpful snippets and tweaks to
make hyde more awesome.
"""
def __init__(self, site):
super(GrouperPlugin, self).__init__(site)
def begin_site(self):
"""
Initialize plugin. Add the specified groups to the
site context variable.
"""
config = self.site.config
if not hasattr(config, 'grouper'):
return
if not hasattr(self.site, 'grouper'):
self.site.grouper = {}
for name, grouping in self.site.config.grouper.__dict__.items():
grouping.name = name
prev_att = 'prev_in_%s' % name
next_att = 'next_in_%s' % name
setattr(Resource, prev_att, None)
setattr(Resource, next_att, None)
self.site.grouper[name] = Group(grouping)
walker = Group.walk_resources(
self.site.content, self.site.grouper[name])
for prev, next in pairwalk(walker):
setattr(next, prev_att, prev)
setattr(prev, next_att, next)