Merge lp:~sinzui/launchpad/oopsless-repr into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Eleanor Berger
Approved revision: no longer in the source branch.
Merged at revision: 10854
Proposed branch: lp:~sinzui/launchpad/oopsless-repr
Merge into: lp:launchpad
Diff against target: 169 lines (+68/-10)
6 files modified
lib/canonical/launchpad/database/account.py (+2/-1)
lib/canonical/launchpad/tests/test_account.py (+17/-0)
lib/lp/registry/model/distribution.py (+4/-3)
lib/lp/registry/model/person.py (+2/-2)
lib/lp/registry/tests/test_distribution.py (+28/-4)
lib/lp/registry/tests/test_person.py (+15/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/oopsless-repr
Reviewer Review Type Date Requested Status
Eleanor Berger (community) code Approve
Review via email: mp+24929@code.launchpad.net

Description of the change

This is my branch to make __repr__ ascii safe for several classes.

    lp:~sinzui/launchpad/oopsless-repr
    Diff size: 169
    Launchpad bug: https://bugs.launchpad.net/bugs/575976
    Test command: ./bin/test -vv \
        -t test_person_repr -t test_distribution_repr -t test_account_repr
    Pre-implementation: matsubara
    Target release: 10.05

Make __repr__ ascii safe for several classes
---------------------------------------------

As seen on OOPS-1582EB1587 a UnicodeEncodeError was raised while GET'ing the
+reviewaccount page for a person and the person display name contains non-
ascii characters. The oops was created *after* the canonical_urldata_iterator
raised its own error because henninge hacked the URL to non-existent person.
The error message wanted to tell henninge to be a good boy, but the
NoCanonicalUrl uses the __repr__ of the object, which for many launchpad
objects, will contain Unicode.

This affects the following models:
    Account
    Distribution
    Person

Rules
-----

    * Use use .encode('ASCII', 'backslashreplace') to make the displayname
      safe.

QA
--

This is difficult to test since we only see this issue during oopses.
One oops that be reproduced is
    * Visit https://edge.launchpad.net/~ayrton.araujo/+reviewaccount
    * Verify the oops is not about:
      UnicodeEncodeError: 'ascii' codec can't encode character u'\xfa'
    * Verify the oops is about NoCanonicalUrl

Lint
----

Linting changed files:
  lib/canonical/launchpad/database/account.py
  lib/canonical/launchpad/tests/test_account.py
  lib/lp/registry/model/distribution.py
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_distribution.py
  lib/lp/registry/tests/test_person.py

Test
----

    * lib/canonical/launchpad/tests/test_account.py
      Added tests for ANSI and Unicode __repr__
    * lib/lp/registry/tests/test_distribution.py
      Added tests for ANSI and Unicode __repr__
    * lib/lp/registry/tests/test_person.py
      Added tests for ANSI and Unicode __repr__

Implementation
--------------

    * lib/canonical/launchpad/database/account.py
      Escaped the displayname.
    * lib/lp/registry/model/distribution.py
      Escaped the displayname and fixed two lint issues.
    * lib/lp/registry/model/person.py
      Escaped the displayname.

To post a comment you must log in.
Revision history for this message
Eleanor Berger (intellectronica) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/database/account.py'
2--- lib/canonical/launchpad/database/account.py 2010-04-06 19:51:22 +0000
3+++ lib/canonical/launchpad/database/account.py 2010-05-12 18:05:45 +0000
4@@ -48,8 +48,9 @@
5 dbName='openid_identifier', notNull=True, default=DEFAULT)
6
7 def __repr__(self):
8+ displayname = self.displayname.encode('ASCII', 'backslashreplace')
9 return "<%s '%s' (%s)>" % (
10- self.__class__.__name__, self.displayname, self.status)
11+ self.__class__.__name__, displayname, self.status)
12
13 def _getEmails(self, status):
14 """Get related `EmailAddress` objects with the given status."""
15
16=== renamed file 'lib/canonical/launchpad/tests/test_personless_accounts.py' => 'lib/canonical/launchpad/tests/test_account.py'
17--- lib/canonical/launchpad/tests/test_personless_accounts.py 2009-06-25 05:30:52 +0000
18+++ lib/canonical/launchpad/tests/test_account.py 2010-05-12 18:05:45 +0000
19@@ -8,6 +8,23 @@
20 from canonical.launchpad.webapp.authorization import check_permission
21 from canonical.testing import DatabaseFunctionalLayer
22
23+class TestAccount(TestCaseWithFactory):
24+
25+ layer = DatabaseFunctionalLayer
26+
27+ def test_account_repr_ansii(self):
28+ # Verify that ANSI displayname is ascii safe.
29+ distro = self.factory.makeAccount(u'\xdc-account')
30+ ignore, displayname, status_1, status_2 = repr(distro).rsplit(' ', 3)
31+ self.assertEqual("'\\xdc-account'", displayname)
32+ self.assertEqual('(Active account)>', '%s %s' % (status_1, status_2))
33+
34+ def test_account_repr_unicode(self):
35+ # Verify that Unicode displayname is ascii safe.
36+ distro = self.factory.makeAccount(u'\u0170-account')
37+ ignore, displayname, status_1, status_2 = repr(distro).rsplit(' ', 3)
38+ self.assertEqual("'\\u0170-account'", displayname)
39+
40
41 class TestPersonlessAccountPermissions(TestCaseWithFactory):
42 """In order for Person-less accounts to see their non-public details and
43
44=== modified file 'lib/lp/registry/model/distribution.py'
45--- lib/lp/registry/model/distribution.py 2010-04-13 13:38:41 +0000
46+++ lib/lp/registry/model/distribution.py 2010-05-12 18:05:45 +0000
47@@ -185,8 +185,9 @@
48 max_bug_heat = Int()
49
50 def __repr__(self):
51+ displayname = self.displayname.encode('ASCII', 'backslashreplace')
52 return "<%s '%s' (%s)>" % (
53- self.__class__.__name__, self.displayname, self.name)
54+ self.__class__.__name__, displayname, self.name)
55
56 def _init(self, *args, **kw):
57 """Initialize an `IBaseDistribution` or `IDerivativeDistribution`."""
58@@ -1530,7 +1531,7 @@
59 """See `IHasBugSupervisor`."""
60 self.bug_supervisor = bug_supervisor
61 if bug_supervisor is not None:
62- subscription = self.addBugSubscription(bug_supervisor, user)
63+ self.addBugSubscription(bug_supervisor, user)
64
65 def userCanEdit(self, user):
66 """See `IDistribution`."""
67@@ -1634,6 +1635,6 @@
68 mugshot=mugshot,
69 logo=logo,
70 icon=icon)
71- archive = getUtility(IArchiveSet).new(distribution=distro,
72+ getUtility(IArchiveSet).new(distribution=distro,
73 owner=owner, purpose=ArchivePurpose.PRIMARY)
74 return distro
75
76=== modified file 'lib/lp/registry/model/person.py'
77--- lib/lp/registry/model/person.py 2010-05-07 20:28:47 +0000
78+++ lib/lp/registry/model/person.py 2010-05-12 18:05:45 +0000
79@@ -263,8 +263,8 @@
80 storm_validator=_validate_name)
81
82 def __repr__(self):
83- return '<Person at 0x%x %s (%s)>' % (
84- id(self), self.name, self.displayname)
85+ displayname = self.displayname.encode('ASCII', 'backslashreplace')
86+ return '<Person at 0x%x %s (%s)>' % (id(self), self.name, displayname)
87
88 displayname = StringCol(dbName='displayname', notNull=True)
89
90
91=== modified file 'lib/lp/registry/tests/test_distribution.py'
92--- lib/lp/registry/tests/test_distribution.py 2009-12-13 11:55:40 +0000
93+++ lib/lp/registry/tests/test_distribution.py 2010-05-12 18:05:45 +0000
94@@ -14,6 +14,32 @@
95 from lp.soyuz.interfaces.distributionsourcepackagerelease import (
96 IDistributionSourcePackageRelease)
97 from lp.registry.interfaces.series import SeriesStatus
98+from lp.testing import TestCaseWithFactory
99+from canonical.testing.layers import (
100+ DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
101+
102+
103+class TestDistribution(TestCaseWithFactory):
104+
105+ layer = DatabaseFunctionalLayer
106+
107+ def setUp(self):
108+ super(TestDistribution, self).setUp('foo.bar@canonical.com')
109+
110+ def test_distribution_repr_ansii(self):
111+ # Verify that ANSI displayname is ascii safe.
112+ distro = self.factory.makeDistribution(
113+ name="distro", displayname=u'\xdc-distro')
114+ ignore, displayname, name = repr(distro).rsplit(' ', 2)
115+ self.assertEqual("'\\xdc-distro'", displayname)
116+ self.assertEqual('(distro)>', name)
117+
118+ def test_distribution_repr_unicode(self):
119+ # Verify that Unicode displayname is ascii safe.
120+ distro = self.factory.makeDistribution(
121+ name="distro", displayname=u'\u0170-distro')
122+ ignore, displayname, name = repr(distro).rsplit(' ', 2)
123+ self.assertEqual("'\\u0170-distro'", displayname)
124
125
126 class TestDistributionCurrentSourceReleases(
127@@ -25,6 +51,7 @@
128 for the latest published source across multiple distro series.
129 """
130
131+ layer = LaunchpadFunctionalLayer
132 release_interface = IDistributionSourcePackageRelease
133
134 @property
135@@ -75,7 +102,4 @@
136
137
138 def test_suite():
139- suite = unittest.TestSuite()
140- suite.addTest(unittest.makeSuite(TestDistributionCurrentSourceReleases))
141- return suite
142-
143+ return unittest.TestLoader().loadTestsFromName(__name__)
144
145=== modified file 'lib/lp/registry/tests/test_person.py'
146--- lib/lp/registry/tests/test_person.py 2010-04-23 14:22:30 +0000
147+++ lib/lp/registry/tests/test_person.py 2010-05-12 18:05:45 +0000
148@@ -435,6 +435,21 @@
149 hasattr(snap, name),
150 "%s should be omitted from the snapshot but is not." % name)
151
152+ def test_person_repr_ansii(self):
153+ # Verify that ANSI displayname is ascii safe.
154+ person = self.factory.makePerson(
155+ name="user", displayname=u'\xdc-tester')
156+ ignore, name, displayname = repr(person).rsplit(' ', 2)
157+ self.assertEqual('user', name)
158+ self.assertEqual('(\\xdc-tester)>', displayname)
159+
160+ def test_person_repr_unicode(self):
161+ # Verify that Unicode displayname is ascii safe.
162+ person = self.factory.makePerson(
163+ name="user", displayname=u'\u0170-tester')
164+ ignore, displayname = repr(person).rsplit(' ', 1)
165+ self.assertEqual('(\\u0170-tester)>', displayname)
166+
167
168 class TestPersonSet(unittest.TestCase):
169 """Test `IPersonSet`."""