Merge ~andrey-fedoseev/launchpad:mypy into launchpad:master
- Git
- lp:~andrey-fedoseev/launchpad
- mypy
- Merge into master
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) |
Related bugs: |
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
Description of the change
Colin Watson (cjwatson) wrote : | # |
BTW, thanks - this looks like a great start, and I'm excited for it!
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.
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.
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.
Jürgen Gmach (jugmac00) wrote : | # |
Which command do you use to run mypy?
I am only able to check single files.
```
jugmac00@
mypy 0.910
jugmac00@
There are no .py[i] files in directory 'lib/lp/answers'
jugmac00@
There are no .py[i] files in directory 'lib/lp/
jugmac00@
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@
enums.py:21: error: Skipping analyzing "lazr.enum": found module but no type hints or library stubs
interfaces/
interfaces/
interfaces/
interfaces/
errors.py:16: error: Skipping analyzing "lazr.restful.
interfaces/
...
```
@Andrey - could you please give me the exact command(s) to run so I can run mypy/verify this MP? Thanks!
Jürgen Gmach (jugmac00) wrote : | # |
Ah, ok... figured it out...
```
jugmac00@
lib/lp/
Found 1 error in 1 file (checked 11 source files)
```
```
jugmac00@
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
Found 7 errors in 5 files (checked 21 source files)
```
```
jugmac00@
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
Found 5 errors in 5 files (checked 15 source files)
```
```
jugmac00@
lib/lp/
Found 1 error in 1 file (checked 7 source files)
```
Apart from fixing these errors, could you please update your commit message?
I...
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:/
https:/
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/
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-
```
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.
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/
commands =
mypy --follow-
lib/lp/answers \
lib/lp/app \
lib/
lib/
lib/
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:/
> 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/
commands =
mypy --follow-
lib/lp/answers \
lib/
lib/
```
and update the commit message appropriately.
The reduced set works great!
```
❯ tox -e mypy -r
mypy recreate: /home/jugmac00/
mypy installdeps: mypy, -rrequirements/
mypy installed: lxml-stubs=
mypy run-test-pre: PYTHONHASHSEED=
mypy run-test: commands[0] | mypy --follow-
Success: no issues found in 160 source files
_______
mypy: commands succeeded
congratulations :)
```
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.
Jürgen Gmach (jugmac00) : | # |
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.
Andrey Fedoseev (andrey-fedoseev) wrote : | # |
Jürgen, I have addressed your inline comments.
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.
Colin Watson (cjwatson) : | # |
Preview Diff
1 | diff --git a/lib/lp/answers/adapters.py b/lib/lp/answers/adapters.py |
2 | index 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.""" |
20 | diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py |
21 | index 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): |
56 | diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py |
57 | index 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 |
120 | diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py |
121 | index 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): |
168 | diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py |
169 | index 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.""" |
191 | diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py |
192 | index 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.""" |
213 | diff --git a/lib/lp/answers/mail/__init__.py b/lib/lp/answers/mail/__init__.py |
214 | index 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] |
224 | diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py |
225 | index 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`.""" |
255 | diff --git a/lib/lp/answers/security.py b/lib/lp/answers/security.py |
256 | index 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" |
277 | diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py |
278 | index 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. |
302 | diff --git a/lib/lp/answers/tests/test_questiontarget.py b/lib/lp/answers/tests/test_questiontarget.py |
303 | index 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.""" |
325 | diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py |
326 | index 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 |
352 | diff --git a/lib/lp/app/browser/badge.py b/lib/lp/app/browser/badge.py |
353 | index 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. |
392 | diff --git a/lib/lp/app/browser/interfaces.py b/lib/lp/app/browser/interfaces.py |
393 | new file mode 100644 |
394 | index 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.""" |
416 | diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py |
417 | index 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): |
483 | diff --git a/lib/lp/app/browser/multistep.py b/lib/lp/app/browser/multistep.py |
484 | index 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 |
504 | diff --git a/lib/lp/app/browser/root.py b/lib/lp/app/browser/root.py |
505 | index 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. |
525 | diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py |
526 | index 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 ( |
538 | diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py |
539 | index 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): |
578 | diff --git a/lib/lp/app/browser/tests/test_webservice.py b/lib/lp/app/browser/tests/test_webservice.py |
579 | index 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.""" |
592 | diff --git a/lib/lp/app/browser/webservice.py b/lib/lp/app/browser/webservice.py |
593 | index 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) |
615 | diff --git a/lib/lp/app/doc/badges.rst b/lib/lp/app/doc/badges.rst |
616 | index 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 | |
638 | diff --git a/lib/lp/app/security.py b/lib/lp/app/security.py |
639 | index 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 |
665 | diff --git a/lib/lp/app/tests/test_yuitests.py b/lib/lp/app/tests/test_yuitests.py |
666 | index 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 | |
685 | diff --git a/lib/lp/app/utilities/celebrities.py b/lib/lp/app/utilities/celebrities.py |
686 | index 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) |
708 | diff --git a/lib/lp/app/validators/__init__.py b/lib/lp/app/validators/__init__.py |
709 | index 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. |
754 | diff --git a/lib/lp/app/validators/interfaces.py b/lib/lp/app/validators/interfaces.py |
755 | new file mode 100644 |
756 | index 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 | + """ |
778 | diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py |
779 | index 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() |
820 | diff --git a/lib/lp/app/widgets/tests/test_itemswidgets.py b/lib/lp/app/widgets/tests/test_itemswidgets.py |
821 | index 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> title") |
840 | |
841 | diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py |
842 | index 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 | ) |
897 | diff --git a/lib/lp/archivepublisher/customupload.py b/lib/lp/archivepublisher/customupload.py |
898 | index 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): |
910 | diff --git a/lib/lp/archivepublisher/debversion.py b/lib/lp/archivepublisher/debversion.py |
911 | index 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 |
930 | diff --git a/lib/lp/archivepublisher/diskpool.py b/lib/lp/archivepublisher/diskpool.py |
931 | index 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") |
1011 | diff --git a/lib/lp/archivepublisher/security.py b/lib/lp/archivepublisher/security.py |
1012 | index 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 |
1031 | diff --git a/lib/lp/archiveuploader/dscfile.py b/lib/lp/archiveuploader/dscfile.py |
1032 | index 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. |
1087 | diff --git a/lib/lp/archiveuploader/tests/test_buildduploads.py b/lib/lp/archiveuploader/tests/test_buildduploads.py |
1088 | index 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 | |
1104 | diff --git a/lib/lp/blueprints/browser/specification.py b/lib/lp/blueprints/browser/specification.py |
1105 | index 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): |
1158 | diff --git a/lib/lp/blueprints/browser/specificationbranch.py b/lib/lp/blueprints/browser/specificationbranch.py |
1159 | index 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): |
1191 | diff --git a/lib/lp/blueprints/browser/specificationsubscription.py b/lib/lp/blueprints/browser/specificationsubscription.py |
1192 | index 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): |
1258 | diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py |
1259 | index 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): |
1306 | diff --git a/lib/lp/blueprints/browser/sprintattendance.py b/lib/lp/blueprints/browser/sprintattendance.py |
1307 | index 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 | |
1321 | diff --git a/lib/lp/blueprints/mail/__init__.py b/lib/lp/blueprints/mail/__init__.py |
1322 | index 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] |
1332 | diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py |
1333 | index 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): |
1414 | diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py |
1415 | index 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 ( |
1455 | diff --git a/lib/lp/bugs/browser/bugbranch.py b/lib/lp/bugs/browser/bugbranch.py |
1456 | index 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): |
1510 | diff --git a/lib/lp/bugs/browser/bugnomination.py b/lib/lp/bugs/browser/bugnomination.py |
1511 | index 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): |
1532 | diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py |
1533 | index 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) |
1587 | diff --git a/lib/lp/bugs/browser/bugsubscriptionfilter.py b/lib/lp/bugs/browser/bugsubscriptionfilter.py |
1588 | index 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): |
1619 | diff --git a/lib/lp/bugs/browser/bugsupervisor.py b/lib/lp/bugs/browser/bugsupervisor.py |
1620 | index 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): |
1634 | diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py |
1635 | index 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 |
1669 | diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py |
1670 | index 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. |
1684 | diff --git a/lib/lp/bugs/browser/bugwatch.py b/lib/lp/bugs/browser/bugwatch.py |
1685 | index 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): |
1710 | diff --git a/lib/lp/bugs/browser/cve.py b/lib/lp/bugs/browser/cve.py |
1711 | index 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 | |
1736 | diff --git a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py |
1737 | index 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): |
1751 | diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py |
1752 | index 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) |
1766 | diff --git a/lib/lp/bugs/externalbugtracker/base.py b/lib/lp/bugs/externalbugtracker/base.py |
1767 | index 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" |
1788 | diff --git a/lib/lp/bugs/externalbugtracker/github.py b/lib/lp/bugs/externalbugtracker/github.py |
1789 | index 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 | |
1841 | diff --git a/lib/lp/bugs/externalbugtracker/interfaces.py b/lib/lp/bugs/externalbugtracker/interfaces.py |
1842 | new file mode 100644 |
1843 | index 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.""" |
1869 | diff --git a/lib/lp/bugs/externalbugtracker/tests/test_github.py b/lib/lp/bugs/externalbugtracker/tests/test_github.py |
1870 | index 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 |
1883 | diff --git a/lib/lp/bugs/interfaces/bugnotification.py b/lib/lp/bugs/interfaces/bugnotification.py |
1884 | index 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 |
1900 | diff --git a/lib/lp/bugs/model/bugtarget.py b/lib/lp/bugs/model/bugtarget.py |
1901 | index 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__) |
1928 | diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py |
1929 | index 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, |
1950 | diff --git a/lib/lp/bugs/model/tests/test_bugtask_status.py b/lib/lp/bugs/model/tests/test_bugtask_status.py |
1951 | index 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 |
1965 | diff --git a/lib/lp/bugs/scripts/checkwatches/base.py b/lib/lp/bugs/scripts/checkwatches/base.py |
1966 | index 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. |
1996 | diff --git a/lib/lp/bugs/scripts/checkwatches/core.py b/lib/lp/bugs/scripts/checkwatches/core.py |
1997 | index 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 |
2017 | diff --git a/lib/lp/bugs/scripts/debbugs.py b/lib/lp/bugs/scripts/debbugs.py |
2018 | index 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: |
2033 | diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py |
2034 | index 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 |
2085 | diff --git a/lib/lp/bugs/tests/externalbugtracker.py b/lib/lp/bugs/tests/externalbugtracker.py |
2086 | index 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 |
2130 | diff --git a/lib/lp/bugs/tests/test_buglinktarget.py b/lib/lp/bugs/tests/test_buglinktarget.py |
2131 | index 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) |
2154 | diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py |
2155 | index 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() |
2169 | diff --git a/lib/lp/bugs/tests/test_bugsearch_conjoined.py b/lib/lp/bugs/tests/test_bugsearch_conjoined.py |
2170 | index 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.""" |
2192 | diff --git a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py |
2193 | index 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") |
2207 | diff --git a/lib/lp/bugs/tests/test_bugtarget.py b/lib/lp/bugs/tests/test_bugtarget.py |
2208 | index 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.""" |
2233 | diff --git a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py |
2234 | index 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() |
2320 | diff --git a/lib/lp/bugs/tests/test_bugtracker_components.py b/lib/lp/bugs/tests/test_bugtracker_components.py |
2321 | index 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) |
2351 | diff --git a/lib/lp/bugs/tests/test_bugwatch.py b/lib/lp/bugs/tests/test_bugwatch.py |
2352 | index 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" |
2398 | diff --git a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py |
2399 | index 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: |
2424 | diff --git a/lib/lp/bugs/tests/test_externalbugtracker.py b/lib/lp/bugs/tests/test_externalbugtracker.py |
2425 | index 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() |
2445 | diff --git a/lib/lp/bugs/tests/test_structuralsubscription.py b/lib/lp/bugs/tests/test_structuralsubscription.py |
2446 | index 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() |
2460 | diff --git a/lib/lp/bugs/tests/test_yuitests.py b/lib/lp/bugs/tests/test_yuitests.py |
2461 | index 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 | |
2480 | diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py |
2481 | index 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): |
2501 | diff --git a/lib/lp/services/feeds/browser.py b/lib/lp/services/feeds/browser.py |
2502 | index 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): |
2526 | diff --git a/lib/lp/services/looptuner.py b/lib/lp/services/looptuner.py |
2527 | index 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): |
2539 | diff --git a/lib/lp/services/mail/commands.py b/lib/lp/services/mail/commands.py |
2540 | index 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 |
2552 | diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py |
2553 | index 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 | |
2567 | diff --git a/lib/lp/services/webapp/breadcrumb.py b/lib/lp/services/webapp/breadcrumb.py |
2568 | index 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 |
2580 | diff --git a/lib/lp/services/webapp/menu.py b/lib/lp/services/webapp/menu.py |
2581 | index 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): |
2610 | diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py |
2611 | index 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. |
2645 | diff --git a/lib/lp/services/webapp/vocabulary.py b/lib/lp/services/webapp/vocabulary.py |
2646 | index 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): |
2675 | diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py |
2676 | index 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 |
2710 | diff --git a/pyproject.toml b/pyproject.toml |
2711 | index 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 | + |
2818 | diff --git a/requirements/types.txt b/requirements/types.txt |
2819 | new file mode 100644 |
2820 | index 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 |
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.