Merge lp:~jelmer/brz/bundle-keywords into lp:brz

Proposed by Jelmer Vernooij
Status: Work in progress
Proposed branch: lp:~jelmer/brz/bundle-keywords
Merge into: lp:brz
Diff against target: 862 lines (+810/-0)
9 files modified
breezy/filters/__init__.py (+3/-0)
breezy/plugins/keywords/NEWS (+18/-0)
breezy/plugins/keywords/README.txt (+43/-0)
breezy/plugins/keywords/TODO (+13/-0)
breezy/plugins/keywords/__init__.py (+240/-0)
breezy/plugins/keywords/keywords.py (+269/-0)
breezy/plugins/keywords/tests/__init__.py (+19/-0)
breezy/plugins/keywords/tests/test_conversion.py (+82/-0)
breezy/plugins/keywords/tests/test_keywords_in_trees.py (+123/-0)
To merge this branch: bzr merge lp:~jelmer/brz/bundle-keywords
Reviewer Review Type Date Requested Status
Breezy developers Pending
Review via email: mp+358357@code.launchpad.net

Description of the change

Bundle the keywords plugin.

To post a comment you must log in.
lp:~jelmer/brz/bundle-keywords updated
6694. By Jelmer Vernooij

Use absolute imports.

Unmerged revisions

6694. By Jelmer Vernooij

Use absolute imports.

6693. By Jelmer Vernooij

Fix tests with breezy.

6692. By Jelmer Vernooij

merge trunk.

6691. By Jelmer Vernooij

