Merge lp:~henninge/launchpad/bug-568355-package-name into lp:launchpad

Proposed by Henning Eggers on 2010-05-06
Status: Merged
Approved by: Henning Eggers on 2010-05-07
Approved revision: no longer in the source branch.
Merged at revision: 10832
Proposed branch: lp:~henninge/launchpad/bug-568355-package-name
Merge into: lp:launchpad
Diff against target: 402 lines (+224/-42)
2 files modified
lib/canonical/buildd/pottery/intltool.py (+72/-38)
lib/lp/translations/tests/test_pottery_detect_intltool.py (+152/-4)
To merge this branch: bzr merge lp:~henninge/launchpad/bug-568355-package-name
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2010-05-06 Approve on 2010-05-07
Review via email: mp+24844@code.launchpad.net

Commit Message

Made pottery translation domain detection smarter.

Description of the Change

= Bug 568355 =

The function "get_translation_domain" scans a package directory to determine the translation domain in an intltool setup. It turned out that these setups can have more variations and complications than orginially expected. This fix addresses the now known cases.

== Proposed fix ==

The files can be ordered into a substitution hierarchy where the bottom file may include definitions from all files above it. The old implemntation kind of did this the wrong way round. The new order of processing is:

- go through the list of config files top to bottom
- try to read the variable from that config file, store its value
- if the value contains a substitution, try to fulfill that from this file or all previous files.
  - if the substitution cannot be satisfied, fail
- if the variable may also be found in the next file, keep searching
  - otherwise, the search is completed.

A new feature is to search AC_INIT parameter list for the domain.

== Tests ==

The test was greatly improved to remove the necessity to create an extra tarball for each test. Also, all tarballs were moved to their own directories.

bin/text -vvt intltool

== QA ==

QA would have to done on dogfood. The package "eog" should now be processed correctly.

To post a comment you must log in.
Abel Deuring (adeuring) wrote :

(11:11:38) adeuring: henninge: lines 51-58 of the diff change the order of locations. The doc string of the method says "This function goes through the ordered list of these possible locations, the order having been copied from intltool-update". My question: Was the old order plain wrong or did the intltools chnage. IOW: Is the new ordering always correct?
(11:12:01) adeuring: or can it be wrong for some versions of intltools?
(11:12:17) henninge: adeuring: the ordering is correct, the comment is not valid any more.
(11:12:56) adeuring: henninge: so, the old ordering was simply based on a misinterpretation of what intltools are doing?
(11:13:19) henninge: adeuring: no, the original order was taken from intltool-update
(11:14:06) henninge: adeuring: basically, what is says "use GETTEXT_PACKGE from Makefile.in.in or else from configure.ac or else from configure.in"
(11:14:15) adeuring: henninge: ok... but the substitution order changed, right?
(11:14:32) henninge: adeuring: this is now reflected in the "keep_trying" value.
(11:14:58) adeuring: henninge: ah, I see. So, can you update the comment? otherwise, r=me
(11:15:14) henninge: adeuring: yes, thanks

review: Approve (code)
Abel Deuring (adeuring) wrote :

forgot a nitpick:

+ return [
+ param.strip()
+ for param in result.group(1).strip().split(',')]

I think it is sufficient to use result.group(1).split(','), since you call strip() for the result of split()

Henning Eggers (henninge) wrote :

Thank you for the review. Here is the incremental diff.

