Merge ~andrey-fedoseev/launchpad:mypy into launchpad:master

Proposed by Andrey Fedoseev
Status: Merged
Approved by: Andrey Fedoseev
Approved revision: 1d00148d82ddb1d8f37d755f2b29bb255d2d1b25
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~andrey-fedoseev/launchpad:mypy
Merge into: launchpad:master
Diff against target: 2830 lines (+604/-293)
92 files modified
lib/lp/answers/adapters.py (+3/-2)
lib/lp/answers/browser/faqcollection.py (+7/-2)
lib/lp/answers/browser/question.py (+15/-6)
lib/lp/answers/browser/questiontarget.py (+12/-4)
lib/lp/answers/browser/tests/test_question.py (+3/-2)
lib/lp/answers/browser/tests/test_views.py (+3/-2)
lib/lp/answers/mail/__init__.py (+2/-1)
lib/lp/answers/model/question.py (+3/-3)
lib/lp/answers/security.py (+3/-1)
lib/lp/answers/tests/test_question_workflow.py (+3/-2)
lib/lp/answers/tests/test_questiontarget.py (+3/-2)
lib/lp/app/__init__.py (+7/-4)
lib/lp/app/browser/badge.py (+2/-16)
lib/lp/app/browser/interfaces.py (+18/-0)
lib/lp/app/browser/launchpadform.py (+15/-8)
lib/lp/app/browser/multistep.py (+2/-1)
lib/lp/app/browser/root.py (+2/-1)
lib/lp/app/browser/tales.py (+1/-1)
lib/lp/app/browser/tests/test_vocabulary.py (+5/-3)
lib/lp/app/browser/tests/test_webservice.py (+2/-1)
lib/lp/app/browser/webservice.py (+3/-2)
lib/lp/app/doc/badges.rst (+3/-2)
lib/lp/app/security.py (+4/-3)
lib/lp/app/tests/test_yuitests.py (+3/-2)
lib/lp/app/utilities/celebrities.py (+4/-1)
lib/lp/app/validators/__init__.py (+5/-16)
lib/lp/app/validators/interfaces.py (+18/-0)
lib/lp/app/widgets/date.py (+2/-8)
lib/lp/app/widgets/tests/test_itemswidgets.py (+2/-1)
lib/lp/archivepublisher/artifactory.py (+11/-5)
lib/lp/archivepublisher/customupload.py (+1/-1)
lib/lp/archivepublisher/debversion.py (+1/-2)
lib/lp/archivepublisher/diskpool.py (+16/-2)
lib/lp/archivepublisher/security.py (+3/-2)
lib/lp/archiveuploader/dscfile.py (+8/-10)
lib/lp/archiveuploader/tests/test_buildduploads.py (+3/-3)
lib/lp/blueprints/browser/specification.py (+11/-4)
lib/lp/blueprints/browser/specificationbranch.py (+6/-2)
lib/lp/blueprints/browser/specificationsubscription.py (+15/-7)
lib/lp/blueprints/browser/sprint.py (+7/-4)
lib/lp/blueprints/browser/sprintattendance.py (+3/-1)
lib/lp/blueprints/mail/__init__.py (+2/-1)
lib/lp/bugs/browser/bug.py (+19/-8)
lib/lp/bugs/browser/bugalsoaffects.py (+6/-3)
lib/lp/bugs/browser/bugbranch.py (+12/-4)
lib/lp/bugs/browser/bugnomination.py (+3/-1)
lib/lp/bugs/browser/bugsubscription.py (+12/-4)
lib/lp/bugs/browser/bugsubscriptionfilter.py (+5/-3)
lib/lp/bugs/browser/bugsupervisor.py (+3/-1)
lib/lp/bugs/browser/bugtask.py (+9/-1)
lib/lp/bugs/browser/bugtracker.py (+3/-1)
lib/lp/bugs/browser/bugwatch.py (+6/-2)
lib/lp/bugs/browser/cve.py (+6/-2)
lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py (+2/-2)
lib/lp/bugs/browser/tests/test_bugtarget_filebug.py (+2/-2)
lib/lp/bugs/externalbugtracker/base.py (+2/-2)
lib/lp/bugs/externalbugtracker/github.py (+1/-20)
lib/lp/bugs/externalbugtracker/interfaces.py (+22/-0)
lib/lp/bugs/externalbugtracker/tests/test_github.py (+1/-1)
lib/lp/bugs/interfaces/bugnotification.py (+2/-2)
lib/lp/bugs/model/bugtarget.py (+3/-3)
lib/lp/bugs/model/tests/test_bugtask.py (+2/-2)
lib/lp/bugs/model/tests/test_bugtask_status.py (+2/-2)
lib/lp/bugs/scripts/checkwatches/base.py (+3/-3)
lib/lp/bugs/scripts/checkwatches/core.py (+2/-1)
lib/lp/bugs/scripts/debbugs.py (+4/-1)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+10/-3)
lib/lp/bugs/tests/externalbugtracker.py (+6/-5)
lib/lp/bugs/tests/test_buglinktarget.py (+3/-2)
lib/lp/bugs/tests/test_bugnomination.py (+2/-2)
lib/lp/bugs/tests/test_bugsearch_conjoined.py (+3/-2)
lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py (+2/-2)
lib/lp/bugs/tests/test_bugtarget.py (+3/-3)
lib/lp/bugs/tests/test_bugtaskflat_triggers.py (+30/-29)
lib/lp/bugs/tests/test_bugtracker_components.py (+5/-4)
lib/lp/bugs/tests/test_bugwatch.py (+8/-6)
lib/lp/bugs/tests/test_bzremotecomponentfinder.py (+3/-3)
lib/lp/bugs/tests/test_externalbugtracker.py (+3/-2)
lib/lp/bugs/tests/test_structuralsubscription.py (+2/-2)
lib/lp/bugs/tests/test_yuitests.py (+3/-2)
lib/lp/registry/browser/product.py (+2/-1)
lib/lp/services/feeds/browser.py (+6/-1)
lib/lp/services/looptuner.py (+1/-1)
lib/lp/services/mail/commands.py (+1/-1)
lib/lp/services/scripts/base.py (+2/-2)
lib/lp/services/webapp/breadcrumb.py (+1/-1)
lib/lp/services/webapp/menu.py (+3/-2)
lib/lp/services/webapp/publisher.py (+8/-2)
lib/lp/services/webapp/vocabulary.py (+7/-2)
lib/lp/testing/__init__.py (+9/-1)
pyproject.toml (+100/-0)
requirements/types.txt (+7/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Jürgen Gmach Approve
Review via email: mp+425377@code.launchpad.net

Commit message

Prepare some packages for type checking with `mypy`:

- lp.answers
- lp.app
- lp.archivepublisher
- lp.archiveuploader
- lp.blueprints
- lp.bugs

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

How are you running `mypy` on this - do you have a `tox` or `pre-commit` setup or something? What Python version are you running it on? I guess you run it outside the normal Launchpad virtualenv due to Python version constraints? I'd like to be able to experiment to see whether any suggestions I make are remotely sensible.

Most of my comments here are questions, and generally apply to several similar places in this branch; I haven't repeated them since I expect in some cases the answer will be an explanation of why I'm misunderstanding something rather than a change to the branch.

review: Needs Information
Revision history for this message
Colin Watson (cjwatson) wrote :

BTW, thanks - this looks like a great start, and I'm excited for it!

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

> How are you running `mypy` on this - do you have a `tox` or `pre-commit` setup or something? What Python version are you running it on? I guess you run it outside the normal Launchpad virtualenv due to Python version constraints?

I actually installed it to the LP virtualenv, I'm running LP on 18.04 locally (and it's working). I run it directly, with `env/bin/mypy path/to/check`. In fact, I didn't realize that it's 3.6+ only.

I'm going to push an update that applies black formatting to `lp.bugs`, I expect this may break some of the inline comments.

Revision history for this message
Colin Watson (cjwatson) wrote :

I guess we won't be able to run this in any kind of reasonable CI setup until we're on >= bionic (well, we could maybe figure out how to run it in the on-demand bionic buildbot instance ...), but I don't think that needs to block this.

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

Colin,

I've updated the MR based on your comments.

Also, we can install `mypy` in `xenial`, the latest version that supports Python 3.5 is 0.910 which is quite recent actually.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Which command do you use to run mypy?

I am only able to check single files.

```
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy --version
mypy 0.910
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/answers
There are no .py[i] files in directory 'lib/lp/answers'
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/answers/browser/
There are no .py[i] files in directory 'lib/lp/answers/browser'
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/answers/browser/question.py
Success: no issues found in 1 source file
```

When I go into the directory and run it from there, I get tons of errors:

```
jugmac00@lpdev:~/launchpad/launchpad/lib/lp/answers$ ../../../env/bin/mypy .
enums.py:21: error: Skipping analyzing "lazr.enum": found module but no type hints or library stubs
interfaces/faqcollection.py:13: error: Skipping analyzing "lazr.enum": found module but no type hints or library stubs
interfaces/faqcollection.py:14: error: Skipping analyzing "zope.interface": found module but no type hints or library stubs
interfaces/faqcollection.py:15: error: Skipping analyzing "zope.schema": found module but no type hints or library stubs
interfaces/faqtarget.py:11: error: Skipping analyzing "lazr.restful.declarations": found module but no type hints or library stubs
errors.py:16: error: Skipping analyzing "lazr.restful.declarations": found module but no type hints or library stubs
interfaces/questionmessage.py:10: error: Skipping analyzing "lazr.restful.declarations": found module but no type hints or library stubs
...
```

@Andrey - could you please give me the exact command(s) to run so I can run mypy/verify this MP? Thanks!

review: Needs Information
Revision history for this message
Jürgen Gmach (jugmac00) wrote :
Download full text (3.4 KiB)

Ah, ok... figured it out...

```
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/answers/*.py
lib/lp/answers/security.py:6: error: Need type comment for "__all__" (hint: "__all__ = ... # type: List[<type>]")
Found 1 error in 1 file (checked 11 source files)
```

```
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/archivepublisher/*.py
lib/lp/archivepublisher/diskpool.py:36: error: Unsupported operand types for / ("Path" and "Path")
lib/lp/archivepublisher/diskpool.py:165: error: Unsupported operand types for / ("Path" and "Path")
lib/lp/archivepublisher/diskpool.py:457: error: Unsupported operand types for / ("Path" and "Path")
lib/lp/archivepublisher/artifactory.py:18: error: Library stubs not installed for "requests" (or incompatible with Python 3.5)
lib/lp/archivepublisher/artifactory.py:18: note: Hint: "python3 -m pip install types-requests"
lib/lp/archivepublisher/artifactory.py:18: note: (or run "mypy --install-types" to install all missing stub packages)
lib/lp/archivepublisher/artifactory.py:18: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
lib/lp/archivepublisher/signing.py:27: error: Library stubs not installed for "pytz" (or incompatible with Python 3.5)
lib/lp/archivepublisher/security.py:6: error: Need type comment for "__all__" (hint: "__all__ = ... # type: List[<type>]")
lib/lp/archivepublisher/deathrow.py:10: error: Library stubs not installed for "pytz" (or incompatible with Python 3.5)
lib/lp/archivepublisher/deathrow.py:10: note: Hint: "python3 -m pip install types-pytz"
Found 7 errors in 5 files (checked 21 source files)
```

```
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/archiveuploader/*.py
lib/lp/archiveuploader/utils.py:40: error: Library stubs not installed for "six" (or incompatible with Python 3.5)
lib/lp/archiveuploader/nascentuploadfile.py:27: error: Library stubs not installed for "six" (or incompatible with Python 3.5)
lib/lp/archiveuploader/dscfile.py:27: error: Library stubs not installed for "six" (or incompatible with Python 3.5)
lib/lp/archiveuploader/changesfile.py:18: error: Library stubs not installed for "six" (or incompatible with Python 3.5)
lib/lp/archiveuploader/changesfile.py:18: note: Hint: "python3 -m pip install types-six"
lib/lp/archiveuploader/changesfile.py:18: note: (or run "mypy --install-types" to install all missing stub packages)
lib/lp/archiveuploader/changesfile.py:18: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
lib/lp/archiveuploader/uploadprocessor.py:53: error: Library stubs not installed for "pytz" (or incompatible with Python 3.5)
lib/lp/archiveuploader/uploadprocessor.py:53: note: Hint: "python3 -m pip install types-pytz"
Found 5 errors in 5 files (checked 15 source files)
```

```
jugmac00@lpdev:~/launchpad/launchpad$ env/bin/mypy lib/lp/blueprints/*.py
lib/lp/blueprints/security.py:182: error: Incompatible types in assignment (expression has type "Type[ISprint]", base class "ModerateByRegistryExpertsOrAdmins" defined the type as "None")
Found 1 error in 1 file (checked 7 source files)
```

Apart from fixing these errors, could you please update your commit message?

I...

Read more...

review: Needs Fixing
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Odd... looks like my version of mypy has issues with traversing recursive directories, but that should have been fixed in 0.800, see

https://github.com/python/mypy/issues/8548
https://github.com/python/mypy/pull/9614

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

Jürgen

I was using a `bionic` instance when I worked on this MP. I installed `mypy` with `pip` to `launchpad/env` and ran it like so:

```
env/bin/mypy ./lib/lp/answers/
```

I think, the easiest way to run it now is do it outside of the LXC container. Create a new virtualenv on your host system, install `mypy` and `requirements/types.txt` there.

I'm getting additional errors from the imported modules when I install it this way, so I have to run it like so:

```
mypy --follow-imports=silent ./lib/lp/answers
```

Note that the code has been changed since I prepared this MP more than a month ago, and new errors were introduced since then. I fixed a couple of them (related to __all__), but I'm not going to spend more time on fixing the rest. This issue is supposed to be time-boxed to 2 days and I'm already over that.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks for the feedback!

Running mypy from outside the LXC container is a brilliant idea!

This way we can easily create a `tox` `testenv` with the following content:

```
[testenv:mypy]
skip_install = true
deps =
    mypy
    -r requirements/types.txt
commands =
    mypy --follow-imports=silent \
    lib/lp/answers \
    lib/lp/app \
    lib/lp/archivepublisher \
    lib/lp/archiveuploader \
    lib/lp/blueprints \
    lib/lp/bugs
```

and run it via

```
tox -e mypy
```

which is much more comfortable than remembering this long command.

Running this command returns 40+ errors though.
https://pastebin.canonical.com/p/ckVb3YbXZK/

> Note that the code has been changed since I prepared this MP more than a month ago, and new errors were introduced since then. I fixed a couple of them (related to __all__), but I'm not going to spend more time on fixing the rest.

Isn't it the normal way of development when a rebase introduces new issues that the one proposing needs to fix it?

> This issue is supposed to be time-boxed to 2 days and I'm already over that.

I just saw the reference for the 2 days on Jira - this was written by Najam, who is no longer our manager. I never heard before that we time box issues that strictly, but I assume it was meant like "let's invest 2 days and let's see whether we can get this working" - and indeed, you got it working! Thanks for that. But it would be a pity to stop 1 m in front of the finish line.

I am looking forward a lot to having mypy in place, but imho we need a smooth start and that means no errors when running it.

Alternatively, when you really do not want to spend more time on this, please apply a reduced tox configuration like this:

```
[testenv:mypy]
skip_install = true
deps =
    mypy
    -r requirements/types.txt
commands =
    mypy --follow-imports=silent \
    lib/lp/answers \
    lib/lp/archivepublisher \
    lib/lp/archiveuploader
```

and update the commit message appropriately.

The reduced set works great!

