Merge lp:~jtv/launchpad/bug-403992 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-403992
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~jtv/launchpad/bug-403992
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+9690@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 403992 =

This is a more complete fix for bug 403992 than the one I'm hoping to
cherrypick from lp:~jtv/launchpad/cp-bug-403992. You could see it as
all-part-of-the-service aftercare.

Besides the fix in that branch, this one adds some related improvements:

 * Make sure the database knows it's dirty after running the script in
   the doctest. It doesn't seem to change anything, but it's the right
   thing to do.

 * Cache some stuff in the script object, mostly so the identifiers get
   a bit shorter and the main code easier to read. A matter of taste,
   mostly.

 * When trying to diverge a shared message that can't be accommodated as
   shared on the representative potmsgset, try it on the most
   representative of the potemplates it participates in first, then the
   second-best, and so on. This is slightly more proper than sorting
   purely by id, although it probably doesn't matter too much.

 * Cull some now-dead methods and variables: current_tms, imported_tms,
   _findUsedMessages. And their unit tests, of course.

 * Make the unit tests' setUp pass test_args to the script object
   constructor. Otherwise the script would receive the arguments that
   the test driver (./bin/test) got on the command line. That _usually_
   isn't a problem, but when the test driver invokes itself recursively
   to run this test, it adds a --resume-layer argument which of course
   means nothing to the script object. This broke the unit tests in
   some combined test runs, but not in individual ones or more fortunate
   combinations.

To test,
{{{
./bin/test -vv -t message.sharing
}}}

To Q/A, run this on staging:
{{{
scripts/rosetta/message-sharing-merge -vvv -P -p elisa
}}}

No lint complaints.

Jeroen

Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (17.4 KiB)

Hi Jeroen,

This branch is a nice clean up. I've got a few comments on style.

I'm curious also about your/Danilo's choice to re-open a bug that was
fix-committed to make these clean-up changes. Seems like an odd way
to handle it, but not really any of my concern here.

> === modified file 'lib/lp/translations/doc/message-sharing-merge-script.txt'
> --- lib/lp/translations/doc/message-sharing-merge-script.txt 2009-07-06 08:04:08 +0000
> +++ lib/lp/translations/doc/message-sharing-merge-script.txt 2009-08-04 13:37:57 +0000
> @@ -15,3 +15,10 @@
>
> >>> retcode
> 0
> +
> +
> +# The script modified the database, even though the database layer may
> +# not have noticed it.
> +
> + >>> from canonical.launchpad.ftests.harness import LaunchpadTestSetup
> + >>> LaunchpadTestSetup().force_dirty_database()

> === modified file 'lib/lp/translations/scripts/message_sharing_migration.py'
> --- lib/lp/translations/scripts/message_sharing_migration.py 2009-07-19 04:41:14 +0000
> +++ lib/lp/translations/scripts/message_sharing_migration.py 2009-08-05 11:44:49 +0000

Would you mind fixing the __all__ line to make it multi-line (is that our convention when there is just one entry?) or remove the extra spaces.

