Exemple #1
0
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None, cache_key=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)
        self.cache_key = cache_key or 'default'

        if cache:
            self.cache = ExtensibleCache(self.cache_dir)
        else:
            self.cache = NullExtensibleCache()

        if env is None:
            env = StandardEnvironment()
        self.env = env
        env.initialize(self)

        stats = env.stats
        stats.registerTimer('SiteConfigLoad')
        stats.registerTimer('PageLoad')
        stats.registerTimer("BuildRenderData")
        stats.registerTimer("BuildLazyPageData")
        stats.registerTimer("PageRender")
        stats.registerTimer("PageRenderSegments")
        stats.registerTimer("PageRenderLayout")
        stats.registerTimer("PageSerialize")
        stats.registerCounter('PageLoads')
        stats.registerCounter('PageRenderSegments')
        stats.registerCounter('PageRenderLayout')
Exemple #2
0
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None, cache_key=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)
        self.cache_key = cache_key or 'default'

        if cache:
            self.cache = ExtensibleCache(self.cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)
        self.env.registerTimer('SiteConfigLoad')
        self.env.registerTimer('PageLoad')
        self.env.registerTimer("PageDataBuild")
        self.env.registerTimer("BuildRenderData")
        self.env.registerTimer("PageRender")
        self.env.registerTimer("PageRenderSegments")
        self.env.registerTimer("PageRenderLayout")
        self.env.registerTimer("PageSerialize")
Exemple #3
0
 def __init__(self):
     self.root_dir = None
     self.debug = False
     self.templates_dirs = []
     self.theme_dir = None
     self.cache_dir = None
     self.config = PieCrustConfiguration()
     self.plugin_loader = PluginLoader(self)
     self.env = None
Exemple #4
0
    def __init__(self,
                 root_dir,
                 cache=True,
                 debug=False,
                 theme_site=False,
                 env=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)

        if cache:
            cache_dir = os.path.join(self.cache_dir, 'default')
            self.cache = ExtensibleCache(cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)
Exemple #5
0
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)

        if cache:
            cache_dir = os.path.join(self.cache_dir, 'default')
            self.cache = ExtensibleCache(cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)
