comparison piecrust/commands/builtin/scaffolding.py @ 852:4850f8c21b6e

core: Start of the big refactor for PieCrust 3.0. * Everything is a `ContentSource`, including assets directories. * Most content sources are subclasses of the base file-system source. * A source is processed by a "pipeline", and there are 2 built-in pipelines, one for assets and one for pages. The asset pipeline is vaguely functional, but the page pipeline is completely broken right now. * Rewrite the baking process as just running appropriate pipelines on each content item. This should allow for better parallelization.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 17 May 2017 00:11:48 -0700
parents 2bb3c1a04e98
children 58ae026b4c31
comparison
equal deleted inserted replaced
851:2c7e57d80bba 852:4850f8c21b6e
1 import os 1 import os
2 import os.path 2 import os.path
3 import re
4 import io 3 import io
5 import time 4 import time
6 import glob 5 import glob
7 import logging 6 import logging
8 import textwrap 7 import textwrap
9 from piecrust import RESOURCES_DIR 8 from piecrust import RESOURCES_DIR
10 from piecrust.chefutil import print_help_item 9 from piecrust.chefutil import print_help_item
11 from piecrust.commands.base import ExtendableChefCommand, ChefCommandExtension 10 from piecrust.commands.base import ExtendableChefCommand, ChefCommandExtension
12 from piecrust.sources.base import MODE_CREATING 11 from piecrust.pathutil import SiteNotFoundError
13 from piecrust.sources.interfaces import IPreparingSource 12 from piecrust.sources.fs import FSContentSourceBase
14 from piecrust.uriutil import multi_replace
15 13
16 14
17 logger = logging.getLogger(__name__) 15 logger = logging.getLogger(__name__)
18
19
20 def make_title(slug):
21 slug = re.sub(r'[\-_]', ' ', slug)
22 return slug.title()
23 16
24 17
25 class PrepareCommand(ExtendableChefCommand): 18 class PrepareCommand(ExtendableChefCommand):
26 """ Chef command for creating pages with some default content. 19 """ Chef command for creating pages with some default content.
27 """ 20 """
33 def setupParser(self, parser, app): 26 def setupParser(self, parser, app):
34 # Don't setup anything if this is a null app 27 # Don't setup anything if this is a null app
35 # (for when `chef` is run from outside a website) 28 # (for when `chef` is run from outside a website)
36 if app.root_dir is None: 29 if app.root_dir is None:
37 return 30 return
31
32 from piecrust.sources.interfaces import IPreparingSource
38 33
39 subparsers = parser.add_subparsers() 34 subparsers = parser.add_subparsers()
40 for src in app.sources: 35 for src in app.sources:
41 if not isinstance(src, IPreparingSource): 36 if not isinstance(src, IPreparingSource):
42 logger.debug("Skipping source '%s' because it's not " 37 logger.debug("Skipping source '%s' because it's not "
45 if src.is_theme_source: 40 if src.is_theme_source:
46 logger.debug("Skipping source '%s' because it's a theme " 41 logger.debug("Skipping source '%s' because it's a theme "
47 "source." % src.name) 42 "source." % src.name)
48 continue 43 continue
49 p = subparsers.add_parser( 44 p = subparsers.add_parser(
50 src.item_name, 45 src.config['item_name'],
51 help=("Creates an empty page in the '%s' source." % 46 help=("Creates an empty page in the '%s' source." %
52 src.name)) 47 src.name))
53 src.setupPrepareParser(p, app) 48 src.setupPrepareParser(p, app)
54 p.add_argument('-t', '--template', default='default', 49 p.add_argument('-t', '--template', default='default',
55 help="The template to use, which will change the " 50 help="The template to use, which will change the "
56 "generated text and header. Run `chef help " 51 "generated text and header. Run `chef help "
57 "scaffolding` for more information.") 52 "scaffolding` for more information.")
53 p.add_argument('-f', '--force', action='store_true',
54 help="Overwrite any existing content.")
58 p.set_defaults(source=src) 55 p.set_defaults(source=src)
59 p.set_defaults(sub_func=self._doRun) 56 p.set_defaults(sub_func=self._doRun)
60 57
61 def checkedRun(self, ctx): 58 def checkedRun(self, ctx):
62 if ctx.app.root_dir is None: 59 if ctx.app.root_dir is None:
66 ctx.parser.parse_args(['prepare', '--help']) 63 ctx.parser.parse_args(['prepare', '--help'])
67 return 64 return
68 ctx.args.sub_func(ctx) 65 ctx.args.sub_func(ctx)
69 66
70 def _doRun(self, ctx): 67 def _doRun(self, ctx):
68 from piecrust.uriutil import multi_replace
69
71 if not hasattr(ctx.args, 'source'): 70 if not hasattr(ctx.args, 'source'):
72 raise Exception("No source specified. " 71 raise Exception("No source specified. "
73 "Please run `chef prepare -h` for usage.") 72 "Please run `chef prepare -h` for usage.")
74 73
75 app = ctx.app 74 app = ctx.app
76 source = ctx.args.source
77 metadata = source.buildMetadata(ctx.args)
78 factory = source.findPageFactory(metadata, MODE_CREATING)
79 path = factory.path
80 name, ext = os.path.splitext(path)
81 if ext == '.*':
82 path = '%s.%s' % (
83 name,
84 app.config.get('site/default_auto_format'))
85 if os.path.exists(path):
86 raise Exception("'%s' already exists." % path)
87
88 tpl_name = ctx.args.template 75 tpl_name = ctx.args.template
89 extensions = self.getExtensions(app) 76 extensions = self.getExtensions(app)
90 ext = next( 77 ext = next(
91 filter( 78 filter(
92 lambda e: tpl_name in e.getTemplateNames(ctx.app), 79 lambda e: tpl_name in e.getTemplateNames(app),
93 extensions), 80 extensions),
94 None) 81 None)
95 if ext is None: 82 if ext is None:
96 raise Exception("No such page template: %s" % tpl_name) 83 raise Exception("No such page template: %s" % tpl_name)
97 84 tpl_text = ext.getTemplate(app, tpl_name)
98 tpl_text = ext.getTemplate(ctx.app, tpl_name)
99 if tpl_text is None: 85 if tpl_text is None:
100 raise Exception("Error loading template: %s" % tpl_name) 86 raise Exception("Error loading template: %s" % tpl_name)
101 title = (metadata.get('slug') or metadata.get('path') or 87
102 'Untitled page') 88 source = ctx.args.source
103 title = make_title(title) 89 content_item = source.createContent(ctx.args)
104 tokens = { 90
105 '%title%': title, 91 config_tokens = {
106 '%time.today%': time.strftime('%Y/%m/%d'), 92 '%title%': "Untitled Content",
107 '%time.now%': time.strftime('%H:%M:%S')} 93 '%time.today%': time.strftime('%Y/%m/%d'),
108 tpl_text = multi_replace(tpl_text, tokens) 94 '%time.now%': time.strftime('%H:%M:%S')
109 95 }
110 logger.info("Creating page: %s" % os.path.relpath(path, app.root_dir)) 96 config = content_item.metadata.get('config')
111 if not os.path.exists(os.path.dirname(path)): 97 if config:
112 os.makedirs(os.path.dirname(path), 0o755) 98 for k, v in config.items():
113 99 config_tokens['%%%s%%' % k] = v
114 with open(path, 'w') as f: 100 tpl_text = multi_replace(tpl_text, config_tokens)
101
102 logger.info("Creating content: %s" % content_item.spec)
103 mode = 'w' if ctx.args.force else 'x'
104 with content_item.open(mode) as f:
115 f.write(tpl_text) 105 f.write(tpl_text)
116 106
107 # If this was a file-system content item, see if we need to auto-open
108 # an editor on it.
117 editor = ctx.app.config.get('prepare/editor') 109 editor = ctx.app.config.get('prepare/editor')
118 editor_type = ctx.app.config.get('prepare/editor_type', 'exe') 110 editor_type = ctx.app.config.get('prepare/editor_type', 'exe')
119 if editor: 111 if editor and isinstance(source, FSContentSourceBase):
120 import shlex 112 import shlex
121 shell = False 113 shell = False
122 args = '%s "%s"' % (editor, path) 114 args = '%s "%s"' % (editor, content_item.spec)
123 if '%path%' in editor: 115 if '%path%' in editor:
124 args = editor.replace('%path%', path) 116 args = editor.replace('%path%', content_item.spec)
125 117
126 if editor_type.lower() == 'shell': 118 if editor_type.lower() == 'shell':
127 shell = True 119 shell = True
128 else: 120 else:
129 args = shlex.split(args) 121 args = shlex.split(args)
144 def getTemplateNames(self, app): 136 def getTemplateNames(self, app):
145 return ['default', 'rss', 'atom'] 137 return ['default', 'rss', 'atom']
146 138
147 def getTemplateDescription(self, app, name): 139 def getTemplateDescription(self, app, name):
148 descs = { 140 descs = {
149 'default': "The default template, for a simple page.", 141 'default': "The default template, for a simple page.",
150 'rss': "A fully functional RSS feed.", 142 'rss': "A fully functional RSS feed.",
151 'atom': "A fully functional Atom feed."} 143 'atom': "A fully functional Atom feed."}
152 return descs[name] 144 return descs[name]
153 145
154 def getTemplate(self, app, name): 146 def getTemplate(self, app, name):
155 assert name in ['default', 'rss', 'atom'] 147 assert name in ['default', 'rss', 'atom']
156 src_path = os.path.join(RESOURCES_DIR, 'prepare', '%s.html' % name) 148 src_path = os.path.join(RESOURCES_DIR, 'prepare', '%s.html' % name)
187 matches = glob.glob(pattern) 179 matches = glob.glob(pattern)
188 if not matches: 180 if not matches:
189 raise Exception("No such page scaffolding template: %s" % name) 181 raise Exception("No such page scaffolding template: %s" % name)
190 if len(matches) > 1: 182 if len(matches) > 1:
191 raise Exception( 183 raise Exception(
192 "More than one scaffolding template has name: %s" % name) 184 "More than one scaffolding template has name: %s" % name)
193 with open(matches[0], 'r', encoding='utf8') as fp: 185 with open(matches[0], 'r', encoding='utf8') as fp:
194 return fp.read() 186 return fp.read()
195 187
196 188
197 class DefaultPrepareTemplatesHelpTopic(ChefCommandExtension): 189 class DefaultPrepareTemplatesHelpTopic(ChefCommandExtension):
212 d = e.getTemplateDescription(app, n) 204 d = e.getTemplateDescription(app, n)
213 print_help_item(tplh, n, d) 205 print_help_item(tplh, n, d)
214 help_list = tplh.getvalue() 206 help_list = tplh.getvalue()
215 207
216 help_txt = ( 208 help_txt = (
217 textwrap.fill( 209 textwrap.fill(
218 "Running the 'prepare' command will let " 210 "Running the 'prepare' command will let "
219 "PieCrust setup a page for you in the correct place, with " 211 "PieCrust setup a page for you in the correct place, with "
220 "some hopefully useful default text.") + 212 "some hopefully useful default text.") +
221 "\n\n" + 213 "\n\n" +
222 textwrap.fill("The following templates are available:") + 214 textwrap.fill("The following templates are available:") +
223 "\n\n" + 215 "\n\n" +
224 help_list + 216 help_list +
225 "\n" + 217 "\n" +
226 "You can add user-defined templates by creating pages in a " 218 "You can add user-defined templates by creating pages in a "
227 "`scaffold/pages` sub-directory in your website.") 219 "`scaffold/pages` sub-directory in your website.")
228 return help_txt 220 return help_txt
229 221