> @@ -8,8 +8,9 @@
> from zope.component import getUtility
> from zope.security.proxy import removeSecurityProxy
>
> +from canonical.launchpad.utilities.orderingcheck import OrderingCheck
> from lp.translations.interfaces.potemplate import IPOTemplateSet
> -from canonical.launchpad.utilities.orderingcheck import OrderingCheck
> +from lp.translations.interfaces.translations import TranslationConstants
> from lp.registry.interfaces.product import IProductSet
> from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
> from lp.services.scripts.base import (
> @@ -146,6 +147,8 @@
>
> class MessageSharingMerge(LaunchpadScript):
>
> + templateset = None

How about 'template_set' as it follows our coding standard?

> def add_my_options(self):
> self.parser.add_option('-d', '--distribution', dest='distribution',
> help="Distribution to merge messages for.")
> @@ -166,6 +169,16 @@
> action='store_true',
> help="Dry run, don't really make any changes.")
>
> + def _setUpUtilities(self):
> + """Prepare a few members that several methods need.
> +
> + Calling this again later does nothing.
> + """
> + if self.templateset is None:
> + self.templateset = getUtility(IPOTemplateSet)
> + self.compare_template_precedence = (
> + self.templateset.compareSharingPrecedence)
> +
> def main(self):
> actions = (
> self.options.merge_potmsgsets or
> @@ -214,7 +227,9 @@
> "Unknown source package name: '%s'" %
> self.options.sourcepackage)
>
> - subset = getUtility(IPOTemplateSet).getSharingSubset(
> + self._setUpUtilities()
> +
> + subset = self.templateset.getSharingSubset(
> product=product, distribution=distribution,
> sourcepackagename=sourcepackagename)
> equivalence...

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Hi Brad! Thanks for the review. This branch has been lying around for
a while, waiting for someone to review it. Unlike its cherry-pickable
sibling it's not urgent, but I'm glad to have it out of the way.

> I'm curious also about your/Danilo's choice to re-open a bug that was
> fix-committed to make these clean-up changes. Seems like an odd way
> to handle it, but not really any of my concern here.

I wouldn't say that was our "choice." What happened was that I prepared
two branches for the same problem: one immediate, absolutely minimal fix
for cherry-picking, and this conventional full fix as a superset of the
former. The cherry-picking branch has landed, and the bug went through
a brief tentative phase of being "fix committed" because of that. But
it all happened well after this branch here went up for review.

> > === modified file 'lib/lp/translations/scripts/message_sharing_migration.py'
> > --- lib/lp/translations/scripts/message_sharing_migration.py 2009-07-19
> 04:41:14 +0000
> > +++ lib/lp/translations/scripts/message_sharing_migration.py 2009-08-05
> 11:44:49 +0000
>
> Would you mind fixing the __all__ line to make it multi-line (is that our
> convention when there is just one entry?) or remove the extra spaces.

Don't mind at all. Done.

> > @@ -146,6 +147,8 @@
> >
> > class MessageSharingMerge(LaunchpadScript):
> >
> > + templateset = None
>
> How about 'template_set' as it follows our coding standard?

In English you never really know two words have become one until one of
them changes, like the "sheep" in "shepherd."

I think I chose "templateset" to distinguish the utility from a mere set
of templates. Then again, naming variables after their type should not
be taken this far anyway.

> > @@ -361,7 +370,13 @@
> > potmsgset (because we start out with one) and potemplate (because
> > that's sorted out in the nested dicts).
> > """
> > - return (tm.language, tm.variant) + tuple(tm.all_msgstrs)
> > + tm = removeSecurityProxy(tm)
> > + msgstr_ids = [
> > + getattr(tm, 'msgstr%dID' % form)
> > + for form in xrange(TranslationConstants.MAX_PLURAL_FORMS)
> > + ]
>
> The ] should be dedented by 4.

Ah, my editor is showing a mind of its own. Can't have that.

Hi ho, hi ho, to PQM I go!

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/doc/message-sharing-merge-script.txt'
2--- lib/lp/translations/doc/message-sharing-merge-script.txt 2009-07-06 08:04:08 +0000
3+++ lib/lp/translations/doc/message-sharing-merge-script.txt 2009-08-04 13:37:57 +0000
4@@ -15,3 +15,10 @@
5
6 >>> retcode
7 0
8+
9+
10+# The script modified the database, even though the database layer may
11+# not have noticed it.
12+
13+ >>> from canonical.launchpad.ftests.harness import LaunchpadTestSetup
14+ >>> LaunchpadTestSetup().force_dirty_database()
15
16=== modified file 'lib/lp/translations/scripts/message_sharing_migration.py'
17--- lib/lp/translations/scripts/message_sharing_migration.py 2009-07-19 04:41:14 +0000
18+++ lib/lp/translations/scripts/message_sharing_migration.py 2009-08-05 11:44:49 +0000
19@@ -8,8 +8,9 @@
20 from zope.component import getUtility
21 from zope.security.proxy import removeSecurityProxy
22
23+from canonical.launchpad.utilities.orderingcheck import OrderingCheck
24 from lp.translations.interfaces.potemplate import IPOTemplateSet
25-from canonical.launchpad.utilities.orderingcheck import OrderingCheck
26+from lp.translations.interfaces.translations import TranslationConstants
27 from lp.registry.interfaces.product import IProductSet
28 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
29 from lp.services.scripts.base import (
30@@ -146,6 +147,8 @@
31
32 class MessageSharingMerge(LaunchpadScript):
33
34+ templateset = None
35+
36 def add_my_options(self):
37 self.parser.add_option('-d', '--distribution', dest='distribution',
38 help="Distribution to merge messages for.")
39@@ -166,6 +169,16 @@
40 action='store_true',
41 help="Dry run, don't really make any changes.")
42
43+ def _setUpUtilities(self):
44+ """Prepare a few members that several methods need.
45+
46+ Calling this again later does nothing.
47+ """
48+ if self.templateset is None:
49+ self.templateset = getUtility(IPOTemplateSet)
50+ self.compare_template_precedence = (
51+ self.templateset.compareSharingPrecedence)
52+
53 def main(self):
54 actions = (
55 self.options.merge_potmsgsets or
56@@ -214,7 +227,9 @@
57 "Unknown source package name: '%s'" %
58 self.options.sourcepackage)
59
60- subset = getUtility(IPOTemplateSet).getSharingSubset(
61+ self._setUpUtilities()
62+
63+ subset = self.templateset.getSharingSubset(
64 product=product, distribution=distribution,
65 sourcepackagename=sourcepackagename)
66 equivalence_classes = subset.groupEquivalentPOTemplates(
67@@ -250,6 +265,7 @@
68
69 def _mergePOTMsgSets(self, potemplates):
70 """Merge POTMsgSets for given sequence of sharing templates."""
71+ self._setUpUtilities()
72
73 # Map each POTMsgSet key (context, msgid, plural) to its
74 # representative POTMsgSet.
75@@ -267,8 +283,7 @@
76 # through the templates, starting at the most representative and
77 # moving towards the least representative. For any unique potmsgset
78 # key we find, the first POTMsgSet is the representative one.
79- templateset = getUtility(IPOTemplateSet)
80- order_check = OrderingCheck(cmp=templateset.compareSharingPrecedence)
81+ order_check = OrderingCheck(cmp=self.compare_template_precedence)
82
83 for template in potemplates:
84 order_check.check(template)
85@@ -288,13 +303,7 @@
86 # translation messages. We don't need do this for subordinates
87 # because the algorithm will refuse to add new duplicates to the
88 # representative POTMsgSet anyway.
89- existing_tms = self._scrubPOTMsgSetTranslations(representative)
90-
91- # Keep track of current/imported TranslationMessages for
92- # representative. These will matter when we try to merge
93- # subordinate TranslationMessages into the representative, some
94- # of which may be current or imported as well.
95- current_tms, imported_tms = self._findUsedMessages(existing_tms)
96+ self._scrubPOTMsgSetTranslations(representative)
97
98 seen_potmsgsets = set([representative])
99
100@@ -309,8 +318,8 @@
101 message = removeSecurityProxy(message)
102
103 clashing_current, clashing_imported, twin = (
104- self._findClashesFromDicts(
105- existing_tms, current_tms, imported_tms, message))
106+ self._findClashes(
107+ message, representative, message.potemplate))
108
109 if clashing_current or clashing_imported:
110 saved = self._saveByDiverging(
111@@ -345,8 +354,8 @@
112
113 def _mergeTranslationMessages(self, potemplates):
114 """Share `TranslationMessage`s between templates where possible."""
115- templateset = getUtility(IPOTemplateSet)
116- order_check = OrderingCheck(cmp=templateset.compareSharingPrecedence)
117+ self._setUpUtilities()
118+ order_check = OrderingCheck(cmp=self.compare_template_precedence)
119 for template in potemplates:
120 order_check.check(template)
121 for potmsgset in template.getPOTMsgSets(False):
122@@ -361,7 +370,13 @@
123 potmsgset (because we start out with one) and potemplate (because
124 that's sorted out in the nested dicts).
125 """
126- return (tm.language, tm.variant) + tuple(tm.all_msgstrs)
127+ tm = removeSecurityProxy(tm)
128+ msgstr_ids = [
129+ getattr(tm, 'msgstr%dID' % form)
130+ for form in xrange(TranslationConstants.MAX_PLURAL_FORMS)
131+ ]
132+
133+ return (tm.language, tm.variant) + tuple(msgstr_ids)
134
135 def _mapExistingMessages(self, potmsgset):
136 """Map out the existing TranslationMessages for `potmsgset`.
137@@ -387,33 +402,6 @@
138
139 return existing_tms
140
141- def _findUsedMessages(self, translations):
142- """Fish current and imported messages out of translations dict.
143-
144- :param translations: a dict as returned by
145- `_mapExistingMessages` or `_scrubPOTMsgSetTranslations`. All
146- information is taken from this dict; no other messages are
147- considered.
148- :return: a pair of dicts, each mapping (potemplate, language,
149- variant) to a TranslationMessage or to None. The dicts map
150- out current and imported messages, respectively. Shared
151- messages have a POTemplate of None.
152- """
153- current_messages = {}
154- imported_messages = {}
155- for per_template in translations.itervalues():
156- for tms in per_template.itervalues():
157- assert len(tms) == 1, (
158- "Unexpected list size: %d items." % len(tms))
159- tm = tms[0]
160- key = (tm.potemplate, tm.language, tm.variant)
161- if tm.is_current:
162- current_messages[key] = tm
163- if tm.is_imported:
164- imported_messages[key] = tm
165-
166- return current_messages, imported_messages
167-
168 def _scrubPOTMsgSetTranslations(self, potmsgset):
169 """Map out translations for `potmsgset`, and eliminate duplicates.
170
171@@ -487,41 +475,6 @@
172 # message can be merged into the twin without problems.
173 return filter_clashes(clashing_current, clashing_imported, twin)
174
175- def _findClashesFromDicts(self, existing_tms, current_tms, imported_tms,
176- message):
177- """Like `_findClashes`, but from cached dicts.
178-
179- This saves some database querying for the common case.
180-
181- :param existing_tms: a dict as returned by
182- `_mapExistingMessages`.
183- :param current_tms: a dict as returned by `_findUsedMessages`.
184- """
185- twins_dict = existing_tms.get(
186- self._getPOTMsgSetTranslationMessageKey(message))
187- if twins_dict is None:
188- twin = None
189- else:
190- twins = twins_dict.get(message.potemplate)
191- if twins is None:
192- twin = None
193- else:
194- assert len(twins) == 1, "Did not expect %d twins." % (
195- len(twins))
196- twin = twins[0]
197-
198- subkey = message.potemplate, message.language, message.variant
199- if message.is_current:
200- clashing_current = current_tms.get(subkey)
201- else:
202- clashing_current = None
203- if message.is_imported:
204- clashing_imported = imported_tms.get(subkey)
205- else:
206- clashing_imported = None
207-
208- return filter_clashes(clashing_current, clashing_imported, twin)
209-
210 def _saveByDiverging(self, message, target_potmsgset, source_potmsgset):
211 """Avoid a TranslationMessage clash during POTMsgSet merge.
212
213@@ -539,8 +492,10 @@
214 # This message was shared. Maybe we can still save it for at
215 # least one template by making it diverged.
216 target_ttis = source_potmsgset.getAllTranslationTemplateItems()
217- for tti in target_ttis:
218- if self._divergeTo(message, target_potmsgset, tti.potemplate):
219+ target_templates = [tti.potemplate for tti in target_ttis]
220+ target_templates.sort(self.compare_template_precedence)
221+ for template in target_templates:
222+ if self._divergeTo(message, target_potmsgset, template):
223 return True
224
225 # No, there's no place where this message can be preserved. It'll
226
227=== modified file 'lib/lp/translations/scripts/tests/test_message_sharing_migration.py'
228--- lib/lp/translations/scripts/tests/test_message_sharing_migration.py 2009-07-19 04:41:14 +0000
229+++ lib/lp/translations/scripts/tests/test_message_sharing_migration.py 2009-08-05 11:44:49 +0000
230@@ -41,7 +41,7 @@
231 self.stable_template.iscurrent = False
232 self.templates = [self.trunk_template, self.stable_template]
233
234- self.script = MessageSharingMerge('tms-merging-test')
235+ self.script = MessageSharingMerge('tms-merging-test', test_args=[])
236 self.script.logger.setLevel(ERROR)
237
238
239@@ -617,47 +617,6 @@
240
241 self.assertEqual(map, expected)
242
243- def test_FindNoUsedMessage(self):
244- # Test _findUsedMessage against a case where there are no used
245- # messages.
246- message = self._makeTranslationMessage(
247- pofile=self.trunk_pofile, potmsgset=self.trunk_potmsgset,
248- text='godot', diverged=False)
249- message.is_current = False
250- key = self.script._getPOTMsgSetTranslationMessageKey(message)
251- map = {key: {self.trunk_template: [message]}}
252-
253- self.assertEqual(self.script._findUsedMessages(map), ({}, {}))
254-
255- def test_FindUsedMessages(self):
256- # _findUsedMessages maps diverged messages based on template,
257- # language, and variant. The difference for shared messages is
258- # that their template is None.
259- imported_message = self._makeTranslationMessage(
260- pofile=self.trunk_pofile, potmsgset=self.trunk_potmsgset,
261- text='pog', diverged=True)
262- imported_message.is_current = False
263- imported_message.is_imported = True
264- current_message = self._makeTranslationMessage(
265- pofile=self.trunk_pofile, potmsgset=self.trunk_potmsgset,
266- text='klexdigal', diverged=False)
267-
268- current_key = self.script._getPOTMsgSetTranslationMessageKey(
269- current_message)
270- imported_key = self.script._getPOTMsgSetTranslationMessageKey(
271- imported_message)
272-
273- map = {
274- current_key: {self.trunk_template: [current_message]},
275- imported_key: { self.trunk_template: [imported_message]},
276- }
277-
278- expected = (
279- {(None, self.dutch, None): current_message},
280- {(self.trunk_template, self.dutch, None): imported_message})
281-
282- self.assertEqual(self.script._findUsedMessages(map), expected)
283-
284 def test_ScrubPOTMsgSetTranslationsWithoutDuplication(self):
285 # _scrubPOTMsgSetTranslations eliminates duplicated
286 # TranslationMessages. If it doesn't find any, nothing happens.
287@@ -776,95 +735,6 @@
288 self.assertEqual(imported_clash, None)
289 self.assertEqual(twin, trunk_message)
290
291- def test_FindCurrentClashFromDicts(self):
292- # Like test_FindCurrentClash, but using cached dicts.
293- trunk_message, stable_message = self._makeTranslationMessages(
294- 'ex', 'why', trunk_diverged=False, stable_diverged=False)
295-
296- trunk_key = self.script._getPOTMsgSetTranslationMessageKey(
297- trunk_message)
298- stable_key = self.script._getPOTMsgSetTranslationMessageKey(
299- stable_message)
300-
301- existing = self.script._mapExistingMessages(self.trunk_potmsgset)
302- current, imported = self.script._findUsedMessages(existing)
303- current_clash, imported_clash, twin = (
304- self.script._findClashesFromDicts(
305- existing, current, imported, stable_message))
306-
307- self.assertEqual(current_clash, trunk_message)
308- self.assertEqual(imported_clash, None)
309- self.assertEqual(twin, None)
310-
311-
312- def test_FindImportedClashFromDicts(self):
313- # Like test_FindImportedClash, but using cached dicts.
314- trunk_message, stable_message = self._makeTranslationMessages(
315- 'ex', 'why', trunk_diverged=False, stable_diverged=False)
316-
317- for message in (trunk_message, stable_message):
318- message.is_current = False
319- message.is_imported = True
320-
321- existing = self.script._mapExistingMessages(self.trunk_potmsgset)
322- current, imported = self.script._findUsedMessages(existing)
323- current_clash, imported_clash, twin = (
324- self.script._findClashesFromDicts(
325- existing, current, imported, stable_message))
326-
327- self.assertEqual(current_clash, None)
328- self.assertEqual(imported_clash, trunk_message)
329- self.assertEqual(twin, None)
330-
331- def test_FindTwinFromDicts(self):
332- # Like test_FindTwin, but using cached dicts.
333- trunk_message, stable_message = self._makeTranslationMessages(
334- 'klob', 'klob', trunk_diverged=False, stable_diverged=False)
335- trunk_message.is_current = False
336-
337- existing = self.script._mapExistingMessages(self.trunk_potmsgset)
338- current, imported = self.script._findUsedMessages(existing)
339- current_clash, imported_clash, twin = (
340- self.script._findClashesFromDicts(
341- existing, current, imported, stable_message))
342-
343- self.assertEqual(current_clash, None)
344- self.assertEqual(imported_clash, None)
345- self.assertEqual(twin, trunk_message)
346-
347- def test_FindClashesWithTwinFromDicts(self):
348- # Like test_FindClashesWithTwin, but using cached dicts.
349- trunk_message, stable_message = self._makeTranslationMessages(
350- 'sniw', 'sniw', trunk_diverged=False, stable_diverged=False)
351-
352- existing = self.script._mapExistingMessages(self.trunk_potmsgset)
353- current, imported = self.script._findUsedMessages(existing)
354- current_clash, imported_clash, twin = (
355- self.script._findClashesFromDicts(
356- existing, current, imported, stable_message))
357-
358- self.assertEqual(current_clash, None)
359- self.assertEqual(imported_clash, None)
360- self.assertEqual(twin, trunk_message)
361-
362- def test_FindClashesWithNonTwinFromDicts(self):
363- # Like test_FindClashesWithNonTwin, but using cached dicts.
364- trunk_message, stable_message = self._makeTranslationMessages(
365- 'sniw', 'sniw', trunk_diverged=False, stable_diverged=False)
366- trunk_message.is_current = False
367- current_message = self._makeTranslationMessage(
368- self.trunk_pofile, self.trunk_potmsgset, 'gah', False)
369-
370- existing = self.script._mapExistingMessages(self.trunk_potmsgset)
371- current, imported = self.script._findUsedMessages(existing)
372- current_clash, imported_clash, twin = (
373- self.script._findClashesFromDicts(
374- existing, current, imported, stable_message))
375-
376- self.assertEqual(current_clash, current_message)
377- self.assertEqual(imported_clash, None)
378- self.assertEqual(twin, trunk_message)
379-
380
381 def test_suite():
382 return TestLoader().loadTestsFromName(__name__)