Exemple #6
0
class PieCrust(object):
    def __init__(self,
                 root_dir,
                 cache=True,
                 debug=False,
                 theme_site=False,
                 env=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)

        if cache:
            cache_dir = os.path.join(self.cache_dir, 'default')
            self.cache = ExtensibleCache(cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)

    @cached_property
    def config(self):
        logger.debug("Creating site configuration...")
        paths = []
        if self.theme_dir:
            paths.append(os.path.join(self.theme_dir, THEME_CONFIG_PATH))
        paths.append(os.path.join(self.root_dir, CONFIG_PATH))

        config_cache = self.cache.getCache('app')
        config = PieCrustConfiguration(paths, config_cache)
        if self.theme_dir:
            # We'll need to patch the templates directories to be relative
            # to the site's root, and not the theme root.
            def _fixupThemeTemplatesDir(index, config):
                if index != 0:
                    return
                sitec = config.get('site')
                if sitec is None:
                    return
                tplc = sitec.get('templates_dirs')
                if tplc is None:
                    return
                if isinstance(tplc, str):
                    tplc = [tplc]
                sitec['templates_dirs'] = list(
                    filter(tplc, lambda p: os.path.join(self.theme_dir, p)))

            config.fixups.append(_fixupThemeTemplatesDir)

            # We'll also need to flag all page sources as coming from
            # the theme.
            def _fixupThemeSources(index, config):
                if index != 0:
                    return
                sitec = config.get('site')
                if sitec is None:
                    sitec = {}
                    config['site'] = sitec
                srcc = sitec.get('sources')
                if srcc is not None:
                    for sn, sc in srcc.items():
                        sc['realm'] = REALM_THEME

            config.fixups.append(_fixupThemeSources)

        return config

    @cached_property
    def assets_dirs(self):
        assets_dirs = self._get_configurable_dirs(ASSETS_DIR,
                                                  'site/assets_dirs')

        # Also add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, ASSETS_DIR)
            if os.path.isdir(default_theme_dir):
                assets_dirs.append(default_theme_dir)

        return assets_dirs

    @cached_property
    def templates_dirs(self):
        templates_dirs = self._get_configurable_dirs(TEMPLATES_DIR,
                                                     'site/templates_dirs')

        # Also, add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, TEMPLATES_DIR)
            if os.path.isdir(default_theme_dir):
                templates_dirs.append(default_theme_dir)

        return templates_dirs

    @cached_property
    def theme_dir(self):
        td = self._get_dir(THEME_DIR)
        if td is not None:
            return td
        return os.path.join(RESOURCES_DIR, 'theme')

    @cached_property
    def cache_dir(self):
        return os.path.join(self.root_dir, CACHE_DIR)

    @property  # Not a cached property because its result can change.
    def sub_cache_dir(self):
        if self.cache.enabled:
            return self.cache.base_dir
        return None

    @cached_property
    def sources(self):
        defs = {}
        for cls in self.plugin_loader.getSources():
            defs[cls.SOURCE_NAME] = cls

        sources = []
        for n, s in self.config.get('site/sources').items():
            cls = defs.get(s['type'])
            if cls is None:
                raise ConfigurationError("No such page source type: %s" %
                                         s['type'])
            src = cls(self, n, s)
            sources.append(src)
        return sources

    @cached_property
    def routes(self):
        routes = []
        for r in self.config.get('site/routes'):
            rte = Route(self, r)
            routes.append(rte)
        return routes

    @cached_property
    def taxonomies(self):
        taxonomies = []
        for tn, tc in self.config.get('site/taxonomies').items():
            tax = Taxonomy(self, tn, tc)
            taxonomies.append(tax)
        return taxonomies

    def getSource(self, source_name):
        for source in self.sources:
            if source.name == source_name:
                return source
        return None

    def getRoutes(self, source_name, *, skip_taxonomies=False):
        for route in self.routes:
            if route.source_name == source_name:
                if not skip_taxonomies or route.taxonomy_name is None:
                    yield route

    def getRoute(self, source_name, route_metadata, *, skip_taxonomies=False):
        for route in self.getRoutes(source_name,
                                    skip_taxonomies=skip_taxonomies):
            if (route_metadata is None
                    or route.matchesMetadata(route_metadata)):
                return route
        return None

    def getTaxonomyRoute(self, tax_name, source_name):
        for route in self.routes:
            if route.taxonomy_name == tax_name and route.source_name == source_name:
                return route
        return None

    def getTaxonomy(self, tax_name):
        for tax in self.taxonomies:
            if tax.name == tax_name:
                return tax
        return None

    def useSubCache(self, cache_name, cache_key):
        cache_hash = hashlib.md5(cache_key.encode('utf8')).hexdigest()
        cache_dir = os.path.join(self.cache_dir,
                                 '%s_%s' % (cache_name, cache_hash))
        self._useSubCacheDir(cache_dir)

    def _useSubCacheDir(self, cache_dir):
        assert cache_dir
        logger.debug("Moving cache to: %s" % cache_dir)
        self.cache = ExtensibleCache(cache_dir)
        self.env._onSubCacheDirChanged(self)

    def _get_dir(self, default_rel_dir):
        abs_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(abs_dir):
            return abs_dir
        return None

    def _get_configurable_dirs(self, default_rel_dir, conf_name):
        dirs = []

        # Add custom directories from the configuration.
        conf_dirs = self.config.get(conf_name)
        if conf_dirs is not None:
            if isinstance(conf_dirs, str):
                dirs.append(os.path.join(self.root_dir, conf_dirs))
            else:
                dirs += [os.path.join(self.root_dir, p) for p in conf_dirs]

        # Add the default directory if it exists.
        default_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(default_dir):
            dirs.append(default_dir)

        return dirs