```
❯ tox -e mypy -r
mypy recreate: /home/jugmac00/launchpad/launchpad/.tox/mypy
mypy installdeps: mypy, -rrequirements/types.txt
mypy installed: lxml-stubs==0.4.0,mypy==0.971,mypy-extensions==0.4.3,tomli==2.0.1,types-beautifulsoup4==4.9.0,types-Markdown==0.1.0,types-pytz==0.1.0,types-requests==0.1.13,types-simplejson==0.1.0,types-six==0.1.9,types-typing-extensions==3.7.3,typing_extensions==4.3.0
mypy run-test-pre: PYTHONHASHSEED='2699848255'
mypy run-test: commands[0] | mypy --follow-imports=silent lib/lp/answers lib/lp/archivepublisher lib/lp/archiveuploader
Success: no issues found in 160 source files
__________________________________________________________________________________________________ summary ___________________________________________________________________________________________________
  mypy: commands succeeded
  congratulations :)
```

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

> Running this command returns 40+ errors though.

See my comment above regarding the errors.

> this was written by Najam, who is no longer our manager. I never heard before that we time box issues that strictly

The time box was agreed upon during the standup, with everyone present there. This story is not meant to be have `mypy` fully working for the entire database.

At the recent planning meeting we have agreed that this MP will be merged in its current state.

Enabling `mypy` check through `pre-commit`, `tox` or whatever else will be a separate issue.

Revision history for this message
Jürgen Gmach (jugmac00) :
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

I see we have quite some different opinions on this MP - I'll remove my "needs fixing" so another team member can approve it.

Revision history for this message
Andrey Fedoseev (andrey-fedoseev) wrote :