1=== modified file 'lib/canonical/buildd/pottery/intltool.py'
2--- lib/canonical/buildd/pottery/intltool.py 2010-05-06 15:49:39 +0000
3+++ lib/canonical/buildd/pottery/intltool.py 2010-05-07 09:35:39 +0000
4@@ -119,12 +119,15 @@
5 translation domain the build environment provides. The domain is usually
6 defined in the GETTEXT_PACKAGE variable in one of the build files. Another
7 variant is DOMAIN in the Makevars file. This function goes through the
8- ordered list of these possible locations, the order having been copied
9- from intltool-update, and tries to find a valid value.
10+ ordered list of these possible locations, top to bottom, and tries to
11+ find a valid value. Since the same variable name may be defined in
12+ multiple files (usually configure.ac and Makefile.in.in), it needs to
13+ keep trying with the next file, until it finds the most specific
14+ definition.
15
16 If the found value contains a substitution, either autoconf style (@...@)
17- or make style ($(...)), the search is continued in the same file and down
18- the list of files, now searching for the substitution. Multiple
19+ or make style ($(...)), the search is continued in the same file and back
20+ up the list of files, now searching for the substitution. Multiple
21 substitutions or multi-level substitutions are not supported.
22 """
23 locations = [
24@@ -226,9 +229,7 @@
25 result = pattern.search(self.content)
26 if result is None:
27 return None
28- return [
29- param.strip()
30- for param in result.group(1).strip().split(',')]
31+ return [param.strip() for param in result.group(1).split(',')]
32
33
34 class Substitution(object):

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/buildd/pottery/intltool.py'
2--- lib/canonical/buildd/pottery/intltool.py 2010-03-10 20:19:39 +0000
3+++ lib/canonical/buildd/pottery/intltool.py 2010-05-07 09:57:31 +0000
4@@ -75,13 +75,38 @@
5 return sorted(filter(check_potfiles_in, find_potfiles_in()))
6
7
8-def _try_substitution(path, substitution):
9- """Try to find a substitution in the given config file.
10+def _get_AC_PACKAGE_NAME(config_file):
11+ """Get the value of AC_PACKAGE_NAME from function parameters.
12+
13+ The value of AC_PACKAGE_NAME is either the first or the fourth
14+ parameter of the AC_INIT call if it is called with at least two
15+ parameters. They may be enclosed in [].
16+ """
17+ params = config_file.getFunctionParams("AC_INIT")
18+ if params is None or len(params) < 2:
19+ return None
20+ if len(params) < 4:
21+ value = params[0]
22+ else:
23+ value = params[3]
24+ return value.strip("[]")
25+
26+
27+def _try_substitution(config_files, varname, substitution):
28+ """Try to find a substitution in the config files.
29
30 :returns: The completed substitution or None if none was found.
31 """
32- subst_value = ConfigFile(path).getVariable(substitution.name)
33- if subst_value is None:
34+ subst_value = None
35+ if varname == substitution.name:
36+ # Do not look for the same name in the current file.
37+ config_files = config_files[:-1]
38+ for config_file in reversed(config_files):
39+ subst_value = config_file.getVariable(substitution.name)
40+ if subst_value is not None:
41+ # Substitution found.
42+ break
43+ else:
44 # No substitution found.
45 return None
46 return substitution.replace(subst_value)
47@@ -94,49 +119,50 @@
48 translation domain the build environment provides. The domain is usually
49 defined in the GETTEXT_PACKAGE variable in one of the build files. Another
50 variant is DOMAIN in the Makevars file. This function goes through the
51- ordered list of these possible locations, the order having been copied
52- from intltool-update, and tries to find a valid value.
53+ ordered list of these possible locations, top to bottom, and tries to
54+ find a valid value. Since the same variable name may be defined in
55+ multiple files (usually configure.ac and Makefile.in.in), it needs to
56+ keep trying with the next file, until it finds the most specific
57+ definition.
58
59 If the found value contains a substitution, either autoconf style (@...@)
60- or make style ($(...)), the search is continued in the same file and down
61- the list of files, now searching for the substitution. Multiple
62+ or make style ($(...)), the search is continued in the same file and back
63+ up the list of files, now searching for the substitution. Multiple
64 substitutions or multi-level substitutions are not supported.
65 """
66 locations = [
67- ('Makefile.in.in', 'GETTEXT_PACKAGE'),
68- ('../configure.ac', 'GETTEXT_PACKAGE'),
69- ('../configure.in', 'GETTEXT_PACKAGE'),
70- ('Makevars', 'DOMAIN'),
71+ ('../configure.ac', 'GETTEXT_PACKAGE', True),
72+ ('../configure.in', 'GETTEXT_PACKAGE', True),
73+ ('Makefile.in.in', 'GETTEXT_PACKAGE', False),
74+ ('Makevars', 'DOMAIN', False),
75 ]
76 value = None
77 substitution = None
78- for filename, varname in locations:
79+ config_files = []
80+ for filename, varname, keep_trying in locations:
81 path = os.path.join(dirname, filename)
82 if not os.access(path, os.R_OK):
83 # Skip non-existent files.
84 continue
85- if substitution is None:
86- value = ConfigFile(path).getVariable(varname)
87- if value is not None:
88+ config_files.append(ConfigFile(path))
89+ new_value = config_files[-1].getVariable(varname)
90+ if new_value is not None:
91+ value = new_value
92+ if value == "AC_PACKAGE_NAME":
93+ value = _get_AC_PACKAGE_NAME(config_files[-1])
94+ else:
95 # Check if the value need a substitution.
96 substitution = Substitution.get(value)
97 if substitution is not None:
98- # Try to substitute with value from current file but
99- # avoid recursion.
100- if substitution.name != varname:
101- value = _try_substitution(path, substitution)
102- else:
103- # The value has not been found yet but is now stored
104- # in the Substitution instance.
105- value = None
106- else:
107- value = _try_substitution(path, substitution)
108- if value is not None:
109+ # Try to substitute with value.
110+ value = _try_substitution(
111+ config_files, varname, substitution)
112+ if value is None:
113+ # No substitution found, the setup is broken.
114+ break
115+ if value is not None and not keep_trying:
116 # A value has been found.
117 break
118- if substitution is not None and not substitution.replaced:
119- # Substitution failed.
120- return None
121 return value
122
123
124@@ -185,17 +211,25 @@
125 conf_file = file(file_or_name)
126 else:
127 conf_file = file_or_name
128- self.content_lines = conf_file.readlines()
129+ self.content = conf_file.read()
130
131 def getVariable(self, name):
132 """Search the file for a variable definition with this name."""
133- pattern = re.compile("^%s[ \t]*=[ \t]*([^\s]*)" % re.escape(name))
134- variable = None
135- for line in self.content_lines:
136- result = pattern.match(line)
137- if result is not None:
138- variable = result.group(1)
139- return variable
140+ pattern = re.compile(
141+ "^%s[ \t]*=[ \t]*([^\s]*)" % re.escape(name), re.M)
142+ result = pattern.search(self.content)
143+ if result is None:
144+ return None
145+ return result.group(1)
146+
147+ def getFunctionParams(self, name):
148+ """Search file for a function call with this name, return parameters.
149+ """
150+ pattern = re.compile("^%s\(([^)]*)\)" % re.escape(name), re.M)
151+ result = pattern.search(self.content)
152+ if result is None:
153+ return None
154+ return [param.strip() for param in result.group(1).split(',')]
155
156
157 class Substitution(object):
158
159=== added directory 'lib/lp/translations/tests/pottery_test_data'
160=== renamed file 'lib/lp/translations/tests/intltool_POTFILES_in_1.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_POTFILES_in_1.tar.bz2'
161=== renamed file 'lib/lp/translations/tests/intltool_POTFILES_in_2.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_POTFILES_in_2.tar.bz2'
162=== added file 'lib/lp/translations/tests/pottery_test_data/intltool_domain_base.tar.bz2'
163Binary files lib/lp/translations/tests/pottery_test_data/intltool_domain_base.tar.bz2 1970-01-01 00:00:00 +0000 and lib/lp/translations/tests/pottery_test_data/intltool_domain_base.tar.bz2 2010-05-07 09:57:31 +0000 differ
164=== renamed file 'lib/lp/translations/tests/intltool_domain_configure_ac.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_configure_ac.tar.bz2'
165=== renamed file 'lib/lp/translations/tests/intltool_domain_configure_in.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_configure_in.tar.bz2'
166=== renamed file 'lib/lp/translations/tests/intltool_domain_configure_in_substitute_version.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_configure_in_substitute_version.tar.bz2'
167=== renamed file 'lib/lp/translations/tests/intltool_domain_makefile_in_in.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makefile_in_in.tar.bz2'
168=== renamed file 'lib/lp/translations/tests/intltool_domain_makefile_in_in_substitute.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makefile_in_in_substitute.tar.bz2'
169=== renamed file 'lib/lp/translations/tests/intltool_domain_makefile_in_in_substitute_broken.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makefile_in_in_substitute_broken.tar.bz2'
170=== renamed file 'lib/lp/translations/tests/intltool_domain_makefile_in_in_substitute_same_file.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makefile_in_in_substitute_same_file.tar.bz2'
171=== renamed file 'lib/lp/translations/tests/intltool_domain_makefile_in_in_substitute_same_name.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makefile_in_in_substitute_same_name.tar.bz2'
172=== renamed file 'lib/lp/translations/tests/intltool_domain_makevars.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_domain_makevars.tar.bz2'
173=== renamed file 'lib/lp/translations/tests/intltool_full_ok.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_full_ok.tar.bz2'
174=== renamed file 'lib/lp/translations/tests/intltool_single_ok.tar.bz2' => 'lib/lp/translations/tests/pottery_test_data/intltool_single_ok.tar.bz2'
175=== modified file 'lib/lp/translations/tests/test_pottery_detect_intltool.py'
176--- lib/lp/translations/tests/test_pottery_detect_intltool.py 2010-03-10 20:19:39 +0000
177+++ lib/lp/translations/tests/test_pottery_detect_intltool.py 2010-05-07 09:57:31 +0000
178@@ -1,6 +1,8 @@
179 # Copyright 2009 Canonical Ltd. This software is licensed under the
180 # GNU Affero General Public License version 3 (see the file LICENSE).
181
182+from __future__ import with_statement
183+
184 import os
185 import tarfile
186 import unittest
187@@ -19,21 +21,44 @@
188
189 class SetupTestPackageMixin(object):
190
191- def prepare_package(self, packagename):
192+ test_data_dir = "pottery_test_data"
193+
194+ def prepare_package(self, packagename, buildfiles=None):
195 """Unpack the specified package in a temporary directory.
196
197 Change into the package's directory.
198+
199+ :param packagename: The name of the package to prepare.
200+ :param buildfiles: A dictionary of path:content describing files to
201+ add to the package.
202 """
203 # First build the path for the package.
204 packagepath = os.path.join(
205- os.getcwd(), os.path.dirname(__file__), packagename + ".tar.bz2")
206+ os.getcwd(), os.path.dirname(__file__),
207+ self.test_data_dir, packagename + ".tar.bz2")
208 # Then change into the temporary directory and unpack it.
209 self.useTempDir()
210- tar = tarfile.open(packagepath, "r:bz2")
211+ tar = tarfile.open(packagepath, "r|bz2")
212 tar.extractall()
213 tar.close()
214 os.chdir(packagename)
215
216+ if buildfiles is None:
217+ return
218+
219+ # Add files as requested.
220+ for path, content in buildfiles.items():
221+ directory = os.path.dirname(path)
222+ if directory != '':
223+ try:
224+ os.makedirs(directory)
225+ except OSError, e:
226+ # Doesn't matter if it already exists.
227+ if e.errno != 17:
228+ raise
229+ with open(path, 'w') as the_file:
230+ the_file.write(content)
231+
232
233 class TestDetectIntltool(TestCase, SetupTestPackageMixin):
234
235@@ -91,6 +116,9 @@
236 self.assertEqual(
237 ["./po-module2"], find_intltool_dirs())
238
239+
240+class TestIntltoolDomain(TestCase, SetupTestPackageMixin):
241+
242 def test_get_translation_domain_makevars(self):
243 # Find a translation domain in Makevars.
244 self.prepare_package("intltool_domain_makevars")
245@@ -98,6 +126,33 @@
246 "translationdomain",
247 get_translation_domain("po"))
248
249+ def test_get_translation_domain_makevars_subst_1(self):
250+ # Find a translation domain in Makevars, substituted from
251+ # Makefile.in.in.
252+ self.prepare_package(
253+ "intltool_domain_base",
254+ {
255+ "po/Makefile.in.in": "PACKAGE=packagename-in-in\n",
256+ "po/Makevars": "DOMAIN = $(PACKAGE)\n",
257+ })
258+ self.assertEqual(
259+ "packagename-in-in",
260+ get_translation_domain("po"))
261+
262+ def test_get_translation_domain_makevars_subst_2(self):
263+ # Find a translation domain in Makevars, substituted from
264+ # configure.ac.
265+ self.prepare_package(
266+ "intltool_domain_base",
267+ {
268+ "configure.ac": "PACKAGE=packagename-ac\n",
269+ "po/Makefile.in.in": "# No domain here.\n",
270+ "po/Makevars": "DOMAIN = $(PACKAGE)\n",
271+ })
272+ self.assertEqual(
273+ "packagename-ac",
274+ get_translation_domain("po"))
275+
276 def test_get_translation_domain_makefile_in_in(self):
277 # Find a translation domain in Makefile.in.in.
278 self.prepare_package("intltool_domain_makefile_in_in")
279@@ -112,6 +167,64 @@
280 "packagename-ac",
281 get_translation_domain("po"))
282
283+ def prepare_ac_init(self, parameters):
284+ # Prepare test for various permutations of AC_INIT parameters
285+ configure_ac_content = dedent("""
286+ AC_INIT(%s)
287+ GETTEXT_PACKAGE=AC_PACKAGE_NAME
288+ """) % parameters
289+ self.prepare_package(
290+ "intltool_domain_base",
291+ {
292+ "configure.ac": configure_ac_content,
293+ })
294+
295+ def test_get_translation_domain_configure_ac_init(self):
296+ # Find a translation domain in configure.ac in AC_INIT.
297+ self.prepare_ac_init("packagename-ac-init, 1.0, http://bug.org")
298+ self.assertEqual(
299+ "packagename-ac-init",
300+ get_translation_domain("po"))
301+
302+ def test_get_translation_domain_configure_ac_init_single_param(self):
303+ # Find a translation domain in configure.ac in AC_INIT.
304+ self.prepare_ac_init("[Just 1 param]")
305+ self.assertIs(None, get_translation_domain("po"))
306+
307+ def test_get_translation_domain_configure_ac_init_brackets(self):
308+ # Find a translation domain in configure.ac in AC_INIT with brackets.
309+ self.prepare_ac_init("[packagename-ac-init], 1.0, http://bug.org")
310+ self.assertEqual(
311+ "packagename-ac-init",
312+ get_translation_domain("po"))
313+
314+ def test_get_translation_domain_configure_ac_init_tarname(self):
315+ # Find a translation domain in configure.ac in AC_INIT tar name
316+ # parameter.
317+ self.prepare_ac_init(
318+ "[Package name], 1.0, http://bug.org, [package-tarname]")
319+ self.assertEqual(
320+ "package-tarname",
321+ get_translation_domain("po"))
322+
323+ def test_get_translation_domain_configure_ac_init_multiline(self):
324+ # Find a translation domain in configure.ac in AC_INIT when it
325+ # spans multiple lines.
326+ self.prepare_ac_init(
327+ "[packagename-ac-init],\n 1.0,\n http://bug.org")
328+ self.assertEqual(
329+ "packagename-ac-init",
330+ get_translation_domain("po"))
331+
332+ def test_get_translation_domain_configure_ac_init_multiline_tarname(self):
333+ # Find a translation domain in configure.ac in AC_INIT tar name
334+ # parameter that is on a different line.
335+ self.prepare_ac_init(
336+ "[Package name], 1.0,\n http://bug.org, [package-tarname]")
337+ self.assertEqual(
338+ "package-tarname",
339+ get_translation_domain("po"))
340+
341 def test_get_translation_domain_configure_in(self):
342 # Find a translation domain in configure.in.
343 self.prepare_package("intltool_domain_configure_in")
344@@ -271,22 +384,57 @@
345 # Demo config file
346 CCC
347 AAA=
348+ FUNC_1(param1)
349 BBB =
350+ FUNC_2(param1, param2,param3 )
351 CCC = ccc # comment
352+ ML_FUNC_1(param1,
353+ param2, param3)
354 DDD=dd.d
355+ ML_FUNC_2(
356+ param1,
357+ param2)
358+ EEE
359+ = eee
360 """)))
361
362 def test_getVariable_exists(self):
363+ self.assertEqual('dd.d', self.configfile.getVariable('DDD'))
364+
365+ def test_getVariable_exists_spaces_comment(self):
366 self.assertEqual('ccc', self.configfile.getVariable('CCC'))
367- self.assertEqual('dd.d', self.configfile.getVariable('DDD'))
368
369 def test_getVariable_empty(self):
370 self.assertEqual('', self.configfile.getVariable('AAA'))
371+
372+ def test_getVariable_empty_spaces(self):
373 self.assertEqual('', self.configfile.getVariable('BBB'))
374
375 def test_getVariable_nonexistent(self):
376 self.assertIs(None, self.configfile.getVariable('FFF'))
377
378+ def test_getVariable_broken(self):
379+ self.assertIs(None, self.configfile.getVariable('EEE'))
380+
381+ def test_getFunctionParams_single(self):
382+ self.assertEqual(
383+ ['param1'], self.configfile.getFunctionParams('FUNC_1'))
384+
385+ def test_getFunctionParams_multiple(self):
386+ self.assertEqual(
387+ ['param1', 'param2', 'param3'],
388+ self.configfile.getFunctionParams('FUNC_2'))
389+
390+ def test_getFunctionParams_multiline_indented(self):
391+ self.assertEqual(
392+ ['param1', 'param2', 'param3'],
393+ self.configfile.getFunctionParams('ML_FUNC_1'))
394+
395+ def test_getFunctionParams_multiline_not_indented(self):
396+ self.assertEqual(
397+ ['param1', 'param2'],
398+ self.configfile.getFunctionParams('ML_FUNC_2'))
399+
400
401 def test_suite():
402 return unittest.TestLoader().loadTestsFromName(__name__)