Exemple #7
0
class PieCrust(object):
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)

        if cache:
            cache_dir = os.path.join(self.cache_dir, 'default')
            self.cache = ExtensibleCache(cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)

    @cached_property
    def config(self):
        logger.debug("Creating site configuration...")
        paths = []
        if self.theme_dir:
            paths.append(os.path.join(self.theme_dir, THEME_CONFIG_PATH))
        paths.append(os.path.join(self.root_dir, CONFIG_PATH))

        config_cache = self.cache.getCache('app')
        config = PieCrustConfiguration(paths, config_cache)
        if self.theme_dir:
            # We'll need to patch the templates directories to be relative
            # to the site's root, and not the theme root.
            def _fixupThemeTemplatesDir(index, config):
                if index != 0:
                    return
                sitec = config.get('site')
                if sitec is None:
                    return
                tplc = sitec.get('templates_dirs')
                if tplc is None:
                    return
                if isinstance(tplc, str):
                    tplc = [tplc]
                sitec['templates_dirs'] = list(filter(tplc,
                        lambda p: os.path.join(self.theme_dir, p)))
            config.fixups.append(_fixupThemeTemplatesDir)

            # We'll also need to flag all page sources as coming from
            # the theme.
            def _fixupThemeSources(index, config):
                if index != 0:
                    return
                sitec = config.get('site')
                if sitec is None:
                    sitec = {}
                    config['site'] = sitec
                srcc = sitec.get('sources')
                if srcc is not None:
                    for sn, sc in srcc.items():
                        sc['realm'] = REALM_THEME
            config.fixups.append(_fixupThemeSources)

        return config

    @cached_property
    def assets_dirs(self):
        assets_dirs = self._get_configurable_dirs(ASSETS_DIR,
                'site/assets_dirs')

        # Also add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, ASSETS_DIR)
            if os.path.isdir(default_theme_dir):
                assets_dirs.append(default_theme_dir)

        return assets_dirs

    @cached_property
    def templates_dirs(self):
        templates_dirs = self._get_configurable_dirs(TEMPLATES_DIR,
                'site/templates_dirs')

        # Also, add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, TEMPLATES_DIR)
            if os.path.isdir(default_theme_dir):
                templates_dirs.append(default_theme_dir)

        return templates_dirs

    @cached_property
    def theme_dir(self):
        td = self._get_dir(THEME_DIR)
        if td is not None:
            return td
        return os.path.join(RESOURCES_DIR, 'theme')

    @cached_property
    def cache_dir(self):
        return os.path.join(self.root_dir, CACHE_DIR)

    @property  # Not a cached property because its result can change.
    def sub_cache_dir(self):
        if self.cache.enabled:
            return self.cache.base_dir
        return None

    @cached_property
    def sources(self):
        defs = {}
        for cls in self.plugin_loader.getSources():
            defs[cls.SOURCE_NAME] = cls

        sources = []
        for n, s in self.config.get('site/sources').items():
            cls = defs.get(s['type'])
            if cls is None:
                raise ConfigurationError("No such page source type: %s" % s['type'])
            src = cls(self, n, s)
            sources.append(src)
        return sources

    @cached_property
    def routes(self):
        routes = []
        for r in self.config.get('site/routes'):
            rte = Route(self, r)
            routes.append(rte)
        return routes

    @cached_property
    def taxonomies(self):
        taxonomies = []
        for tn, tc in self.config.get('site/taxonomies').items():
            tax = Taxonomy(self, tn, tc)
            taxonomies.append(tax)
        return taxonomies

    def getSource(self, source_name):
        for source in self.sources:
            if source.name == source_name:
                return source
        return None

    def getRoutes(self, source_name, *, skip_taxonomies=False):
        for route in self.routes:
            if route.source_name == source_name:
                if not skip_taxonomies or route.taxonomy_name is None:
                    yield route

    def getRoute(self, source_name, route_metadata, *, skip_taxonomies=False):
        for route in self.getRoutes(source_name,
                                    skip_taxonomies=skip_taxonomies):
            if (route_metadata is None or
                    route.matchesMetadata(route_metadata)):
                return route
        return None

    def getTaxonomyRoute(self, tax_name, source_name):
        for route in self.routes:
            if route.taxonomy_name == tax_name and route.source_name == source_name:
                return route
        return None

    def getTaxonomy(self, tax_name):
        for tax in self.taxonomies:
            if tax.name == tax_name:
                return tax
        return None

    def useSubCache(self, cache_name, cache_key):
        cache_hash = hashlib.md5(cache_key.encode('utf8')).hexdigest()
        cache_dir = os.path.join(self.cache_dir,
                                 '%s_%s' % (cache_name, cache_hash))
        self._useSubCacheDir(cache_dir)

    def _useSubCacheDir(self, cache_dir):
        assert cache_dir
        logger.debug("Moving cache to: %s" % cache_dir)
        self.cache = ExtensibleCache(cache_dir)
        self.env._onSubCacheDirChanged(self)

    def _get_dir(self, default_rel_dir):
        abs_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(abs_dir):
            return abs_dir
        return None

    def _get_configurable_dirs(self, default_rel_dir, conf_name):
        dirs = []

        # Add custom directories from the configuration.
        conf_dirs = self.config.get(conf_name)
        if conf_dirs is not None:
            if isinstance(conf_dirs, str):
                dirs.append(os.path.join(self.root_dir, conf_dirs))
            else:
                dirs += [os.path.join(self.root_dir, p) for p in conf_dirs]

        # Add the default directory if it exists.
        default_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(default_dir):
            dirs.append(default_dir)

        return dirs