Bundle keywords plugin.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/filters/__init__.py'
2--- breezy/filters/__init__.py 2018-07-17 22:59:51 +0000
3+++ breezy/filters/__init__.py 2018-11-12 21:28:40 +0000
4@@ -91,6 +91,9 @@
5 """Relative path of file to tree-root."""
6 return self._relpath
7
8+ def file_id(self):
9+ return self.source_tree().path2id(self.relpath())
10+
11 def source_tree(self):
12 """Source Tree object."""
13 return self._tree
14
15=== added directory 'breezy/plugins/keywords'
16=== added file 'breezy/plugins/keywords/NEWS'
17--- breezy/plugins/keywords/NEWS 1970-01-01 00:00:00 +0000
18+++ breezy/plugins/keywords/NEWS 2018-11-12 21:28:40 +0000
19@@ -0,0 +1,18 @@
20+##########################
21+bzr-keywords Release Notes
22+##########################
23+
24+.. contents::
25+
26+In Development
27+##############
28+
29+This version is suitable for use with Bazaar 1.14 or later using
30+trees in the 1.14 format.
31+
32+
33+0.1 28-Jul-2008
34+###############
35+
36+This version is suitable for testing with the development branch,
37+~ian-clatworthy/bzr/bzr.content-filters.
38
39=== added file 'breezy/plugins/keywords/README.txt'
40--- breezy/plugins/keywords/README.txt 1970-01-01 00:00:00 +0000
41+++ breezy/plugins/keywords/README.txt 2018-11-12 21:28:40 +0000
42@@ -0,0 +1,43 @@
43+bzr-keywords: RCS-like keyword templates
44+========================================
45+
46+Overview
47+--------
48+
49+This plugin adds keyword filtering to selected files. This allows
50+you to do things like include the current user and date in a web page.
51+
52+
53+Installation
54+------------
55+
56+The easiest way to install this plugin is to either copy or symlink the
57+directory into your ~/.bazaar/plugins directory. Be sure to rename the
58+directory to keywords (instead of bzr-keywords).
59+
60+See http://bazaar-vcs.org/UsingPlugins for other options such as
61+using the BZR_PLUGIN_PATH environment variable.
62+
63+
64+Testing
65+-------
66+
67+To test the plugin after installation:
68+
69+ bzr selftest keywords.tests
70+
71+
72+Documentation
73+-------------
74+
75+To see the documentation after installation:
76+
77+ bzr help keywords
78+
79+
80+Licensing
81+---------
82+
83+This plugin is (C) Copyright Canonical Limited 2008 under the
84+GPL Version 2 or later. Please see the file COPYING.txt for the licence
85+details.
86
87=== added file 'breezy/plugins/keywords/TODO'
88--- breezy/plugins/keywords/TODO 1970-01-01 00:00:00 +0000
89+++ breezy/plugins/keywords/TODO 2018-11-12 21:28:40 +0000
90@@ -0,0 +1,13 @@
91+Things to consider:
92+ * python_escape (maybe called string_escape) ala xml_escape?
93+ * some sort of block construct so easier to include in ReST and properties
94+ files, e.g.
95+ .. $begin-keywords$
96+ :name1: value1
97+ :name2: value2
98+ .. $end-keywords$
99+
100+* Add tests for:
101+ * untested keyword values (including date formatting)
102+ * escaping
103+ * style formatting
104
105=== added file 'breezy/plugins/keywords/__init__.py'
106--- breezy/plugins/keywords/__init__.py 1970-01-01 00:00:00 +0000
107+++ breezy/plugins/keywords/__init__.py 2018-11-12 21:28:40 +0000
108@@ -0,0 +1,240 @@
109+# Copyright (C) 2008 Canonical Ltd
110+#
111+# This program is free software; you can redistribute it and/or modify
112+# it under the terms of the GNU General Public License as published by
113+# the Free Software Foundation; either version 2 of the License, or
114+# (at your option) any later version.
115+#
116+# This program is distributed in the hope that it will be useful,
117+# but WITHOUT ANY WARRANTY; without even the implied warranty of
118+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
119+# GNU General Public License for more details.
120+#
121+# You should have received a copy of the GNU General Public License
122+# along with this program; if not, write to the Free Software
123+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
124+
125+r'''Keyword Templating
126+==================
127+
128+Keyword templating is provided as a content filter where Bazaar internally
129+stores a canonical format but outputs a convenience format. See
130+``bzr help content-filters`` for general information about using these.
131+
132+Note: Content filtering is only supported in recently added formats,
133+e.g. 1.14.
134+
135+Keyword templates are specified using the following patterns:
136+
137+ * in canonical/compressed format: $Keyword$
138+ * in convenience/expanded format: $Keyword: value $
139+
140+When expanding, the existing text is retained if an unknown keyword is
141+found. If the keyword is already expanded but known, the value is replaced.
142+When compressing, the values of known keywords are removed.
143+
144+Keyword filtering needs to be enabled for selected branches and files via
145+rules. See ``bzr help rules`` for general information on defining rules.
146+For example, to enable keywords for all ``txt`` files on your system, add
147+these lines to your ``BZR_HOME/rules`` file::
148+
149+ [name *.txt]
150+ keywords = on
151+
152+To disable keywords for ``txt`` files but enable them for ``html`` files::
153+
154+ [name *.txt]
155+ keywords = off
156+
157+ [name *.html]
158+ keywords = xml_escape
159+
160+``xml_escape`` enables keyword expansion but it escapes special characters
161+in keyword values so they can be safely included in HTML or XML files.
162+
163+The currently supported keywords are given below.
164+
165+ ============= =========================================================
166+ Keyword Description
167+ ============= =========================================================
168+ Date the date and time the file was last modified
169+ Committer the committer (name and email) of the last change
170+ Authors the authors (names and emails) of the last change
171+ Revision-Id the unique id of the revision that last changed the file
172+ Path the relative path of the file in the tree
173+ Filename just the name part of the relative path
174+ Directory just the directory part of the relative path
175+ File-Id the unique id assigned to this file
176+ Now the current date and time
177+ User the current user (name and email)
178+ ============= =========================================================
179+
180+If you want finer control over the formatting of names and email
181+addresses, you can use the following keywords.
182+
183+ ============= =======================================================
184+ Keyword Description
185+ ============= =======================================================
186+ Committer-Name just the name of the current committer
187+ Committer-Email just the email address of the current committer
188+ Author1-Name just the name of the first author
189+ Author1-Email just the email address of the first author
190+ Author2-Name just the name of the second author
191+ Author2-Email just the email address of the second author
192+ Author3-Name just the name of the third author
193+ Author3-Email just the email address of the third author
194+ User-Name just the name of the current user
195+ User-Email just the email address of the current user
196+ ============= =======================================================
197+
198+Note: If you have more than 3 authors for a given revision, please
199+ask on the Bazaar mailing list for an enhancement to support the
200+number you need.
201+
202+By default, dates/times are output using this format::
203+
204+ YYYY-MM-DD HH:MM:SS+HH:MM
205+
206+To specify a custom format, add a configuration setting to
207+``BZR_HOME/bazaar.conf`` like this::
208+
209+ keywords.format.Now = %A, %B %d, %Y
210+
211+The last part of the key needs to match the keyword name. The value must be
212+a legal strftime (http://docs.python.org/lib/module-time.html) format.
213+'''
214+
215+from __future__ import absolute_import
216+
217+
218+from ... import (
219+ builtins,
220+ commands,
221+ filters,
222+ option,
223+ )
224+
225+
226+def test_suite():
227+ """Called by breezy to fetch tests for this plugin"""
228+ from unittest import TestSuite, TestLoader
229+ from .tests import (
230+ test_conversion,
231+ test_keywords_in_trees,
232+ )
233+ loader = TestLoader()
234+ suite = TestSuite()
235+ for module in [
236+ test_conversion,
237+ test_keywords_in_trees,
238+ ]:
239+ suite.addTests(loader.loadTestsFromModule(module))
240+ return suite
241+
242+
243+# Define and register the filter stack map
244+def _keywords_filter_stack_lookup(k):
245+ from .keywords import (
246+ _kw_compressor,
247+ _normal_kw_expander,
248+ _xml_escape_kw_expander,
249+ )
250+ filter_stack_map = {
251+ 'off': [],
252+ 'on':
253+ [filters.ContentFilter(_kw_compressor, _normal_kw_expander)],
254+ 'xml_escape':
255+ [filters.ContentFilter(_kw_compressor, _xml_escape_kw_expander)],
256+ }
257+ return filter_stack_map.get(k)
258+
259+try:
260+ register_filter = filters.filter_stacks_registry.register
261+except AttributeError:
262+ register_filter = filters.register_filter_stack_map
263+
264+register_filter('keywords', _keywords_filter_stack_lookup)
265+
266+
267+class cmd_cat(builtins.cmd_cat):
268+ """
269+ The ``--keywords`` option specifies the keywords expansion
270+ style. By default (``raw`` style), no expansion is done.
271+ Other styles enable expansion in a ``cooked`` mode where both
272+ the keyword and its value are displayed inside $ markers, or in
273+ numerous publishing styles - ``publish``, ``publish-values`` and
274+ ``publish-names`` - where the $ markers are completely removed.
275+ The publishing styles do not support round-tripping back to the
276+ raw content but are useful for improving the readability of
277+ published web pages for example.
278+
279+ Note: Files must have the ``keywords`` preference defined for them
280+ in order for the ``--keywords`` option to take effect. In particular,
281+ the preference specifies how keyword values are encoded for different
282+ filename patterns. See ``bzr help keywords`` for more information on
283+ how to specify the required preference using rules.
284+ """
285+
286+ # Add a new option to the builtin command and
287+ # override the inherited run() and help() methods
288+
289+ takes_options = builtins.cmd_cat.takes_options + [
290+ option.RegistryOption('keywords',
291+ lazy_registry=(__name__ + ".keywords",
292+ "_keyword_style_registry"),
293+ converter=lambda s: s,
294+ help='Keyword expansion style.')]
295+
296+ def run(self, *args, **kwargs):
297+ """Process special options and delegate to superclass."""
298+ if 'keywords' in kwargs:
299+ from .keywords import (
300+ _keyword_style_registry,
301+ )
302+ # Implicitly set the filters option
303+ kwargs['filters'] = True
304+ style = kwargs['keywords']
305+ _keyword_style_registry.default_key = style
306+ del kwargs['keywords']
307+ return super(cmd_cat, self).run(*args, **kwargs)
308+
309+ def help(self):
310+ """Return help message including text from superclass."""
311+ from inspect import getdoc
312+ return getdoc(super(cmd_cat, self)) + '\n\n' + getdoc(self)
313+
314+
315+class cmd_export(builtins.cmd_export):
316+ # Add a new option to the builtin command and
317+ # override the inherited run() and help() methods
318+
319+ takes_options = builtins.cmd_export.takes_options + [
320+ option.RegistryOption('keywords',
321+ lazy_registry=(__name__ + ".keywords",
322+ "_keyword_style_registry"),
323+ converter=lambda s: s,
324+ help='Keyword expansion style.')]
325+
326+ def run(self, *args, **kwargs):
327+ """Process special options and delegate to superclass."""
328+ if 'keywords' in kwargs:
329+ from .keywords import (
330+ _keyword_style_registry,
331+ )
332+ # Implicitly set the filters option
333+ kwargs['filters'] = True
334+ style = kwargs['keywords']
335+ _keyword_style_registry.default_key = style
336+ del kwargs['keywords']
337+ return super(cmd_export, self).run(*args, **kwargs)
338+
339+ def help(self):
340+ """Return help message including text from superclass."""
341+ from inspect import getdoc
342+ # NOTE: Reuse of cmd_cat help below is deliberate, not a bug
343+ return getdoc(super(cmd_export, self)) + '\n\n' + getdoc(cmd_cat)
344+
345+
346+# Register the command wrappers
347+commands.register_command(cmd_cat, decorate=False)
348+commands.register_command(cmd_export, decorate=False)
349
350=== added file 'breezy/plugins/keywords/keywords.py'
351--- breezy/plugins/keywords/keywords.py 1970-01-01 00:00:00 +0000
352+++ breezy/plugins/keywords/keywords.py 2018-11-12 21:28:40 +0000
353@@ -0,0 +1,269 @@
354+# Copyright (C) 2008 Canonical Ltd
355+#
356+# This program is free software; you can redistribute it and/or modify
357+# it under the terms of the GNU General Public License as published by
358+# the Free Software Foundation; either version 2 of the License, or
359+# (at your option) any later version.
360+#
361+# This program is distributed in the hope that it will be useful,
362+# but WITHOUT ANY WARRANTY; without even the implied warranty of
363+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
364+# GNU General Public License for more details.
365+#
366+# You should have received a copy of the GNU General Public License
367+# along with this program; if not, write to the Free Software
368+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
369+
370+from __future__ import absolute_import
371+
372+import re, time
373+from ... import (
374+ debug,
375+ osutils,
376+ registry,
377+ trace,
378+ )
379+from ...sixish import text_type
380+
381+# Expansion styles
382+# Note: Round-tripping is only required between the raw and cooked styles
383+_keyword_style_registry = registry.Registry()
384+_keyword_style_registry.register('raw', b'$%(name)s$')
385+_keyword_style_registry.register('cooked', b'$%(name)s: %(value)s $')
386+_keyword_style_registry.register('publish', b'%(name)s: %(value)s')
387+_keyword_style_registry.register('publish-values', b'%(value)s')
388+_keyword_style_registry.register('publish-names', b'%(name)s')
389+_keyword_style_registry.default_key = 'cooked'
390+
391+
392+# Regular expressions for matching the raw and cooked patterns
393+_KW_RAW_RE = re.compile(b'\\$([\\w\\-]+)(:[^$]*)?\\$')
394+_KW_COOKED_RE = re.compile(b'\\$([\\w\\-]+):([^$]+)\\$')
395+
396+
397+# The registry of keywords. Other plugins may wish to add entries to this.
398+keyword_registry = registry.Registry()
399+
400+# Revision-related keywords
401+keyword_registry.register('Date',
402+ lambda c: format_date(c.revision().timestamp, c.revision().timezone,
403+ c.config(), 'Date'))
404+keyword_registry.register('Committer',
405+ lambda c: c.revision().committer)
406+keyword_registry.register('Authors',
407+ lambda c: ", ".join(c.revision().get_apparent_authors()))
408+keyword_registry.register('Revision-Id',
409+ lambda c: c.revision_id())
410+keyword_registry.register('Path',
411+ lambda c: c.relpath())
412+keyword_registry.register('Directory',
413+ lambda c: osutils.split(c.relpath())[0])
414+keyword_registry.register('Filename',
415+ lambda c: osutils.split(c.relpath())[1])
416+keyword_registry.register('File-Id',
417+ lambda c: c.file_id())
418+
419+# Environment-related keywords
420+keyword_registry.register('Now',
421+ lambda c: format_date(time.time(), time.timezone, c.config(), 'Now'))
422+keyword_registry.register('User',
423+ lambda c: c.config().username())
424+
425+# Keywords for finer control over name & address formatting
426+keyword_registry.register('Committer-Name',
427+ lambda c: extract_name(c.revision().committer))
428+keyword_registry.register('Committer-Email',
429+ lambda c: extract_email(c.revision().committer))
430+keyword_registry.register('Author1-Name',
431+ lambda c: extract_name_item(c.revision().get_apparent_authors(), 0))
432+keyword_registry.register('Author1-Email',
433+ lambda c: extract_email_item(c.revision().get_apparent_authors(), 0))
434+keyword_registry.register('Author2-Name',
435+ lambda c: extract_name_item(c.revision().get_apparent_authors(), 1))
436+keyword_registry.register('Author2-Email',
437+ lambda c: extract_email_item(c.revision().get_apparent_authors(), 1))
438+keyword_registry.register('Author3-Name',
439+ lambda c: extract_name_item(c.revision().get_apparent_authors(), 2))
440+keyword_registry.register('Author3-Email',
441+ lambda c: extract_email_item(c.revision().get_apparent_authors(), 2))
442+keyword_registry.register('User-Name',
443+ lambda c: extract_name(c.config().username()))
444+keyword_registry.register('User-Email',
445+ lambda c: extract_email(c.config().username()))
446+
447+
448+def format_date(timestamp, offset=0, cfg=None, name=None):
449+ """Return a formatted date string.
450+
451+ :param timestamp: Seconds since the epoch.
452+ :param offset: Timezone offset in seconds east of utc.
453+ """
454+ if cfg is not None and name is not None:
455+ cfg_key = 'keywords.format.%s' % (name,)
456+ format = cfg.get_user_option(cfg_key)
457+ else:
458+ format = None
459+ return osutils.format_date(timestamp, offset, date_fmt=format)
460+
461+
462+def extract_name(userid):
463+ """Extract the name out of a user-id string.
464+
465+ user-id strings have the format 'name <email>'.
466+ """
467+ if userid and userid[-1] == '>':
468+ return userid[:-1].rsplit('<', 1)[0].rstrip()
469+ else:
470+ return userid
471+
472+
473+def extract_email(userid):
474+ """Extract the email address out of a user-id string.
475+
476+ user-id strings have the format 'name <email>'.
477+ """
478+ if userid and userid[-1] == '>':
479+ return userid[:-1].rsplit('<', 1)[1]
480+ else:
481+ return userid
482+
483+def extract_name_item(seq, n):
484+ """Extract the name out of the nth item in a sequence of user-ids.
485+
486+ :return: the user-name or an empty string
487+ """
488+ try:
489+ return extract_name(seq[n])
490+ except IndexError:
491+ return ""
492+
493+
494+def extract_email_item(seq, n):
495+ """Extract the email out of the nth item in a sequence of user-ids.
496+
497+ :return: the email address or an empty string
498+ """
499+ try:
500+ return extract_email(seq[n])
501+ except IndexError:
502+ return ""
503+
504+
505+def compress_keywords(s, keyword_dicts):
506+ """Replace cooked style keywords with raw style in a string.
507+
508+ Note: If the keyword is not known, the text is not modified.
509+
510+ :param s: the string
511+ :param keyword_dicts: an iterable of keyword dictionaries.
512+ :return: the string with keywords compressed
513+ """
514+ _raw_style = _keyword_style_registry.get('raw')
515+ result = b''
516+ rest = s
517+ while True:
518+ match = _KW_COOKED_RE.search(rest)
519+ if not match:
520+ break
521+ result += rest[:match.start()]
522+ keyword = match.group(1)
523+ expansion = _get_from_dicts(keyword_dicts, keyword.decode('ascii'))
524+ if expansion is None:
525+ # Unknown expansion - leave as is
526+ result += match.group(0)
527+ else:
528+ result += _raw_style % {b'name': keyword}
529+ rest = rest[match.end():]
530+ return result + rest
531+
532+
533+def expand_keywords(s, keyword_dicts, context=None, encoder=None, style=None):
534+ """Replace raw style keywords with another style in a string.
535+
536+ Note: If the keyword is already in the expanded style, the value is
537+ not replaced.
538+
539+ :param s: the string
540+ :param keyword_dicts: an iterable of keyword dictionaries. If values
541+ are callables, they are executed to find the real value.
542+ :param context: the parameter to pass to callable values
543+ :param style: the style of expansion to use of None for the default
544+ :return: the string with keywords expanded
545+ """
546+ _expanded_style = _keyword_style_registry.get(style)
547+ result = b''
548+ rest = s
549+ while True:
550+ match = _KW_RAW_RE.search(rest)
551+ if not match:
552+ break
553+ result += rest[:match.start()]
554+ keyword = match.group(1)
555+ expansion = _get_from_dicts(keyword_dicts, keyword.decode('ascii'))
556+ if callable(expansion):
557+ try:
558+ expansion = expansion(context)
559+ except AttributeError as err:
560+ if 'error' in debug.debug_flags:
561+ trace.note("error evaluating %s for keyword %s: %s",
562+ expansion, keyword, err)
563+ expansion = b"(evaluation error)"
564+ if isinstance(expansion, text_type):
565+ expansion = expansion.encode('utf-8')
566+ if expansion is None:
567+ # Unknown expansion - leave as is
568+ result += match.group(0)
569+ rest = rest[match.end():]
570+ continue
571+ if b'$' in expansion:
572+ # Expansion is not safe to be collapsed later
573+ expansion = b"(value unsafe to expand)"
574+ if encoder is not None:
575+ expansion = encoder(expansion)
576+ params = {b'name': keyword, b'value': expansion}
577+ result += _expanded_style % params
578+ rest = rest[match.end():]
579+ return result + rest
580+
581+
582+def _get_from_dicts(dicts, key, default=None):
583+ """Search a sequence of dictionaries or registries for a key.
584+
585+ :return: the value, or default if not found
586+ """
587+ for dict in dicts:
588+ if key in dict:
589+ return dict.get(key)
590+ return default
591+
592+
593+def _xml_escape(s):
594+ """Escape a string so it can be included safely in XML/HTML."""
595+ # Compile the regular expressions if not already done
596+ from ... import xml8
597+ xml8._ensure_utf8_re()
598+ # Convert and strip the trailing quote
599+ return xml8._encode_and_escape(s)[:-1]
600+
601+
602+def _kw_compressor(chunks, context=None):
603+ """Filter that replaces keywords with their compressed form."""
604+ text = b''.join(chunks)
605+ return [compress_keywords(text, [keyword_registry])]
606+
607+
608+def _kw_expander(chunks, context, encoder=None):
609+ """Keyword expander."""
610+ text = b''.join(chunks)
611+ return [expand_keywords(text, [keyword_registry], context=context,
612+ encoder=encoder)]
613+
614+
615+def _normal_kw_expander(chunks, context=None):
616+ """Filter that replaces keywords with their expanded form."""
617+ return _kw_expander(chunks, context)
618+
619+
620+def _xml_escape_kw_expander(chunks, context=None):
621+ """Filter that replaces keywords with a form suitable for use in XML."""
622+ return _kw_expander(chunks, context, encoder=_xml_escape)
623
624=== added directory 'breezy/plugins/keywords/tests'
625=== added file 'breezy/plugins/keywords/tests/__init__.py'
626--- breezy/plugins/keywords/tests/__init__.py 1970-01-01 00:00:00 +0000
627+++ breezy/plugins/keywords/tests/__init__.py 2018-11-12 21:28:40 +0000
628@@ -0,0 +1,19 @@
629+# Copyright (C) 2008 Canonical Limited.
630+#
631+# This program is free software; you can redistribute it and/or modify
632+# it under the terms of the GNU General Public License as published by
633+# the Free Software Foundation; version 2 of the License.
634+#
635+# This program is distributed in the hope that it will be useful,
636+# but WITHOUT ANY WARRANTY; without even the implied warranty of
637+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
638+# GNU General Public License for more details.
639+#
640+# You should have received a copy of the GNU General Public License
641+# along with this program; if not, write to the Free Software
642+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
643+#
644+
645+"""Tests for bzr-keywords."""
646+
647+from __future__ import absolute_import
648
649=== added file 'breezy/plugins/keywords/tests/test_conversion.py'
650--- breezy/plugins/keywords/tests/test_conversion.py 1970-01-01 00:00:00 +0000
651+++ breezy/plugins/keywords/tests/test_conversion.py 2018-11-12 21:28:40 +0000
652@@ -0,0 +1,82 @@
653+# Copyright (C) 2008 Canonical Limited.
654+#
655+# This program is free software; you can redistribute it and/or modify
656+# it under the terms of the GNU General Public License as published by
657+# the Free Software Foundation; version 2 of the License.
658+#
659+# This program is distributed in the hope that it will be useful,
660+# but WITHOUT ANY WARRANTY; without even the implied warranty of
661+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
662+# GNU General Public License for more details.
663+#
664+# You should have received a copy of the GNU General Public License
665+# along with this program; if not, write to the Free Software
666+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
667+#
668+
669+from __future__ import absolute_import
670+
671+"""Tests for keyword expansion/contraction."""
672+
673+
674+from .... import tests
675+from ..keywords import (
676+ compress_keywords,
677+ expand_keywords,
678+ )
679+
680+
681+# Sample unexpanded and expanded pairs for a keyword dictionary
682+_keywords = {'Foo': 'FOO!', 'Bar': 'bar', 'CallMe': lambda c: "now!"}
683+_keywords_dicts = [{'Foo': 'FOO!'}, {'Bar': 'bar', 'CallMe': lambda c: "now!"}]
684+_samples = [
685+ (b'$Foo$', b'$Foo: FOO! $'),
686+ (b'$Foo', b'$Foo'),
687+ (b'Foo$', b'Foo$'),
688+ (b'$Foo$ xyz', b'$Foo: FOO! $ xyz'),
689+ (b'abc $Foo$', b'abc $Foo: FOO! $'),
690+ (b'abc $Foo$ xyz', b'abc $Foo: FOO! $ xyz'),
691+ (b'$Foo$$Bar$', b'$Foo: FOO! $$Bar: bar $'),
692+ (b'abc $Foo$ xyz $Bar$ qwe', b'abc $Foo: FOO! $ xyz $Bar: bar $ qwe'),
693+ (b'$Unknown$$Bar$', b'$Unknown$$Bar: bar $'),
694+ (b'$Unknown: unkn $$Bar$', b'$Unknown: unkn $$Bar: bar $'),
695+ (b'$Foo$$Unknown$', b'$Foo: FOO! $$Unknown$'),
696+ (b'$CallMe$', b'$CallMe: now! $'),
697+ ]
698+
699+
700+class TestKeywordsConversion(tests.TestCase):
701+
702+ def test_compression(self):
703+ # Test keyword expansion
704+ for raw, cooked in _samples:
705+ self.assertEqual(raw, compress_keywords(cooked, [_keywords]))
706+
707+ def test_expansion(self):
708+ # Test keyword expansion
709+ for raw, cooked in _samples:
710+ self.assertEqual(cooked, expand_keywords(raw, [_keywords]))
711+
712+ def test_expansion_across_multiple_dictionaries(self):
713+ # Check all still works when keywords in different dictionaries
714+ for raw, cooked in _samples:
715+ self.assertEqual(cooked, expand_keywords(raw, _keywords_dicts))
716+
717+ def test_expansion_feedback_when_unsafe(self):
718+ kw_dict = {'Xxx': 'y$z'}
719+ self.assertEqual(b'$Xxx: (value unsafe to expand) $',
720+ expand_keywords(b'$Xxx$', [kw_dict]))
721+
722+ def test_expansion_feedback_when_error(self):
723+ kw_dict = {'Xxx': lambda ctx: ctx.unknownMethod}
724+ self.assertEqual(b'$Xxx: (evaluation error) $',
725+ expand_keywords(b'$Xxx$', [kw_dict]))
726+
727+ def test_expansion_replaced_if_already_expanded(self):
728+ s = b'$Xxx: old value $'
729+ kw_dict = {'Xxx': 'new value'}
730+ self.assertEqual(b'$Xxx: new value $', expand_keywords(s, [kw_dict]))
731+
732+ def test_expansion_ignored_if_already_expanded_but_unknown(self):
733+ s = b'$Xxx: old value $'
734+ self.assertEqual(b'$Xxx: old value $', expand_keywords(s, [{}]))
735
736=== added file 'breezy/plugins/keywords/tests/test_keywords_in_trees.py'
737--- breezy/plugins/keywords/tests/test_keywords_in_trees.py 1970-01-01 00:00:00 +0000
738+++ breezy/plugins/keywords/tests/test_keywords_in_trees.py 2018-11-12 21:28:40 +0000
739@@ -0,0 +1,123 @@
740+# Copyright (C) 2009 Canonical Limited.
741+#
742+# This program is free software; you can redistribute it and/or modify
743+# it under the terms of the GNU General Public License as published by
744+# the Free Software Foundation; version 2 of the License.
745+#
746+# This program is distributed in the hope that it will be useful,
747+# but WITHOUT ANY WARRANTY; without even the implied warranty of
748+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
749+# GNU General Public License for more details.
750+#
751+# You should have received a copy of the GNU General Public License
752+# along with this program; if not, write to the Free Software
753+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
754+#
755+
756+from __future__ import absolute_import
757+
758+"""Tests for keyword expansion/contraction in trees."""
759+
760+## TODO: add tests for xml_escaped
761+
762+from .... import rules
763+from ....tests import TestCaseWithTransport
764+from ....workingtree import WorkingTree
765+
766+
767+# Sample files. We exclude keywords that change from one run to another,
768+# TODO: Test Date, Path, Now, User, User-Email
769+_sample_text_raw = b"""
770+Committer: $Committer$
771+Committer-Name: $Committer-Name$
772+Authors: $Authors$
773+Author1-Email: $Author1-Email$
774+Revision-Id: $Revision-Id$
775+Filename: $Filename$
776+Directory: $Directory$
777+File-Id: $File-Id$
778+"""
779+#User: $User$
780+#User-Email: $User-Email$
781+_sample_text_cooked = b"""
782+Committer: $Committer: Jane Smith <jane@example.com> $
783+Committer-Name: $Committer-Name: Jane Smith $
784+Authors: $Authors: Sue Smith <sue@example.com> $
785+Author1-Email: $Author1-Email: sue@example.com $
786+Revision-Id: $Revision-Id: rev1-id $
787+Filename: $Filename: file1 $
788+Directory: $Directory: $
789+File-Id: $File-Id: file1-id $
790+"""
791+#User: $User: Dave Smith <dave@example.com>$
792+#User-Email: $User-Email: dave@example.com $
793+_sample_binary = _sample_text_raw + b"""\x00"""
794+
795+
796+class TestKeywordsInTrees(TestCaseWithTransport):
797+
798+ def patch_rules_searcher(self, keywords):
799+ """Patch in a custom rules searcher with a given keywords setting."""
800+ if keywords is None:
801+ WorkingTree._get_rules_searcher = self.real_rules_searcher
802+ else:
803+ def custom__rules_searcher(tree, default_searcher):
804+ return rules._IniBasedRulesSearcher([
805+ '[name *]\n',
806+ 'keywords=%s\n' % keywords,
807+ ])
808+ WorkingTree._get_rules_searcher = custom__rules_searcher
809+
810+ def prepare_tree(self, content, keywords=None):
811+ """Prepare a working tree and commit some content."""
812+ def restore_real_rules_searcher():
813+ WorkingTree._get_rules_searcher = self.real_rules_searcher
814+ self.real_rules_searcher = WorkingTree._get_rules_searcher
815+ self.addCleanup(restore_real_rules_searcher)
816+ self.patch_rules_searcher(keywords)
817+ t = self.make_branch_and_tree('tree1')
818+ # Patch is a custom username
819+ #def custom_global_config():
820+ # config_file = StringIO(
821+ # "[DEFAULT]\nemail=Dave Smith <dave@example.com>\n")
822+ # my_config = config.GlobalConfig()
823+ # my_config._parser = my_config._get_parser(file=config_file)
824+ # return my_config
825+ #t.branch.get_config()._get_global_config = custom_global_config
826+ self.build_tree_contents([('tree1/file1', content)])
827+ t.add(['file1'], [b'file1-id'])
828+ t.commit("add file1", rev_id=b"rev1-id",
829+ committer="Jane Smith <jane@example.com>",
830+ authors=["Sue Smith <sue@example.com>"])
831+ basis = t.basis_tree()
832+ basis.lock_read()
833+ self.addCleanup(basis.unlock)
834+ return t, basis
835+
836+ def assertNewContentForSetting(self, wt, keywords, expected):
837+ """Clone a working tree and check the convenience content."""
838+ self.patch_rules_searcher(keywords)
839+ wt2 = wt.controldir.sprout('tree-%s' % keywords).open_workingtree()
840+ # To see exactly what got written to disk, we need an unfiltered read
841+ content = wt2.get_file_text('file1', filtered=False)
842+ self.assertEqual(expected, content)
843+
844+ def assertContent(self, wt, basis, expected_raw, expected_cooked):
845+ """Check the committed content and content in cloned trees."""
846+ basis_content = basis.get_file_text('file1')
847+ self.assertEqualDiff(expected_raw, basis_content)
848+ self.assertNewContentForSetting(wt, None, expected_raw)
849+ self.assertNewContentForSetting(wt, 'on', expected_cooked)
850+ self.assertNewContentForSetting(wt, 'off', expected_raw)
851+
852+ def test_keywords_no_rules(self):
853+ wt, basis = self.prepare_tree(_sample_text_raw)
854+ self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)
855+
856+ def test_keywords_on(self):
857+ wt, basis = self.prepare_tree(_sample_text_raw, keywords='on')
858+ self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)
859+
860+ def test_keywords_off(self):
861+ wt, basis = self.prepare_tree(_sample_text_raw, keywords='off')
862+ self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)

Subscribers

People subscribed via source and target branches