decoupage

view decoupage/formatters.py @ 67:fdb77c57bd22

make sort formatter more extensible and add a random method
author Jeff Hammel <jhammel@mozilla.com>
date Fri Nov 18 22:34:08 2011 -0800 (6 months ago)
parents ac1dc088e37e
children
line source
1 #!/usr/bin/env python
3 import random
4 import sys
5 from fnmatch import fnmatch
6 from pkg_resources import iter_entry_points
8 ### abstract base classes for formatters
10 class FormatterBase(object):
11 """
12 abstract base class if you want to use __init__ methods
13 in the form of
14 'arg1, arg2, arg3, kw1=foo, kw2=bar, kw3=baz
15 """
17 defaults = {} # default values for attrs to be set on the instance
19 def __init__(self, string):
20 args = [ i.strip() for i in string.split(',')]
21 for index, arg in enumerate(args):
22 if '=' in arg:
23 break
24 else:
25 self.args = args
26 for key, default in self.defaults.items():
27 setattr(self, key, default)
28 return
29 self.args = args[:index]
30 self.kw = dict([i.split('=', 1) for i in args[index:]])
31 for key, default in self.defaults.items():
32 value = self.kw.pop(key, default)
33 setattr(self, key, value)
36 ### formatters
38 class Ignore(object):
39 """
40 ignore files of a glob patterns.
41 These files will not be linked to in the template.
42 e.g. /ignore = .* *.pdf # don't list dotfiles and PDFs
43 """
45 def __init__(self, ignore):
46 self.match = ignore.split()
48 def __call__(self, request, data):
49 _files = []
50 for f in data['files']:
51 for pattern in self.match:
52 if fnmatch(f['name'], pattern):
53 break
54 else:
55 _files.append(f)
56 data['files'] = _files
59 class All(object):
60 """
61 only pass files of a certain pattern;
62 the inverse of ignore
63 calling all with no arguments means only files with descriptions are used
64 """
66 def __init__(self, pattern):
67 self.match = pattern.split()
69 def __call__(self, request, data):
70 _files = []
71 for f in data['files']:
72 if self.match:
73 for pattern in self.match:
74 if fnmatch(f['name'], pattern):
75 _files.append(f)
76 break
77 else:
78 # use only files where the description is not None
79 if f['description'] is not None:
80 _files.append(f)
81 data['files'] = _files
83 class Sort(FormatterBase):
84 """
85 determines how to sort the files in a directory;
86 right now only by case-insensitive alphabetically
87 * reverse : reverse the order of the sorting
88 """
89 defaults = {'order': 'name'}
91 def __init__(self, pattern):
92 FormatterBase.__init__(self, pattern)
93 self.orders = {'name': self.name,
94 'random': self.random,
95 }
97 def __call__(self, request, data):
99 sort = self.orders.get(request.GET.get('order', self.order), self.name)
100 data['files'] = sort(data['files'])
102 if 'reverse' in self.args:
103 data['files'] = list(reversed(data['files']))
105 def name(self, files):
106 return sorted(files, key=lambda x: x['name'].lower())
108 def random(self, files):
109 random.shuffle(files)
110 return files
113 class Order(object):
114 """
115 put the files in a particular order
116 """
117 def __init__(self, pattern):
118 if '=' in pattern:
119 key, value = pattern.split('=', 1)
120 assert key == 'file'
121 self.file = value
122 else:
123 self.order = [i.strip() for i in pattern.split(',')]
125 def __call__(self, request, data):
127 if self.file:
128 raise NotImplementedError
130 files = []
131 file_hash = dict([(i['name'], i) for i in data['files']])
132 for f in self.order:
133 files.append(file_hash.get(f, None))
134 files = [ i for i in files if i is not None ]
137 class FilenameDescription(FormatterBase):
138 """
139 obtain the description from the filename
140 the file extension (if any) will be dropped and
141 spaces will be substituted for underscores
142 """
143 # TODO : deal with CamelCaseFilenames
145 separators = ['_', '-'] # space substitute separators
146 lesser_words = [ 'or', 'a', 'the', 'on', 'of' ] # unimportant words
148 def __call__(self, request, data):
149 for f in data['files']:
150 if f['description'] is None:
151 description = f['name']
152 if '.' in description:
153 description = description.rsplit('.', 1)[0]
154 decription = description.strip('_')
155 for separator in self.separators:
156 if separator in description:
157 description = ' '.join([(i in self.lesser_words) and i or i.title()
158 for i in description.split(separator)])
159 description = description[0].upper() + description[1:]
160 f['description'] = description
163 class TitleDescription(FormatterBase):
164 """
165 splits a description into a title and a description via a separator in
166 the description. The template will now have an additional variable,
167 'title', per file
168 Arguments:
169 * separator: what separator to use (':' by default)
170 """
171 # XXX what about setting the page title?
173 defaults = { 'separator': ':' }
175 def __call__(self, request, data):
176 for f in data['files']:
177 if f['description'] and self.separator in f['description']:
178 title, description = f['description'].split(self.separator, 1)
179 title = title.strip()
180 description = description.strip()
181 if not title:
182 title = f['name']
183 f['title'] = title
184 f['description'] = description
185 else:
186 f['title'] = f['description']
187 f['description'] = None
189 class Links(FormatterBase):
190 """
191 allow list of links per item:
192 foo.html = description of foo; [PDF]=foo.pdf; [TXT]=foo.txt
193 """
195 defaults = { 'separator': ';' }
197 def __call__(self, request, data):
198 for f in data['files']:
199 if f['description'] and self.separator in f['description']:
200 f['description'], links = f['description'].split(self.separator, 1)
201 links = links.split(self.separator)
202 assert min(['=' in link for link in links])
203 links = [ link.split('=', 1) for link in links ]
204 f['links'] = [{ 'text': text, 'link': link }
205 for text, link in links]
208 class Up(object):
209 """
210 provides an up link to the path above:
211 /up = ..
212 """
214 def __init__(self, arg):
215 self.up = arg.strip()
217 def __call__(self, request, data):
218 path = request.path_info
219 if (path != '/') and self.up:
220 data['files'].insert(0, {'path': '..',
221 'type': 'directory',
222 'name': path.rsplit('/', 1)[0] + '/',
223 'description': self.up})
225 class CSS(object):
226 """specify CSS used (whitespace separated list)"""
228 def __init__(self, arg):
229 self.css = arg.split()
230 def __call__(self, request, data):
231 data['css'] = self.css
233 class JavaScript(object):
234 """specify JS for the page"""
236 def __init__(self, arg):
237 self.scripts = arg.split()
238 def __call__(self, request, data):
239 data['scripts'] = self.scripts
241 class Favicon(object):
242 """specify favicons for the page"""
244 def __init__(self, icon):
245 self.icon = icon
246 def __call__(self, request, data):
247 data['icon'] = self.icon
249 class Include(object):
250 """include a file at the top of the body"""
252 def __init__(self, arg):
253 self.include = arg
254 def __call__(self, request, data):
255 data['include'] = self.include
258 def formatters():
259 formatters = {}
260 for entry_point in iter_entry_points('decoupage.formatters'):
261 try:
262 formatter = entry_point.load()
263 except:
264 continue
265 formatters[entry_point.name] = formatter
266 return formatters
268 def main(args=sys.argv[1:]):
269 for name, formatter in formatters().items():
270 print '%s%s' % (name, formatter.__doc__ and ': ' + formatter.__doc__ or '')
272 if __name__ == '__main__':
273 main()