Exemple #8
0
class PieCrust(object):
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None, cache_key=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)
        self.cache_key = cache_key or 'default'

        if cache:
            self.cache = ExtensibleCache(self.cache_dir)
        else:
            self.cache = NullExtensibleCache()

        self.env = env
        if self.env is None:
            self.env = StandardEnvironment()
        self.env.initialize(self)
        self.env.registerTimer('SiteConfigLoad')
        self.env.registerTimer('PageLoad')
        self.env.registerTimer("PageDataBuild")
        self.env.registerTimer("BuildRenderData")
        self.env.registerTimer("PageRender")
        self.env.registerTimer("PageRenderSegments")
        self.env.registerTimer("PageRenderLayout")
        self.env.registerTimer("PageSerialize")

    @cached_property
    def config(self):
        logger.debug("Creating site configuration...")
        start_time = time.perf_counter()

        if not self.theme_site:
            path = os.path.join(self.root_dir, CONFIG_PATH)
        else:
            path = os.path.join(self.root_dir, THEME_CONFIG_PATH)

        theme_path = None
        if not self.theme_site and self.theme_dir:
            theme_path = os.path.join(self.theme_dir, THEME_CONFIG_PATH)

        config_cache = self.cache.getCache('app')
        config = PieCrustConfiguration(
                path=path, theme_path=theme_path,
                cache=config_cache, theme_config=self.theme_site)

        if self.theme_site:
            variant_path = os.path.join(
                    self.root_dir, 'configs', 'theme_preview.yml')
            config.addVariant(variant_path, raise_if_not_found=False)

        self.env.stepTimer('SiteConfigLoad', time.perf_counter() - start_time)
        return config

    @cached_property
    def assets_dirs(self):
        assets_dirs = self._get_configurable_dirs(
                ASSETS_DIR, 'site/assets_dirs')

        # Also add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, ASSETS_DIR)
            if os.path.isdir(default_theme_dir):
                assets_dirs.append(default_theme_dir)

        return assets_dirs

    @cached_property
    def templates_dirs(self):
        templates_dirs = self._get_configurable_dirs(
                TEMPLATES_DIR, 'site/templates_dirs')

        # Also, add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, TEMPLATES_DIR)
            if os.path.isdir(default_theme_dir):
                templates_dirs.append(default_theme_dir)

        return templates_dirs

    @cached_property
    def theme_dir(self):
        # No theme if the curent site is already a theme.
        if self.theme_site:
            return None

        # See if there's a theme we absolutely want.
        td = self._get_dir(THEME_DIR)
        if td is not None:
            return td

        # Try to load a theme specified in the configuration.
        from piecrust.themes.base import ThemeLoader
        loader = ThemeLoader(self.root_dir)
        theme_dir = loader.getThemeDir()
        if theme_dir is not None:
            return theme_dir

        # Nothing... use the default theme.
        return os.path.join(RESOURCES_DIR, 'theme')

    @cached_property
    def cache_dir(self):
        return os.path.join(self.root_dir, CACHE_DIR, self.cache_key)

    @cached_property
    def sources(self):
        defs = {}
        for cls in self.plugin_loader.getSources():
            defs[cls.SOURCE_NAME] = cls

        sources = []
        for n, s in self.config.get('site/sources').items():
            cls = defs.get(s['type'])
            if cls is None:
                raise ConfigurationError("No such page source type: %s" %
                                         s['type'])
            src = cls(self, n, s)
            sources.append(src)
        return sources

    @cached_property
    def routes(self):
        routes = []
        for r in self.config.get('site/routes'):
            rte = Route(self, r)
            routes.append(rte)
        return routes

    @cached_property
    def generators(self):
        defs = {}
        for cls in self.plugin_loader.getPageGenerators():
            defs[cls.GENERATOR_NAME] = cls

        gens = []
        for n, g in self.config.get('site/generators').items():
            cls = defs.get(g['type'])
            if cls is None:
                raise ConfigurationError("No such page generator type: %s" %
                                         g['type'])
            gen = cls(self, n, g)
            gens.append(gen)
        return gens

    @cached_property
    def publishers(self):
        defs_by_name = {}
        defs_by_scheme = {}
        for cls in self.plugin_loader.getPublishers():
            defs_by_name[cls.PUBLISHER_NAME] = cls
            if cls.PUBLISHER_SCHEME:
                defs_by_scheme[cls.PUBLISHER_SCHEME] = cls

        tgts = []
        publish_config = self.config.get('publish')
        if publish_config is None:
            return tgts
        for n, t in publish_config.items():
            pub_type = None
            is_scheme = False
            if isinstance(t, dict):
                pub_type = t.get('type')
            elif isinstance(t, str):
                comps = urllib.parse.urlparse(t)
                pub_type = comps.scheme
                is_scheme = True
            cls = (defs_by_scheme.get(pub_type) if is_scheme
                    else defs_by_name.get(pub_type))
            if cls is None:
                raise ConfigurationError("No such publisher: %s" % pub_type)
            tgt = cls(self, n, t)
            tgts.append(tgt)
        return tgts

    def getSource(self, source_name):
        for source in self.sources:
            if source.name == source_name:
                return source
        return None

    def getGenerator(self, generator_name):
        for gen in self.generators:
            if gen.name == generator_name:
                return gen
        return None

    def getSourceRoutes(self, source_name):
        for route in self.routes:
            if route.source_name == source_name:
                yield route

    def getSourceRoute(self, source_name, route_metadata):
        for route in self.getSourceRoutes(source_name):
            if (route_metadata is None or
                    route.matchesMetadata(route_metadata)):
                return route
        return None

    def getGeneratorRoute(self, generator_name):
        for route in self.routes:
            if route.generator_name == generator_name:
                return route
        return None

    def getPublisher(self, target_name):
        for pub in self.publishers:
            if pub.target == target_name:
                return pub
        return None

    def _get_dir(self, default_rel_dir):
        abs_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(abs_dir):
            return abs_dir
        return None

    def _get_configurable_dirs(self, default_rel_dir, conf_name):
        dirs = []

        # Add custom directories from the configuration.
        conf_dirs = self.config.get(conf_name)
        if conf_dirs is not None:
            if isinstance(conf_dirs, str):
                dirs.append(os.path.join(self.root_dir, conf_dirs))
            else:
                dirs += [os.path.join(self.root_dir, p) for p in conf_dirs]

        # Add the default directory if it exists.
        default_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(default_dir):
            dirs.append(default_dir)

        return dirs
