Merge lp:~jelmer/bzr-keywords/lazy into lp:bzr-keywords
- lazy
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | John A Meinel |
Approved revision: | no longer in the source branch. |
Merged at revision: | 18 |
Proposed branch: | lp:~jelmer/bzr-keywords/lazy |
Merge into: | lp:bzr-keywords |
Diff against target: |
652 lines (+294/-266) 4 files modified
__init__.py (+23/-258) keywords.py (+264/-0) tests/test_conversion.py (+4/-1) tests/test_keywords_in_trees.py (+3/-7) |
To merge this branch: | bzr merge lp:~jelmer/bzr-keywords/lazy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel | Needs Information | ||
Martin Pool (community) | Approve | ||
Review via email: mp+51444@code.launchpad.net |
Commit message
Description of the change
Lazily load the keywords plugin.
Martin Pool (mbp) : | # |
Jelmer Vernooij (jelmer) wrote : | # |
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> On 2/27/2011 4:57 AM, Jelmer Vernooij wrote:
> > Jelmer Vernooij has proposed merging lp:~jelmer/bzr-keywords/lazy into lp
> :bzr-keywords.
> >
> > Requested reviews:
> > Bazaar Developers (bzr)
> >
> > For more details, see:
> > https:/
> >
> > Lazily load the keywords plugin.
>
> I'm a bit surprised at how much code is added here, versus how much is
> removed.
> All the format_date, extract_name, etc don't seem to come from somewhere
> else.
>
> Is this just a large rewrite of the internals?
>
> The changes seem fine to me, but I didn't go over them in detail, with
> the change being surprisingly large.
>
> Care to explain a bit more what you changed?
There was a conflict with some earlier changes, which caused the code I moved around to stay in __init__.py in the conflicts. Should be fixed now.
- 18. By Jelmer Vernooij
-
Merge lazy loading support.
Preview Diff
1 | === modified file '__init__.py' | |||
2 | --- __init__.py 2010-10-01 19:44:29 +0000 | |||
3 | +++ __init__.py 2011-03-02 12:34:10 +0000 | |||
4 | @@ -105,18 +105,11 @@ | |||
5 | 105 | ''' | 105 | ''' |
6 | 106 | 106 | ||
7 | 107 | 107 | ||
8 | 108 | import re, time | ||
9 | 109 | from bzrlib import ( | 108 | from bzrlib import ( |
10 | 110 | builtins, | 109 | builtins, |
11 | 111 | commands, | 110 | commands, |
12 | 112 | config, | ||
13 | 113 | debug, | ||
14 | 114 | filters, | 111 | filters, |
15 | 115 | option, | 112 | option, |
16 | 116 | osutils, | ||
17 | 117 | registry, | ||
18 | 118 | trace, | ||
19 | 119 | xml8, | ||
20 | 120 | ) | 113 | ) |
21 | 121 | 114 | ||
22 | 122 | 115 | ||
23 | @@ -137,249 +130,13 @@ | |||
24 | 137 | return suite | 130 | return suite |
25 | 138 | 131 | ||
26 | 139 | 132 | ||
27 | 140 | # Expansion styles | ||
28 | 141 | # Note: Round-tripping is only required between the raw and cooked styles | ||
29 | 142 | _keyword_style_registry = registry.Registry() | ||
30 | 143 | _keyword_style_registry.register('raw', '$%(name)s$') | ||
31 | 144 | _keyword_style_registry.register('cooked', '$%(name)s: %(value)s $') | ||
32 | 145 | _keyword_style_registry.register('publish', '%(name)s: %(value)s') | ||
33 | 146 | _keyword_style_registry.register('publish-values', '%(value)s') | ||
34 | 147 | _keyword_style_registry.register('publish-names', '%(name)s') | ||
35 | 148 | _keyword_style_registry.default_key = 'cooked' | ||
36 | 149 | |||
37 | 150 | |||
38 | 151 | # Regular expressions for matching the raw and cooked patterns | ||
39 | 152 | _KW_RAW_RE = re.compile(r'\$([\w\-]+)(:[^$]*)?\$') | ||
40 | 153 | _KW_COOKED_RE = re.compile(r'\$([\w\-]+):([^$]+)\$') | ||
41 | 154 | |||
42 | 155 | |||
43 | 156 | # The registry of keywords. Other plugins may wish to add entries to this. | ||
44 | 157 | keyword_registry = registry.Registry() | ||
45 | 158 | |||
46 | 159 | # Revision-related keywords | ||
47 | 160 | keyword_registry.register('Date', | ||
48 | 161 | lambda c: format_date(c.revision().timestamp, c.revision().timezone, | ||
49 | 162 | c.config(), 'Date')) | ||
50 | 163 | keyword_registry.register('Committer', | ||
51 | 164 | lambda c: c.revision().committer) | ||
52 | 165 | keyword_registry.register('Authors', | ||
53 | 166 | lambda c: ", ".join(c.revision().get_apparent_authors())) | ||
54 | 167 | keyword_registry.register('Revision-Id', | ||
55 | 168 | lambda c: c.revision_id()) | ||
56 | 169 | keyword_registry.register('Path', | ||
57 | 170 | lambda c: c.relpath()) | ||
58 | 171 | keyword_registry.register('Directory', | ||
59 | 172 | lambda c: osutils.split(c.relpath())[0]) | ||
60 | 173 | keyword_registry.register('Filename', | ||
61 | 174 | lambda c: osutils.split(c.relpath())[1]) | ||
62 | 175 | keyword_registry.register('File-Id', | ||
63 | 176 | lambda c: c.file_id()) | ||
64 | 177 | |||
65 | 178 | # Environment-related keywords | ||
66 | 179 | keyword_registry.register('Now', | ||
67 | 180 | lambda c: format_date(time.time(), time.timezone, c.config(), 'Now')) | ||
68 | 181 | keyword_registry.register('User', | ||
69 | 182 | lambda c: c.config().username()) | ||
70 | 183 | |||
71 | 184 | # Keywords for finer control over name & address formatting | ||
72 | 185 | keyword_registry.register('Committer-Name', | ||
73 | 186 | lambda c: extract_name(c.revision().committer)) | ||
74 | 187 | keyword_registry.register('Committer-Email', | ||
75 | 188 | lambda c: extract_email(c.revision().committer)) | ||
76 | 189 | keyword_registry.register('Author1-Name', | ||
77 | 190 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 0)) | ||
78 | 191 | keyword_registry.register('Author1-Email', | ||
79 | 192 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 0)) | ||
80 | 193 | keyword_registry.register('Author2-Name', | ||
81 | 194 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 1)) | ||
82 | 195 | keyword_registry.register('Author2-Email', | ||
83 | 196 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 1)) | ||
84 | 197 | keyword_registry.register('Author3-Name', | ||
85 | 198 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 2)) | ||
86 | 199 | keyword_registry.register('Author3-Email', | ||
87 | 200 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 2)) | ||
88 | 201 | keyword_registry.register('User-Name', | ||
89 | 202 | lambda c: extract_name(c.config().username())) | ||
90 | 203 | keyword_registry.register('User-Email', | ||
91 | 204 | lambda c: extract_email(c.config().username())) | ||
92 | 205 | |||
93 | 206 | |||
94 | 207 | def format_date(timestamp, offset=0, cfg=None, name=None): | ||
95 | 208 | """Return a formatted date string. | ||
96 | 209 | |||
97 | 210 | :param timestamp: Seconds since the epoch. | ||
98 | 211 | :param offset: Timezone offset in seconds east of utc. | ||
99 | 212 | """ | ||
100 | 213 | if cfg is not None and name is not None: | ||
101 | 214 | cfg_key = 'keywords.format.%s' % (name,) | ||
102 | 215 | format = cfg.get_user_option(cfg_key) | ||
103 | 216 | else: | ||
104 | 217 | format = None | ||
105 | 218 | return osutils.format_date(timestamp, offset, date_fmt=format) | ||
106 | 219 | |||
107 | 220 | |||
108 | 221 | def extract_name(userid): | ||
109 | 222 | """Extract the name out of a user-id string. | ||
110 | 223 | |||
111 | 224 | user-id strings have the format 'name <email>'. | ||
112 | 225 | """ | ||
113 | 226 | if userid and userid[-1] == '>': | ||
114 | 227 | return userid[:-1].rsplit('<', 1)[0].rstrip() | ||
115 | 228 | else: | ||
116 | 229 | return userid | ||
117 | 230 | |||
118 | 231 | |||
119 | 232 | def extract_email(userid): | ||
120 | 233 | """Extract the email address out of a user-id string. | ||
121 | 234 | |||
122 | 235 | user-id strings have the format 'name <email>'. | ||
123 | 236 | """ | ||
124 | 237 | if userid and userid[-1] == '>': | ||
125 | 238 | return userid[:-1].rsplit('<', 1)[1] | ||
126 | 239 | else: | ||
127 | 240 | return userid | ||
128 | 241 | |||
129 | 242 | def extract_name_item(seq, n): | ||
130 | 243 | """Extract the name out of the nth item in a sequence of user-ids. | ||
131 | 244 | |||
132 | 245 | :return: the user-name or an empty string | ||
133 | 246 | """ | ||
134 | 247 | try: | ||
135 | 248 | return extract_name(seq[n]) | ||
136 | 249 | except IndexError: | ||
137 | 250 | return "" | ||
138 | 251 | |||
139 | 252 | |||
140 | 253 | def extract_email_item(seq, n): | ||
141 | 254 | """Extract the email out of the nth item in a sequence of user-ids. | ||
142 | 255 | |||
143 | 256 | :return: the email address or an empty string | ||
144 | 257 | """ | ||
145 | 258 | try: | ||
146 | 259 | return extract_email(seq[n]) | ||
147 | 260 | except IndexError: | ||
148 | 261 | return "" | ||
149 | 262 | |||
150 | 263 | |||
151 | 264 | def compress_keywords(s, keyword_dicts): | ||
152 | 265 | """Replace cooked style keywords with raw style in a string. | ||
153 | 266 | |||
154 | 267 | Note: If the keyword is not known, the text is not modified. | ||
155 | 268 | |||
156 | 269 | :param s: the string | ||
157 | 270 | :param keyword_dicts: an iterable of keyword dictionaries. | ||
158 | 271 | :return: the string with keywords compressed | ||
159 | 272 | """ | ||
160 | 273 | _raw_style = _keyword_style_registry.get('raw') | ||
161 | 274 | result = '' | ||
162 | 275 | rest = s | ||
163 | 276 | while (True): | ||
164 | 277 | match = _KW_COOKED_RE.search(rest) | ||
165 | 278 | if not match: | ||
166 | 279 | break | ||
167 | 280 | result += rest[:match.start()] | ||
168 | 281 | keyword = match.group(1) | ||
169 | 282 | expansion = _get_from_dicts(keyword_dicts, keyword) | ||
170 | 283 | if expansion is None: | ||
171 | 284 | # Unknown expansion - leave as is | ||
172 | 285 | result += match.group(0) | ||
173 | 286 | else: | ||
174 | 287 | result += _raw_style % {'name': keyword} | ||
175 | 288 | rest = rest[match.end():] | ||
176 | 289 | return result + rest | ||
177 | 290 | |||
178 | 291 | |||
179 | 292 | def expand_keywords(s, keyword_dicts, context=None, encoder=None, style=None): | ||
180 | 293 | """Replace raw style keywords with another style in a string. | ||
181 | 294 | |||
182 | 295 | Note: If the keyword is already in the expanded style, the value is | ||
183 | 296 | not replaced. | ||
184 | 297 | |||
185 | 298 | :param s: the string | ||
186 | 299 | :param keyword_dicts: an iterable of keyword dictionaries. If values | ||
187 | 300 | are callables, they are executed to find the real value. | ||
188 | 301 | :param context: the parameter to pass to callable values | ||
189 | 302 | :param style: the style of expansion to use of None for the default | ||
190 | 303 | :return: the string with keywords expanded | ||
191 | 304 | """ | ||
192 | 305 | _expanded_style = _keyword_style_registry.get(style) | ||
193 | 306 | result = '' | ||
194 | 307 | rest = s | ||
195 | 308 | while (True): | ||
196 | 309 | match = _KW_RAW_RE.search(rest) | ||
197 | 310 | if not match: | ||
198 | 311 | break | ||
199 | 312 | result += rest[:match.start()] | ||
200 | 313 | keyword = match.group(1) | ||
201 | 314 | expansion = _get_from_dicts(keyword_dicts, keyword) | ||
202 | 315 | if callable(expansion): | ||
203 | 316 | try: | ||
204 | 317 | expansion = expansion(context) | ||
205 | 318 | except AttributeError, err: | ||
206 | 319 | if 'error' in debug.debug_flags: | ||
207 | 320 | trace.note("error evaluating %s for keyword %s: %s", | ||
208 | 321 | expansion, keyword, err) | ||
209 | 322 | expansion = "(evaluation error)" | ||
210 | 323 | if expansion is None: | ||
211 | 324 | # Unknown expansion - leave as is | ||
212 | 325 | result += match.group(0) | ||
213 | 326 | rest = rest[match.end():] | ||
214 | 327 | continue | ||
215 | 328 | if '$' in expansion: | ||
216 | 329 | # Expansion is not safe to be collapsed later | ||
217 | 330 | expansion = "(value unsafe to expand)" | ||
218 | 331 | if encoder is not None: | ||
219 | 332 | expansion = encoder(expansion) | ||
220 | 333 | params = {'name': keyword, 'value': expansion} | ||
221 | 334 | result += _expanded_style % params | ||
222 | 335 | rest = rest[match.end():] | ||
223 | 336 | return result + rest | ||
224 | 337 | |||
225 | 338 | |||
226 | 339 | def _get_from_dicts(dicts, key, default=None): | ||
227 | 340 | """Search a sequence of dictionaries or registries for a key. | ||
228 | 341 | |||
229 | 342 | :return: the value, or default if not found | ||
230 | 343 | """ | ||
231 | 344 | for dict in dicts: | ||
232 | 345 | if key in dict: | ||
233 | 346 | return dict.get(key) | ||
234 | 347 | return default | ||
235 | 348 | |||
236 | 349 | |||
237 | 350 | def _xml_escape(s): | ||
238 | 351 | """Escape a string so it can be included safely in XML/HTML.""" | ||
239 | 352 | # Complie the regular expressions if not already done | ||
240 | 353 | xml8._ensure_utf8_re() | ||
241 | 354 | # Convert and strip the trailing quote | ||
242 | 355 | return xml8._encode_and_escape(s)[:-1] | ||
243 | 356 | |||
244 | 357 | |||
245 | 358 | def _kw_compressor(chunks, context=None): | ||
246 | 359 | """Filter that replaces keywords with their compressed form.""" | ||
247 | 360 | text = ''.join(chunks) | ||
248 | 361 | return [compress_keywords(text, [keyword_registry])] | ||
249 | 362 | |||
250 | 363 | |||
251 | 364 | def _kw_expander(chunks, context, encoder=None): | ||
252 | 365 | """Keyword expander.""" | ||
253 | 366 | text = ''.join(chunks) | ||
254 | 367 | return [expand_keywords(text, [keyword_registry], context=context, | ||
255 | 368 | encoder=encoder)] | ||
256 | 369 | |||
257 | 370 | |||
258 | 371 | def _normal_kw_expander(chunks, context=None): | ||
259 | 372 | """Filter that replaces keywords with their expanded form.""" | ||
260 | 373 | return _kw_expander(chunks, context) | ||
261 | 374 | |||
262 | 375 | |||
263 | 376 | def _xml_escape_kw_expander(chunks, context=None): | ||
264 | 377 | """Filter that replaces keywords with a form suitable for use in XML.""" | ||
265 | 378 | return _kw_expander(chunks, context, encoder=_xml_escape) | ||
266 | 379 | |||
267 | 380 | |||
268 | 381 | # Define and register the filter stack map | 133 | # Define and register the filter stack map |
269 | 382 | def _keywords_filter_stack_lookup(k): | 134 | def _keywords_filter_stack_lookup(k): |
270 | 135 | from bzrlib.plugins.keywords.keywords import ( | ||
271 | 136 | _kw_compressor, | ||
272 | 137 | _normal_kw_expander, | ||
273 | 138 | _xml_escape_kw_expander, | ||
274 | 139 | ) | ||
275 | 383 | filter_stack_map = { | 140 | filter_stack_map = { |
276 | 384 | 'off': [], | 141 | 'off': [], |
277 | 385 | 'on': | 142 | 'on': |
278 | @@ -416,21 +173,25 @@ | |||
279 | 416 | # override the inherited run() and help() methods | 173 | # override the inherited run() and help() methods |
280 | 417 | 174 | ||
281 | 418 | takes_options = builtins.cmd_cat.takes_options + [ | 175 | takes_options = builtins.cmd_cat.takes_options + [ |
287 | 419 | option.RegistryOption('keywords', | 176 | option.RegistryOption('keywords', |
288 | 420 | registry=_keyword_style_registry, | 177 | lazy_registry=("bzrlib.plugins.keywords.keywords", |
289 | 421 | converter=lambda s: s, | 178 | "_keyword_style_registry"), |
290 | 422 | help='Keyword expansion style.')] | 179 | converter=lambda s: s, |
291 | 423 | 180 | help='Keyword expansion style.')] | |
292 | 181 | |||
293 | 424 | def run(self, *args, **kwargs): | 182 | def run(self, *args, **kwargs): |
294 | 425 | """Process special options and delegate to superclass.""" | 183 | """Process special options and delegate to superclass.""" |
295 | 426 | if 'keywords' in kwargs: | 184 | if 'keywords' in kwargs: |
296 | 185 | from bzrlib.plugins.keywords.keywords import ( | ||
297 | 186 | _keyword_style_registry, | ||
298 | 187 | ) | ||
299 | 427 | # Implicitly set the filters option | 188 | # Implicitly set the filters option |
300 | 428 | kwargs['filters'] = True | 189 | kwargs['filters'] = True |
301 | 429 | style = kwargs['keywords'] | 190 | style = kwargs['keywords'] |
302 | 430 | _keyword_style_registry.default_key = style | 191 | _keyword_style_registry.default_key = style |
303 | 431 | del kwargs['keywords'] | 192 | del kwargs['keywords'] |
304 | 432 | return super(cmd_cat, self).run(*args, **kwargs) | 193 | return super(cmd_cat, self).run(*args, **kwargs) |
306 | 433 | 194 | ||
307 | 434 | def help(self): | 195 | def help(self): |
308 | 435 | """Return help message including text from superclass.""" | 196 | """Return help message including text from superclass.""" |
309 | 436 | from inspect import getdoc | 197 | from inspect import getdoc |
310 | @@ -442,21 +203,25 @@ | |||
311 | 442 | # override the inherited run() and help() methods | 203 | # override the inherited run() and help() methods |
312 | 443 | 204 | ||
313 | 444 | takes_options = builtins.cmd_export.takes_options + [ | 205 | takes_options = builtins.cmd_export.takes_options + [ |
316 | 445 | option.RegistryOption('keywords', | 206 | option.RegistryOption('keywords', |
317 | 446 | registry=_keyword_style_registry, | 207 | lazy_registry=("bzrlib.plugins.keywords.keywords", |
318 | 208 | "_keyword_style_registry"), | ||
319 | 447 | converter=lambda s: s, | 209 | converter=lambda s: s, |
320 | 448 | help='Keyword expansion style.')] | 210 | help='Keyword expansion style.')] |
322 | 449 | 211 | ||
323 | 450 | def run(self, *args, **kwargs): | 212 | def run(self, *args, **kwargs): |
324 | 451 | """Process special options and delegate to superclass.""" | 213 | """Process special options and delegate to superclass.""" |
325 | 452 | if 'keywords' in kwargs: | 214 | if 'keywords' in kwargs: |
326 | 215 | from bzrlib.plugins.keywords.keywords import ( | ||
327 | 216 | _keyword_style_registry, | ||
328 | 217 | ) | ||
329 | 453 | # Implicitly set the filters option | 218 | # Implicitly set the filters option |
330 | 454 | kwargs['filters'] = True | 219 | kwargs['filters'] = True |
331 | 455 | style = kwargs['keywords'] | 220 | style = kwargs['keywords'] |
332 | 456 | _keyword_style_registry.default_key = style | 221 | _keyword_style_registry.default_key = style |
333 | 457 | del kwargs['keywords'] | 222 | del kwargs['keywords'] |
334 | 458 | return super(cmd_export, self).run(*args, **kwargs) | 223 | return super(cmd_export, self).run(*args, **kwargs) |
336 | 459 | 224 | ||
337 | 460 | def help(self): | 225 | def help(self): |
338 | 461 | """Return help message including text from superclass.""" | 226 | """Return help message including text from superclass.""" |
339 | 462 | from inspect import getdoc | 227 | from inspect import getdoc |
340 | 463 | 228 | ||
341 | === added file 'keywords.py' | |||
342 | --- keywords.py 1970-01-01 00:00:00 +0000 | |||
343 | +++ keywords.py 2011-03-02 12:34:10 +0000 | |||
344 | @@ -0,0 +1,264 @@ | |||
345 | 1 | # Copyright (C) 2008 Canonical Ltd | ||
346 | 2 | # | ||
347 | 3 | # This program is free software; you can redistribute it and/or modify | ||
348 | 4 | # it under the terms of the GNU General Public License as published by | ||
349 | 5 | # the Free Software Foundation; either version 2 of the License, or | ||
350 | 6 | # (at your option) any later version. | ||
351 | 7 | # | ||
352 | 8 | # This program is distributed in the hope that it will be useful, | ||
353 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
354 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
355 | 11 | # GNU General Public License for more details. | ||
356 | 12 | # | ||
357 | 13 | # You should have received a copy of the GNU General Public License | ||
358 | 14 | # along with this program; if not, write to the Free Software | ||
359 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
360 | 16 | |||
361 | 17 | import re, time | ||
362 | 18 | from bzrlib import ( | ||
363 | 19 | debug, | ||
364 | 20 | osutils, | ||
365 | 21 | registry, | ||
366 | 22 | trace, | ||
367 | 23 | ) | ||
368 | 24 | |||
369 | 25 | # Expansion styles | ||
370 | 26 | # Note: Round-tripping is only required between the raw and cooked styles | ||
371 | 27 | _keyword_style_registry = registry.Registry() | ||
372 | 28 | _keyword_style_registry.register('raw', '$%(name)s$') | ||
373 | 29 | _keyword_style_registry.register('cooked', '$%(name)s: %(value)s $') | ||
374 | 30 | _keyword_style_registry.register('publish', '%(name)s: %(value)s') | ||
375 | 31 | _keyword_style_registry.register('publish-values', '%(value)s') | ||
376 | 32 | _keyword_style_registry.register('publish-names', '%(name)s') | ||
377 | 33 | _keyword_style_registry.default_key = 'cooked' | ||
378 | 34 | |||
379 | 35 | |||
380 | 36 | # Regular expressions for matching the raw and cooked patterns | ||
381 | 37 | _KW_RAW_RE = re.compile(r'\$([\w\-]+)(:[^$]*)?\$') | ||
382 | 38 | _KW_COOKED_RE = re.compile(r'\$([\w\-]+):([^$]+)\$') | ||
383 | 39 | |||
384 | 40 | |||
385 | 41 | # The registry of keywords. Other plugins may wish to add entries to this. | ||
386 | 42 | keyword_registry = registry.Registry() | ||
387 | 43 | |||
388 | 44 | # Revision-related keywords | ||
389 | 45 | keyword_registry.register('Date', | ||
390 | 46 | lambda c: format_date(c.revision().timestamp, c.revision().timezone, | ||
391 | 47 | c.config(), 'Date')) | ||
392 | 48 | keyword_registry.register('Committer', | ||
393 | 49 | lambda c: c.revision().committer) | ||
394 | 50 | keyword_registry.register('Authors', | ||
395 | 51 | lambda c: ", ".join(c.revision().get_apparent_authors())) | ||
396 | 52 | keyword_registry.register('Revision-Id', | ||
397 | 53 | lambda c: c.revision_id()) | ||
398 | 54 | keyword_registry.register('Path', | ||
399 | 55 | lambda c: c.relpath()) | ||
400 | 56 | keyword_registry.register('Directory', | ||
401 | 57 | lambda c: osutils.split(c.relpath())[0]) | ||
402 | 58 | keyword_registry.register('Filename', | ||
403 | 59 | lambda c: osutils.split(c.relpath())[1]) | ||
404 | 60 | keyword_registry.register('File-Id', | ||
405 | 61 | lambda c: c.file_id()) | ||
406 | 62 | |||
407 | 63 | # Environment-related keywords | ||
408 | 64 | keyword_registry.register('Now', | ||
409 | 65 | lambda c: format_date(time.time(), time.timezone, c.config(), 'Now')) | ||
410 | 66 | keyword_registry.register('User', | ||
411 | 67 | lambda c: c.config().username()) | ||
412 | 68 | |||
413 | 69 | # Keywords for finer control over name & address formatting | ||
414 | 70 | keyword_registry.register('Committer-Name', | ||
415 | 71 | lambda c: extract_name(c.revision().committer)) | ||
416 | 72 | keyword_registry.register('Committer-Email', | ||
417 | 73 | lambda c: extract_email(c.revision().committer)) | ||
418 | 74 | keyword_registry.register('Author1-Name', | ||
419 | 75 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 0)) | ||
420 | 76 | keyword_registry.register('Author1-Email', | ||
421 | 77 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 0)) | ||
422 | 78 | keyword_registry.register('Author2-Name', | ||
423 | 79 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 1)) | ||
424 | 80 | keyword_registry.register('Author2-Email', | ||
425 | 81 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 1)) | ||
426 | 82 | keyword_registry.register('Author3-Name', | ||
427 | 83 | lambda c: extract_name_item(c.revision().get_apparent_authors(), 2)) | ||
428 | 84 | keyword_registry.register('Author3-Email', | ||
429 | 85 | lambda c: extract_email_item(c.revision().get_apparent_authors(), 2)) | ||
430 | 86 | keyword_registry.register('User-Name', | ||
431 | 87 | lambda c: extract_name(c.config().username())) | ||
432 | 88 | keyword_registry.register('User-Email', | ||
433 | 89 | lambda c: extract_email(c.config().username())) | ||
434 | 90 | |||
435 | 91 | |||
436 | 92 | def format_date(timestamp, offset=0, cfg=None, name=None): | ||
437 | 93 | """Return a formatted date string. | ||
438 | 94 | |||
439 | 95 | :param timestamp: Seconds since the epoch. | ||
440 | 96 | :param offset: Timezone offset in seconds east of utc. | ||
441 | 97 | """ | ||
442 | 98 | if cfg is not None and name is not None: | ||
443 | 99 | cfg_key = 'keywords.format.%s' % (name,) | ||
444 | 100 | format = cfg.get_user_option(cfg_key) | ||
445 | 101 | else: | ||
446 | 102 | format = None | ||
447 | 103 | return osutils.format_date(timestamp, offset, date_fmt=format) | ||
448 | 104 | |||
449 | 105 | |||
450 | 106 | def extract_name(userid): | ||
451 | 107 | """Extract the name out of a user-id string. | ||
452 | 108 | |||
453 | 109 | user-id strings have the format 'name <email>'. | ||
454 | 110 | """ | ||
455 | 111 | if userid and userid[-1] == '>': | ||
456 | 112 | return userid[:-1].rsplit('<', 1)[0].rstrip() | ||
457 | 113 | else: | ||
458 | 114 | return userid | ||
459 | 115 | |||
460 | 116 | |||
461 | 117 | def extract_email(userid): | ||
462 | 118 | """Extract the email address out of a user-id string. | ||
463 | 119 | |||
464 | 120 | user-id strings have the format 'name <email>'. | ||
465 | 121 | """ | ||
466 | 122 | if userid and userid[-1] == '>': | ||
467 | 123 | return userid[:-1].rsplit('<', 1)[1] | ||
468 | 124 | else: | ||
469 | 125 | return userid | ||
470 | 126 | |||
471 | 127 | def extract_name_item(seq, n): | ||
472 | 128 | """Extract the name out of the nth item in a sequence of user-ids. | ||
473 | 129 | |||
474 | 130 | :return: the user-name or an empty string | ||
475 | 131 | """ | ||
476 | 132 | try: | ||
477 | 133 | return extract_name(seq[n]) | ||
478 | 134 | except IndexError: | ||
479 | 135 | return "" | ||
480 | 136 | |||
481 | 137 | |||
482 | 138 | def extract_email_item(seq, n): | ||
483 | 139 | """Extract the email out of the nth item in a sequence of user-ids. | ||
484 | 140 | |||
485 | 141 | :return: the email address or an empty string | ||
486 | 142 | """ | ||
487 | 143 | try: | ||
488 | 144 | return extract_email(seq[n]) | ||
489 | 145 | except IndexError: | ||
490 | 146 | return "" | ||
491 | 147 | |||
492 | 148 | |||
493 | 149 | def compress_keywords(s, keyword_dicts): | ||
494 | 150 | """Replace cooked style keywords with raw style in a string. | ||
495 | 151 | |||
496 | 152 | Note: If the keyword is not known, the text is not modified. | ||
497 | 153 | |||
498 | 154 | :param s: the string | ||
499 | 155 | :param keyword_dicts: an iterable of keyword dictionaries. | ||
500 | 156 | :return: the string with keywords compressed | ||
501 | 157 | """ | ||
502 | 158 | _raw_style = _keyword_style_registry.get('raw') | ||
503 | 159 | result = '' | ||
504 | 160 | rest = s | ||
505 | 161 | while (True): | ||
506 | 162 | match = _KW_COOKED_RE.search(rest) | ||
507 | 163 | if not match: | ||
508 | 164 | break | ||
509 | 165 | result += rest[:match.start()] | ||
510 | 166 | keyword = match.group(1) | ||
511 | 167 | expansion = _get_from_dicts(keyword_dicts, keyword) | ||
512 | 168 | if expansion is None: | ||
513 | 169 | # Unknown expansion - leave as is | ||
514 | 170 | result += match.group(0) | ||
515 | 171 | else: | ||
516 | 172 | result += _raw_style % {'name': keyword} | ||
517 | 173 | rest = rest[match.end():] | ||
518 | 174 | return result + rest | ||
519 | 175 | |||
520 | 176 | |||
521 | 177 | def expand_keywords(s, keyword_dicts, context=None, encoder=None, style=None): | ||
522 | 178 | """Replace raw style keywords with another style in a string. | ||
523 | 179 | |||
524 | 180 | Note: If the keyword is already in the expanded style, the value is | ||
525 | 181 | not replaced. | ||
526 | 182 | |||
527 | 183 | :param s: the string | ||
528 | 184 | :param keyword_dicts: an iterable of keyword dictionaries. If values | ||
529 | 185 | are callables, they are executed to find the real value. | ||
530 | 186 | :param context: the parameter to pass to callable values | ||
531 | 187 | :param style: the style of expansion to use of None for the default | ||
532 | 188 | :return: the string with keywords expanded | ||
533 | 189 | """ | ||
534 | 190 | _expanded_style = _keyword_style_registry.get(style) | ||
535 | 191 | result = '' | ||
536 | 192 | rest = s | ||
537 | 193 | while (True): | ||
538 | 194 | match = _KW_RAW_RE.search(rest) | ||
539 | 195 | if not match: | ||
540 | 196 | break | ||
541 | 197 | result += rest[:match.start()] | ||
542 | 198 | keyword = match.group(1) | ||
543 | 199 | expansion = _get_from_dicts(keyword_dicts, keyword) | ||
544 | 200 | if callable(expansion): | ||
545 | 201 | try: | ||
546 | 202 | expansion = expansion(context) | ||
547 | 203 | except AttributeError, err: | ||
548 | 204 | if 'error' in debug.debug_flags: | ||
549 | 205 | trace.note("error evaluating %s for keyword %s: %s", | ||
550 | 206 | expansion, keyword, err) | ||
551 | 207 | expansion = "(evaluation error)" | ||
552 | 208 | if expansion is None: | ||
553 | 209 | # Unknown expansion - leave as is | ||
554 | 210 | result += match.group(0) | ||
555 | 211 | rest = rest[match.end():] | ||
556 | 212 | continue | ||
557 | 213 | if '$' in expansion: | ||
558 | 214 | # Expansion is not safe to be collapsed later | ||
559 | 215 | expansion = "(value unsafe to expand)" | ||
560 | 216 | if encoder is not None: | ||
561 | 217 | expansion = encoder(expansion) | ||
562 | 218 | params = {'name': keyword, 'value': expansion} | ||
563 | 219 | result += _expanded_style % params | ||
564 | 220 | rest = rest[match.end():] | ||
565 | 221 | return result + rest | ||
566 | 222 | |||
567 | 223 | |||
568 | 224 | def _get_from_dicts(dicts, key, default=None): | ||
569 | 225 | """Search a sequence of dictionaries or registries for a key. | ||
570 | 226 | |||
571 | 227 | :return: the value, or default if not found | ||
572 | 228 | """ | ||
573 | 229 | for dict in dicts: | ||
574 | 230 | if key in dict: | ||
575 | 231 | return dict.get(key) | ||
576 | 232 | return default | ||
577 | 233 | |||
578 | 234 | |||
579 | 235 | def _xml_escape(s): | ||
580 | 236 | """Escape a string so it can be included safely in XML/HTML.""" | ||
581 | 237 | # Compile the regular expressions if not already done | ||
582 | 238 | from bzrlib import xml8 | ||
583 | 239 | xml8._ensure_utf8_re() | ||
584 | 240 | # Convert and strip the trailing quote | ||
585 | 241 | return xml8._encode_and_escape(s)[:-1] | ||
586 | 242 | |||
587 | 243 | |||
588 | 244 | def _kw_compressor(chunks, context=None): | ||
589 | 245 | """Filter that replaces keywords with their compressed form.""" | ||
590 | 246 | text = ''.join(chunks) | ||
591 | 247 | return [compress_keywords(text, [keyword_registry])] | ||
592 | 248 | |||
593 | 249 | |||
594 | 250 | def _kw_expander(chunks, context, encoder=None): | ||
595 | 251 | """Keyword expander.""" | ||
596 | 252 | text = ''.join(chunks) | ||
597 | 253 | return [expand_keywords(text, [keyword_registry], context=context, | ||
598 | 254 | encoder=encoder)] | ||
599 | 255 | |||
600 | 256 | |||
601 | 257 | def _normal_kw_expander(chunks, context=None): | ||
602 | 258 | """Filter that replaces keywords with their expanded form.""" | ||
603 | 259 | return _kw_expander(chunks, context) | ||
604 | 260 | |||
605 | 261 | |||
606 | 262 | def _xml_escape_kw_expander(chunks, context=None): | ||
607 | 263 | """Filter that replaces keywords with a form suitable for use in XML.""" | ||
608 | 264 | return _kw_expander(chunks, context, encoder=_xml_escape) | ||
609 | 0 | 265 | ||
610 | === modified file 'tests/test_conversion.py' | |||
611 | --- tests/test_conversion.py 2010-10-01 17:31:51 +0000 | |||
612 | +++ tests/test_conversion.py 2011-03-02 12:34:10 +0000 | |||
613 | @@ -18,7 +18,10 @@ | |||
614 | 18 | 18 | ||
615 | 19 | 19 | ||
616 | 20 | from bzrlib import tests | 20 | from bzrlib import tests |
618 | 21 | from bzrlib.plugins.keywords import compress_keywords, expand_keywords | 21 | from bzrlib.plugins.keywords.keywords import ( |
619 | 22 | compress_keywords, | ||
620 | 23 | expand_keywords, | ||
621 | 24 | ) | ||
622 | 22 | 25 | ||
623 | 23 | 26 | ||
624 | 24 | # Sample unexpanded and expanded pairs for a keyword dictionary | 27 | # Sample unexpanded and expanded pairs for a keyword dictionary |
625 | 25 | 28 | ||
626 | === modified file 'tests/test_keywords_in_trees.py' | |||
627 | --- tests/test_keywords_in_trees.py 2009-08-11 05:14:31 +0000 | |||
628 | +++ tests/test_keywords_in_trees.py 2011-03-02 12:34:10 +0000 | |||
629 | @@ -18,12 +18,8 @@ | |||
630 | 18 | 18 | ||
631 | 19 | ## TODO: add tests for xml_escaped | 19 | ## TODO: add tests for xml_escaped |
632 | 20 | 20 | ||
639 | 21 | from cStringIO import StringIO | 21 | from bzrlib import rules |
640 | 22 | import sys | 22 | from bzrlib.tests import TestCaseWithTransport |
635 | 23 | |||
636 | 24 | from bzrlib import config, rules | ||
637 | 25 | from bzrlib.tests import TestCaseWithTransport, TestSkipped | ||
638 | 26 | from bzrlib.tests.per_workingtree import TestCaseWithWorkingTree | ||
641 | 27 | from bzrlib.workingtree import WorkingTree | 23 | from bzrlib.workingtree import WorkingTree |
642 | 28 | 24 | ||
643 | 29 | 25 | ||
644 | @@ -90,7 +86,7 @@ | |||
645 | 90 | t.add('file1', 'file1-id') | 86 | t.add('file1', 'file1-id') |
646 | 91 | t.commit("add file1", rev_id="rev1-id", | 87 | t.commit("add file1", rev_id="rev1-id", |
647 | 92 | committer="Jane Smith <jane@example.com>", | 88 | committer="Jane Smith <jane@example.com>", |
649 | 93 | author="Sue Smith <sue@example.com>") | 89 | authors=["Sue Smith <sue@example.com>"]) |
650 | 94 | basis = t.basis_tree() | 90 | basis = t.basis_tree() |
651 | 95 | basis.lock_read() | 91 | basis.lock_read() |
652 | 96 | self.addCleanup(basis.unlock) | 92 | self.addCleanup(basis.unlock) |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 2/27/2011 4:57 AM, Jelmer Vernooij wrote: /code.launchpad .net/~jelmer/ bzr-keywords/ lazy/+merge/ 51444
> Jelmer Vernooij has proposed merging lp:~jelmer/bzr-keywords/lazy into lp:bzr-keywords.
>
> Requested reviews:
> Bazaar Developers (bzr)
>
> For more details, see:
> https:/
>
> Lazily load the keywords plugin.
I'm a bit surprised at how much code is added here, versus how much is
removed.
All the format_date, extract_name, etc don't seem to come from somewhere
else.
Is this just a large rewrite of the internals?
The changes seem fine to me, but I didn't go over them in detail, with
the change being surprisingly large.
Care to explain a bit more what you changed?
review: needsinfo
John
=:->
-----BEGIN PGP SIGNATURE----- enigmail. mozdev. org/
uFfAACgkQJdeBCY SNAAPKpQCcD0Epu FviTVfxSMY5ikxI wPrW gGztSHhpYnyzoNk 5l
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk1
KV8AoIGp7aUs4DD
=3j0u
-----END PGP SIGNATURE-----