launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26138
[Merge] ~cjwatson/launchpad:py3-app-exception-modules into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:py3-app-exception-modules into launchpad:master.
Commit message:
lp.app: Use IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/397154
This allows doctests that test tracebacks to work on both Python 2 and 3.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:py3-app-exception-modules into launchpad:master.
diff --git a/lib/lp/app/doc/batch-navigation.txt b/lib/lp/app/doc/batch-navigation.txt
index 86436d7..eebef5d 100644
--- a/lib/lp/app/doc/batch-navigation.txt
+++ b/lib/lp/app/doc/batch-navigation.txt
@@ -81,9 +81,10 @@ InvalidBatchSizeError is raised.
... """))
>>> request = build_request({"start": "0", "batch": "20"})
>>> BatchNavigator(reindeer, request=request )
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- InvalidBatchSizeError: Maximum for "batch" parameter is 5.
+ lazr.batchnavigator.interfaces.InvalidBatchSizeError: Maximum for "batch" parameter is 5.
>>> ignored = config.pop('max-batch-size')
diff --git a/lib/lp/app/doc/displaying-numbers.txt b/lib/lp/app/doc/displaying-numbers.txt
index fcae7bc..59ed96b 100644
--- a/lib/lp/app/doc/displaying-numbers.txt
+++ b/lib/lp/app/doc/displaying-numbers.txt
@@ -56,9 +56,10 @@ Is the same as:
Here's a set of exhaustive examples:
>>> test_tales('foo/fmt:float', foo=12345.67890)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LocationError: 'fmt:float requires a single decimal argument'
+ zope.location.interfaces.LocationError: 'fmt:float requires a single decimal argument'
>>> test_tales('foo/fmt:float/0.3', foo=-12345.67890)
'-12345.679'
diff --git a/lib/lp/app/doc/launchpadform.txt b/lib/lp/app/doc/launchpadform.txt
index 571002f..86a5fc3 100644
--- a/lib/lp/app/doc/launchpadform.txt
+++ b/lib/lp/app/doc/launchpadform.txt
@@ -554,9 +554,10 @@ request:
... 'field.actions.change': 'Change'})
>>> view = UnsafeActionTestView(context, request)
>>> view.initialize()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- UnsafeFormGetSubmissionError: field.actions.change
+ lp.services.webapp.interfaces.UnsafeFormGetSubmissionError: field.actions.change
>>> request = LaunchpadTestRequest(
... method='POST',
diff --git a/lib/lp/app/doc/object-privacy.txt b/lib/lp/app/doc/object-privacy.txt
index cf4d5ba..18c14e5 100644
--- a/lib/lp/app/doc/object-privacy.txt
+++ b/lib/lp/app/doc/object-privacy.txt
@@ -30,9 +30,10 @@ private.
>>> question = getUtility(IQuestionSet).get(1)
>>> question.private
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- ForbiddenAttribute:...
+ zope.security.interfaces.ForbiddenAttribute: ...
>>> IObjectPrivacy(question).is_private
False
diff --git a/lib/lp/app/doc/tales.txt b/lib/lp/app/doc/tales.txt
index 75e2629..a6b31fe 100644
--- a/lib/lp/app/doc/tales.txt
+++ b/lib/lp/app/doc/tales.txt
@@ -1314,9 +1314,10 @@ Everything you can do with 'something/fmt:foo', you should be able to do
with 'None/fmt:foo'.
>>> test_tales('foo/fmt:shorten', foo=None)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LocationError: 'you need to traverse a number after fmt:shorten'
+ zope.location.interfaces.LocationError: 'you need to traverse a number after fmt:shorten'
>>> test_tales('foo/fmt:shorten/8', foo=None)
''
@@ -1416,10 +1417,10 @@ We don't get a ValueError when we use a value that doesn't appear in the
DBSchema the item comes from.
>>> test_tales('deb/enumvalue:CHEESEFISH', deb=udeb)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LocationError: 'The enumerated type BinaryPackageFormat does not have a
- value CHEESEFISH.'
+ zope.location.interfaces.LocationError: 'The enumerated type BinaryPackageFormat does not have a value CHEESEFISH.'
It is possible for dbschemas to have a 'None' value. This is a bit
awkward, because when the value is None, we can't do any checking
@@ -1438,10 +1439,10 @@ dbschema items too:
False
>>> test_tales('deb/enumvalue:CHEESEFISH', deb=wrapped_deb)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LocationError: 'The enumerated type BinaryPackageFormat does not have a
- value CHEESEFISH.'
+ zope.location.interfaces.LocationError: 'The enumerated type BinaryPackageFormat does not have a value CHEESEFISH.'
Formatting timedelta objects
diff --git a/lib/lp/app/stories/basics/marketing.txt b/lib/lp/app/stories/basics/marketing.txt
index fd4c1ce..714be3a 100644
--- a/lib/lp/app/stories/basics/marketing.txt
+++ b/lib/lp/app/stories/basics/marketing.txt
@@ -44,9 +44,10 @@ But the source directory isn't available:
>>> browser.open(
... 'http://launchpad.test/+tour/source/code-hosting_SVG.svg')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
== +about compatibility ==
diff --git a/lib/lp/app/stories/basics/max-batch-size.txt b/lib/lp/app/stories/basics/max-batch-size.txt
index 12b15e0..9bd585b 100644
--- a/lib/lp/app/stories/basics/max-batch-size.txt
+++ b/lib/lp/app/stories/basics/max-batch-size.txt
@@ -7,9 +7,10 @@ large and what is the current maximum.
>>> anon_browser.handleErrors = True
>>> anon_browser.open('http://launchpad.test/projects/+all?start=0&batch=1000')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- HTTPError: HTTP Error 400: Bad Request
+ urllib.error.HTTPError: HTTP Error 400: Bad Request
>>> print(extract_text(find_main_content(anon_browser.contents)))
Invalid Batch Size
diff --git a/lib/lp/app/stories/basics/xx-developerexceptions.txt b/lib/lp/app/stories/basics/xx-developerexceptions.txt
index 57eed1a..a8d08b6 100644
--- a/lib/lp/app/stories/basics/xx-developerexceptions.txt
+++ b/lib/lp/app/stories/basics/xx-developerexceptions.txt
@@ -104,7 +104,8 @@ want to see tracebacks instead of error pages.
>>> print(http(r"""
... GET /whatever HTTP/1.1
... """, handle_errors=False))
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound: ...
+ zope.publisher.interfaces.NotFound: ...
diff --git a/lib/lp/app/stories/basics/xx-launchpad-statistics.txt b/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
index 542384a..891b1d0 100644
--- a/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
+++ b/lib/lp/app/stories/basics/xx-launchpad-statistics.txt
@@ -3,9 +3,10 @@ We also have the special Launchpad Statistics summary page. This is only
acessible to launchpad Admins:
>>> user_browser.open('http://launchpad.test/+statistics')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- Unauthorized:...
+ zope.security.interfaces.Unauthorized: ...
When we login as an admin, we can see all the stats listed:
diff --git a/lib/lp/app/stories/basics/xx-offsite-form-post.txt b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
index 4b1bcc8..045c34a 100644
--- a/lib/lp/app/stories/basics/xx-offsite-form-post.txt
+++ b/lib/lp/app/stories/basics/xx-offsite-form-post.txt
@@ -44,9 +44,10 @@ If we try to create a new team with with the referrer set to
>>> browser.getControl('Name', index=0).value = 'team1'
>>> browser.getControl('Display Name').value = 'Team 1'
>>> browser.getControl('Create').click()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- OffsiteFormPostError: http://evil.people.com/
+ lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
Similarly, posting with a garbage referer fails:
@@ -56,9 +57,10 @@ Similarly, posting with a garbage referer fails:
>>> browser.getControl('Name', index=0).value = 'team2'
>>> browser.getControl('Display Name').value = 'Team 2'
>>> browser.getControl('Create').click()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- OffsiteFormPostError: not a url
+ lp.services.webapp.interfaces.OffsiteFormPostError: not a url
It also fails if there is no referrer.
@@ -68,9 +70,10 @@ It also fails if there is no referrer.
>>> browser.getControl('Name', index=0).value = 'team3'
>>> browser.getControl('Display Name').value = 'Team 3'
>>> browser.getControl('Create').click()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NoReferrerError: No value for REFERER header
+ lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
When a POST request is rejected because the REFERER header is missing, it
may be because the user is trying to enforce anonymity. Therefore, we
@@ -81,9 +84,10 @@ present a hopefully helpful error message.
>>> browser.getControl('Name', index=0).value = 'team3'
>>> browser.getControl('Display Name').value = 'Team 3'
>>> browser.getControl('Create').click()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- HTTPError...
+ urllib.error.HTTPError: ...
>>> print(browser.headers['status'])
403 Forbidden
>>> print(extract_text(find_main_content(browser.contents)))
@@ -113,9 +117,10 @@ exception for +access-token, it would result in an
OffsiteFormPostError.
>>> browser.post('http://launchpad.test/+access-token', 'x=1')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- HTTPError: HTTP Error 401: Unauthorized
+ urllib.error.HTTPError: HTTP Error 401: Unauthorized
We also let the request go through if the referrer is from a site
managed by launchpad:
@@ -148,30 +153,34 @@ a referrerless POST request to the browser-accessible API.
>>> no_referrer_browser = setupBrowserWithReferrer(None)
>>> browser.post('http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- OffsiteFormPostError: http://evil.people.com/
+ lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
>>> no_referrer_browser.post(
... 'http://launchpad.test/api/devel/people', 'ws.op=foo&x=1')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NoReferrerError: No value for REFERER header
+ lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
You can't cheat by making your referrerless POST request seem as
though it were signed with OAuth.
>>> browser.post(
... 'http://launchpad.test/', 'oauth_consumer_key=foo&oauth_token=bar')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- OffsiteFormPostError: http://evil.people.com/
+ lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
>>> no_referrer_browser.post(
... 'http://launchpad.test/', 'oauth_consumer_key=foo&oauth_token=bar')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NoReferrerError: No value for REFERER header
+ lp.services.webapp.interfaces.NoReferrerError: No value for REFERER header
You might think you can actually sign a request with an anonymous
OAuth credential. You don't need any knowledge of the user account to
@@ -188,17 +197,19 @@ But the browser-accessible API ignores OAuth credentials altogether.
>>> browser.post(
... 'http://launchpad.test/api/devel/projects', sig)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- OffsiteFormPostError: http://evil.people.com/
+ lp.services.webapp.interfaces.OffsiteFormPostError: http://evil.people.com/
If you go through the 'api' vhost, the signed request will be
processed despite the bogus referrer, but...
>>> browser.post('http://api.launchpad.test/devel/projects', sig)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NoneError: None isn't acceptable as a value for Product.owner
+ storm.exceptions.NoneError: None isn't acceptable as a value for Product.owner
You're making an _anonymous_ request. That's a request that 1) is not
associated with any Launchpad user account (thus the NoneError when
diff --git a/lib/lp/app/stories/basics/xx-opstats.txt b/lib/lp/app/stories/basics/xx-opstats.txt
index f502ede..400cb75 100644
--- a/lib/lp/app/stories/basics/xx-opstats.txt
+++ b/lib/lp/app/stories/basics/xx-opstats.txt
@@ -292,9 +292,10 @@ But our database connections are broken.
>>> from lp.services.database.interfaces import IStore
>>> from lp.registry.model.person import Person
>>> IStore(Person).find(Person, name='janitor')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- DisconnectionError: FATAL: database "nonexistant" does not exist
+ storm.exceptions.DisconnectionError: FATAL: database "nonexistant" does not exist
>>> dummy = config.pop('no_db')
>>> getUtility(IZStorm)._reset()
diff --git a/lib/lp/app/stories/folder.txt b/lib/lp/app/stories/folder.txt
index 02f5547..ee9d9db 100644
--- a/lib/lp/app/stories/folder.txt
+++ b/lib/lp/app/stories/folder.txt
@@ -65,10 +65,10 @@ Requesting a directory raises a NotFound.
>>> view = MyFolder(object(), FakeRequest(version="devel"))
>>> view = view.publishTraverse(view.request, 'a_dir')
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
By default, subdirectories are not exported. (See below on how to enable
this)
@@ -76,27 +76,27 @@ this)
>>> view = MyFolder(object(), FakeRequest(version="devel"))
>>> view = view.publishTraverse(view.request, 'a_dir')
>>> view = view.publishTraverse(view.request, 'other.txt')
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
Not requesting any file, also raises NotFound.
>>> view = MyFolder(object(), FakeRequest(version="devel"))
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
As requesting a non-existent file.
>>> view = MyFolder(object(), FakeRequest(version="devel"))
>>> view = view.publishTraverse(view.request, 'image2')
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
== ExportedImageFolder ==
@@ -172,20 +172,20 @@ But traversing to the subdirectory itself will raise a NotFound.
>>> view = MyTree(object(), FakeRequest(version="devel"))
>>> view = view.publishTraverse(view.request, 'public')
- >>> print(view())
+ >>> print(view()) # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
Trying to request a non-existent file, will also raise a NotFound.
>>> view = MyTree(object(), FakeRequest(version="devel"))
>>> view = view.publishTraverse(view.request, 'public')
>>> view = view.publishTraverse(view.request, 'nosuchfile.txt')
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
Traversing beyond an existing file to a non-existant file raises a
NotFound.
@@ -195,10 +195,10 @@ NotFound.
>>> view = view.publishTraverse(view.request, 'subdir1')
>>> view = view.publishTraverse(view.request, 'test1.txt')
>>> view = view.publishTraverse(view.request, 'nosuchpath')
- >>> view()
+ >>> view() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound:...
+ zope.publisher.interfaces.NotFound: ...
== Clean-up ==
diff --git a/lib/lp/app/stories/form/xx-form-layout.txt b/lib/lp/app/stories/form/xx-form-layout.txt
index 01b3ac1..c9b09ea 100644
--- a/lib/lp/app/stories/form/xx-form-layout.txt
+++ b/lib/lp/app/stories/form/xx-form-layout.txt
@@ -98,14 +98,16 @@ display forms.
>>> cprov_browser.open(
... 'http://launchpad.test/~cprov/+editsshkeys/++form++')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- NotFound...
+ zope.publisher.interfaces.NotFound: ...
Nor will it allow unauthorized access to data that it should not present.
>>> browser.open(
... 'http://launchpad.test/~cprov/+archive/ppa/+edit/++form++')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- Unauthorized...
+ zope.security.interfaces.Unauthorized: ...
diff --git a/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt b/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
index 3fa6f24..2ca6482 100644
--- a/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
+++ b/lib/lp/app/stories/launchpad-root/xx-featuredprojects.txt
@@ -49,17 +49,19 @@ projects' pages in Launchpad. The "project of the day" is listed separately.
Anonymous users cannot see the link to administer featured projects:
>>> anon_browser.getLink(MANAGE_LINK)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LinkNotFoundError
+ zope.testbrowser.browser.LinkNotFoundError
A user without privileges cannot see the administration link, either:
>>> user_browser.open('http://launchpad.test/')
>>> user_browser.getLink(MANAGE_LINK)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LinkNotFoundError
+ zope.testbrowser.browser.LinkNotFoundError
But Foo Bar, who is an administrator, can see the management link:
@@ -71,9 +73,10 @@ But Foo Bar, who is an administrator, can see the management link:
No Privilege persons is denied access to this page:
>>> user_browser.open('http://launchpad.test/+featuredprojects')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- Unauthorized: ...
+ zope.security.interfaces.Unauthorized: ...
Administrators can add a project. Here Foo Bar adds apache as a featured
project:
diff --git a/lib/lp/app/validators/email.py b/lib/lp/app/validators/email.py
index d3789bc..ce959fc 100644
--- a/lib/lp/app/validators/email.py
+++ b/lib/lp/app/validators/email.py
@@ -79,9 +79,10 @@ def email_validator(emailaddr):
>>> email_validator('bugs@xxxxxxxxxxx')
True
>>> email_validator('not-valid')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError: Invalid email 'not-valid'.
+ lp.app.validators.LaunchpadValidationError: Invalid email 'not-valid'.
"""
if not valid_email(emailaddr):
raise LaunchpadValidationError(_(
diff --git a/lib/lp/app/validators/tests/test_validators.py b/lib/lp/app/validators/tests/test_validators.py
index e746416..cb11eea 100644
--- a/lib/lp/app/validators/tests/test_validators.py
+++ b/lib/lp/app/validators/tests/test_validators.py
@@ -12,6 +12,8 @@ from doctest import (
)
from unittest import TestSuite
+from zope.testing.renormalizing import OutputChecker
+
from lp.testing.layers import LaunchpadFunctionalLayer
from lp.testing.systemdocs import (
setUp,
@@ -42,7 +44,7 @@ def suitefor(module):
"""Make a doctest suite with common setUp and tearDown functions."""
suite = DocTestSuite(
module, setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
- optionflags=ELLIPSIS | NORMALIZE_WHITESPACE)
+ optionflags=ELLIPSIS | NORMALIZE_WHITESPACE, checker=OutputChecker())
# We have to invoke the LaunchpadFunctionalLayer in order to
# initialize the ZCA machinery, which is a pre-requisite for using
# login().
diff --git a/lib/lp/app/validators/tests/validation.txt b/lib/lp/app/validators/tests/validation.txt
index b7ea290..a8d4a8e 100644
--- a/lib/lp/app/validators/tests/validation.txt
+++ b/lib/lp/app/validators/tests/validation.txt
@@ -38,14 +38,16 @@ fail for that specific series.
>>> nomination = bug.addNomination(no_priv, firefox.series[0])
>>> can_be_nominated_for_series(firefox.series)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError...
+ lp.app.validators.LaunchpadValidationError: ...
>>> can_be_nominated_for_series([firefox.series[0]])
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError...
+ lp.app.validators.LaunchpadValidationError: ...
It will pass for the rest of the series, though.
@@ -60,9 +62,10 @@ fail:
... 'foo.bar@xxxxxxxxxxxxx')
>>> nomination.approve(foo_bar)
>>> can_be_nominated_for_series([firefox.series[0]])
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError...
+ lp.app.validators.LaunchpadValidationError: ...
The validation message will contain all the series that can't be
nominated.
@@ -70,10 +73,10 @@ nominated.
>>> trunk_nomination = bug.addNomination(
... no_priv, firefox.series[1])
>>> can_be_nominated_for_series(firefox.series)
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError:
- This bug has already been nominated for these series: 1.0, Trunk
+ lp.app.validators.LaunchpadValidationError: This bug has already been nominated for these series: 1.0, Trunk
Declined nominations can be re-nominated.
@@ -112,7 +115,7 @@ Or a name that is not already in use.
But you can't use Mark's name, of course. ;)
>>> field.validate(u'mark')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError: ...mark is already in use by another
- person or team...
+ lp.app.validators.LaunchpadValidationError: ...mark is already in use by another person or team...
diff --git a/lib/lp/app/validators/url.py b/lib/lp/app/validators/url.py
index 08511cf..bf4d998 100644
--- a/lib/lp/app/validators/url.py
+++ b/lib/lp/app/validators/url.py
@@ -154,9 +154,10 @@ def valid_webref(web_ref):
>>> valid_webref('sftp://example.com//absolute/path/maybe')
True
>>> valid_webref('other://example.com/moo')
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- LaunchpadValidationError: ...
+ lp.app.validators.LaunchpadValidationError: ...
"""
if validate_url(web_ref, ['http', 'https', 'ftp', 'sftp']):
# Allow ftp so valid_webref can be used for download_url, and so
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 0c317cb..dd8ef78 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -84,10 +84,12 @@ class DateTimeWidget(TextWidget):
>>> widget.request.form[widget.name] = '2005-07-03'
>>> widget.from_date = datetime(2006, 5, 23,
... tzinfo=pytz.timezone('UTC'))
- >>> print(widget.getInputValue()) #doctest: +ELLIPSIS
+ >>> print(widget.getInputValue())
+ ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: (...Please pick a date after 2006-05-22 17:00:00...)
+ zope.formlib.interfaces.WidgetInputError: (...Please pick a date after 2006-05-22 17:00:00...)
If the date provided is greater than from_date then the widget works as
expected.
@@ -100,10 +102,12 @@ class DateTimeWidget(TextWidget):
>>> widget.to_date = datetime(2008, 1, 26,
... tzinfo=pytz.timezone('UTC'))
- >>> print(widget.getInputValue()) #doctest: +ELLIPSIS
+ >>> print(widget.getInputValue())
+ ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: (...Please pick a date before 2008-01-25 16:00:00...)
+ zope.formlib.interfaces.WidgetInputError: (...Please pick a date before 2008-01-25 16:00:00...)
A datetime picker can be disabled initially:
@@ -380,10 +384,12 @@ class DateTimeWidget(TextWidget):
Invalid dates result in a ConversionError:
- >>> print(widget._parseInput('not a date')) #doctest: +ELLIPSIS
+ >>> print(widget._parseInput('not a date'))
+ ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- ConversionError: ('Invalid date value', ...)
+ zope.formlib.interfaces.ConversionError: ('Invalid date value', ...)
"""
if input == self._missing:
return self.context.missing_value
@@ -541,10 +547,12 @@ class DateWidget(DateTimeWidget):
Invalid dates result in a ConversionError:
- >>> print(widget._toFieldValue('not a date')) #doctest: +ELLIPSIS
+ >>> print(widget._toFieldValue('not a date'))
+ ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- ConversionError: ('Invalid date value', ...)
+ zope.formlib.interfaces.ConversionError: ('Invalid date value', ...)
"""
parsed = self._parseInput(input)
diff --git a/lib/lp/app/widgets/doc/announcement-date-widget.txt b/lib/lp/app/widgets/doc/announcement-date-widget.txt
index 3fc2dfd..39e69dc 100644
--- a/lib/lp/app/widgets/doc/announcement-date-widget.txt
+++ b/lib/lp/app/widgets/doc/announcement-date-widget.txt
@@ -56,12 +56,10 @@ If you choose to publish immediately, the date field must be empty.
>>> action_widget.request.form[action_widget.name] = 'immediately'
>>> date_widget.request.form[date_widget.name] = '2005-07-23'
>>> print(widget.getInputValue())
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.foo', u'Foo',
- LaunchpadValidationError(u'Please do not provide a date
- if you want to publish
- immediately.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.foo', u'Foo', LaunchpadValidationError(u'Please do not provide a date if you want to publish immediately.'))
If you choose to publish at a specific date in the future, the date field
must be filled.
@@ -69,7 +67,7 @@ must be filled.
>>> action_widget.request.form[action_widget.name] = 'specific'
>>> date_widget.request.form[date_widget.name] = ''
>>> print(widget.getInputValue())
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.foo', u'Foo',
- LaunchpadValidationError(u'Please provide a publication date.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.foo', u'Foo', LaunchpadValidationError(u'Please provide a publication date.'))
diff --git a/lib/lp/app/widgets/doc/image-widget.txt b/lib/lp/app/widgets/doc/image-widget.txt
index 23d4c86..fb740cf 100644
--- a/lib/lp/app/widgets/doc/image-widget.txt
+++ b/lib/lp/app/widgets/doc/image-widget.txt
@@ -266,12 +266,10 @@ dimensions smaller than person_mugshot.dimensions, it must be rejected.
>>> form = {'field.mugshot.action': 'change', 'field.mugshot.image': logo}
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'\nThis image is not exactly
- 192x192\npixels in size.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'\nThis image is not exactly 192x192\npixels in size.'))
This is what we see when the image is the correct dimensions, and within
the max_size:
@@ -312,13 +310,10 @@ image, we'll get a validation error.
>>> _ = mugshot.seek(0)
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'\nThis image exceeds the
- maximum allowed size in
- bytes.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'\nThis image exceeds the maximum allowed size in bytes.'))
A similar error will be raised if the image's dimensions are bigger than
the maximum we allow.
@@ -328,23 +323,19 @@ the maximum we allow.
>>> _ = mugshot.seek(0)
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'\nThis image is not exactly
- 191x193\npixels in size.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'\nThis image is not exactly 191x193\npixels in size.'))
>>> person_mugshot.dimensions = (image.size[0] + 1, image.size[1] - 1)
>>> _ = mugshot.seek(0)
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'\nThis image is not exactly
- 193x191\npixels in size.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'\nThis image is not exactly 193x191\npixels in size.'))
Finally, if the user specifies the 'change' action they must also provide
a file to be uploaded.
@@ -352,12 +343,10 @@ a file to be uploaded.
>>> form = {'field.mugshot.action': 'change', 'field.mugshot.image': ''}
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'Please specify the image you
- want to use.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'Please specify the image you want to use.'))
Non-exact Image Dimensions
@@ -376,12 +365,10 @@ by setting the exact_dimensions attribute of the field to False:
... 'field.mugshot.image': mugshot}
>>> widget = ImageChangeWidget(
... person_mugshot, LaunchpadTestRequest(form=form), edit_style)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.mugshot', u'Mugshot',
- LaunchpadValidationError(u'\nThis image is larger than
- 64x64\npixels in size.'))
+ zope.formlib.interfaces.WidgetInputError: ('field.mugshot', u'Mugshot', LaunchpadValidationError(u'\nThis image is larger than 64x64\npixels in size.'))
If the image is smaller than the dimensions, the input validates:
diff --git a/lib/lp/app/widgets/doc/project-scope-widget.txt b/lib/lp/app/widgets/doc/project-scope-widget.txt
index 35c3ed0..4c3980f 100644
--- a/lib/lp/app/widgets/doc/project-scope-widget.txt
+++ b/lib/lp/app/widgets/doc/project-scope-widget.txt
@@ -51,10 +51,10 @@ If the widget is required, getInputValue() raises UnexpectedFormData if
there is no input.
>>> widget.required = True
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- UnexpectedFormData: No valid option was selected.
+ lp.app.errors.UnexpectedFormData: No valid option was selected.
By default, the 'All projects' scope is selected:
@@ -117,11 +117,10 @@ raised:
>>> widget.hasInput()
True
>>> selected_scope = widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.scope', u'',
- LaunchpadValidationError(u'There is no project named
- 'invalid' registered in Launchpad'))
+ zope.formlib.interfaces.WidgetInputError: ('field.scope', u'', LaunchpadValidationError(u'There is no project named 'invalid' registered in Launchpad'))
The same error text is returned by error():
@@ -137,10 +136,10 @@ If no project name is given at all, a widget error is also raised:
>>> widget.hasInput()
True
>>> selected_scope = widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.scope', u'',
- LaunchpadValidationError(u'Please enter a project name'))
+ zope.formlib.interfaces.WidgetInputError: ('field.scope', u'', LaunchpadValidationError(u'Please enter a project name'))
>>> print(widget.error())
Please enter a project name
@@ -152,10 +151,10 @@ If no project name is given at all, a widget error is also raised:
>>> widget.hasInput()
True
>>> selected_scope = widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field.scope', u'',
- LaunchpadValidationError(u'Please enter a project name'))
+ zope.formlib.interfaces.WidgetInputError: ('field.scope', u'', LaunchpadValidationError(u'Please enter a project name'))
>>> print(widget.error())
Please enter a project name
diff --git a/lib/lp/app/widgets/doc/stripped-text-widget.txt b/lib/lp/app/widgets/doc/stripped-text-widget.txt
index 9d363c2..c98c6e2 100644
--- a/lib/lp/app/widgets/doc/stripped-text-widget.txt
+++ b/lib/lp/app/widgets/doc/stripped-text-widget.txt
@@ -60,7 +60,7 @@ provided.
... __name__=six.ensure_str('field'), title=u'Title', required=True)
>>> request = LaunchpadTestRequest(form={'field.field':' \n '})
>>> widget = StrippedTextWidget(required_field, request)
- >>> widget.getInputValue()
+ >>> widget.getInputValue() # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- WidgetInputError: ('field', u'Title', RequiredMissing('field'))
+ zope.formlib.interfaces.WidgetInputError: ('field', u'Title', RequiredMissing('field'))
diff --git a/lib/lp/app/widgets/doc/zope3-widgets-use-form-ng.txt b/lib/lp/app/widgets/doc/zope3-widgets-use-form-ng.txt
index 393f06b..241526d 100644
--- a/lib/lp/app/widgets/doc/zope3-widgets-use-form-ng.txt
+++ b/lib/lp/app/widgets/doc/zope3-widgets-use-form-ng.txt
@@ -40,18 +40,20 @@ contains more than one argument.
>>> Zope3WidgetsUseIBrowserFormNGMonkeyPatch.install()
>>> int_widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- UnexpectedFormData: ...
+ lp.app.errors.UnexpectedFormData: ...
>>> text_field = schema.TextLine(__name__='text')
>>> request = LaunchpadTestRequest(
... form={'field.text': ['two', 'strings']})
>>> text_widget = TextWidget(text_field, request)
>>> text_widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- UnexpectedFormData: ...
+ lp.app.errors.UnexpectedFormData: ...
Since the SimpleInputWidget._getFormValue is overridden, it also works
with the Launchpad widgets extending it:
@@ -60,9 +62,10 @@ with the Launchpad widgets extending it:
>>> stripped_text_widget = StrippedTextWidget(text_field, request)
>>> stripped_text_widget.getInputValue()
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
Traceback (most recent call last):
...
- UnexpectedFormData: ...
+ lp.app.errors.UnexpectedFormData: ...
Widgets expecting a variable number of values continue to work
with this monkey patch:
diff --git a/lib/lp/app/widgets/tests/test_widget_doctests.py b/lib/lp/app/widgets/tests/test_widget_doctests.py
index cbad1dc..5bcf0f0 100644
--- a/lib/lp/app/widgets/tests/test_widget_doctests.py
+++ b/lib/lp/app/widgets/tests/test_widget_doctests.py
@@ -6,12 +6,17 @@ __metaclass__ = type
import doctest
import unittest
+from zope.testing.renormalizing import OutputChecker
+
from lp.testing.layers import DatabaseFunctionalLayer
def test_suite():
suite = unittest.TestSuite()
suite.layer = DatabaseFunctionalLayer
- suite.addTest(doctest.DocTestSuite('lp.app.widgets.textwidgets'))
- suite.addTest(doctest.DocTestSuite('lp.app.widgets.date'))
+ checker = OutputChecker()
+ suite.addTest(doctest.DocTestSuite(
+ 'lp.app.widgets.textwidgets', checker=checker))
+ suite.addTest(doctest.DocTestSuite(
+ 'lp.app.widgets.date', checker=checker))
return suite
diff --git a/lib/lp/app/widgets/textwidgets.py b/lib/lp/app/widgets/textwidgets.py
index f4dd12c..fa666b4 100644
--- a/lib/lp/app/widgets/textwidgets.py
+++ b/lib/lp/app/widgets/textwidgets.py
@@ -56,6 +56,93 @@ class NoneableTextWidget(StrippedTextWidget):
return value
+<<<<<<< lib/lp/app/widgets/textwidgets.py
+=======
+class LocalDateTimeWidget(TextWidget):
+ """A datetime widget that uses a particular time zone."""
+
+ timeZoneName = 'UTC'
+
+ def _toFieldValue(self, input):
+ """Convert a string to a datetime value.
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> from zope.schema import Field
+ >>> field = Field(__name__='foo', title=u'Foo')
+ >>> widget = LocalDateTimeWidget(field, TestRequest())
+
+ The widget converts an empty string to the missing value:
+
+ >>> widget._toFieldValue('') == field.missing_value
+ True
+
+ By default, the date is interpreted as UTC:
+
+ >>> print(widget._toFieldValue('2006-01-01 12:00:00'))
+ 2006-01-01 12:00:00+00:00
+
+ But it will handle other time zones:
+
+ >>> widget.timeZoneName = 'Australia/Perth'
+ >>> print(widget._toFieldValue('2006-01-01 12:00:00'))
+ 2006-01-01 12:00:00+08:00
+
+ Invalid dates result in a ConversionError:
+
+ >>> print(widget._toFieldValue('not a date'))
+ ... # doctest: +NORMALIZE_WHITESPACE,+ELLIPSIS
+ ... # doctest: +IGNORE_EXCEPTION_MODULE_IN_PYTHON2
+ Traceback (most recent call last):
+ ...
+ zope.formlib.interfaces.ConversionError: ('Invalid date value', ...)
+ """
+ if input == self._missing:
+ return self.context.missing_value
+ try:
+ year, month, day, hour, minute, second, dummy_tz = parse(input)
+ second, micro = divmod(second, 1.0)
+ micro = round_half_up(micro * 1000000)
+ dt = datetime.datetime(year, month, day,
+ hour, minute, int(second), micro)
+ except (DateTimeError, ValueError, IndexError) as v:
+ raise ConversionError('Invalid date value', v)
+ tz = pytz.timezone(self.timeZoneName)
+ return tz.localize(dt)
+
+ def _toFormValue(self, value):
+ """Convert a date to its string representation.
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> from zope.schema import Field
+ >>> field = Field(__name__='foo', title=u'Foo')
+ >>> widget = LocalDateTimeWidget(field, TestRequest())
+
+ The 'missing' value is converted to an empty string:
+
+ >>> print(widget._toFormValue(field.missing_value))
+ <BLANKLINE>
+
+ Dates are displayed without an associated time zone:
+
+ >>> dt = datetime.datetime(2006, 1, 1, 12, 0, 0,
+ ... tzinfo=pytz.timezone('UTC'))
+ >>> widget._toFormValue(dt)
+ '2006-01-01 12:00:00'
+
+ The date value will be converted to the widget's time zone
+ before being displayed:
+
+ >>> widget.timeZoneName = 'Australia/Perth'
+ >>> widget._toFormValue(dt)
+ '2006-01-01 20:00:00'
+ """
+ if value == self.context.missing_value:
+ return self._missing
+ tz = pytz.timezone(self.timeZoneName)
+ return value.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S')
+
+
+>>>>>>> lib/lp/app/widgets/textwidgets.py
class URIWidget(StrippedTextWidget):
"""A widget that represents a URI."""