class Plugin(object): """Plugin base class. Volt plugins are subclasses of Plugin that perform a set of operations to Unit objects of a given engine. They are executed after all Engines finish parsing their units and before any output files are written. Plugin execution is handled by the Generator object in volt.gen. During a Generator run, Volt tries first to look up a given plugin in the plugins directory in the project's root folder. Failing that, Volt will try to load the plugin from volt.plugins. Default settings for a Plugin object should be stored as a Config object set as a class attribute with the name DEFAULTS. Another class attribute named USER_CONF_ENTRY may also be defined. This tells the Plugin which Config object in the user's voltconf.py will be consolidated with the default configurations in DEFAULTS. Finally, all Plugin subclasses must implement a run method, which is the entry point for plugin execution by the Generator class. """ __metaclass__ = abc.ABCMeta DEFAULTS = Config() USER_CONF_ENTRY = None def __init__(self): """Initializes Plugin.""" self.config = Config(self.DEFAULTS) def prime(self): """Consolidates default plugin Config and user-defined Config.""" # only override defaults if USER_CONF_ENTRY is defined if self.USER_CONF_ENTRY is not None: # get user config object conf_name = os.path.splitext(os.path.basename(CONFIG.VOLT.USER_CONF))[0] voltconf = path_import(conf_name, CONFIG.VOLT.ROOT_DIR) # use default Config if the user does not list any try: user_config = getattr(voltconf, self.USER_CONF_ENTRY) except AttributeError: user_config = Config() # to ensure proper Config consolidation if not isinstance(user_config, Config): raise TypeError("User Config object '%s' must be a Config instance." % self.USER_CONF_ENTRY) else: self.config.update(user_config) @abc.abstractmethod def run(self): """Runs the plugin."""
class Engine(LoggableMixin): """Base Volt Engine class. Engine is the core component of Volt that performs initial processing of each unit. This base engine class does not perform any processing by itself, but provides convenient unit processing methods for the subclassing engine. Any subclass of Engine must create a 'units' property and override the dispatch method. Optionally, the preprocess method may be overridden if any unit processing prior to plugin run needs to be performed. """ __metaclass__ = abc.ABCMeta DEFAULTS = Config() def __init__(self): self.config = Config(self.DEFAULTS) self.logger.debug('created: %s' % type(self).__name__) self._templates = {} # attributes below are placeholders for template access later on self.widgets = {} def preprocess(self): """Performs initial processing of units before plugins are run.""" pass @abc.abstractmethod def dispatch(self): """Performs final processing after all plugins are run.""" @abc.abstractmethod def units(self): """Units of the engine.""" def prime(self): """Consolidates default engine Config and user-defined Config. In addition to consolidating Config values, this method also sets the values of CONTENT_DIR, and *_TEMPLATE to absolute directory paths. """ # get user config object conf_name = os.path.splitext(os.path.basename(CONFIG.VOLT.USER_CONF))[0] user_conf = path_import(conf_name, CONFIG.VOLT.ROOT_DIR) # custom engines must define an entry name for the user's voltconf if not hasattr (self, 'USER_CONF_ENTRY'): message = "%s must define a %s value as a class attribute." % \ (type(self).__name__, 'USER_CONF_ENTRY') self.logger.error(message) # use default config if the user does not specify any try: user_config = getattr(user_conf, self.USER_CONF_ENTRY) except AttributeError: user_config = Config() # to ensure proper Config consolidation if not isinstance(user_config, Config): message = "User Config object '%s' must be a Config instance." % \ self.USER_CONF_ENTRY self.logger.error(message) raise TypeError(message) else: self.config.update(user_config) # check attributes that must exist for attr in _REQUIRED_ENGINE_CONFIG: try: getattr(self.config, attr) except AttributeError: message = "%s Config '%s' value is undefined." % \ (type(self).__name__, attr) self.logger.error(message) self.logger.debug(format_exc()) raise # set engine config paths to absolute paths self.config.CONTENT_DIR = os.path.join(CONFIG.VOLT.CONTENT_DIR, \ self.config.CONTENT_DIR) for template in [x for x in self.config.keys() if x.endswith('_TEMPLATE')]: self.config[template] = os.path.join(CONFIG.VOLT.TEMPLATE_DIR, \ self.config[template]) def chain_units(self): """Sets the previous and next permalink attributes of each unit.""" chain_item_permalinks(self.units) self.logger.debug('done: chaining units') def sort_units(self): """Sorts a list of units according to the given header field name.""" sort_key = self.config.SORT_KEY reversed = sort_key.startswith('-') sort_key = sort_key.strip('-') try: self.units.sort(key=lambda x: getattr(x, sort_key), reverse=reversed) except AttributeError: message = "Sort key '%s' not present in all units." % sort_key self.logger.error(message) self.logger.debug(format_exc()) raise self.logger.debug("done: sorting units based on '%s'" % self.config.SORT_KEY) @cachedproperty def paginations(self): """Paginations of engine units in a dictionary. The computation will expand the supplied patterns according to the values present in all units. For example, if the pattern is '{time:%Y}' and there are five units with a datetime.year attribute 2010 and another five with 2011, create_paginations will return a dictionary with one key pointing to a list containing paginations for 'time/2010' and 'time/2011'. The number of actual paginations vary, depending on how many units are in one pagination. """ # check attributes that must exist for attr in _REQUIRED_ENGINE_PAGINATIONS: try: getattr(self.config, attr) except AttributeError: message = "%s Config '%s' value is undefined." % \ (type(self).__name__, attr) self.logger.error(message) self.logger.debug(format_exc()) raise base_url = self.config.URL.strip('/') units_per_pagination = self.config.UNITS_PER_PAGINATION pagination_patterns = self.config.PAGINATIONS # create_paginations operates on self.units units = self.units if not units: warnings.warn("%s has no units to paginate." % type(self).__name__, \ EmptyUnitsWarning) # exit function if there's no units to process return {} paginator_map = { 'all': self._paginate_all, 'str': self._paginate_single, 'int': self._paginate_single, 'float': self._paginate_single, 'list': self._paginate_multiple, 'tuple': self._paginate_multiple, 'datetime': self._paginate_datetime, } paginations = {} for pattern in pagination_patterns: perm_tokens = re.findall(_RE_PERMALINK, pattern.strip('/') + '/') base_permalist = [base_url] + perm_tokens # only the last token is allowed to be enclosed in '{}' for token in base_permalist[:-1]: if '{%s}' % token[1:-1] == token: message = "Pagination pattern %s is invalid." % pattern self.logger.error(message) raise ValueError(message) # determine which paginate method to use based on field type last_token = base_permalist[-1] field = last_token[1:-1] if '{%s}' % field != last_token: field_type = 'all' else: sample = getattr(units[0], field.split(':')[0]) field_type = sample.__class__.__name__ try: paginate = paginator_map[field_type] except KeyError: message = "Pagination method for '%s' has not been " \ "implemented." % field_type self.logger.error(message) self.logger.debug(format_exc()) raise else: args = [field, base_permalist, units_per_pagination] # if pagination_patterns is a dict, then use the supplied # title pattern if isinstance(pagination_patterns, dict): args.append(pagination_patterns[pattern]) pagination_in_pattern = paginate(*args) key = '/'.join(base_permalist) paginations[key] = pagination_in_pattern return paginations def _paginate_all(self, field, base_permalist, units_per_pagination, \ title_pattern=''): """Create paginations for all field values (PRIVATE).""" paginated = self._paginator(self.units, base_permalist, \ units_per_pagination, title_pattern) self.logger.debug('created: %d %s paginations' % (len(paginated), 'all')) return paginated def _paginate_single(self, field, base_permalist, units_per_pagination, \ title_pattern=''): """Create paginations for string/int/float header field values (PRIVATE).""" units = self.units str_set = set([getattr(x, field) for x in units]) paginated = [] for item in str_set: matches = [x for x in units if item == getattr(x, field)] base_permalist = base_permalist[:-1] + [str(item)] if title_pattern: title = title_pattern % str(item) else: title = title_pattern pagin = self._paginator(matches, base_permalist, \ units_per_pagination, title) paginated.extend(pagin) self.logger.debug('created: %d %s paginations' % (len(paginated), field)) return paginated def _paginate_multiple(self, field, base_permalist, units_per_pagination, \ title_pattern=''): """Create paginations for list or tuple header field values (PRIVATE).""" units = self.units item_list_per_unit = (getattr(x, field) for x in units) item_set = reduce(set.union, [set(x) for x in item_list_per_unit]) paginated = [] for item in item_set: matches = [x for x in units if item in getattr(x, field)] base_permalist = base_permalist[:-1] + [str(item)] if title_pattern: title = title_pattern % str(item) else: title = title_pattern pagin = self._paginator(matches, base_permalist, \ units_per_pagination, title) paginated.extend(pagin) self.logger.debug('created: %d %s paginations' % (len(paginated), field)) return paginated def _paginate_datetime(self, field, base_permalist, \ units_per_pagination, title_pattern=''): """Create paginations for datetime header field values (PRIVATE).""" units = self.units # separate the field name from the datetime formatting field, time_fmt = field.split(':') time_tokens = time_fmt.strip('/').split('/') unit_times = [getattr(x, field) for x in units] # construct set of all datetime combinations in units according to # the user's supplied pagination URL; e.g. if URL == '%Y/%m' and # there are two units with 2009/10 and one with 2010/03 then # time_set == set([('2009', '10), ('2010', '03']) time_strs = [[x.strftime(y) for x in unit_times] for y in time_tokens] time_set = set(zip(*time_strs)) paginated = [] # create placeholders for new tokens base_permalist = base_permalist[:-1] + [None] * len(time_tokens) for item in time_set: # get all units whose datetime values match 'item' matches = [] for unit in units: val = getattr(unit, field) time_str = [[val.strftime(y)] for y in time_tokens] time_tuple = zip(*time_str) assert len(time_tuple) == 1 if item in time_tuple: matches.append(unit) base_permalist = base_permalist[:-(len(time_tokens))] + list(item) if title_pattern: title = getattr(matches[0], field).strftime(title_pattern) else: title = title_pattern pagin = self._paginator(matches, base_permalist, \ units_per_pagination, title) paginated.extend(pagin) self.logger.debug('created: %d %s paginations' % (len(paginated), field)) return paginated def _paginator(self, units, base_permalist, units_per_pagination, title=''): """Create paginations from units (PRIVATE). units -- List of all units which will be paginated. base_permalist -- List of permalink tokens that will be used by all paginations of the given units. units_per_pagination -- Number of units to show per pagination. title -- String to use as the pagination title. """ paginations = [] # count how many paginations we need is_last = len(units) % units_per_pagination != 0 pagination_len = len(units) // units_per_pagination + int(is_last) # construct pagination objects for each pagination page for idx in range(pagination_len): start = idx * units_per_pagination if idx != pagination_len - 1: stop = (idx + 1) * units_per_pagination units_in_pagination = units[start:stop] else: units_in_pagination = units[start:] pagination = Pagination(units_in_pagination, idx, base_permalist, \ title) paginations.append(pagination) if len(paginations) > 1: chain_item_permalinks(paginations) self.logger.debug('done: chaining paginations') return paginations def write_units(self): """Writes units using the unit template file.""" self._write_items(self.units, self.config.UNIT_TEMPLATE) self.logger.debug('written: %d %s unit(s)' % (len(self.units), \ type(self).__name__[:-len('Engine')])) def write_paginations(self): """Writes paginations using the pagination template file.""" for pattern in self.paginations: self._write_items(self.paginations[pattern], self.config.PAGINATION_TEMPLATE) self.logger.debug("written: '%s' pagination(s)" % pattern) def _write_items(self, items, template_path): """Writes Page objects using the given template file (PRIVATE). items -- List of Page objects to be written. template_path -- Template file name, must exist in the defined template directory. """ template_env = CONFIG.SITE.TEMPLATE_ENV template_file = os.path.basename(template_path) # get template from cache if it's already loaded if template_file not in self._templates: template = template_env.get_template(template_file) self._templates[template_file] = template else: template = self._templates[template_file] for item in items: # warn if files are overwritten # this indicates a duplicate post, which could result in # unexpected results if os.path.exists(item.path): message = "File %s already exists. Make sure there are no "\ "other entries leading to this file path." % item.path self.logger.error(message) raise IOError(message) else: rendered = template.render(page=item, CONFIG=CONFIG, \ widgets=self.widgets) if sys.version_info[0] < 3: rendered = rendered.encode('utf-8') write_file(item.path, rendered)