Mercurial > piecrust2
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 |