Jürgen, I have addressed your inline comments.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Let's move this forward - I'll create a follow-up MP with the tox configuration so we can run mypy more easily.

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/answers/adapters.py b/lib/lp/answers/adapters.py
2index 3c7f037..8a3865a 100644
3--- a/lib/lp/answers/adapters.py
4+++ b/lib/lp/answers/adapters.py
5@@ -3,11 +3,12 @@
6
7 """Adapters used in the Answer Tracker."""
8
9-__all__ = []
10-
11+from typing import List
12
13 from lp.answers.interfaces.faqtarget import IFAQTarget
14
15+__all__ = [] # type: List[str]
16+
17
18 def question_to_questiontarget(question):
19 """Adapts an IQuestion to its IQuestionTarget."""
20diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py
21index f00b3b2..bf05b47 100644
22--- a/lib/lp/answers/browser/faqcollection.py
23+++ b/lib/lp/answers/browser/faqcollection.py
24@@ -8,8 +8,11 @@ __all__ = [
25 "SearchFAQsView",
26 ]
27
28+from typing import Type
29 from urllib.parse import urlencode
30
31+from zope.interface import Interface
32+
33 from lp import _
34 from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH, QuestionSort
35 from lp.answers.interfaces.faqcollection import (
36@@ -28,7 +31,7 @@ from lp.services.webapp.menu import enabled_with_permission
37 class FAQCollectionMenu(NavigationMenu):
38 """Base menu definition for `IFAQCollection`."""
39
40- usedfor = IFAQCollection
41+ usedfor = IFAQCollection # type: Type[Interface]
42 facet = "answers"
43 links = ["list_all", "create_faq"]
44
45@@ -82,7 +85,9 @@ class SearchFAQsView(LaunchpadFormView):
46 else:
47 return _("FAQs for $displayname", mapping=replacements)
48
49- label = page_title
50+ @property
51+ def label(self):
52+ return self.page_title
53
54 @property
55 def empty_listing_message(self):
56diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
57index 12a8fdb..7e5926a 100644
58--- a/lib/lp/answers/browser/question.py
59+++ b/lib/lp/answers/browser/question.py
60@@ -801,7 +801,9 @@ class QuestionChangeStatusView(LaunchpadFormView):
61 def next_url(self):
62 return canonical_url(self.context)
63
64- cancel_url = next_url
65+ @property
66+ def cancel_url(self):
67+ return self.next_url
68
69
70 class QuestionTargetWidget(LaunchpadTargetWidget):
71@@ -820,7 +822,6 @@ class QuestionEditView(LaunchpadEditFormView):
72 """View for editing a Question."""
73
74 schema = IQuestion
75- label = "Edit question"
76 field_names = [
77 "language",
78 "title",
79@@ -838,7 +839,9 @@ class QuestionEditView(LaunchpadEditFormView):
80 def page_title(self):
81 return "Edit question #%s details" % self.context.id
82
83- label = page_title
84+ @property
85+ def label(self):
86+ return self.page_title
87
88 def setUpFields(self):
89 """Select the subset of fields to display.
90@@ -874,7 +877,9 @@ class QuestionEditView(LaunchpadEditFormView):
91 def next_url(self):
92 return canonical_url(self.context)
93
94- cancel_url = next_url
95+ @property
96+ def cancel_url(self):
97+ return self.next_url
98
99
100 class QuestionRejectView(LaunchpadFormView):
101@@ -921,7 +926,9 @@ class QuestionRejectView(LaunchpadFormView):
102 def next_url(self):
103 return canonical_url(self.context)
104
105- cancel_url = next_url
106+ @property
107+ def cancel_url(self):
108+ return self.next_url
109
110
111 class LinkFAQMixin:
112@@ -1587,4 +1594,6 @@ class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
113 def next_url(self):
114 return canonical_url(self.context)
115
116- cancel_url = next_url
117+ @property
118+ def cancel_url(self):
119+ return self.next_url
120diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
121index 4e6f929..bbb6d26 100644
122--- a/lib/lp/answers/browser/questiontarget.py
123+++ b/lib/lp/answers/browser/questiontarget.py
124@@ -231,7 +231,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
125 else:
126 return _("Questions for ${context}", mapping=replacements)
127
128- label = page_title
129+ @property
130+ def label(self):
131+ return self.page_title
132
133 @property
134 def display_target_column(self):
135@@ -562,7 +564,9 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
136 mapping={"context": self.context.displayname},
137 )
138
139- label = page_title
140+ @property
141+ def label(self):
142+ return self.page_title
143
144 @property
145 def empty_listing_message(self):
146@@ -615,7 +619,9 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
147 mapping={"context": self.context.displayname},
148 )
149
150- label = page_title
151+ @property
152+ def label(self):
153+ return self.page_title
154
155 @property
156 def empty_listing_message(self):
157@@ -691,7 +697,9 @@ class QuestionCollectionByLanguageView(SearchQuestionsView):
158 else:
159 return _("${language} questions in ${context}", mapping=mapping)
160
161- label = page_title
162+ @property
163+ def label(self):
164+ return self.page_title
165
166 @property
167 def empty_listing_message(self):
168diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py
169index 0619fc3..c1583aa 100644
170--- a/lib/lp/answers/browser/tests/test_question.py
171+++ b/lib/lp/answers/browser/tests/test_question.py
172@@ -2,8 +2,7 @@
173 # GNU Affero General Public License version 3 (see the file LICENSE).
174
175 """Tests for the question module."""
176-
177-__all__ = []
178+from typing import List
179
180 from zope.security.proxy import removeSecurityProxy
181
182@@ -15,6 +14,8 @@ from lp.testing import TestCaseWithFactory, login_person, person_logged_in
183 from lp.testing.layers import DatabaseFunctionalLayer
184 from lp.testing.views import create_initialized_view
185
186+__all__ = [] # type: List[str]
187+
188
189 class TestQuestionAddView(TestCaseWithFactory):
190 """Verify the behaviour of the QuestionAddView."""
191diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py
192index 0fd7031..9dd7757 100644
193--- a/lib/lp/answers/browser/tests/test_views.py
194+++ b/lib/lp/answers/browser/tests/test_views.py
195@@ -3,14 +3,15 @@
196
197 """Test harness for Answer Tracker related unit tests."""
198
199-__all__ = []
200-
201 import unittest
202+from typing import List
203
204 from lp.testing import BrowserTestCase
205 from lp.testing.layers import DatabaseFunctionalLayer
206 from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
207
208+__all__ = [] # type: List[str]
209+
210
211 class TestEmailObfuscated(BrowserTestCase):
212 """Test for obfuscated emails on answers pages."""
213diff --git a/lib/lp/answers/mail/__init__.py b/lib/lp/answers/mail/__init__.py
214index 6b3f0d4..11b146c 100644
215--- a/lib/lp/answers/mail/__init__.py
216+++ b/lib/lp/answers/mail/__init__.py
217@@ -1,4 +1,5 @@
218 # Copyright 2010 Canonical Ltd. This software is licensed under the
219 # GNU Affero General Public License version 3 (see the file LICENSE).
220+from typing import List
221
222-__all__ = []
223+__all__ = [] # type: List[str]
224diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
225index b79c58c..a0089ee 100644
226--- a/lib/lp/answers/model/question.py
227+++ b/lib/lp/answers/model/question.py
228@@ -270,6 +270,7 @@ class Question(StormBase, BugLinkTargetMixin):
229 return list(self._messages)
230
231 # attributes
232+ @property
233 def target(self):
234 """See `IQuestion`."""
235 if self.product:
236@@ -279,7 +280,8 @@ class Question(StormBase, BugLinkTargetMixin):
237 else:
238 return self.distribution
239
240- def _settarget(self, question_target):
241+ @target.setter
242+ def target(self, question_target):
243 """See Question.target."""
244 if not IQuestionTarget.providedBy(question_target):
245 raise QuestionTargetError("The target must be an IQuestionTarget")
246@@ -300,8 +302,6 @@ class Question(StormBase, BugLinkTargetMixin):
247 "Unknown IQuestionTarget type of %s" % question_target
248 )
249
250- target = property(target, _settarget, doc=target.__doc__)
251-
252 @property
253 def followup_subject(self):
254 """See `IMessageTarget`."""
255diff --git a/lib/lp/answers/security.py b/lib/lp/answers/security.py
256index c10cc67..530b4f8 100644
257--- a/lib/lp/answers/security.py
258+++ b/lib/lp/answers/security.py
259@@ -3,7 +3,7 @@
260
261 """Security adapters for the answers package."""
262
263-__all__ = []
264+from typing import List
265
266 from lp.answers.interfaces.faq import IFAQ
267 from lp.answers.interfaces.faqtarget import IFAQTarget
268@@ -17,6 +17,8 @@ from lp.registry.interfaces.distributionsourcepackage import (
269 )
270 from lp.registry.security import EditByOwnersOrAdmins
271
272+__all__ = [] # type: List[str]
273+
274
275 class AdminQuestion(AuthorizationBase):
276 permission = "launchpad.Admin"
277diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py
278index d31d8aa..18dbc10 100644
279--- a/lib/lp/answers/tests/test_question_workflow.py
280+++ b/lib/lp/answers/tests/test_question_workflow.py
281@@ -9,10 +9,9 @@ but testing all the possible transitions makes the documentation more heavy
282 than necessary. This is tested here.
283 """
284
285-__all__ = []
286-
287 import traceback
288 from datetime import datetime, timedelta
289+from typing import List
290
291 from lazr.lifecycle.interfaces import IObjectCreatedEvent, IObjectModifiedEvent
292 from pytz import UTC
293@@ -40,6 +39,8 @@ from lp.testing import (
294 from lp.testing.fixture import ZopeEventHandlerFixture
295 from lp.testing.layers import DatabaseFunctionalLayer
296
297+__all__ = [] # type: List[str]
298+
299
300 class BaseAnswerTrackerWorkflowTestCase(TestCase):
301 """Base class for test cases related to the Answer Tracker workflow.
302diff --git a/lib/lp/answers/tests/test_questiontarget.py b/lib/lp/answers/tests/test_questiontarget.py
303index c8f56e8..dcaf579 100644
304--- a/lib/lp/answers/tests/test_questiontarget.py
305+++ b/lib/lp/answers/tests/test_questiontarget.py
306@@ -2,8 +2,7 @@
307 # GNU Affero General Public License version 3 (see the file LICENSE).
308
309 """Tests related to IQuestionTarget."""
310-
311-__all__ = []
312+from typing import List
313
314 from zope.component import getUtility
315 from zope.security.proxy import removeSecurityProxy
316@@ -18,6 +17,8 @@ from lp.testing import (
317 )
318 from lp.testing.layers import DatabaseFunctionalLayer
319
320+__all__ = [] # type: List[str]
321+
322
323 class QuestionTargetAnswerContactTestCase(TestCaseWithFactory):
324 """Tests for changing an answer contact."""
325diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py
326index 69ebb95..1df36b8 100644
327--- a/lib/lp/app/__init__.py
328+++ b/lib/lp/app/__init__.py
329@@ -8,15 +8,18 @@ together. As such, it can import from any modules, but nothing should import
330 from it.
331 """
332
333-__all__ = []
334+from typing import List
335
336-# Zope recently changed the behaviour of items widgets with regards to missing
337-# values, but they kindly left this global variable for you to monkey patch if
338-# you want the old behaviour, just like we do.
339 from zope.formlib import itemswidgets
340
341 # Load versioninfo.py so that we get errors on start-up rather than waiting
342 # for first page load.
343 import lp.app.versioninfo # noqa: F401
344
345+__all__ = [] # type: List[str]
346+
347+
348+# Zope recently changed the behaviour of items widgets with regards to missing
349+# values, but they kindly left this global variable for you to monkey patch if
350+# you want the old behaviour, just like we do.
351 itemswidgets.EXPLICIT_EMPTY_SELECTION = False
352diff --git a/lib/lp/app/browser/badge.py b/lib/lp/app/browser/badge.py
353index d3fd5a6..cf4db60 100644
354--- a/lib/lp/app/browser/badge.py
355+++ b/lib/lp/app/browser/badge.py
356@@ -11,12 +11,12 @@ Badges are shown in two main places:
357 __all__ = [
358 "Badge",
359 "HasBadgeBase",
360- "IHasBadges",
361 "STANDARD_BADGES",
362 ]
363
364-from zope.interface import Interface, implementer
365+from zope.interface import implementer
366
367+from lp.app.browser.interfaces import IHasBadges
368 from lp.services.privacy.interfaces import IObjectPrivacy
369
370
371@@ -109,20 +109,6 @@ STANDARD_BADGES = {
372 }
373
374
375-class IHasBadges(Interface):
376- """A method to determine visible badges.
377-
378- Badges are used to show connections between different content objects, for
379- example a BugBranch is a link between a bug and a branch. To represent
380- this link a bug has a branch badge, and the branch has a bug badge.
381-
382- Badges should honour the visibility of the linked objects.
383- """
384-
385- def getVisibleBadges():
386- """Return a list of `Badge` objects that the logged in user can see."""
387-
388-
389 @implementer(IHasBadges)
390 class HasBadgeBase:
391 """The standard base implementation for badge visibility.
392diff --git a/lib/lp/app/browser/interfaces.py b/lib/lp/app/browser/interfaces.py
393new file mode 100644
394index 0000000..32152d8
395--- /dev/null
396+++ b/lib/lp/app/browser/interfaces.py
397@@ -0,0 +1,18 @@
398+# Copyright 2022 Canonical Ltd. This software is licensed under the
399+# GNU Affero General Public License version 3 (see the file LICENSE).
400+
401+from zope.interface import Interface
402+
403+
404+class IHasBadges(Interface):
405+ """A method to determine visible badges.
406+
407+ Badges are used to show connections between different content objects, for
408+ example a BugBranch is a link between a bug and a branch. To represent
409+ this link a bug has a branch badge, and the branch has a bug badge.
410+
411+ Badges should honour the visibility of the linked objects.
412+ """
413+
414+ def getVisibleBadges():
415+ """Return a list of `Badge` objects that the logged-in user can see."""
416diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
417index 201c948..01ed055 100644
418--- a/lib/lp/app/browser/launchpadform.py
419+++ b/lib/lp/app/browser/launchpadform.py
420@@ -14,6 +14,8 @@ __all__ = [
421 "safe_action",
422 ]
423
424+from typing import List, Optional, Type
425+
426 import simplejson
427 import transaction
428 from lazr.lifecycle.event import ObjectModifiedEvent
429@@ -31,7 +33,7 @@ from zope.formlib.widgets import (
430 RadioWidget,
431 TextAreaWidget,
432 )
433-from zope.interface import classImplements, implementer, providedBy
434+from zope.interface import Interface, classImplements, implementer, providedBy
435 from zope.traversing.interfaces import ITraversable, TraversalError
436
437 from lp.services.webapp.escaping import html_escape
438@@ -60,15 +62,15 @@ class LaunchpadFormView(LaunchpadView):
439 prefix = "field"
440
441 # The form schema
442- schema = None
443+ schema = None # type: Type[Interface]
444 # Subset of fields to use
445- field_names = None
446+ field_names = None # type: Optional[List[str]]
447
448 # The next URL to redirect to on successful form submission
449- next_url = None
450+ next_url = None # type: Optional[str]
451 # The cancel URL is rendered as a Cancel link in the form
452 # macro if set in a derived class.
453- cancel_url = None
454+ cancel_url = None # type: Optional[str]
455
456 # The name of the widget that will receive initial focus in the form.
457 # By default, the first widget will receive focus. Set this to None
458@@ -87,7 +89,7 @@ class LaunchpadFormView(LaunchpadView):
459 # The for_input is passed through to create the fields. If this value
460 # is set to true in derived classes, then fields that are marked
461 # read only will have editable widgets created for them.
462- for_input = None
463+ for_input = None # type: Optional[bool]
464
465 def __init__(self, context, request):
466 LaunchpadView.__init__(self, context, request)
467@@ -568,8 +570,13 @@ class ReturnToReferrerMixin:
468 else:
469 return canonical_url(self.context)
470
471- next_url = _return_url
472- cancel_url = _return_url
473+ @property
474+ def next_url(self):
475+ return self._return_url
476+
477+ @property
478+ def cancel_url(self):
479+ return self._return_url
480
481
482 def has_structured_doc(field):
483diff --git a/lib/lp/app/browser/multistep.py b/lib/lp/app/browser/multistep.py
484index a6b7aee..a8df433 100644
485--- a/lib/lp/app/browser/multistep.py
486+++ b/lib/lp/app/browser/multistep.py
487@@ -8,6 +8,7 @@ __all__ = [
488 "StepView",
489 ]
490
491+from typing import List
492
493 from zope.formlib import form
494 from zope.formlib.widget import CustomWidgetFactory
495@@ -148,7 +149,7 @@ class StepView(LaunchpadFormView):
496 TextWidget, visible=False
497 )
498
499- _field_names = []
500+ _field_names = [] # type: List[str]
501 step_name = ""
502 main_action_label = "Continue"
503 next_step = None
504diff --git a/lib/lp/app/browser/root.py b/lib/lp/app/browser/root.py
505index 7d5acda..5dc4181 100644
506--- a/lib/lp/app/browser/root.py
507+++ b/lib/lp/app/browser/root.py
508@@ -10,6 +10,7 @@ __all__ = [
509
510 import re
511 import time
512+from typing import Any, List
513
514 import feedparser
515 import requests
516@@ -55,7 +56,7 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
517 """An view for the default view of the LaunchpadRoot."""
518
519 page_title = "Launchpad"
520- featured_projects = []
521+ featured_projects = [] # type: List[Any]
522 featured_projects_top = None
523
524 # Used by the footer to display the lp-arcana section.
525diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
526index bac5777..a413347 100644
527--- a/lib/lp/app/browser/tales.py
528+++ b/lib/lp/app/browser/tales.py
529@@ -32,7 +32,7 @@ from zope.traversing.interfaces import (
530 )
531
532 from lp import _
533-from lp.app.browser.badge import IHasBadges
534+from lp.app.browser.interfaces import IHasBadges
535 from lp.app.browser.stringformatter import FormattersAPI
536 from lp.app.enums import PRIVATE_INFORMATION_TYPES
537 from lp.app.interfaces.launchpad import (
538diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
539index 7925f86..35676e0 100644
540--- a/lib/lp/app/browser/tests/test_vocabulary.py
541+++ b/lib/lp/app/browser/tests/test_vocabulary.py
542@@ -4,6 +4,7 @@
543 """Test vocabulary adapters."""
544
545 from datetime import datetime
546+from typing import List
547 from urllib.parse import urlencode
548
549 import pytz
550@@ -23,6 +24,7 @@ from lp.app.errors import UnexpectedFormData
551 from lp.registry.interfaces.irc import IIrcIDSet
552 from lp.registry.interfaces.person import TeamMembershipPolicy
553 from lp.registry.interfaces.series import SeriesStatus
554+from lp.registry.model.person import Person
555 from lp.services.webapp.interfaces import ILaunchpadRoot
556 from lp.services.webapp.vocabulary import (
557 CountableIterator,
558@@ -506,16 +508,16 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
559
560 @implementer(IHugeVocabulary)
561 class TestPersonVocabulary:
562- test_persons = []
563+ test_persons = [] # type: List[Person]
564
565 @classmethod
566- def setTestData(cls, person_list):
567+ def setTestData(cls, person_list: List[Person]):
568 cls.test_persons = person_list
569
570 def __init__(self, context):
571 self.context = context
572
573- def toTerm(self, person):
574+ def toTerm(self, person: Person):
575 return SimpleTerm(person, person.name, person.displayname)
576
577 def searchForTerms(self, query=None, vocab_filter=None):
578diff --git a/lib/lp/app/browser/tests/test_webservice.py b/lib/lp/app/browser/tests/test_webservice.py
579index 2f3850d..b1f72ad 100644
580--- a/lib/lp/app/browser/tests/test_webservice.py
581+++ b/lib/lp/app/browser/tests/test_webservice.py
582@@ -54,7 +54,8 @@ class BaseMissingObjectWebService:
583 """Base test of NotFound errors for top-level webservice objects."""
584
585 layer = DatabaseFunctionalLayer
586- object_type = None
587+
588+ object_type = None # type: str
589
590 def test_object_not_found(self):
591 """Missing top-level objects generate 404s but not OOPS."""
592diff --git a/lib/lp/app/browser/webservice.py b/lib/lp/app/browser/webservice.py
593index 8439f7e..6fc2943 100644
594--- a/lib/lp/app/browser/webservice.py
595+++ b/lib/lp/app/browser/webservice.py
596@@ -2,8 +2,7 @@
597 # GNU Affero General Public License version 3 (see the file LICENSE).
598
599 """Adapters for registry objects for the webservice."""
600-
601-__all__ = []
602+from typing import List
603
604 from lazr.restful.interfaces import (
605 IFieldHTMLRenderer,
606@@ -17,6 +16,8 @@ from zope.schema.interfaces import IText
607 from lp.app.browser.stringformatter import FormattersAPI
608 from lp.app.browser.tales import format_link
609
610+__all__ = [] # type: List[str]
611+
612
613 @component.adapter(Interface, IReference, IWebServiceClientRequest)
614 @implementer(IFieldHTMLRenderer)
615diff --git a/lib/lp/app/doc/badges.rst b/lib/lp/app/doc/badges.rst
616index dc2dee4..a152df9 100644
617--- a/lib/lp/app/doc/badges.rst
618+++ b/lib/lp/app/doc/badges.rst
619@@ -85,7 +85,8 @@ implementation of IHasBadges. HasBadgeBase is also a default adapter
620 for Interface, which just provides the privacy badge.
621
622 >>> from zope.interface import Interface, Attribute, implementer
623- >>> from lp.app.browser.badge import IHasBadges, HasBadgeBase
624+ >>> from lp.app.browser.interfaces import IHasBadges
625+ >>> from lp.app.browser.badge import HasBadgeBase
626 >>> from lp.testing import verifyObject
627 >>> @implementer(Interface)
628 ... class PrivateClass:
629@@ -196,7 +197,7 @@ IHasBadges. Here is the sample from the branch.zcml to illustrate.
630
631 <adapter
632 for="lp.code.interfaces.branch.IBranch"
633- provides="lp.app.browser.badge.IHasBadges"
634+ provides="lp.app.browser.interfaces.IHasBadges"
635 factory="lp.code.browser.branchlisting.BranchBadges"
636 />
637
638diff --git a/lib/lp/app/security.py b/lib/lp/app/security.py
639index 30ec73b..37ccc27 100644
640--- a/lib/lp/app/security.py
641+++ b/lib/lp/app/security.py
642@@ -10,9 +10,10 @@ __all__ = [
643 ]
644
645 from itertools import repeat
646+from typing import Optional, Type
647
648 from zope.component import queryAdapter
649-from zope.interface import implementer
650+from zope.interface import Interface, implementer
651 from zope.security.permission import checkPermission
652
653 from lp.app.interfaces.security import IAuthorization
654@@ -20,8 +21,8 @@ from lp.app.interfaces.security import IAuthorization
655
656 @implementer(IAuthorization)
657 class AuthorizationBase:
658- permission = None
659- usedfor = None
660+ permission = None # type: Optional[str]
661+ usedfor = None # type: Optional[Type[Interface]]
662
663 def __init__(self, obj):
664 self.obj = obj
665diff --git a/lib/lp/app/tests/test_yuitests.py b/lib/lp/app/tests/test_yuitests.py
666index 371a972..1f8537f 100644
667--- a/lib/lp/app/tests/test_yuitests.py
668+++ b/lib/lp/app/tests/test_yuitests.py
669@@ -2,12 +2,13 @@
670 # GNU Affero General Public License version 3 (see the file LICENSE).
671
672 """Run YUI.test tests."""
673-
674-__all__ = []
675+from typing import List
676
677 from lp.testing import YUIUnitTestCase, build_yui_unittest_suite
678 from lp.testing.layers import YUITestLayer
679
680+__all__ = [] # type: List[str]
681+
682
683 class AppYUIUnitTestCase(YUIUnitTestCase):
684
685diff --git a/lib/lp/app/utilities/celebrities.py b/lib/lp/app/utilities/celebrities.py
686index 3df5989..3f54349 100644
687--- a/lib/lp/app/utilities/celebrities.py
688+++ b/lib/lp/app/utilities/celebrities.py
689@@ -5,6 +5,8 @@
690
691 __all__ = ["LaunchpadCelebrities"]
692
693+from typing import Set
694+
695 from zope.component import getUtility
696 from zope.interface import implementer
697
698@@ -102,7 +104,8 @@ class PersonCelebrityDescriptor(CelebrityDescriptor):
699 if a given person is a celebrity for special handling.
700 """
701
702- names = set() # Populated by the constructor.
703+ # Populated by the constructor.
704+ names = set() # type: Set[str]
705
706 def __init__(self, name):
707 PersonCelebrityDescriptor.names.add(name)
708diff --git a/lib/lp/app/validators/__init__.py b/lib/lp/app/validators/__init__.py
709index 1806143..c0fbb40 100644
710--- a/lib/lp/app/validators/__init__.py
711+++ b/lib/lp/app/validators/__init__.py
712@@ -12,20 +12,18 @@ See README.txt for discussion
713 from zope.formlib.exception import (
714 WidgetInputErrorView as Z3WidgetInputErrorView,
715 )
716-from zope.formlib.interfaces import IWidgetInputError
717-from zope.interface import Interface, implementer
718+from zope.interface import implementer
719 from zope.schema.interfaces import ValidationError
720
721+from lp.app.validators.interfaces import (
722+ ILaunchpadValidationError,
723+ ILaunchpadWidgetInputErrorView,
724+)
725 from lp.services.webapp.escaping import html_escape
726
727 __all__ = ["LaunchpadValidationError"]
728
729
730-class ILaunchpadValidationError(IWidgetInputError):
731- def snippet():
732- """Render as an HTML error message, as per IWidgetInputErrorView"""
733-
734-
735 @implementer(ILaunchpadValidationError)
736 class LaunchpadValidationError(ValidationError):
737 """A LaunchpadValidationError may be raised from a schema field
738@@ -69,15 +67,6 @@ class LaunchpadValidationError(ValidationError):
739 return self.snippet()
740
741
742-class ILaunchpadWidgetInputErrorView(Interface):
743- def snippet():
744- """Convert a widget input error to an html snippet
745-
746- If the error implements provides a snippet() method, just return it.
747- Otherwise, fall back to the default Z3 mechanism
748- """
749-
750-
751 @implementer(ILaunchpadWidgetInputErrorView)
752 class WidgetInputErrorView(Z3WidgetInputErrorView):
753 """Display an input error as a snippet of text.
754diff --git a/lib/lp/app/validators/interfaces.py b/lib/lp/app/validators/interfaces.py
755new file mode 100644
756index 0000000..ff631bd
757--- /dev/null
758+++ b/lib/lp/app/validators/interfaces.py
759@@ -0,0 +1,18 @@
760+# Copyright 2022 Canonical Ltd. This software is licensed under the
761+# GNU Affero General Public License version 3 (see the file LICENSE).
762+from zope.formlib.interfaces import IWidgetInputError
763+from zope.interface import Interface
764+
765+
766+class ILaunchpadValidationError(IWidgetInputError):
767+ def snippet():
768+ """Render as an HTML error message, as per IWidgetInputErrorView"""
769+
770+
771+class ILaunchpadWidgetInputErrorView(Interface):
772+ def snippet():
773+ """Convert a widget input error to an html snippet
774+
775+ If the error implements provides a snippet() method, just return it.
776+ Otherwise, fall back to the default Z3 mechanism
777+ """
778diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
779index 25b090d..66fb615 100644
780--- a/lib/lp/app/widgets/date.py
781+++ b/lib/lp/app/widgets/date.py
782@@ -158,8 +158,7 @@ class DateTimeWidget(TextWidget):
783
784 return [o.strip() for o in outputs]
785
786- # @property XXX: do as a property when we have python2.5 for tests of
787- # properties
788+ @property
789 def time_zone(self):
790 """The widget time zone.
791
792@@ -215,8 +214,6 @@ class DateTimeWidget(TextWidget):
793 ), "DateTime widget needs a time zone."
794 return self.system_time_zone
795
796- time_zone = property(time_zone, doc=time_zone.__doc__)
797-
798 @property
799 def time_zone_name(self):
800 """The name of the widget time zone for display in the widget."""
801@@ -251,8 +248,7 @@ class DateTimeWidget(TextWidget):
802 else:
803 return None
804
805- # @property XXX: do as a property when we have python2.5 for tests of
806- # properties
807+ @property
808 def daterange(self):
809 """The javascript variable giving the allowed date range to pick.
810
811@@ -318,8 +314,6 @@ class DateTimeWidget(TextWidget):
812 daterange += self.to_date.strftime("[%Y,%m,%d]]")
813 return daterange
814
815- daterange = property(daterange, doc=daterange.__doc__)
816-
817 def getInputValue(self):
818 """Return the date, if it is in the allowed date range."""
819 value = super().getInputValue()
820diff --git a/lib/lp/app/widgets/tests/test_itemswidgets.py b/lib/lp/app/widgets/tests/test_itemswidgets.py
821index 8d17443..57bdae9 100644
822--- a/lib/lp/app/widgets/tests/test_itemswidgets.py
823+++ b/lib/lp/app/widgets/tests/test_itemswidgets.py
824@@ -2,6 +2,7 @@
825 # GNU Affero General Public License version 3 (see the file LICENSE).
826
827 import doctest
828+from typing import Any, Type
829
830 from lazr.enum import EnumeratedType, Item
831 from lazr.enum._enum import DBEnumeratedType, DBItem
832@@ -31,7 +32,7 @@ class ItemWidgetTestCase(TestCaseWithFactory):
833
834 layer = DatabaseFunctionalLayer
835
836- WIDGET_CLASS = None
837+ WIDGET_CLASS = None # type: Type[Any]
838 SAFE_TERM = SimpleTerm("object-1", "token-1", "Safe title")
839 UNSAFE_TERM = SimpleTerm("object-2", "token-2", "<unsafe> &nbsp; title")
840
841diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
842index e39e6f5..dbf1342 100644
843--- a/lib/lp/archivepublisher/artifactory.py
844+++ b/lib/lp/archivepublisher/artifactory.py
845@@ -12,7 +12,7 @@ import os
846 import tempfile
847 from collections import defaultdict
848 from pathlib import Path, PurePath
849-from typing import List, Optional
850+from typing import TYPE_CHECKING, List, Optional
851 from urllib.parse import quote_plus
852
853 import requests
854@@ -42,7 +42,7 @@ def _path_for(
855 source_name: str,
856 source_version: str,
857 pub_file: IPackageReleaseFile,
858-) -> Path:
859+) -> ArtifactoryPath:
860 repository_format = archive.repository_format
861 if repository_format == ArchiveRepositoryFormat.DEBIAN:
862 path = rootpath / poolify(source_name)
863@@ -89,7 +89,7 @@ class ArtifactoryPoolEntry:
864 def debug(self, *args, **kwargs) -> None:
865 self.logger.debug(*args, **kwargs)
866
867- def pathFor(self, component: Optional[str] = None) -> Path:
868+ def pathFor(self, component: Optional[str] = None) -> ArtifactoryPath:
869 """Return the path for this file in the given component."""
870 # For Artifactory publication, we ignore the component. There's
871 # only marginal benefit in having it be explicitly represented in
872@@ -116,9 +116,13 @@ class ArtifactoryPoolEntry:
873 be set as the "launchpad.release-id" property to keep track of this.
874 """
875 if ISourcePackageReleaseFile.providedBy(pub_file):
876- return "source:%d" % pub_file.sourcepackagereleaseID
877+ return "source:{:d}".format(
878+ pub_file.sourcepackagereleaseID
879+ ) # type: ignore
880 elif IBinaryPackageFile.providedBy(pub_file):
881- return "binary:%d" % pub_file.binarypackagereleaseID
882+ return "binary:{:d}".format(
883+ pub_file.binarypackagereleaseID
884+ ) # type: ignore
885 else:
886 raise AssertionError("Unsupported file: %r" % pub_file)
887
888@@ -416,6 +420,8 @@ class ArtifactoryPool:
889 # the pool structure, and doing so would introduce significant
890 # complications in terms of having to keep track of components just
891 # in order to update an artifact's properties.
892+ if TYPE_CHECKING:
893+ assert pub_file is not None
894 return _path_for(
895 self.archive, self.rootpath, source_name, source_version, pub_file
896 )
897diff --git a/lib/lp/archivepublisher/customupload.py b/lib/lp/archivepublisher/customupload.py
898index 6193ca4..533e488 100644
899--- a/lib/lp/archivepublisher/customupload.py
900+++ b/lib/lp/archivepublisher/customupload.py
901@@ -113,7 +113,7 @@ class CustomUpload:
902 """Base class for custom upload handlers"""
903
904 # This should be set as a class property on each subclass.
905- custom_type = None
906+ custom_type = None # type: str
907
908 @classmethod
909 def publish(cls, packageupload, libraryfilealias, logger=None):
910diff --git a/lib/lp/archivepublisher/debversion.py b/lib/lp/archivepublisher/debversion.py
911index 7fd1ac5..ddbecd9 100644
912--- a/lib/lp/archivepublisher/debversion.py
913+++ b/lib/lp/archivepublisher/debversion.py
914@@ -13,14 +13,13 @@ special methods to make dealing with them sweet.
915 import re
916
917 from debian import changelog
918+from debian.changelog import VersionError
919
920 # Regular expressions make validating things easy
921 valid_epoch = re.compile(r"^[0-9]+$")
922 valid_upstream = re.compile(r"^[0-9][A-Za-z0-9+:.~-]*$")
923 valid_revision = re.compile(r"^[A-Za-z0-9+.~]+$")
924
925-VersionError = changelog.VersionError
926-
927
928 class BadInputError(VersionError):
929 pass
930diff --git a/lib/lp/archivepublisher/diskpool.py b/lib/lp/archivepublisher/diskpool.py
931index 7c01d46..61803e9 100644
932--- a/lib/lp/archivepublisher/diskpool.py
933+++ b/lib/lp/archivepublisher/diskpool.py
934@@ -12,7 +12,7 @@ import logging
935 import os
936 import tempfile
937 from pathlib import Path
938-from typing import Optional, Union
939+from typing import TYPE_CHECKING, Optional, Union
940
941 from lp.archivepublisher import HARDCODED_COMPONENT_ORDER
942 from lp.services.librarian.utils import copy_and_close, sha1_from_path
943@@ -189,9 +189,13 @@ class DiskPoolEntry:
944 if component in components:
945 return component
946
947+ return
948+
949 @cachedproperty
950 def file_hash(self) -> str:
951 """Return the SHA1 sum of this file."""
952+ if TYPE_CHECKING:
953+ assert self.file_component is not None
954 targetpath = self.pathFor(self.file_component)
955 return sha1_from_path(str(targetpath))
956
957@@ -294,6 +298,8 @@ class DiskPoolEntry:
958 # shuffle the symlinks, so that the one we want to delete will
959 # just be one of the links, and becomes safe.
960 targetcomponent = self.preferredComponent(remove=component)
961+ if TYPE_CHECKING:
962+ assert targetcomponent is not None
963 self._shufflesymlinks(targetcomponent)
964
965 return self._reallyRemove(component)
966@@ -322,6 +328,9 @@ class DiskPoolEntry:
967 def _shufflesymlinks(self, targetcomponent: str) -> None:
968 """Shuffle the symlinks for filename so that targetcomponent contains
969 the real file and the rest are symlinks to the right place..."""
970+ if TYPE_CHECKING:
971+ assert self.file_component is not None
972+
973 if targetcomponent == self.file_component:
974 # We're already in the right place.
975 return
976@@ -391,6 +400,8 @@ class DiskPoolEntry:
977 """
978 component = self.preferredComponent()
979 if not self.file_component == component:
980+ if TYPE_CHECKING:
981+ assert component is not None
982 self._shufflesymlinks(component)
983
984
985@@ -413,7 +424,6 @@ class DiskPool:
986 self.archive = archive
987 self.rootpath = Path(rootpath)
988 self.temppath = Path(temppath) if temppath is not None else None
989- self.entries = {}
990 self.logger = logger
991
992 def _getEntry(
993@@ -423,6 +433,8 @@ class DiskPool:
994 pub_file: IPackageReleaseFile,
995 ) -> DiskPoolEntry:
996 """Return a new DiskPoolEntry for the given source and file."""
997+ if TYPE_CHECKING:
998+ assert self.temppath is not None
999 return DiskPoolEntry(
1000 self.archive,
1001 self.rootpath,
1002@@ -443,6 +455,8 @@ class DiskPool:
1003 ) -> Path:
1004 """Return the path for the given pool file."""
1005 if file is None:
1006+ if TYPE_CHECKING:
1007+ assert pub_file is not None
1008 file = pub_file.libraryfile.filename
1009 if file is None:
1010 raise AssertionError("Must pass either pub_file or file")
1011diff --git a/lib/lp/archivepublisher/security.py b/lib/lp/archivepublisher/security.py
1012index b5be6c1..62a2d87 100644
1013--- a/lib/lp/archivepublisher/security.py
1014+++ b/lib/lp/archivepublisher/security.py
1015@@ -2,12 +2,13 @@
1016 # GNU Affero General Public License version 3 (see the file LICENSE).
1017
1018 """Security adapters for the archivepublisher package."""
1019-
1020-__all__ = []
1021+from typing import List
1022
1023 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfig
1024 from lp.security import AdminByAdminsTeam
1025
1026+__all__ = [] # type: List[str]
1027+
1028
1029 class ViewPublisherConfig(AdminByAdminsTeam):
1030 usedfor = IPublisherConfig
1031diff --git a/lib/lp/archiveuploader/dscfile.py b/lib/lp/archiveuploader/dscfile.py
1032index 9a00f35..d16de4c 100644
1033--- a/lib/lp/archiveuploader/dscfile.py
1034+++ b/lib/lp/archiveuploader/dscfile.py
1035@@ -394,14 +394,13 @@ class DSCFile(SourceUploadFile, SignableTagFile):
1036 all exceptions that are generated while processing DSC file checks.
1037 """
1038
1039- for error in SourceUploadFile.verify(self):
1040- yield error
1041+ yield from SourceUploadFile.verify(self)
1042
1043 # Check size and checksum of the DSC file itself
1044 try:
1045 self.checkSizeAndCheckSum()
1046- except UploadError as error:
1047- yield error
1048+ except UploadError as e:
1049+ yield e
1050
1051 try:
1052 raw_files = parse_and_merge_file_lists(self._dict, changes=False)
1053@@ -426,8 +425,8 @@ class DSCFile(SourceUploadFile, SignableTagFile):
1054 file_instance = DSCUploadedFile(
1055 filepath, hashes, size, self.policy, self.logger
1056 )
1057- except UploadError as error:
1058- yield error
1059+ except UploadError as e:
1060+ yield e
1061 else:
1062 files.append(file_instance)
1063 self.files = files
1064@@ -463,10 +462,10 @@ class DSCFile(SourceUploadFile, SignableTagFile):
1065 with warnings.catch_warnings():
1066 warnings.simplefilter("error")
1067 PkgRelation.parse_relations(field)
1068- except Warning as error:
1069+ except Warning as e:
1070 yield UploadError(
1071 "%s: invalid %s field; cannot be parsed by deb822: %s"
1072- % (self.filename, field_name, error)
1073+ % (self.filename, field_name, e)
1074 )
1075
1076 # Verify if version declared in changesfile is the same than that
1077@@ -478,8 +477,7 @@ class DSCFile(SourceUploadFile, SignableTagFile):
1078 % (self.filename, self.dsc_version, self.version)
1079 )
1080
1081- for error in self.checkFiles():
1082- yield error
1083+ yield from self.checkFiles()
1084
1085 def _getFileByName(self, filename):
1086 """Return the corresponding file reference in the policy context.
1087diff --git a/lib/lp/archiveuploader/tests/test_buildduploads.py b/lib/lp/archiveuploader/tests/test_buildduploads.py
1088index 8fced97..0425a43 100644
1089--- a/lib/lp/archiveuploader/tests/test_buildduploads.py
1090+++ b/lib/lp/archiveuploader/tests/test_buildduploads.py
1091@@ -25,9 +25,9 @@ from lp.testing.gpgkeys import import_public_test_keys
1092 class TestStagedBinaryUploadBase(TestUploadProcessorBase):
1093 name = "baz"
1094 version = "1.0-1"
1095- distribution_name = None
1096- distroseries_name = None
1097- pocket = None
1098+ distribution_name = None # type: str
1099+ distroseries_name = None # type: str
1100+ pocket = None # type: PackagePublishingPocket
1101 policy = "buildd"
1102 no_mails = True
1103
1104diff --git a/lib/lp/blueprints/browser/specification.py b/lib/lp/blueprints/browser/specification.py
1105index c5fea56..db7b465 100644
1106--- a/lib/lp/blueprints/browser/specification.py
1107+++ b/lib/lp/blueprints/browser/specification.py
1108@@ -39,6 +39,7 @@ __all__ = [
1109 import os
1110 from operator import attrgetter
1111 from subprocess import PIPE, Popen
1112+from typing import List
1113
1114 import six
1115 from lazr.restful.interface import copy_field, use_template
1116@@ -965,7 +966,9 @@ class SpecificationInformationTypeEditView(LaunchpadFormView):
1117 """Return the next URL to call when this call completes."""
1118 return None
1119
1120- cancel_url = next_url
1121+ @property
1122+ def cancel_url(self):
1123+ return self.next_url
1124
1125 @property
1126 def initial_values(self):
1127@@ -1043,7 +1046,7 @@ class SpecificationGoalDecideView(LaunchpadFormView):
1128 """
1129
1130 schema = Interface
1131- field_names = []
1132+ field_names = [] # type: List[str]
1133
1134 @property
1135 def label(self):
1136@@ -1061,7 +1064,9 @@ class SpecificationGoalDecideView(LaunchpadFormView):
1137 def next_url(self):
1138 return canonical_url(self.context)
1139
1140- cancel_url = next_url
1141+ @property
1142+ def cancel_url(self):
1143+ return self.next_url
1144
1145
1146 class ISpecificationRetargetingSchema(Interface):
1147@@ -1645,7 +1650,9 @@ class SpecificationLinkBranchView(LaunchpadFormView):
1148 def next_url(self):
1149 return canonical_url(self.context)
1150
1151- cancel_url = next_url
1152+ @property
1153+ def cancel_url(self):
1154+ return self.next_url
1155
1156
1157 class SpecificationSetView(AppFrontPageSearchView, HasSpecificationsView):
1158diff --git a/lib/lp/blueprints/browser/specificationbranch.py b/lib/lp/blueprints/browser/specificationbranch.py
1159index 5b54cba..1ee6fe9 100644
1160--- a/lib/lp/blueprints/browser/specificationbranch.py
1161+++ b/lib/lp/blueprints/browser/specificationbranch.py
1162@@ -9,6 +9,8 @@ __all__ = [
1163 "SpecificationBranchURL",
1164 ]
1165
1166+from typing import List
1167+
1168 from zope.interface import implementer
1169
1170 from lp import _
1171@@ -45,7 +47,7 @@ class SpecificationBranchStatusView(LaunchpadEditFormView):
1172 """Edit the summary of the SpecificationBranch link."""
1173
1174 schema = ISpecificationBranch
1175- field_names = []
1176+ field_names = [] # type: List[str]
1177 label = _("Delete link between specification and branch")
1178
1179 def initialize(self):
1180@@ -84,7 +86,9 @@ class BranchLinkToSpecificationView(LaunchpadFormView):
1181 def next_url(self):
1182 return canonical_url(self.context)
1183
1184- cancel_url = next_url
1185+ @property
1186+ def cancel_url(self):
1187+ return self.next_url
1188
1189 @action(_("Continue"), name="continue")
1190 def continue_action(self, action, data):
1191diff --git a/lib/lp/blueprints/browser/specificationsubscription.py b/lib/lp/blueprints/browser/specificationsubscription.py
1192index 79598e8..bb93933 100644
1193--- a/lib/lp/blueprints/browser/specificationsubscription.py
1194+++ b/lib/lp/blueprints/browser/specificationsubscription.py
1195@@ -9,6 +9,8 @@ __all__ = [
1196 "SpecificationSubscriptionEditView",
1197 ]
1198
1199+from typing import List
1200+
1201 from lazr.delegates import delegate_to
1202 from simplejson import dumps
1203 from zope.component import getUtility
1204@@ -37,10 +39,12 @@ class SpecificationSubscriptionAddView(LaunchpadFormView):
1205 label = "Subscribe to blueprint"
1206
1207 @property
1208- def cancel_url(self):
1209+ def next_url(self):
1210 return canonical_url(self.context)
1211
1212- next_url = cancel_url
1213+ @property
1214+ def cancel_url(self):
1215+ return self.next_url
1216
1217 def _subscribe(self, person, essential):
1218 self.context.subscribe(person, self.user, essential)
1219@@ -75,7 +79,7 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
1220 """Used to unsubscribe someone from a blueprint."""
1221
1222 schema = ISpecificationSubscription
1223- field_names = []
1224+ field_names = [] # type: List[str]
1225
1226 @property
1227 def label(self):
1228@@ -87,10 +91,12 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
1229 page_title = label
1230
1231 @property
1232- def cancel_url(self):
1233+ def next_url(self):
1234 return canonical_url(self.context.specification)
1235
1236- next_url = cancel_url
1237+ @property
1238+ def cancel_url(self):
1239+ return self.next_url
1240
1241 @action("Unsubscribe", name="unsubscribe")
1242 def unsubscribe_action(self, action, data):
1243@@ -116,10 +122,12 @@ class SpecificationSubscriptionEditView(LaunchpadEditFormView):
1244 return "Modify subscription to %s" % self.context.specification.title
1245
1246 @property
1247- def cancel_url(self):
1248+ def next_url(self):
1249 return canonical_url(self.context.specification)
1250
1251- next_url = cancel_url
1252+ @property
1253+ def cancel_url(self):
1254+ return self.next_url
1255
1256 @action(_("Change"), name="change")
1257 def change_action(self, action, data):
1258diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
1259index 4a2be23..047084a 100644
1260--- a/lib/lp/blueprints/browser/sprint.py
1261+++ b/lib/lp/blueprints/browser/sprint.py
1262@@ -25,6 +25,7 @@ __all__ = [
1263 import csv
1264 import io
1265 from collections import defaultdict
1266+from typing import List
1267
1268 import pytz
1269 from lazr.restful.utils import smartquote
1270@@ -401,7 +402,7 @@ class SprintDeleteView(LaunchpadFormView):
1271 """Form for deleting sprints."""
1272
1273 schema = ISprint
1274- field_names = []
1275+ field_names = [] # type: List[str]
1276
1277 @property
1278 def label(self):
1279@@ -430,7 +431,9 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
1280 'Review discussion topics for "%s" sprint' % self.context.title
1281 )
1282
1283- page_title = label
1284+ @property
1285+ def page_title(self):
1286+ return self.label
1287
1288 def initialize(self):
1289 self.status_message = None
1290@@ -591,13 +594,13 @@ class SprintSetNavigationMenu(RegistryCollectionActionMenuBase):
1291 """Action menu for sprints index."""
1292
1293 usedfor = ISprintSet
1294- links = (
1295+ links = [
1296 "register_team",
1297 "register_project",
1298 "register_sprint",
1299 "create_account",
1300 "view_all_sprints",
1301- )
1302+ ]
1303
1304 @enabled_with_permission("launchpad.View")
1305 def register_sprint(self):
1306diff --git a/lib/lp/blueprints/browser/sprintattendance.py b/lib/lp/blueprints/browser/sprintattendance.py
1307index acfa792..2964ffd 100644
1308--- a/lib/lp/blueprints/browser/sprintattendance.py
1309+++ b/lib/lp/blueprints/browser/sprintattendance.py
1310@@ -133,7 +133,9 @@ class BaseSprintAttendanceAddView(LaunchpadFormView):
1311 def next_url(self):
1312 return canonical_url(self.context)
1313
1314- cancel_url = next_url
1315+ @property
1316+ def cancel_url(self):
1317+ return self.next_url
1318
1319 _local_timeformat = "%H:%M on %A, %Y-%m-%d"
1320
1321diff --git a/lib/lp/blueprints/mail/__init__.py b/lib/lp/blueprints/mail/__init__.py
1322index 6b3f0d4..11b146c 100644
1323--- a/lib/lp/blueprints/mail/__init__.py
1324+++ b/lib/lp/blueprints/mail/__init__.py
1325@@ -1,4 +1,5 @@
1326 # Copyright 2010 Canonical Ltd. This software is licensed under the
1327 # GNU Affero General Public License version 3 (see the file LICENSE).
1328+from typing import List
1329
1330-__all__ = []
1331+__all__ = [] # type: List[str]
1332diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
1333index e3f3361..028b4e8 100644
1334--- a/lib/lp/bugs/browser/bug.py
1335+++ b/lib/lp/bugs/browser/bug.py
1336@@ -28,6 +28,7 @@ import json
1337 import re
1338 from email.mime.multipart import MIMEMultipart
1339 from email.mime.text import MIMEText
1340+from typing import Type
1341
1342 from lazr.enum import EnumeratedType, Item
1343 from lazr.lifecycle.event import ObjectModifiedEvent
1344@@ -188,7 +189,7 @@ class BugSetNavigation(Navigation):
1345 class BugContextMenu(ContextMenu):
1346 """Context menu of actions that can be performed upon a Bug."""
1347
1348- usedfor = IBug
1349+ usedfor = IBug # type: Type[Interface]
1350 links = [
1351 "editdescription",
1352 "markduplicate",
1353@@ -408,11 +409,13 @@ class MaloneView(LaunchpadFormView):
1354 schema = IFrontPageBugTaskSearch
1355 field_names = ["searchtext", "scope"]
1356
1357- # Test: standalone/xx-slash-malone-slash-bugs.rst
1358- error_message = None
1359-
1360 page_title = "Launchpad Bugs"
1361
1362+ # Test: standalone/xx-slash-malone-slash-bugs.rst
1363+ @property
1364+ def error_message(self):
1365+ return None
1366+
1367 @property
1368 def target_css_class(self):
1369 """The CSS class for used in the target widget."""
1370@@ -790,7 +793,9 @@ class BugEditViewBase(LaunchpadEditFormView):
1371 """Return the next URL to call when this call completes."""
1372 return canonical_url(self.context)
1373
1374- cancel_url = next_url
1375+ @property
1376+ def cancel_url(self):
1377+ return self.next_url
1378
1379
1380 class BugEditView(BugEditViewBase):
1381@@ -805,7 +810,9 @@ class BugEditView(BugEditViewBase):
1382 """The form label."""
1383 return "Edit details for bug #%d" % self.context.bug.id
1384
1385- page_title = label
1386+ @property
1387+ def page_title(self):
1388+ return self.label
1389
1390 @action("Change", name="change")
1391 def change_action(self, action, data):
1392@@ -876,7 +883,9 @@ class BugLockStatusEditView(LaunchpadEditFormView):
1393 return canonical_url(self.context)
1394 return None
1395
1396- cancel_url = next_url
1397+ @property
1398+ def cancel_url(self):
1399+ return self.next_url
1400
1401
1402 class BugMarkAsDuplicateView(BugEditViewBase):
1403@@ -999,7 +1008,9 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
1404 return canonical_url(self.context)
1405 return None
1406
1407- cancel_url = next_url
1408+ @property
1409+ def cancel_url(self):
1410+ return self.next_url
1411
1412 @property
1413 def initial_values(self):
1414diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py
1415index 4805360..0ec2af3 100644
1416--- a/lib/lp/bugs/browser/bugalsoaffects.py
1417+++ b/lib/lp/bugs/browser/bugalsoaffects.py
1418@@ -8,6 +8,7 @@ __all__ = [
1419 ]
1420
1421 from textwrap import dedent
1422+from typing import Tuple, Type
1423
1424 from lazr.enum import EnumeratedType, Item
1425 from lazr.lifecycle.event import ObjectCreatedEvent
1426@@ -212,7 +213,7 @@ class BugTaskCreationStep(AlsoAffectsStep):
1427
1428 initial_focus_widget = "bug_url"
1429 step_name = "specify_remote_bug_url"
1430- target_field_names = ()
1431+ target_field_names = () # type: Tuple[str, ...]
1432
1433 # This is necessary so that other views which dispatch work to this one
1434 # have access to the newly created task.
1435@@ -354,7 +355,9 @@ class IAddDistroBugTaskForm(IAddBugTaskForm):
1436
1437
1438 class DistroBugTaskCreationStep(BugTaskCreationStep):
1439- """Specialized BugTaskCreationStep for reporting a bug in a distro."""
1440+ """
1441+ Specialized BugTaskCreationStep for reporting a bug in a distribution.
1442+ """
1443
1444 @property
1445 def schema(self):
1446@@ -755,7 +758,7 @@ class BugTrackerCreationStep(AlsoAffectsStep):
1447 )
1448 step_name = "bugtracker_creation"
1449 main_action_label = "Register Bug Tracker and Add to Bug Report"
1450- _next_step = None
1451+ _next_step = None # type: Type[StepView]
1452
1453 def main_action(self, data):
1454 assert (
1455diff --git a/lib/lp/bugs/browser/bugbranch.py b/lib/lp/bugs/browser/bugbranch.py
1456index a2a650d..458e1e8 100644
1457--- a/lib/lp/bugs/browser/bugbranch.py
1458+++ b/lib/lp/bugs/browser/bugbranch.py
1459@@ -10,6 +10,8 @@ __all__ = [
1460 "BugBranchView",
1461 ]
1462
1463+from typing import List
1464+
1465 from lazr.restful.interfaces import IWebServiceClientRequest
1466 from zope.component import adapter, getMultiAdapter
1467 from zope.interface import Interface, implementer
1468@@ -56,7 +58,9 @@ class BugBranchAddView(LaunchpadFormView):
1469 def label(self):
1470 return "Add a branch to bug #%i" % self.context.bug.id
1471
1472- cancel_url = next_url
1473+ @property
1474+ def cancel_url(self):
1475+ return self.next_url
1476
1477
1478 class BugBranchDeleteView(LaunchpadEditFormView):
1479@@ -64,7 +68,7 @@ class BugBranchDeleteView(LaunchpadEditFormView):
1480
1481 schema = IBugBranch
1482
1483- field_names = []
1484+ field_names = [] # type: List[str]
1485
1486 def initialize(self):
1487 LaunchpadEditFormView.initialize(self)
1488@@ -73,7 +77,9 @@ class BugBranchDeleteView(LaunchpadEditFormView):
1489 def next_url(self):
1490 return canonical_url(self.context.bug)
1491
1492- cancel_url = next_url
1493+ @property
1494+ def cancel_url(self):
1495+ return self.next_url
1496
1497 @action("Remove link", name="delete")
1498 def delete_action(self, action, data):
1499@@ -124,7 +130,9 @@ class BranchLinkToBugView(LaunchpadFormView):
1500 def next_url(self):
1501 return canonical_url(self.context)
1502
1503- cancel_url = next_url
1504+ @property
1505+ def cancel_url(self):
1506+ return self.next_url
1507
1508 @action(_("Continue"), name="continue")
1509 def continue_action(self, action, data):
1510diff --git a/lib/lp/bugs/browser/bugnomination.py b/lib/lp/bugs/browser/bugnomination.py
1511index 7319d28..cf6c816 100644
1512--- a/lib/lp/bugs/browser/bugnomination.py
1513+++ b/lib/lp/bugs/browser/bugnomination.py
1514@@ -10,6 +10,8 @@ __all__ = [
1515 "BugNominationTableRowView",
1516 ]
1517
1518+from typing import List
1519+
1520 from zope.component import getUtility
1521 from zope.interface import Interface
1522
1523@@ -171,7 +173,7 @@ class BugNominationEditView(LaunchpadFormView):
1524 """Browser view class for approving and declining nominations."""
1525
1526 schema = Interface
1527- field_names = []
1528+ field_names = [] # type: List[str]
1529
1530 @property
1531 def label(self):
1532diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py
1533index 0710d66..642b14b 100644
1534--- a/lib/lp/bugs/browser/bugsubscription.py
1535+++ b/lib/lp/bugs/browser/bugsubscription.py
1536@@ -11,6 +11,8 @@ __all__ = [
1537 "BugSubscriptionListView",
1538 ]
1539
1540+from typing import List
1541+
1542 from lazr.delegates import delegate_to
1543 from lazr.restful.interfaces import IJSONRequestCache, IWebServiceClientRequest
1544 from simplejson import dumps
1545@@ -82,7 +84,9 @@ class BugSubscriptionAddView(LaunchpadFormView):
1546 def next_url(self):
1547 return canonical_url(self.context)
1548
1549- cancel_url = next_url
1550+ @property
1551+ def cancel_url(self):
1552+ return self.next_url
1553
1554 @property
1555 def label(self):
1556@@ -196,7 +200,9 @@ class BugSubscriptionSubscribeSelfView(
1557 next_url = context_url
1558 return next_url
1559
1560- cancel_url = next_url
1561+ @property
1562+ def cancel_url(self):
1563+ return self.next_url
1564
1565 @cachedproperty
1566 def _subscribers_for_current_user(self):
1567@@ -706,7 +712,7 @@ class BugMuteSelfView(LaunchpadFormView):
1568 """A view to mute a user's bug mail for a given bug."""
1569
1570 schema = IBugSubscription
1571- field_names = []
1572+ field_names = [] # type: List[str]
1573
1574 @property
1575 def label(self):
1576@@ -721,7 +727,9 @@ class BugMuteSelfView(LaunchpadFormView):
1577 def next_url(self):
1578 return canonical_url(self.context)
1579
1580- cancel_url = next_url
1581+ @property
1582+ def cancel_url(self):
1583+ return self.next_url
1584
1585 def initialize(self):
1586 self.is_muted = self.context.bug.isMuted(self.user)
1587diff --git a/lib/lp/bugs/browser/bugsubscriptionfilter.py b/lib/lp/bugs/browser/bugsubscriptionfilter.py
1588index 6e41c8d..79c8af8 100644
1589--- a/lib/lp/bugs/browser/bugsubscriptionfilter.py
1590+++ b/lib/lp/bugs/browser/bugsubscriptionfilter.py
1591@@ -115,14 +115,14 @@ class BugSubscriptionFilterEditViewBase(
1592 """Base class for edit or create views of `IBugSubscriptionFilter`."""
1593
1594 schema = IBugSubscriptionFilter
1595- field_names = (
1596+ field_names = [
1597 "description",
1598 "statuses",
1599 "importances",
1600 "information_types",
1601 "tags",
1602 "find_all_tags",
1603- )
1604+ ]
1605
1606 custom_widget_description = CustomWidgetFactory(
1607 TextWidget, displayWidth=50
1608@@ -159,7 +159,9 @@ class BugSubscriptionFilterEditViewBase(
1609 """Return to the user's structural subscriptions page."""
1610 return canonical_url(self.user, view_name="+structural-subscriptions")
1611
1612- cancel_url = next_url
1613+ @property
1614+ def cancel_url(self):
1615+ return self.next_url
1616
1617
1618 class BugSubscriptionFilterEditView(BugSubscriptionFilterEditViewBase):
1619diff --git a/lib/lp/bugs/browser/bugsupervisor.py b/lib/lp/bugs/browser/bugsupervisor.py
1620index 731eb69..0514526 100644
1621--- a/lib/lp/bugs/browser/bugsupervisor.py
1622+++ b/lib/lp/bugs/browser/bugsupervisor.py
1623@@ -54,7 +54,9 @@ class BugSupervisorEditView(LaunchpadEditFormView):
1624 """See `LaunchpadFormView`."""
1625 return canonical_url(self.context)
1626
1627- cancel_url = next_url
1628+ @property
1629+ def cancel_url(self):
1630+ return self.next_url
1631
1632 @action("Change", name="change")
1633 def change_action(self, action, data):
1634diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
1635index af99675..d000562 100644
1636--- a/lib/lp/bugs/browser/bugtask.py
1637+++ b/lib/lp/bugs/browser/bugtask.py
1638@@ -30,6 +30,7 @@ from collections import defaultdict
1639 from datetime import datetime, timedelta
1640 from itertools import groupby
1641 from operator import attrgetter
1642+from typing import List
1643 from urllib.parse import quote
1644
1645 import transaction
1646@@ -1731,7 +1732,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
1647 """Used to delete a bugtask."""
1648
1649 schema = IBugTask
1650- field_names = []
1651+ field_names = [] # type: List[str]
1652
1653 label = "Remove bug task"
1654 page_title = label
1655@@ -1743,6 +1744,13 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
1656 return self._next_url or self._return_url
1657 return None
1658
1659+ @property
1660+ def cancel_url(self):
1661+ # We have to explicitly define `cancel_url` as a property here
1662+ # to make `mypy` happy - the base classes both define `cancel_url`
1663+ # in a non-compatible fashion
1664+ return super().cancel_url
1665+
1666 @action("Delete", name="delete_bugtask")
1667 def delete_bugtask_action(self, action, data):
1668 bugtask = self.context
1669diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py
1670index 2002ba3..7460e9b 100644
1671--- a/lib/lp/bugs/browser/bugtracker.py
1672+++ b/lib/lp/bugs/browser/bugtracker.py
1673@@ -521,7 +521,9 @@ class BugTrackerEditComponentView(LaunchpadEditFormView):
1674 def next_url(self):
1675 return canonical_url(self.context.component_group.bug_tracker)
1676
1677- cancel_url = next_url
1678+ @property
1679+ def cancel_url(self):
1680+ return self.next_url
1681
1682 def updateContextFromData(self, data, context=None):
1683 """Link component to specified distro source package.
1684diff --git a/lib/lp/bugs/browser/bugwatch.py b/lib/lp/bugs/browser/bugwatch.py
1685index 4d67816..70ba5eb 100644
1686--- a/lib/lp/bugs/browser/bugwatch.py
1687+++ b/lib/lp/bugs/browser/bugwatch.py
1688@@ -181,7 +181,9 @@ class BugWatchEditView(LaunchpadFormView):
1689 def next_url(self):
1690 return canonical_url(getUtility(ILaunchBag).bug)
1691
1692- cancel_url = next_url
1693+ @property
1694+ def cancel_url(self):
1695+ return self.next_url
1696
1697
1698 class BugWatchActivityPortletView(LaunchpadFormView):
1699@@ -212,7 +214,9 @@ class BugWatchActivityPortletView(LaunchpadFormView):
1700 def next_url(self):
1701 return canonical_url(getUtility(ILaunchBag).bug)
1702
1703- cancel_url = next_url
1704+ @property
1705+ def cancel_url(self):
1706+ return self.next_url
1707
1708 @property
1709 def recent_watch_activity(self):
1710diff --git a/lib/lp/bugs/browser/cve.py b/lib/lp/bugs/browser/cve.py
1711index 4e440bc..1067612 100644
1712--- a/lib/lp/bugs/browser/cve.py
1713+++ b/lib/lp/bugs/browser/cve.py
1714@@ -105,7 +105,9 @@ class CveLinkView(LaunchpadFormView):
1715 def next_url(self):
1716 return canonical_url(self.context)
1717
1718- cancel_url = next_url
1719+ @property
1720+ def cancel_url(self):
1721+ return self.next_url
1722
1723
1724 class CveUnlinkView(CveLinkView):
1725@@ -123,7 +125,9 @@ class CveUnlinkView(CveLinkView):
1726 def label(self):
1727 return "Bug # %s Remove link to CVE report" % self.context.bug.id
1728
1729- page_title = label
1730+ @property
1731+ def page_title(self):
1732+ return self.label
1733
1734 heading = "Remove links to bug reports"
1735
1736diff --git a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
1737index 930dd7e..090cae7 100644
1738--- a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
1739+++ b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
1740@@ -32,8 +32,8 @@ from lp.testing.views import create_initialized_view
1741
1742
1743 class TestBugSubscriptionFilterBase:
1744- def setUp(self):
1745- super().setUp()
1746+ def setUp(self, *args, **kwargs):
1747+ super().setUp(*args, **kwargs)
1748 self.owner = self.factory.makePerson(name="foo")
1749 self.structure = self.factory.makeProduct(owner=self.owner, name="bar")
1750 with person_logged_in(self.owner):
1751diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
1752index bb1691f..b101b86 100644
1753--- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
1754+++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
1755@@ -314,8 +314,8 @@ class FileBugViewMixin:
1756 # Disable redirects on validation failure.
1757 pass
1758
1759- def setUp(self):
1760- super().setUp()
1761+ def setUp(self, *args, **kwargs):
1762+ super().setUp(*args, **kwargs)
1763 self.target = self.factory.makeProduct()
1764 transaction.commit()
1765 login_person(self.target.owner)
1766diff --git a/lib/lp/bugs/externalbugtracker/base.py b/lib/lp/bugs/externalbugtracker/base.py
1767index 4d22577..ee7be38 100644
1768--- a/lib/lp/bugs/externalbugtracker/base.py
1769+++ b/lib/lp/bugs/externalbugtracker/base.py
1770@@ -25,7 +25,7 @@ __all__ = [
1771 "UnsupportedBugTrackerVersion",
1772 ]
1773
1774-
1775+from typing import Optional
1776 from urllib.parse import urljoin, urlparse
1777
1778 import requests
1779@@ -157,7 +157,7 @@ def repost_on_redirect_hook(response, *args, **kwargs):
1780 class ExternalBugTracker:
1781 """Base class for an external bug tracker."""
1782
1783- batch_size = None
1784+ batch_size = None # type: Optional[int]
1785 batch_query_threshold = config.checkwatches.batch_query_threshold
1786 timeout = config.checkwatches.default_socket_timeout
1787 comment_template = "default_remotecomment_template.txt"
1788diff --git a/lib/lp/bugs/externalbugtracker/github.py b/lib/lp/bugs/externalbugtracker/github.py
1789index be3a2d6..701ce14 100644
1790--- a/lib/lp/bugs/externalbugtracker/github.py
1791+++ b/lib/lp/bugs/externalbugtracker/github.py
1792@@ -7,7 +7,6 @@ __all__ = [
1793 "BadGitHubURL",
1794 "GitHub",
1795 "GitHubRateLimit",
1796- "IGitHubRateLimit",
1797 ]
1798
1799 import http.client
1800@@ -18,7 +17,6 @@ from urllib.parse import urlencode, urlunsplit
1801 import pytz
1802 import requests
1803 from zope.component import getUtility
1804-from zope.interface import Interface
1805
1806 from lp.bugs.externalbugtracker import (
1807 BugTrackerConnectError,
1808@@ -28,6 +26,7 @@ from lp.bugs.externalbugtracker import (
1809 UnparsableBugTrackerVersion,
1810 )
1811 from lp.bugs.externalbugtracker.base import LP_USER_AGENT
1812+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
1813 from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
1814 from lp.bugs.interfaces.externalbugtracker import UNKNOWN_REMOTE_IMPORTANCE
1815 from lp.services.config import config
1816@@ -48,24 +47,6 @@ class GitHubExceededRateLimit(BugWatchUpdateError):
1817 )
1818
1819
1820-class IGitHubRateLimit(Interface):
1821- """Interface for rate-limit tracking for the GitHub Issues API."""
1822-
1823- def checkLimit(url, token=None):
1824- """A context manager that checks the remote host's rate limit.
1825-
1826- :param url: The URL being requested.
1827- :param token: If not None, an OAuth token to use as authentication
1828- to the remote host when asking it for the current rate limit.
1829- :return: A suitable `Authorization` header (from the context
1830- manager's `__enter__` method).
1831- :raises GitHubExceededRateLimit: if the rate limit was exceeded.
1832- """
1833-
1834- def clearCache():
1835- """Forget any cached rate limits."""
1836-
1837-
1838 class GitHubRateLimit:
1839 """Rate-limit tracking for the GitHub Issues API."""
1840
1841diff --git a/lib/lp/bugs/externalbugtracker/interfaces.py b/lib/lp/bugs/externalbugtracker/interfaces.py
1842new file mode 100644
1843index 0000000..0ce8030
1844--- /dev/null
1845+++ b/lib/lp/bugs/externalbugtracker/interfaces.py
1846@@ -0,0 +1,22 @@
1847+# Copyright 2022 Canonical Ltd. This software is licensed under the
1848+# GNU Affero General Public License version 3 (see the file LICENSE).
1849+
1850+from zope.interface import Interface
1851+
1852+
1853+class IGitHubRateLimit(Interface):
1854+ """Interface for rate-limit tracking for the GitHub Issues API."""
1855+
1856+ def checkLimit(url, token=None):
1857+ """A context manager that checks the remote host's rate limit.
1858+
1859+ :param url: The URL being requested.
1860+ :param token: If not None, an OAuth token to use as authentication
1861+ to the remote host when asking it for the current rate limit.
1862+ :return: A suitable `Authorization` header (from the context
1863+ manager's `__enter__` method).
1864+ :raises GitHubExceededRateLimit: if the rate limit was exceeded.
1865+ """
1866+
1867+ def clearCache():
1868+ """Forget any cached rate limits."""
1869diff --git a/lib/lp/bugs/externalbugtracker/tests/test_github.py b/lib/lp/bugs/externalbugtracker/tests/test_github.py
1870index 2604002..a693f02 100644
1871--- a/lib/lp/bugs/externalbugtracker/tests/test_github.py
1872+++ b/lib/lp/bugs/externalbugtracker/tests/test_github.py
1873@@ -30,8 +30,8 @@ from lp.bugs.externalbugtracker.github import (
1874 BadGitHubURL,
1875 GitHub,
1876 GitHubExceededRateLimit,
1877- IGitHubRateLimit,
1878 )
1879+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
1880 from lp.bugs.interfaces.bugtask import BugTaskStatus
1881 from lp.bugs.interfaces.bugtracker import BugTrackerType
1882 from lp.bugs.interfaces.externalbugtracker import IExternalBugTracker
1883diff --git a/lib/lp/bugs/interfaces/bugnotification.py b/lib/lp/bugs/interfaces/bugnotification.py
1884index 707e687..a3fd63f 100644
1885--- a/lib/lp/bugs/interfaces/bugnotification.py
1886+++ b/lib/lp/bugs/interfaces/bugnotification.py
1887@@ -69,10 +69,10 @@ class IBugNotificationSet(Interface):
1888 def getDeferredNotifications():
1889 """Returns the deferred notifications.
1890
1891- A deferred noticiation is one that is pending but has no recipients.
1892+ A deferred notification is one that is pending but has no recipients.
1893 """
1894
1895- def addNotification(self, bug, is_comment, message, recipients, activity):
1896+ def addNotification(bug, is_comment, message, recipients, activity):
1897 """Create a new `BugNotification`.
1898
1899 Create a new `BugNotification` object and the corresponding
1900diff --git a/lib/lp/bugs/model/bugtarget.py b/lib/lp/bugs/model/bugtarget.py
1901index e4f3b3b..7c2e0c4 100644
1902--- a/lib/lp/bugs/model/bugtarget.py
1903+++ b/lib/lp/bugs/model/bugtarget.py
1904@@ -234,6 +234,7 @@ class OfficialBugTag(Storm):
1905 product_id = Int(name="product")
1906 product = Reference(product_id, "Product.id")
1907
1908+ @property
1909 def target(self):
1910 """See `IOfficialBugTag`."""
1911 # A database constraint ensures that either distribution or
1912@@ -243,7 +244,8 @@ class OfficialBugTag(Storm):
1913 else:
1914 return self.product
1915
1916- def _settarget(self, target):
1917+ @target.setter
1918+ def target(self, target):
1919 """See `IOfficialBugTag`."""
1920 if IDistribution.providedBy(target):
1921 self.distribution = target
1922@@ -254,5 +256,3 @@ class OfficialBugTag(Storm):
1923 "The target of an OfficialBugTag must be either an "
1924 "IDistribution instance or an IProduct instance."
1925 )
1926-
1927- target = property(target, _settarget, doc=target.__doc__)
1928diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
1929index c170055..598a27a 100644
1930--- a/lib/lp/bugs/model/tests/test_bugtask.py
1931+++ b/lib/lp/bugs/model/tests/test_bugtask.py
1932@@ -1185,7 +1185,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
1933
1934 layer = DatabaseFunctionalLayer
1935
1936- def setUp(self):
1937+ def setUp(self, *args, **kwargs):
1938 """Create the test setup.
1939
1940 We need
1941@@ -1196,7 +1196,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
1942 owners, bug supervisors, drivers
1943 - bug tasks for the targets
1944 """
1945- super().setUp()
1946+ super().setUp(*args, **kwargs)
1947 self.target_owner_member = self.factory.makePerson()
1948 self.target_owner_team = self.factory.makeTeam(
1949 owner=self.target_owner_member,
1950diff --git a/lib/lp/bugs/model/tests/test_bugtask_status.py b/lib/lp/bugs/model/tests/test_bugtask_status.py
1951index 6db1fa2..0ad43a2 100644
1952--- a/lib/lp/bugs/model/tests/test_bugtask_status.py
1953+++ b/lib/lp/bugs/model/tests/test_bugtask_status.py
1954@@ -303,8 +303,8 @@ class TestBugTaskStatusTransitionForPrivilegedUserBase:
1955
1956 layer = DatabaseFunctionalLayer
1957
1958- def setUp(self):
1959- super().setUp()
1960+ def setUp(self, *args, **kwargs):
1961+ super().setUp(*args, **kwargs)
1962 # Creation of task and target are deferred to subclasses.
1963 self.task = None
1964 self.person = None
1965diff --git a/lib/lp/bugs/scripts/checkwatches/base.py b/lib/lp/bugs/scripts/checkwatches/base.py
1966index 2b8abec..f7365cc 100644
1967--- a/lib/lp/bugs/scripts/checkwatches/base.py
1968+++ b/lib/lp/bugs/scripts/checkwatches/base.py
1969@@ -127,7 +127,7 @@ class WorkingBase:
1970 self._transaction_manager = parent._transaction_manager
1971 self.logger = parent.logger
1972
1973- @property
1974+ @property # type: ignore
1975 @contextmanager
1976 def interaction(self):
1977 """Context manager for interaction as the given user.
1978@@ -145,7 +145,7 @@ class WorkingBase:
1979 else:
1980 yield
1981
1982- @property
1983+ @property # type: ignore
1984 @contextmanager
1985 def transaction(self):
1986 """Context manager to ring-fence database activity.
1987@@ -184,7 +184,7 @@ class WorkingBase:
1988 self._statement_logging_stop()
1989 self._statement_logging_start()
1990
1991- @property
1992+ @property # type: ignore
1993 @contextmanager
1994 def statement_logging(self):
1995 """Context manager to start and stop SQL statement logging.
1996diff --git a/lib/lp/bugs/scripts/checkwatches/core.py b/lib/lp/bugs/scripts/checkwatches/core.py
1997index fc7eb4a..43850c2 100644
1998--- a/lib/lp/bugs/scripts/checkwatches/core.py
1999+++ b/lib/lp/bugs/scripts/checkwatches/core.py
2000@@ -21,6 +21,7 @@ from contextlib import contextmanager
2001 from copy import copy
2002 from datetime import datetime, timedelta
2003 from itertools import chain, islice
2004+from typing import List
2005 from xmlrpc.client import ProtocolError
2006
2007 import pytz
2008@@ -60,7 +61,7 @@ from lp.services.scripts.logger import log as default_log
2009 LOGIN = "bugwatch@bugs.launchpad.net"
2010
2011 # A list of product names for which comments should be synchronized.
2012-SYNCABLE_GNOME_PRODUCTS = []
2013+SYNCABLE_GNOME_PRODUCTS = [] # type: List[str]
2014
2015 # When syncing with a remote bug tracker that reports its idea of the
2016 # current time, this defined the maximum acceptable skew between the
2017diff --git a/lib/lp/bugs/scripts/debbugs.py b/lib/lp/bugs/scripts/debbugs.py
2018index fe321ac..fc85e34 100644
2019--- a/lib/lp/bugs/scripts/debbugs.py
2020+++ b/lib/lp/bugs/scripts/debbugs.py
2021@@ -301,7 +301,10 @@ class Database:
2022
2023
2024 if __name__ == "__main__":
2025- for bug in Database("/srv/debzilla.no-name-yet.com/debbugs"):
2026+ for bug in Database(
2027+ "/srv/debzilla.no-name-yet.com/debbugs",
2028+ os.path.join(os.path.dirname(__file__), "debbugs-log.pl"),
2029+ ):
2030 try:
2031 print(bug, bug.subject)
2032 except Exception as e:
2033diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
2034index a2f34d7..f9aa65d 100644
2035--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
2036+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
2037@@ -6,6 +6,7 @@ import re
2038 import unittest
2039 from datetime import datetime, timedelta
2040 from smtplib import SMTPException
2041+from typing import Any, List, Optional, Type
2042
2043 import pytz
2044 from fixtures import FakeLogger
2045@@ -66,6 +67,7 @@ from lp.services.mail.helpers import (
2046 from lp.services.mail.sendmail import set_immediate_mail_delivery
2047 from lp.services.mail.stub import TestMailer
2048 from lp.services.messages.interfaces.message import IMessageSet
2049+from lp.services.messages.model.message import Message
2050 from lp.services.propertycache import cachedproperty
2051 from lp.testing import TestCase, TestCaseWithFactory, login, person_logged_in
2052 from lp.testing.dbuser import lp_dbuser, switch_dbuser
2053@@ -80,7 +82,7 @@ class MockBug:
2054
2055 duplicateof = None
2056 information_type = InformationType.PUBLIC
2057- messages = []
2058+ messages = [] # type: List[Message]
2059
2060 def __init__(self, id, owner):
2061 self.id = id
2062@@ -725,7 +727,12 @@ class EmailNotificationTestBase(TestCaseWithFactory):
2063
2064 class EmailNotificationsBugMixin:
2065
2066- change_class = change_name = old = new = alt = unexpected_bytes = None
2067+ change_class = None # type: Optional[Type[Any]]
2068+ change_name = None # type: Optional[str]
2069+ old = None # type: Any
2070+ new = None # type: Any
2071+ alt = None # type: Any
2072+ unexpected_bytes = None # type: Optional[bytes]
2073
2074 def change(self, old, new):
2075 self.bug.addChange(
2076@@ -814,7 +821,7 @@ class EmailNotificationsBugTaskMixin(EmailNotificationsBugMixin):
2077
2078 class EmailNotificationsAddedRemovedMixin:
2079
2080- old = new = added_message = removed_message = None
2081+ old = new = added_message = removed_message = b""
2082
2083 def add(self, item):
2084 raise NotImplementedError
2085diff --git a/lib/lp/bugs/tests/externalbugtracker.py b/lib/lp/bugs/tests/externalbugtracker.py
2086index 2b7f6c0..6a126b5 100644
2087--- a/lib/lp/bugs/tests/externalbugtracker.py
2088+++ b/lib/lp/bugs/tests/externalbugtracker.py
2089@@ -12,6 +12,7 @@ from contextlib import contextmanager
2090 from copy import deepcopy
2091 from datetime import datetime, timedelta
2092 from operator import itemgetter
2093+from typing import Any, Dict, Tuple
2094 from urllib.parse import parse_qs, urljoin, urlsplit
2095
2096 import responses
2097@@ -546,7 +547,7 @@ class TestBugzillaXMLRPCTransport(RequestsTransport):
2098 "add_comment",
2099 "login_required",
2100 "set_link",
2101- )
2102+ ) # type: Tuple[str, ...]
2103
2104 expired_cookie = None
2105
2106@@ -878,10 +879,10 @@ class TestBugzillaAPIXMLRPCTransport(TestBugzillaXMLRPCTransport):
2107 }
2108
2109 # Methods that require authentication.
2110- auth_required_methods = [
2111+ auth_required_methods = (
2112 "add_comment",
2113 "login_required",
2114- ]
2115+ )
2116
2117 # The list of users that can log in.
2118 users = [
2119@@ -1362,8 +1363,8 @@ def strip_trac_comment(comment):
2120 class TestTracXMLRPCTransport(RequestsTransport):
2121 """An XML-RPC transport to be used when testing Trac."""
2122
2123- remote_bugs = {}
2124- launchpad_bugs = {}
2125+ remote_bugs = {} # type: Dict[str, Dict[str, Any]]
2126+ launchpad_bugs = {} # type: Dict[str, int]
2127 seconds_since_epoch = None
2128 local_timezone = "UTC"
2129 utc_offset = 0
2130diff --git a/lib/lp/bugs/tests/test_buglinktarget.py b/lib/lp/bugs/tests/test_buglinktarget.py
2131index b10d33b..9618d00 100644
2132--- a/lib/lp/bugs/tests/test_buglinktarget.py
2133+++ b/lib/lp/bugs/tests/test_buglinktarget.py
2134@@ -7,9 +7,8 @@ This module will run the interface test against the CVE, Specification,
2135 Question, and BranchMergeProposal implementations of that interface.
2136 """
2137
2138-__all__ = []
2139-
2140 import unittest
2141+from typing import List
2142
2143 from zope.component import getUtility
2144 from zope.security.proxy import ProxyFactory
2145@@ -21,6 +20,8 @@ from lp.testing.factory import LaunchpadObjectFactory
2146 from lp.testing.layers import LaunchpadFunctionalLayer
2147 from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
2148
2149+__all__ = [] # type: List[str]
2150+
2151
2152 def questionSetUp(test):
2153 setUp(test)
2154diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py
2155index 8963822..870686c 100644
2156--- a/lib/lp/bugs/tests/test_bugnomination.py
2157+++ b/lib/lp/bugs/tests/test_bugnomination.py
2158@@ -214,8 +214,8 @@ class CanBeNominatedForTestMixin:
2159
2160 layer = DatabaseFunctionalLayer
2161
2162- def setUp(self):
2163- super().setUp()
2164+ def setUp(self, *args, **kwargs):
2165+ super().setUp(*args, **kwargs)
2166 login("foo.bar@canonical.com")
2167 self.eric = self.factory.makePerson(name="eric")
2168 self.setUpTarget()
2169diff --git a/lib/lp/bugs/tests/test_bugsearch_conjoined.py b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
2170index 754b8c5..f1c5be1 100644
2171--- a/lib/lp/bugs/tests/test_bugsearch_conjoined.py
2172+++ b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
2173@@ -2,8 +2,7 @@
2174 # GNU Affero General Public License version 3 (see the file LICENSE).
2175
2176 """Test for the exclude_conjoined_tasks param for BugTaskSearchParams."""
2177-
2178-__all__ = []
2179+from typing import List
2180
2181 from storm.store import Store
2182 from testtools.matchers import Equals
2183@@ -21,6 +20,8 @@ from lp.testing import (
2184 from lp.testing.layers import DatabaseFunctionalLayer
2185 from lp.testing.matchers import HasQueryCount
2186
2187+__all__ = [] # type: List[str]
2188+
2189
2190 class TestSearchBase(TestCaseWithFactory):
2191 """Tests of exclude_conjoined_tasks param."""
2192diff --git a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
2193index d725da1..6d0b182 100644
2194--- a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
2195+++ b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
2196@@ -23,8 +23,8 @@ class AddNominationTestMixin:
2197
2198 layer = DatabaseFunctionalLayer
2199
2200- def setUp(self):
2201- super().setUp()
2202+ def setUp(self, *args, **kwargs):
2203+ super().setUp(*args, **kwargs)
2204 login("foo.bar@canonical.com")
2205 self.user = self.factory.makePerson(name="ordinary-user")
2206 self.bug_supervisor = self.factory.makePerson(name="no-ordinary-user")
2207diff --git a/lib/lp/bugs/tests/test_bugtarget.py b/lib/lp/bugs/tests/test_bugtarget.py
2208index b5b937c..f1427b7 100644
2209--- a/lib/lp/bugs/tests/test_bugtarget.py
2210+++ b/lib/lp/bugs/tests/test_bugtarget.py
2211@@ -7,11 +7,9 @@ This module runs the interface test against the Product, ProductSeries
2212 ProjectGroup, DistributionSourcePackage, and DistroSeries implementations
2213 IBugTarget. It runs the bugtarget-questiontarget.rst test.
2214 """
2215-
2216-__all__ = []
2217-
2218 import random
2219 import unittest
2220+from typing import List
2221
2222 from zope.component import getUtility
2223
2224@@ -25,6 +23,8 @@ from lp.testing import TestCaseWithFactory, person_logged_in
2225 from lp.testing.layers import DatabaseFunctionalLayer
2226 from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
2227
2228+__all__ = [] # type: List[str]
2229+
2230
2231 def bugtarget_filebug(bugtarget, summary, status=None):
2232 """File a bug as the current user on the bug target and return it."""
2233diff --git a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
2234index 6d1b9a3..8330111 100644
2235--- a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
2236+++ b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
2237@@ -1,8 +1,8 @@
2238 # Copyright 2012 Canonical Ltd. This software is licensed under the
2239 # GNU Affero General Public License version 3 (see the file LICENSE).
2240
2241-from collections import namedtuple
2242 from contextlib import contextmanager
2243+from typing import Any, NamedTuple
2244
2245 from testtools.matchers import MatchesStructure
2246 from zope.component import getUtility
2247@@ -22,35 +22,36 @@ from lp.testing import TestCaseWithFactory, login_person, person_logged_in
2248 from lp.testing.dbuser import dbuser
2249 from lp.testing.layers import DatabaseFunctionalLayer
2250
2251-BUGTASKFLAT_COLUMNS = (
2252- "bugtask",
2253- "bug",
2254- "datecreated",
2255- "latest_patch_uploaded",
2256- "date_closed",
2257- "date_last_updated",
2258- "duplicateof",
2259- "bug_owner",
2260- "fti",
2261- "information_type",
2262- "heat",
2263- "product",
2264- "productseries",
2265- "distribution",
2266- "distroseries",
2267- "sourcepackagename",
2268- "status",
2269- "importance",
2270- "assignee",
2271- "milestone",
2272- "owner",
2273- "active",
2274- "access_policies",
2275- "access_grants",
2276+BugTaskFlat = NamedTuple(
2277+ "BugTaskFlat",
2278+ (
2279+ ("bugtask", Any),
2280+ ("bug", Any),
2281+ ("datecreated", Any),
2282+ ("latest_patch_uploaded", Any),
2283+ ("date_closed", Any),
2284+ ("date_last_updated", Any),
2285+ ("duplicateof", Any),
2286+ ("bug_owner", Any),
2287+ ("fti", Any),
2288+ ("information_type", Any),
2289+ ("heat", Any),
2290+ ("product", Any),
2291+ ("productseries", Any),
2292+ ("distribution", Any),
2293+ ("distroseries", Any),
2294+ ("sourcepackagename", Any),
2295+ ("status", Any),
2296+ ("importance", Any),
2297+ ("assignee", Any),
2298+ ("milestone", Any),
2299+ ("owner", Any),
2300+ ("active", Any),
2301+ ("access_policies", Any),
2302+ ("access_grants", Any),
2303+ ),
2304 )
2305
2306-BugTaskFlat = namedtuple("BugTaskFlat", BUGTASKFLAT_COLUMNS)
2307-
2308
2309 class BugTaskFlatTestMixin(TestCaseWithFactory):
2310 def checkFlattened(self, bugtask, check_only=True):
2311@@ -81,7 +82,7 @@ class BugTaskFlatTestMixin(TestCaseWithFactory):
2312 IStore(Bug)
2313 .execute(
2314 "SELECT %s FROM bugtaskflat WHERE bugtask = ?"
2315- % ", ".join(BUGTASKFLAT_COLUMNS),
2316+ % ", ".join(BugTaskFlat._fields),
2317 (bugtask,),
2318 )
2319 .get_one()
2320diff --git a/lib/lp/bugs/tests/test_bugtracker_components.py b/lib/lp/bugs/tests/test_bugtracker_components.py
2321index ea91509..3194101 100644
2322--- a/lib/lp/bugs/tests/test_bugtracker_components.py
2323+++ b/lib/lp/bugs/tests/test_bugtracker_components.py
2324@@ -2,21 +2,22 @@
2325 # GNU Affero General Public License version 3 (see the file LICENSE).
2326
2327 """Test for components and component groups (products) in bug trackers."""
2328-
2329-__all__ = []
2330+from typing import List
2331
2332 import transaction
2333
2334 from lp.testing import TestCaseWithFactory, login_person, ws_object
2335 from lp.testing.layers import AppServerLayer, DatabaseFunctionalLayer
2336
2337+__all__ = [] # type: List[str]
2338+
2339
2340 class BugTrackerComponentTestCase(TestCaseWithFactory):
2341
2342 layer = DatabaseFunctionalLayer
2343
2344- def setUp(self):
2345- super().setUp()
2346+ def setUp(self, *args, **kwargs):
2347+ super().setUp(*args, **kwargs)
2348
2349 regular_user = self.factory.makePerson()
2350 login_person(regular_user)
2351diff --git a/lib/lp/bugs/tests/test_bugwatch.py b/lib/lp/bugs/tests/test_bugwatch.py
2352index 03230ae..a71a7d0 100644
2353--- a/lib/lp/bugs/tests/test_bugwatch.py
2354+++ b/lib/lp/bugs/tests/test_bugwatch.py
2355@@ -5,6 +5,7 @@
2356
2357 import re
2358 from datetime import datetime, timedelta
2359+from typing import List, Optional
2360 from urllib.parse import urlunsplit
2361
2362 import transaction
2363@@ -228,16 +229,16 @@ class ExtractBugTrackerAndBugTest(WithScenarios, TestCase):
2364 layer = LaunchpadFunctionalLayer
2365
2366 # A URL to an unregistered bug tracker.
2367- base_url = None
2368+ base_url = None # type: str
2369
2370 # The bug tracker type to be tested.
2371 bugtracker_type = None
2372
2373 # A sample URL to a bug in the bug tracker.
2374- bug_url = None
2375+ bug_url = None # type: str
2376
2377 # The bug id in the sample bug_url.
2378- bug_id = None
2379+ bug_id = None # type: Optional[str]
2380
2381 # True if the bug tracker is already registered in sampledata.
2382 already_registered = False
2383@@ -379,10 +380,11 @@ class SFExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTest):
2384
2385
2386 class EmailAddressExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTest):
2387- """Ensure BugWatchSet.extractBugTrackerAndBug works with email
2388- addresses."""
2389+ """
2390+ Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.
2391+ """
2392
2393- scenarios = None
2394+ scenarios = [] # type: List
2395 bugtracker_type = BugTrackerType.EMAILADDRESS
2396 bug_url = "mailto:foo.bar@example.com"
2397 base_url = "mailto:foo.bar@example.com"
2398diff --git a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
2399index 46969d0..78abb69 100644
2400--- a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
2401+++ b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
2402@@ -2,11 +2,9 @@
2403 # GNU Affero General Public License version 3 (see the file LICENSE).
2404
2405 """Tests cronscript for retrieving components from remote Bugzillas."""
2406-
2407-__all__ = []
2408-
2409 import os
2410 import re
2411+from typing import List
2412
2413 import responses
2414 import transaction
2415@@ -21,6 +19,8 @@ from lp.testing import TestCaseWithFactory, login
2416 from lp.testing.layers import DatabaseFunctionalLayer
2417 from lp.testing.sampledata import ADMIN_EMAIL
2418
2419+__all__ = [] # type: List[str]
2420+
2421
2422 def read_test_file(name):
2423 """Return the contents of the test file named :name:
2424diff --git a/lib/lp/bugs/tests/test_externalbugtracker.py b/lib/lp/bugs/tests/test_externalbugtracker.py
2425index 98affe9..44d2398 100644
2426--- a/lib/lp/bugs/tests/test_externalbugtracker.py
2427+++ b/lib/lp/bugs/tests/test_externalbugtracker.py
2428@@ -3,13 +3,14 @@
2429
2430 """Test related to ExternalBugtracker test infrastructure."""
2431
2432-__all__ = []
2433-
2434 import unittest
2435+from typing import List
2436
2437 from lp.testing.layers import LaunchpadFunctionalLayer
2438 from lp.testing.systemdocs import LayeredDocFileSuite, setUp, tearDown
2439
2440+__all__ = [] # type: List[str]
2441+
2442
2443 def test_suite():
2444 suite = unittest.TestSuite()
2445diff --git a/lib/lp/bugs/tests/test_structuralsubscription.py b/lib/lp/bugs/tests/test_structuralsubscription.py
2446index c44ebc8..d839266 100644
2447--- a/lib/lp/bugs/tests/test_structuralsubscription.py
2448+++ b/lib/lp/bugs/tests/test_structuralsubscription.py
2449@@ -148,8 +148,8 @@ class FilteredStructuralSubscriptionTestBase:
2450 def makeBugTask(self):
2451 return self.factory.makeBugTask(target=self.target)
2452
2453- def setUp(self):
2454- super().setUp()
2455+ def setUp(self, *args, **kwargs):
2456+ super().setUp(*args, **kwargs)
2457 self.ordinary_subscriber = self.factory.makePerson()
2458 login_person(self.ordinary_subscriber)
2459 self.target = self.makeTarget()
2460diff --git a/lib/lp/bugs/tests/test_yuitests.py b/lib/lp/bugs/tests/test_yuitests.py
2461index 237a575..431fbd9 100644
2462--- a/lib/lp/bugs/tests/test_yuitests.py
2463+++ b/lib/lp/bugs/tests/test_yuitests.py
2464@@ -2,12 +2,13 @@
2465 # GNU Affero General Public License version 3 (see the file LICENSE).
2466
2467 """Run YUI.test tests."""
2468-
2469-__all__ = []
2470+from typing import List
2471
2472 from lp.testing import YUIUnitTestCase, build_yui_unittest_suite
2473 from lp.testing.layers import YUITestLayer
2474
2475+__all__ = [] # type: List[str]
2476+
2477
2478 class BugsYUIUnitTestCase(YUIUnitTestCase):
2479
2480diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py
2481index afcd8cf..93e20f4 100644
2482--- a/lib/lp/registry/browser/product.py
2483+++ b/lib/lp/registry/browser/product.py
2484@@ -40,6 +40,7 @@ __all__ = [
2485
2486
2487 from operator import attrgetter
2488+from typing import Type
2489 from urllib.parse import urlunsplit
2490
2491 from breezy import urlutils
2492@@ -1410,7 +1411,7 @@ class ProductBrandingView(BrandingChangeView):
2493
2494 @implementer(IProductEditMenu)
2495 class ProductConfigureBase(ReturnToReferrerMixin, LaunchpadEditFormView):
2496- schema = IProduct
2497+ schema = IProduct # type: Type[Interface]
2498 usage_fieldname = None
2499
2500 def setUpFields(self):
2501diff --git a/lib/lp/services/feeds/browser.py b/lib/lp/services/feeds/browser.py
2502index 0be5396..d07c793 100644
2503--- a/lib/lp/services/feeds/browser.py
2504+++ b/lib/lp/services/feeds/browser.py
2505@@ -21,6 +21,11 @@ __all__ = [
2506 "RootAnnouncementsFeedLink",
2507 ]
2508
2509+from typing import (
2510+ Tuple,
2511+ Type,
2512+ )
2513+
2514 from zope.component import getUtility
2515 from zope.interface import implementer
2516 from zope.publisher.interfaces import NotFound
2517@@ -378,7 +383,7 @@ class FeedsMixin:
2518 ProjectBranchesFeedLink,
2519 ProjectRevisionsFeedLink,
2520 RootAnnouncementsFeedLink,
2521- )
2522+ ) # type: Tuple[Type[FeedLinkBase, ...]]
2523
2524 @property
2525 def feed_links(self):
2526diff --git a/lib/lp/services/looptuner.py b/lib/lp/services/looptuner.py
2527index e99dce3..40c0882 100644
2528--- a/lib/lp/services/looptuner.py
2529+++ b/lib/lp/services/looptuner.py
2530@@ -396,7 +396,7 @@ class TunableLoop:
2531
2532 goal_seconds = 2
2533 minimum_chunk_size = 1
2534- maximum_chunk_size = None # Override.
2535+ maximum_chunk_size = None # type: int
2536 cooldown_time = 0
2537
2538 def __init__(self, log, abort_time=None):
2539diff --git a/lib/lp/services/mail/commands.py b/lib/lp/services/mail/commands.py
2540index f510a82..5f30846 100644
2541--- a/lib/lp/services/mail/commands.py
2542+++ b/lib/lp/services/mail/commands.py
2543@@ -58,7 +58,7 @@ class EmailCommand:
2544 Both name the values in the args list are strings.
2545 """
2546
2547- _numberOfArguments = None
2548+ _numberOfArguments = None # type: int
2549
2550 # Should command arguments be converted to lowercase?
2551 case_insensitive_args = True
2552diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py
2553index a119aab..36d34b0 100644
2554--- a/lib/lp/services/scripts/base.py
2555+++ b/lib/lp/services/scripts/base.py
2556@@ -137,8 +137,8 @@ class LaunchpadScript:
2557
2558 lock = None
2559 txn = None
2560- usage = None
2561- description = None
2562+ usage = ""
2563+ description = ""
2564 lockfilepath = None
2565 loglevel = logging.INFO
2566
2567diff --git a/lib/lp/services/webapp/breadcrumb.py b/lib/lp/services/webapp/breadcrumb.py
2568index 5f75f5a..8bf39b7 100644
2569--- a/lib/lp/services/webapp/breadcrumb.py
2570+++ b/lib/lp/services/webapp/breadcrumb.py
2571@@ -23,7 +23,7 @@ class Breadcrumb:
2572 This class is intended for use as an adapter.
2573 """
2574
2575- text = None
2576+ text = None # type: str
2577 _detail = None
2578 _url = None
2579 inside = None
2580diff --git a/lib/lp/services/webapp/menu.py b/lib/lp/services/webapp/menu.py
2581index 48e7574..4341425 100644
2582--- a/lib/lp/services/webapp/menu.py
2583+++ b/lib/lp/services/webapp/menu.py
2584@@ -19,6 +19,7 @@ __all__ = [
2585 ]
2586
2587 import types
2588+from typing import Sequence
2589
2590 from lazr.delegates import delegate_to
2591 from lazr.restful.utils import get_current_browser_request
2592@@ -198,7 +199,7 @@ MENU_ANNOTATION_KEY = "lp.services.webapp.menu.links"
2593 class MenuBase(UserAttributeCache):
2594 """Base class for facets and menus."""
2595
2596- links = None
2597+ links = None # type: Sequence[str]
2598 extra_attributes = None
2599 enable_only = ALL_LINKS
2600 _baseclassname = "MenuBase"
2601@@ -400,7 +401,7 @@ class NavigationMenu(MenuBase):
2602
2603 _baseclassname = "NavigationMenu"
2604
2605- title = None
2606+ title = None # type: str
2607 disabled = False
2608
2609 def initLink(self, linkname, request_url):
2610diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
2611index 3072d4b..c44c87b 100644
2612--- a/lib/lp/services/webapp/publisher.py
2613+++ b/lib/lp/services/webapp/publisher.py
2614@@ -29,6 +29,12 @@ import http.client
2615 import json
2616 import re
2617 from cgi import FieldStorage
2618+from typing import (
2619+ Any,
2620+ Dict,
2621+ Optional,
2622+ Type,
2623+)
2624 from urllib.parse import urlparse
2625 from wsgiref.headers import Headers
2626
2627@@ -517,7 +523,7 @@ class LaunchpadView(UserAttributeCache):
2628 return None
2629
2630 # Names of feature flags which affect a view.
2631- related_features = ()
2632+ related_features = {} # type: Dict[str, bool]
2633
2634 @property
2635 def related_feature_info(self):
2636@@ -899,7 +905,7 @@ class Navigation:
2637 self.request = request
2638
2639 # Set this if you want to set a new layer before doing any traversal.
2640- newlayer = None
2641+ newlayer = None # type: Optional[Type[Any]]
2642
2643 def traverse(self, name):
2644 """Override this method to handle traversal.
2645diff --git a/lib/lp/services/webapp/vocabulary.py b/lib/lp/services/webapp/vocabulary.py
2646index aed23be..e7e4d2c 100644
2647--- a/lib/lp/services/webapp/vocabulary.py
2648+++ b/lib/lp/services/webapp/vocabulary.py
2649@@ -22,9 +22,14 @@ __all__ = [
2650 ]
2651
2652 from collections import namedtuple
2653+from typing import (
2654+ Optional,
2655+ Union,
2656+ )
2657
2658 import six
2659 from storm.base import Storm
2660+from storm.expr import Expr
2661 from storm.store import EmptyResultSet
2662 from zope.interface import Attribute, Interface, implementer
2663 from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
2664@@ -279,8 +284,8 @@ class SQLObjectVocabularyBase(FilteredVocabularyBase):
2665 should derive from SQLObjectVocabularyBase.
2666 """
2667
2668- _orderBy = None
2669- _filter = None
2670+ _orderBy = None # type: Optional[str]
2671+ _filter = None # type: Optional[Union[Expr, bool]]
2672 _clauseTables = None
2673
2674 def __init__(self, context=None):
2675diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
2676index 6703b9d..735ab8e 100644
2677--- a/lib/lp/testing/__init__.py
2678+++ b/lib/lp/testing/__init__.py
2679@@ -60,6 +60,10 @@ import subprocess
2680 import sys
2681 import tempfile
2682 import time
2683+from typing import (
2684+ Type,
2685+ TYPE_CHECKING,
2686+ )
2687 import unittest
2688 from contextlib import contextmanager
2689 from datetime import datetime, timedelta
2690@@ -154,6 +158,10 @@ from lp.testing.fixture import CaptureOops, ZopeEventHandlerFixture
2691 from lp.testing.karma import KarmaRecorder
2692 from lp.testing.mail_helpers import pop_notifications
2693
2694+
2695+if TYPE_CHECKING:
2696+ from lp.testing.layers import BaseLayer
2697+
2698 # The following names have been imported for the purpose of being
2699 # exported. They are referred to here to silence lint warnings.
2700 admin_logged_in
2701@@ -1070,7 +1078,7 @@ class WebServiceTestCase(TestCaseWithFactory):
2702
2703 class AbstractYUITestCase(TestCase):
2704
2705- layer = None
2706+ layer = None # type: Type[BaseLayer]
2707 suite_name = ""
2708 # 30 seconds for the suite.
2709 suite_timeout = 30000
2710diff --git a/pyproject.toml b/pyproject.toml
2711index 1f331da..b9e4772 100644
2712--- a/pyproject.toml
2713+++ b/pyproject.toml
2714@@ -1,3 +1,103 @@
2715 [tool.black]
2716 line-length = 79
2717 target-version = ['py35']
2718+
2719+[tool.mypy]
2720+python_version = "3.5"
2721+exclude = [
2722+ '/interfaces/',
2723+ 'interfaces\.py$',
2724+]
2725+
2726+[[tool.mypy.overrides]]
2727+module = "apt_inst"
2728+ignore_missing_imports = true
2729+
2730+[[tool.mypy.overrides]]
2731+module = "apt_pkg"
2732+ignore_missing_imports = true
2733+
2734+[[tool.mypy.overrides]]
2735+module = "artifactory"
2736+ignore_missing_imports = true
2737+
2738+[[tool.mypy.overrides]]
2739+module = "breezy.*"
2740+ignore_missing_imports = true
2741+
2742+[[tool.mypy.overrides]]
2743+module = "debian.*"
2744+ignore_missing_imports = true
2745+
2746+[[tool.mypy.overrides]]
2747+module = "defusedxml.*"
2748+ignore_missing_imports = true
2749+
2750+[[tool.mypy.overrides]]
2751+module = "dohq_artifactory.*"
2752+ignore_missing_imports = true
2753+
2754+[[tool.mypy.overrides]]
2755+module = "feedparser"
2756+ignore_missing_imports = true
2757+
2758+[[tool.mypy.overrides]]
2759+module = "fixtures"
2760+ignore_missing_imports = true
2761+
2762+[[tool.mypy.overrides]]
2763+module = "gpgme"
2764+ignore_missing_imports = true
2765+
2766+[[tool.mypy.overrides]]
2767+module = "iso8601"
2768+ignore_missing_imports = true
2769+
2770+[[tool.mypy.overrides]]
2771+module = "lazr.*"
2772+ignore_missing_imports = true
2773+
2774+[[tool.mypy.overrides]]
2775+module = "pymacaroons"
2776+ignore_missing_imports = true
2777+
2778+[[tool.mypy.overrides]]
2779+module = "pystache"
2780+ignore_missing_imports = true
2781+
2782+[[tool.mypy.overrides]]
2783+module = "responses"
2784+ignore_missing_imports = true
2785+
2786+[[tool.mypy.overrides]]
2787+module = "soupmatchers"
2788+ignore_missing_imports = true
2789+
2790+[[tool.mypy.overrides]]
2791+module = "storm.*"
2792+ignore_missing_imports = true
2793+
2794+[[tool.mypy.overrides]]
2795+module = "testscenarios.*"
2796+ignore_missing_imports = true
2797+
2798+[[tool.mypy.overrides]]
2799+module = "testtools.*"
2800+ignore_missing_imports = true
2801+
2802+[[tool.mypy.overrides]]
2803+module = "transaction"
2804+ignore_missing_imports = true
2805+
2806+[[tool.mypy.overrides]]
2807+module = "treq"
2808+ignore_missing_imports = true
2809+
2810+[[tool.mypy.overrides]]
2811+module = "twisted.*"
2812+ignore_missing_imports = true
2813+
2814+[[tool.mypy.overrides]]
2815+module = "zope.*"
2816+ignore_missing_imports = true
2817+
2818diff --git a/requirements/types.txt b/requirements/types.txt
2819new file mode 100644
2820index 0000000..d9a338e
2821--- /dev/null
2822+++ b/requirements/types.txt
2823@@ -0,0 +1,7 @@
2824+types-pytz==0.1.0
2825+types-simplejson==0.1.0
2826+types-six==0.1.9
2827+types-beautifulsoup4==4.9.0
2828+types-requests==0.1.13
2829+lxml-stubs==0.4.0
2830+types-Markdown==0.1.0

Subscribers

People subscribed via source and target branches

to status/vote changes: