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
=== modified file 'breezy/filters/__init__.py'
--- breezy/filters/__init__.py 2018-07-17 22:59:51 +0000
+++ breezy/filters/__init__.py 2018-11-12 21:28:40 +0000
@@ -91,6 +91,9 @@
91 """Relative path of file to tree-root."""91 """Relative path of file to tree-root."""
92 return self._relpath92 return self._relpath
9393
94 def file_id(self):
95 return self.source_tree().path2id(self.relpath())
96
94 def source_tree(self):97 def source_tree(self):
95 """Source Tree object."""98 """Source Tree object."""
96 return self._tree99 return self._tree
97100
=== added directory 'breezy/plugins/keywords'
=== added file 'breezy/plugins/keywords/NEWS'
--- breezy/plugins/keywords/NEWS 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/NEWS 2018-11-12 21:28:40 +0000
@@ -0,0 +1,18 @@
1##########################
2bzr-keywords Release Notes
3##########################
4
5.. contents::
6
7In Development
8##############
9
10This version is suitable for use with Bazaar 1.14 or later using
11trees in the 1.14 format.
12
13
140.1 28-Jul-2008
15###############
16
17This version is suitable for testing with the development branch,
18~ian-clatworthy/bzr/bzr.content-filters.
019
=== added file 'breezy/plugins/keywords/README.txt'
--- breezy/plugins/keywords/README.txt 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/README.txt 2018-11-12 21:28:40 +0000
@@ -0,0 +1,43 @@
1bzr-keywords: RCS-like keyword templates
2========================================
3
4Overview
5--------
6
7This plugin adds keyword filtering to selected files. This allows
8you to do things like include the current user and date in a web page.
9
10
11Installation
12------------
13
14The easiest way to install this plugin is to either copy or symlink the
15directory into your ~/.bazaar/plugins directory. Be sure to rename the
16directory to keywords (instead of bzr-keywords).
17
18See http://bazaar-vcs.org/UsingPlugins for other options such as
19using the BZR_PLUGIN_PATH environment variable.
20
21
22Testing
23-------
24
25To test the plugin after installation:
26
27 bzr selftest keywords.tests
28
29
30Documentation
31-------------
32
33To see the documentation after installation:
34
35 bzr help keywords
36
37
38Licensing
39---------
40
41This plugin is (C) Copyright Canonical Limited 2008 under the
42GPL Version 2 or later. Please see the file COPYING.txt for the licence
43details.
044
=== added file 'breezy/plugins/keywords/TODO'
--- breezy/plugins/keywords/TODO 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/TODO 2018-11-12 21:28:40 +0000
@@ -0,0 +1,13 @@
1Things to consider:
2 * python_escape (maybe called string_escape) ala xml_escape?
3 * some sort of block construct so easier to include in ReST and properties
4 files, e.g.
5 .. $begin-keywords$
6 :name1: value1
7 :name2: value2
8 .. $end-keywords$
9
10* Add tests for:
11 * untested keyword values (including date formatting)
12 * escaping
13 * style formatting
014
=== added file 'breezy/plugins/keywords/__init__.py'
--- breezy/plugins/keywords/__init__.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/__init__.py 2018-11-12 21:28:40 +0000
@@ -0,0 +1,240 @@
1# Copyright (C) 2008 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17r'''Keyword Templating
18==================
19
20Keyword templating is provided as a content filter where Bazaar internally
21stores a canonical format but outputs a convenience format. See
22``bzr help content-filters`` for general information about using these.
23
24Note: Content filtering is only supported in recently added formats,
25e.g. 1.14.
26
27Keyword templates are specified using the following patterns:
28
29 * in canonical/compressed format: $Keyword$
30 * in convenience/expanded format: $Keyword: value $
31
32When expanding, the existing text is retained if an unknown keyword is
33found. If the keyword is already expanded but known, the value is replaced.
34When compressing, the values of known keywords are removed.
35
36Keyword filtering needs to be enabled for selected branches and files via
37rules. See ``bzr help rules`` for general information on defining rules.
38For example, to enable keywords for all ``txt`` files on your system, add
39these lines to your ``BZR_HOME/rules`` file::
40
41 [name *.txt]
42 keywords = on
43
44To disable keywords for ``txt`` files but enable them for ``html`` files::
45
46 [name *.txt]
47 keywords = off
48
49 [name *.html]
50 keywords = xml_escape
51
52``xml_escape`` enables keyword expansion but it escapes special characters
53in keyword values so they can be safely included in HTML or XML files.
54
55The currently supported keywords are given below.
56
57 ============= =========================================================
58 Keyword Description
59 ============= =========================================================
60 Date the date and time the file was last modified
61 Committer the committer (name and email) of the last change
62 Authors the authors (names and emails) of the last change
63 Revision-Id the unique id of the revision that last changed the file
64 Path the relative path of the file in the tree
65 Filename just the name part of the relative path
66 Directory just the directory part of the relative path
67 File-Id the unique id assigned to this file
68 Now the current date and time
69 User the current user (name and email)
70 ============= =========================================================
71
72If you want finer control over the formatting of names and email
73addresses, you can use the following keywords.
74
75 ============= =======================================================
76 Keyword Description
77 ============= =======================================================
78 Committer-Name just the name of the current committer
79 Committer-Email just the email address of the current committer
80 Author1-Name just the name of the first author
81 Author1-Email just the email address of the first author
82 Author2-Name just the name of the second author
83 Author2-Email just the email address of the second author
84 Author3-Name just the name of the third author
85 Author3-Email just the email address of the third author
86 User-Name just the name of the current user
87 User-Email just the email address of the current user
88 ============= =======================================================
89
90Note: If you have more than 3 authors for a given revision, please
91ask on the Bazaar mailing list for an enhancement to support the
92number you need.
93
94By default, dates/times are output using this format::
95
96 YYYY-MM-DD HH:MM:SS+HH:MM
97
98To specify a custom format, add a configuration setting to
99``BZR_HOME/bazaar.conf`` like this::
100
101 keywords.format.Now = %A, %B %d, %Y
102
103The last part of the key needs to match the keyword name. The value must be
104a legal strftime (http://docs.python.org/lib/module-time.html) format.
105'''
106
107from __future__ import absolute_import
108
109
110from ... import (
111 builtins,
112 commands,
113 filters,
114 option,
115 )
116
117
118def test_suite():
119 """Called by breezy to fetch tests for this plugin"""
120 from unittest import TestSuite, TestLoader
121 from .tests import (
122 test_conversion,
123 test_keywords_in_trees,
124 )
125 loader = TestLoader()
126 suite = TestSuite()
127 for module in [
128 test_conversion,
129 test_keywords_in_trees,
130 ]:
131 suite.addTests(loader.loadTestsFromModule(module))
132 return suite
133
134
135# Define and register the filter stack map
136def _keywords_filter_stack_lookup(k):
137 from .keywords import (
138 _kw_compressor,
139 _normal_kw_expander,
140 _xml_escape_kw_expander,
141 )
142 filter_stack_map = {
143 'off': [],
144 'on':
145 [filters.ContentFilter(_kw_compressor, _normal_kw_expander)],
146 'xml_escape':
147 [filters.ContentFilter(_kw_compressor, _xml_escape_kw_expander)],
148 }
149 return filter_stack_map.get(k)
150
151try:
152 register_filter = filters.filter_stacks_registry.register
153except AttributeError:
154 register_filter = filters.register_filter_stack_map
155
156register_filter('keywords', _keywords_filter_stack_lookup)
157
158
159class cmd_cat(builtins.cmd_cat):
160 """
161 The ``--keywords`` option specifies the keywords expansion
162 style. By default (``raw`` style), no expansion is done.
163 Other styles enable expansion in a ``cooked`` mode where both
164 the keyword and its value are displayed inside $ markers, or in
165 numerous publishing styles - ``publish``, ``publish-values`` and
166 ``publish-names`` - where the $ markers are completely removed.
167 The publishing styles do not support round-tripping back to the
168 raw content but are useful for improving the readability of
169 published web pages for example.
170
171 Note: Files must have the ``keywords`` preference defined for them
172 in order for the ``--keywords`` option to take effect. In particular,
173 the preference specifies how keyword values are encoded for different
174 filename patterns. See ``bzr help keywords`` for more information on
175 how to specify the required preference using rules.
176 """
177
178 # Add a new option to the builtin command and
179 # override the inherited run() and help() methods
180
181 takes_options = builtins.cmd_cat.takes_options + [
182 option.RegistryOption('keywords',
183 lazy_registry=(__name__ + ".keywords",
184 "_keyword_style_registry"),
185 converter=lambda s: s,
186 help='Keyword expansion style.')]
187
188 def run(self, *args, **kwargs):
189 """Process special options and delegate to superclass."""
190 if 'keywords' in kwargs:
191 from .keywords import (
192 _keyword_style_registry,
193 )
194 # Implicitly set the filters option
195 kwargs['filters'] = True
196 style = kwargs['keywords']
197 _keyword_style_registry.default_key = style
198 del kwargs['keywords']
199 return super(cmd_cat, self).run(*args, **kwargs)
200
201 def help(self):
202 """Return help message including text from superclass."""
203 from inspect import getdoc
204 return getdoc(super(cmd_cat, self)) + '\n\n' + getdoc(self)
205
206
207class cmd_export(builtins.cmd_export):
208 # Add a new option to the builtin command and
209 # override the inherited run() and help() methods
210
211 takes_options = builtins.cmd_export.takes_options + [
212 option.RegistryOption('keywords',
213 lazy_registry=(__name__ + ".keywords",
214 "_keyword_style_registry"),
215 converter=lambda s: s,
216 help='Keyword expansion style.')]
217
218 def run(self, *args, **kwargs):
219 """Process special options and delegate to superclass."""
220 if 'keywords' in kwargs:
221 from .keywords import (
222 _keyword_style_registry,
223 )
224 # Implicitly set the filters option
225 kwargs['filters'] = True
226 style = kwargs['keywords']
227 _keyword_style_registry.default_key = style
228 del kwargs['keywords']
229 return super(cmd_export, self).run(*args, **kwargs)
230
231 def help(self):
232 """Return help message including text from superclass."""
233 from inspect import getdoc
234 # NOTE: Reuse of cmd_cat help below is deliberate, not a bug
235 return getdoc(super(cmd_export, self)) + '\n\n' + getdoc(cmd_cat)
236
237
238# Register the command wrappers
239commands.register_command(cmd_cat, decorate=False)
240commands.register_command(cmd_export, decorate=False)
0241
=== added file 'breezy/plugins/keywords/keywords.py'
--- breezy/plugins/keywords/keywords.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/keywords.py 2018-11-12 21:28:40 +0000
@@ -0,0 +1,269 @@
1# Copyright (C) 2008 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17from __future__ import absolute_import
18
19import re, time
20from ... import (
21 debug,
22 osutils,
23 registry,
24 trace,
25 )
26from ...sixish import text_type
27
28# Expansion styles
29# Note: Round-tripping is only required between the raw and cooked styles
30_keyword_style_registry = registry.Registry()
31_keyword_style_registry.register('raw', b'$%(name)s$')
32_keyword_style_registry.register('cooked', b'$%(name)s: %(value)s $')
33_keyword_style_registry.register('publish', b'%(name)s: %(value)s')
34_keyword_style_registry.register('publish-values', b'%(value)s')
35_keyword_style_registry.register('publish-names', b'%(name)s')
36_keyword_style_registry.default_key = 'cooked'
37
38
39# Regular expressions for matching the raw and cooked patterns
40_KW_RAW_RE = re.compile(b'\\$([\\w\\-]+)(:[^$]*)?\\$')
41_KW_COOKED_RE = re.compile(b'\\$([\\w\\-]+):([^$]+)\\$')
42
43
44# The registry of keywords. Other plugins may wish to add entries to this.
45keyword_registry = registry.Registry()
46
47# Revision-related keywords
48keyword_registry.register('Date',
49 lambda c: format_date(c.revision().timestamp, c.revision().timezone,
50 c.config(), 'Date'))
51keyword_registry.register('Committer',
52 lambda c: c.revision().committer)
53keyword_registry.register('Authors',
54 lambda c: ", ".join(c.revision().get_apparent_authors()))
55keyword_registry.register('Revision-Id',
56 lambda c: c.revision_id())
57keyword_registry.register('Path',
58 lambda c: c.relpath())
59keyword_registry.register('Directory',
60 lambda c: osutils.split(c.relpath())[0])
61keyword_registry.register('Filename',
62 lambda c: osutils.split(c.relpath())[1])
63keyword_registry.register('File-Id',
64 lambda c: c.file_id())
65
66# Environment-related keywords
67keyword_registry.register('Now',
68 lambda c: format_date(time.time(), time.timezone, c.config(), 'Now'))
69keyword_registry.register('User',
70 lambda c: c.config().username())
71
72# Keywords for finer control over name & address formatting
73keyword_registry.register('Committer-Name',
74 lambda c: extract_name(c.revision().committer))
75keyword_registry.register('Committer-Email',
76 lambda c: extract_email(c.revision().committer))
77keyword_registry.register('Author1-Name',
78 lambda c: extract_name_item(c.revision().get_apparent_authors(), 0))
79keyword_registry.register('Author1-Email',
80 lambda c: extract_email_item(c.revision().get_apparent_authors(), 0))
81keyword_registry.register('Author2-Name',
82 lambda c: extract_name_item(c.revision().get_apparent_authors(), 1))
83keyword_registry.register('Author2-Email',
84 lambda c: extract_email_item(c.revision().get_apparent_authors(), 1))
85keyword_registry.register('Author3-Name',
86 lambda c: extract_name_item(c.revision().get_apparent_authors(), 2))
87keyword_registry.register('Author3-Email',
88 lambda c: extract_email_item(c.revision().get_apparent_authors(), 2))
89keyword_registry.register('User-Name',
90 lambda c: extract_name(c.config().username()))
91keyword_registry.register('User-Email',
92 lambda c: extract_email(c.config().username()))
93
94
95def format_date(timestamp, offset=0, cfg=None, name=None):
96 """Return a formatted date string.
97
98 :param timestamp: Seconds since the epoch.
99 :param offset: Timezone offset in seconds east of utc.
100 """
101 if cfg is not None and name is not None:
102 cfg_key = 'keywords.format.%s' % (name,)
103 format = cfg.get_user_option(cfg_key)
104 else:
105 format = None
106 return osutils.format_date(timestamp, offset, date_fmt=format)
107
108
109def extract_name(userid):
110 """Extract the name out of a user-id string.
111
112 user-id strings have the format 'name <email>'.
113 """
114 if userid and userid[-1] == '>':
115 return userid[:-1].rsplit('<', 1)[0].rstrip()
116 else:
117 return userid
118
119
120def extract_email(userid):
121 """Extract the email address out of a user-id string.
122
123 user-id strings have the format 'name <email>'.
124 """
125 if userid and userid[-1] == '>':
126 return userid[:-1].rsplit('<', 1)[1]
127 else:
128 return userid
129
130def extract_name_item(seq, n):
131 """Extract the name out of the nth item in a sequence of user-ids.
132
133 :return: the user-name or an empty string
134 """
135 try:
136 return extract_name(seq[n])
137 except IndexError:
138 return ""
139
140
141def extract_email_item(seq, n):
142 """Extract the email out of the nth item in a sequence of user-ids.
143
144 :return: the email address or an empty string
145 """
146 try:
147 return extract_email(seq[n])
148 except IndexError:
149 return ""
150
151
152def compress_keywords(s, keyword_dicts):
153 """Replace cooked style keywords with raw style in a string.
154
155 Note: If the keyword is not known, the text is not modified.
156
157 :param s: the string
158 :param keyword_dicts: an iterable of keyword dictionaries.
159 :return: the string with keywords compressed
160 """
161 _raw_style = _keyword_style_registry.get('raw')
162 result = b''
163 rest = s
164 while True:
165 match = _KW_COOKED_RE.search(rest)
166 if not match:
167 break
168 result += rest[:match.start()]
169 keyword = match.group(1)
170 expansion = _get_from_dicts(keyword_dicts, keyword.decode('ascii'))
171 if expansion is None:
172 # Unknown expansion - leave as is
173 result += match.group(0)
174 else:
175 result += _raw_style % {b'name': keyword}
176 rest = rest[match.end():]
177 return result + rest
178
179
180def expand_keywords(s, keyword_dicts, context=None, encoder=None, style=None):
181 """Replace raw style keywords with another style in a string.
182
183 Note: If the keyword is already in the expanded style, the value is
184 not replaced.
185
186 :param s: the string
187 :param keyword_dicts: an iterable of keyword dictionaries. If values
188 are callables, they are executed to find the real value.
189 :param context: the parameter to pass to callable values
190 :param style: the style of expansion to use of None for the default
191 :return: the string with keywords expanded
192 """
193 _expanded_style = _keyword_style_registry.get(style)
194 result = b''
195 rest = s
196 while True:
197 match = _KW_RAW_RE.search(rest)
198 if not match:
199 break
200 result += rest[:match.start()]
201 keyword = match.group(1)
202 expansion = _get_from_dicts(keyword_dicts, keyword.decode('ascii'))
203 if callable(expansion):
204 try:
205 expansion = expansion(context)
206 except AttributeError as err:
207 if 'error' in debug.debug_flags:
208 trace.note("error evaluating %s for keyword %s: %s",
209 expansion, keyword, err)
210 expansion = b"(evaluation error)"
211 if isinstance(expansion, text_type):
212 expansion = expansion.encode('utf-8')
213 if expansion is None:
214 # Unknown expansion - leave as is
215 result += match.group(0)
216 rest = rest[match.end():]
217 continue
218 if b'$' in expansion:
219 # Expansion is not safe to be collapsed later
220 expansion = b"(value unsafe to expand)"
221 if encoder is not None:
222 expansion = encoder(expansion)
223 params = {b'name': keyword, b'value': expansion}
224 result += _expanded_style % params
225 rest = rest[match.end():]
226 return result + rest
227
228
229def _get_from_dicts(dicts, key, default=None):
230 """Search a sequence of dictionaries or registries for a key.
231
232 :return: the value, or default if not found
233 """
234 for dict in dicts:
235 if key in dict:
236 return dict.get(key)
237 return default
238
239
240def _xml_escape(s):
241 """Escape a string so it can be included safely in XML/HTML."""
242 # Compile the regular expressions if not already done
243 from ... import xml8
244 xml8._ensure_utf8_re()
245 # Convert and strip the trailing quote
246 return xml8._encode_and_escape(s)[:-1]
247
248
249def _kw_compressor(chunks, context=None):
250 """Filter that replaces keywords with their compressed form."""
251 text = b''.join(chunks)
252 return [compress_keywords(text, [keyword_registry])]
253
254
255def _kw_expander(chunks, context, encoder=None):
256 """Keyword expander."""
257 text = b''.join(chunks)
258 return [expand_keywords(text, [keyword_registry], context=context,
259 encoder=encoder)]
260
261
262def _normal_kw_expander(chunks, context=None):
263 """Filter that replaces keywords with their expanded form."""
264 return _kw_expander(chunks, context)
265
266
267def _xml_escape_kw_expander(chunks, context=None):
268 """Filter that replaces keywords with a form suitable for use in XML."""
269 return _kw_expander(chunks, context, encoder=_xml_escape)
0270
=== added directory 'breezy/plugins/keywords/tests'
=== added file 'breezy/plugins/keywords/tests/__init__.py'
--- breezy/plugins/keywords/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/tests/__init__.py 2018-11-12 21:28:40 +0000
@@ -0,0 +1,19 @@
1# Copyright (C) 2008 Canonical Limited.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; version 2 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15#
16
17"""Tests for bzr-keywords."""
18
19from __future__ import absolute_import
020
=== added file 'breezy/plugins/keywords/tests/test_conversion.py'
--- breezy/plugins/keywords/tests/test_conversion.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/tests/test_conversion.py 2018-11-12 21:28:40 +0000
@@ -0,0 +1,82 @@
1# Copyright (C) 2008 Canonical Limited.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; version 2 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15#
16
17from __future__ import absolute_import
18
19"""Tests for keyword expansion/contraction."""
20
21
22from .... import tests
23from ..keywords import (
24 compress_keywords,
25 expand_keywords,
26 )
27
28
29# Sample unexpanded and expanded pairs for a keyword dictionary
30_keywords = {'Foo': 'FOO!', 'Bar': 'bar', 'CallMe': lambda c: "now!"}
31_keywords_dicts = [{'Foo': 'FOO!'}, {'Bar': 'bar', 'CallMe': lambda c: "now!"}]
32_samples = [
33 (b'$Foo$', b'$Foo: FOO! $'),
34 (b'$Foo', b'$Foo'),
35 (b'Foo$', b'Foo$'),
36 (b'$Foo$ xyz', b'$Foo: FOO! $ xyz'),
37 (b'abc $Foo$', b'abc $Foo: FOO! $'),
38 (b'abc $Foo$ xyz', b'abc $Foo: FOO! $ xyz'),
39 (b'$Foo$$Bar$', b'$Foo: FOO! $$Bar: bar $'),
40 (b'abc $Foo$ xyz $Bar$ qwe', b'abc $Foo: FOO! $ xyz $Bar: bar $ qwe'),
41 (b'$Unknown$$Bar$', b'$Unknown$$Bar: bar $'),
42 (b'$Unknown: unkn $$Bar$', b'$Unknown: unkn $$Bar: bar $'),
43 (b'$Foo$$Unknown$', b'$Foo: FOO! $$Unknown$'),
44 (b'$CallMe$', b'$CallMe: now! $'),
45 ]
46
47
48class TestKeywordsConversion(tests.TestCase):
49
50 def test_compression(self):
51 # Test keyword expansion
52 for raw, cooked in _samples:
53 self.assertEqual(raw, compress_keywords(cooked, [_keywords]))
54
55 def test_expansion(self):
56 # Test keyword expansion
57 for raw, cooked in _samples:
58 self.assertEqual(cooked, expand_keywords(raw, [_keywords]))
59
60 def test_expansion_across_multiple_dictionaries(self):
61 # Check all still works when keywords in different dictionaries
62 for raw, cooked in _samples:
63 self.assertEqual(cooked, expand_keywords(raw, _keywords_dicts))
64
65 def test_expansion_feedback_when_unsafe(self):
66 kw_dict = {'Xxx': 'y$z'}
67 self.assertEqual(b'$Xxx: (value unsafe to expand) $',
68 expand_keywords(b'$Xxx$', [kw_dict]))
69
70 def test_expansion_feedback_when_error(self):
71 kw_dict = {'Xxx': lambda ctx: ctx.unknownMethod}
72 self.assertEqual(b'$Xxx: (evaluation error) $',
73 expand_keywords(b'$Xxx$', [kw_dict]))
74
75 def test_expansion_replaced_if_already_expanded(self):
76 s = b'$Xxx: old value $'
77 kw_dict = {'Xxx': 'new value'}
78 self.assertEqual(b'$Xxx: new value $', expand_keywords(s, [kw_dict]))
79
80 def test_expansion_ignored_if_already_expanded_but_unknown(self):
81 s = b'$Xxx: old value $'
82 self.assertEqual(b'$Xxx: old value $', expand_keywords(s, [{}]))
083
=== added file 'breezy/plugins/keywords/tests/test_keywords_in_trees.py'
--- breezy/plugins/keywords/tests/test_keywords_in_trees.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/keywords/tests/test_keywords_in_trees.py 2018-11-12 21:28:40 +0000
@@ -0,0 +1,123 @@
1# Copyright (C) 2009 Canonical Limited.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; version 2 of the License.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with this program; if not, write to the Free Software
14# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
15#
16
17from __future__ import absolute_import
18
19"""Tests for keyword expansion/contraction in trees."""
20
21## TODO: add tests for xml_escaped
22
23from .... import rules
24from ....tests import TestCaseWithTransport
25from ....workingtree import WorkingTree
26
27
28# Sample files. We exclude keywords that change from one run to another,
29# TODO: Test Date, Path, Now, User, User-Email
30_sample_text_raw = b"""
31Committer: $Committer$
32Committer-Name: $Committer-Name$
33Authors: $Authors$
34Author1-Email: $Author1-Email$
35Revision-Id: $Revision-Id$
36Filename: $Filename$
37Directory: $Directory$
38File-Id: $File-Id$
39"""
40#User: $User$
41#User-Email: $User-Email$
42_sample_text_cooked = b"""
43Committer: $Committer: Jane Smith <jane@example.com> $
44Committer-Name: $Committer-Name: Jane Smith $
45Authors: $Authors: Sue Smith <sue@example.com> $
46Author1-Email: $Author1-Email: sue@example.com $
47Revision-Id: $Revision-Id: rev1-id $
48Filename: $Filename: file1 $
49Directory: $Directory: $
50File-Id: $File-Id: file1-id $
51"""
52#User: $User: Dave Smith <dave@example.com>$
53#User-Email: $User-Email: dave@example.com $
54_sample_binary = _sample_text_raw + b"""\x00"""
55
56
57class TestKeywordsInTrees(TestCaseWithTransport):
58
59 def patch_rules_searcher(self, keywords):
60 """Patch in a custom rules searcher with a given keywords setting."""
61 if keywords is None:
62 WorkingTree._get_rules_searcher = self.real_rules_searcher
63 else:
64 def custom__rules_searcher(tree, default_searcher):
65 return rules._IniBasedRulesSearcher([
66 '[name *]\n',
67 'keywords=%s\n' % keywords,
68 ])
69 WorkingTree._get_rules_searcher = custom__rules_searcher
70
71 def prepare_tree(self, content, keywords=None):
72 """Prepare a working tree and commit some content."""
73 def restore_real_rules_searcher():
74 WorkingTree._get_rules_searcher = self.real_rules_searcher
75 self.real_rules_searcher = WorkingTree._get_rules_searcher
76 self.addCleanup(restore_real_rules_searcher)
77 self.patch_rules_searcher(keywords)
78 t = self.make_branch_and_tree('tree1')
79 # Patch is a custom username
80 #def custom_global_config():
81 # config_file = StringIO(
82 # "[DEFAULT]\nemail=Dave Smith <dave@example.com>\n")
83 # my_config = config.GlobalConfig()
84 # my_config._parser = my_config._get_parser(file=config_file)
85 # return my_config
86 #t.branch.get_config()._get_global_config = custom_global_config
87 self.build_tree_contents([('tree1/file1', content)])
88 t.add(['file1'], [b'file1-id'])
89 t.commit("add file1", rev_id=b"rev1-id",
90 committer="Jane Smith <jane@example.com>",
91 authors=["Sue Smith <sue@example.com>"])
92 basis = t.basis_tree()
93 basis.lock_read()
94 self.addCleanup(basis.unlock)
95 return t, basis
96
97 def assertNewContentForSetting(self, wt, keywords, expected):
98 """Clone a working tree and check the convenience content."""
99 self.patch_rules_searcher(keywords)
100 wt2 = wt.controldir.sprout('tree-%s' % keywords).open_workingtree()
101 # To see exactly what got written to disk, we need an unfiltered read
102 content = wt2.get_file_text('file1', filtered=False)
103 self.assertEqual(expected, content)
104
105 def assertContent(self, wt, basis, expected_raw, expected_cooked):
106 """Check the committed content and content in cloned trees."""
107 basis_content = basis.get_file_text('file1')
108 self.assertEqualDiff(expected_raw, basis_content)
109 self.assertNewContentForSetting(wt, None, expected_raw)
110 self.assertNewContentForSetting(wt, 'on', expected_cooked)
111 self.assertNewContentForSetting(wt, 'off', expected_raw)
112
113 def test_keywords_no_rules(self):
114 wt, basis = self.prepare_tree(_sample_text_raw)
115 self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)
116
117 def test_keywords_on(self):
118 wt, basis = self.prepare_tree(_sample_text_raw, keywords='on')
119 self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)
120
121 def test_keywords_off(self):
122 wt, basis = self.prepare_tree(_sample_text_raw, keywords='off')
123 self.assertContent(wt, basis, _sample_text_raw, _sample_text_cooked)

Subscribers

People subscribed via source and target branches