Exemple #9
0
class PieCrust(object):
    def __init__(self, root_dir, cache=True, debug=False, theme_site=False,
                 env=None, cache_key=None):
        self.root_dir = root_dir
        self.debug = debug
        self.theme_site = theme_site
        self.plugin_loader = PluginLoader(self)
        self.cache_key = cache_key or 'default'

        if cache:
            self.cache = ExtensibleCache(self.cache_dir)
        else:
            self.cache = NullExtensibleCache()

        if env is None:
            env = StandardEnvironment()
        self.env = env
        env.initialize(self)

        stats = env.stats
        stats.registerTimer('SiteConfigLoad')
        stats.registerTimer('PageLoad')
        stats.registerTimer("BuildRenderData")
        stats.registerTimer("BuildLazyPageData")
        stats.registerTimer("PageRender")
        stats.registerTimer("PageRenderSegments")
        stats.registerTimer("PageRenderLayout")
        stats.registerTimer("PageSerialize")
        stats.registerCounter('PageLoads')
        stats.registerCounter('PageRenderSegments')
        stats.registerCounter('PageRenderLayout')

    @cached_property
    def config(self):
        logger.debug("Creating site configuration...")
        start_time = time.perf_counter()

        if not self.theme_site:
            path = os.path.join(self.root_dir, CONFIG_PATH)
        else:
            path = os.path.join(self.root_dir, THEME_CONFIG_PATH)

        theme_path = None
        if not self.theme_site and self.theme_dir:
            theme_path = os.path.join(self.theme_dir, THEME_CONFIG_PATH)

        config_cache = self.cache.getCache('app')
        config = PieCrustConfiguration(
            path=path, theme_path=theme_path,
            cache=config_cache, theme_config=self.theme_site)

        local_path = os.path.join(
            self.root_dir, 'configs', 'local.yml')
        config.addVariant(local_path, raise_if_not_found=False)

        if self.theme_site:
            variant_path = os.path.join(
                self.root_dir, 'configs', 'theme_preview.yml')
            config.addVariant(variant_path, raise_if_not_found=False)

        self.env.stats.stepTimer('SiteConfigLoad',
                                 time.perf_counter() - start_time)
        return config

    @cached_property
    def assets_dirs(self):
        assets_dirs = self._get_configurable_dirs(
            ASSETS_DIR, 'site/assets_dirs')

        # Also add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, ASSETS_DIR)
            if os.path.isdir(default_theme_dir):
                assets_dirs.append(default_theme_dir)

        return assets_dirs

    @cached_property
    def templates_dirs(self):
        templates_dirs = self._get_configurable_dirs(
            TEMPLATES_DIR, 'site/templates_dirs')

        # Also, add the theme directory, if any.
        if self.theme_dir:
            default_theme_dir = os.path.join(self.theme_dir, TEMPLATES_DIR)
            if os.path.isdir(default_theme_dir):
                templates_dirs.append(default_theme_dir)

        return templates_dirs

    @cached_property
    def theme_dir(self):
        # No theme if the curent site is already a theme.
        if self.theme_site:
            return None

        # See if there's a theme we absolutely want.
        td = os.path.join(self.root_dir, THEME_DIR)
        if os.path.isdir(td):
            return td

        # Try to load a theme specified in the configuration.
        from piecrust.themes.base import ThemeLoader
        loader = ThemeLoader(self.root_dir)
        theme_dir = loader.getThemeDir()
        if theme_dir is not None:
            return theme_dir

        # Nothing... use the default theme.
        return os.path.join(RESOURCES_DIR, 'theme')

    @cached_property
    def plugins_dirs(self):
        return self._get_configurable_dirs(PLUGINS_DIR, 'site/plugins_dirs')

    @cached_property
    def cache_dir(self):
        return os.path.join(self.root_dir, CACHE_DIR, self.cache_key)

    @cached_property
    def sources(self):
        defs = {}
        for cls in self.plugin_loader.getSources():
            defs[cls.SOURCE_NAME] = cls

        sources = []
        for n, s in self.config.get('site/sources').items():
            cls = defs.get(s['type'])
            if cls is None:
                raise ConfigurationError("No such page source type: %s" %
                                         s['type'])
            src = cls(self, n, s)
            sources.append(src)

        return sources

    @cached_property
    def routes(self):
        routes = []
        for r in self.config.get('site/routes'):
            rte = Route(self, r)
            routes.append(rte)
        routes = sorted(routes, key=lambda r: r.pass_num)
        return routes

    @cached_property
    def publishers(self):
        defs_by_name = {}
        defs_by_scheme = {}
        for cls in self.plugin_loader.getPublishers():
            defs_by_name[cls.PUBLISHER_NAME] = cls
            if cls.PUBLISHER_SCHEME:
                defs_by_scheme[cls.PUBLISHER_SCHEME] = cls

        tgts = []
        publish_config = self.config.get('publish')
        if publish_config is None:
            return tgts

        for n, t in publish_config.items():
            pub_class = None
            if isinstance(t, dict):
                pub_type = t.get('type')
                pub_class = defs_by_name[pub_type]
                pub_cfg = t
            elif isinstance(t, str):
                comps = urllib.parse.urlparse(t)
                pub_type = comps.scheme
                pub_class = defs_by_scheme[pub_type]
                pub_cfg = None
            if pub_class is None:
                raise ConfigurationError("No such publisher: %s" % pub_type)

            tgt = pub_class(self, n, pub_cfg)
            if pub_cfg is None:
                tgt.parseUrlTarget(comps)

            tgts.append(tgt)
        return tgts

    def getSource(self, source_name):
        for source in self.sources:
            if source.name == source_name:
                return source

        from piecrust.sources.base import SourceNotFoundError
        raise SourceNotFoundError(source_name)

    def getSourceRoute(self, source_name):
        for route in self.routes:
            if route.source_name == source_name:
                return route

        from piecrust.routing import RouteNotFoundError
        raise RouteNotFoundError(source_name)

    def getPublisher(self, target_name):
        for pub in self.publishers:
            if pub.target == target_name:
                return pub
        return None

    def getPage(self, source, content_item):
        cache_key = '%s@%s' % (source.name, content_item.spec)
        return self.env.page_repository.get(
            cache_key,
            lambda: Page(source, content_item))

    def resolvePath(self, path):
        path = multi_replace(path, {'%theme_dir%': self.theme_dir})
        return os.path.join(self.root_dir, path)

    def _get_configurable_dirs(self, default_rel_dir, conf_name):
        dirs = []

        # Add custom directories from the configuration.
        conf_dirs = self.config.get(conf_name)
        if conf_dirs is not None:
            if isinstance(conf_dirs, str):
                dirs.append(self.resolvePath(conf_dirs))
            else:
                dirs += [self.resolvePath(p) for p in conf_dirs]

        # Add the default directory if it exists.
        default_dir = os.path.join(self.root_dir, default_rel_dir)
        if os.path.isdir(default_dir):
            dirs.append(default_dir)

        return dirs