launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28660
[Merge] ~andrey-fedoseev/launchpad:mypy into launchpad:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:mypy into launchpad:master.
Commit message:
Prepare some packages for type checking with `mypy`:
- lp.answers
- lp.app
- lp.archivepublisher
- lp.archiveuploader
- lp.blueprint
- lp.bugs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/425377
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:mypy into launchpad:master.
diff --git a/lib/lp/answers/adapters.py b/lib/lp/answers/adapters.py
index 3c7f037..17e0f81 100644
--- a/lib/lp/answers/adapters.py
+++ b/lib/lp/answers/adapters.py
@@ -3,9 +3,6 @@
"""Adapters used in the Answer Tracker."""
-__all__ = []
-
-
from lp.answers.interfaces.faqtarget import IFAQTarget
diff --git a/lib/lp/answers/browser/faqcollection.py b/lib/lp/answers/browser/faqcollection.py
index f00b3b2..bf05b47 100644
--- a/lib/lp/answers/browser/faqcollection.py
+++ b/lib/lp/answers/browser/faqcollection.py
@@ -8,8 +8,11 @@ __all__ = [
"SearchFAQsView",
]
+from typing import Type
from urllib.parse import urlencode
+from zope.interface import Interface
+
from lp import _
from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH, QuestionSort
from lp.answers.interfaces.faqcollection import (
@@ -28,7 +31,7 @@ from lp.services.webapp.menu import enabled_with_permission
class FAQCollectionMenu(NavigationMenu):
"""Base menu definition for `IFAQCollection`."""
- usedfor = IFAQCollection
+ usedfor = IFAQCollection # type: Type[Interface]
facet = "answers"
links = ["list_all", "create_faq"]
@@ -82,7 +85,9 @@ class SearchFAQsView(LaunchpadFormView):
else:
return _("FAQs for $displayname", mapping=replacements)
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
@property
def empty_listing_message(self):
diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
index 32d63dc..485df06 100644
--- a/lib/lp/answers/browser/question.py
+++ b/lib/lp/answers/browser/question.py
@@ -801,7 +801,9 @@ class QuestionChangeStatusView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class QuestionTargetWidget(LaunchpadTargetWidget):
@@ -820,7 +822,6 @@ class QuestionEditView(LaunchpadEditFormView):
"""View for editing a Question."""
schema = IQuestion
- label = "Edit question"
field_names = [
"language",
"title",
@@ -838,7 +839,9 @@ class QuestionEditView(LaunchpadEditFormView):
def page_title(self):
return "Edit question #%s details" % self.context.id
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
def setUpFields(self):
"""Select the subset of fields to display.
@@ -874,7 +877,9 @@ class QuestionEditView(LaunchpadEditFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class QuestionRejectView(LaunchpadFormView):
@@ -921,7 +926,9 @@ class QuestionRejectView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class LinkFAQMixin:
@@ -1587,4 +1594,6 @@ class QuestionLinkFAQView(LinkFAQMixin, LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
index 488c860..4ee9263 100644
--- a/lib/lp/answers/browser/questiontarget.py
+++ b/lib/lp/answers/browser/questiontarget.py
@@ -231,7 +231,9 @@ class SearchQuestionsView(UserSupportLanguagesMixin, LaunchpadFormView):
else:
return _("Questions for ${context}", mapping=replacements)
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
@property
def display_target_column(self):
@@ -562,7 +564,9 @@ class QuestionCollectionMyQuestionsView(SearchQuestionsView):
mapping={"context": self.context.displayname},
)
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
@property
def empty_listing_message(self):
@@ -615,7 +619,9 @@ class QuestionCollectionNeedAttentionView(SearchQuestionsView):
mapping={"context": self.context.displayname},
)
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
@property
def empty_listing_message(self):
@@ -691,7 +697,9 @@ class QuestionCollectionByLanguageView(SearchQuestionsView):
else:
return _("${language} questions in ${context}", mapping=mapping)
- label = page_title
+ @property
+ def label(self):
+ return self.page_title
@property
def empty_listing_message(self):
diff --git a/lib/lp/answers/browser/tests/test_question.py b/lib/lp/answers/browser/tests/test_question.py
index 0619fc3..b901faf 100644
--- a/lib/lp/answers/browser/tests/test_question.py
+++ b/lib/lp/answers/browser/tests/test_question.py
@@ -3,8 +3,6 @@
"""Tests for the question module."""
-__all__ = []
-
from zope.security.proxy import removeSecurityProxy
from lp.answers.browser.question import QuestionTargetWidget
diff --git a/lib/lp/answers/browser/tests/test_views.py b/lib/lp/answers/browser/tests/test_views.py
index 0fd7031..c01790a 100644
--- a/lib/lp/answers/browser/tests/test_views.py
+++ b/lib/lp/answers/browser/tests/test_views.py
@@ -3,8 +3,6 @@
"""Test harness for Answer Tracker related unit tests."""
-__all__ = []
-
import unittest
from lp.testing import BrowserTestCase
diff --git a/lib/lp/answers/mail/__init__.py b/lib/lp/answers/mail/__init__.py
index 6b3f0d4..516532e 100644
--- a/lib/lp/answers/mail/__init__.py
+++ b/lib/lp/answers/mail/__init__.py
@@ -1,4 +1,2 @@
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__all__ = []
diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
index 186c7a4..1d8e670 100644
--- a/lib/lp/answers/model/question.py
+++ b/lib/lp/answers/model/question.py
@@ -270,6 +270,7 @@ class Question(StormBase, BugLinkTargetMixin):
return list(self._messages)
# attributes
+ @property
def target(self):
"""See `IQuestion`."""
if self.product:
@@ -279,7 +280,8 @@ class Question(StormBase, BugLinkTargetMixin):
else:
return self.distribution
- def _settarget(self, question_target):
+ @target.setter
+ def target(self, question_target):
"""See Question.target."""
if not IQuestionTarget.providedBy(question_target):
raise QuestionTargetError("The target must be an IQuestionTarget")
@@ -300,8 +302,6 @@ class Question(StormBase, BugLinkTargetMixin):
"Unknown IQuestionTarget type of %s" % question_target
)
- target = property(target, _settarget, doc=target.__doc__)
-
@property
def followup_subject(self):
"""See `IMessageTarget`."""
diff --git a/lib/lp/answers/tests/test_question_workflow.py b/lib/lp/answers/tests/test_question_workflow.py
index d31d8aa..809170d 100644
--- a/lib/lp/answers/tests/test_question_workflow.py
+++ b/lib/lp/answers/tests/test_question_workflow.py
@@ -9,8 +9,6 @@ but testing all the possible transitions makes the documentation more heavy
than necessary. This is tested here.
"""
-__all__ = []
-
import traceback
from datetime import datetime, timedelta
diff --git a/lib/lp/answers/tests/test_questiontarget.py b/lib/lp/answers/tests/test_questiontarget.py
index c8f56e8..dbe73e7 100644
--- a/lib/lp/answers/tests/test_questiontarget.py
+++ b/lib/lp/answers/tests/test_questiontarget.py
@@ -3,8 +3,6 @@
"""Tests related to IQuestionTarget."""
-__all__ = []
-
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
diff --git a/lib/lp/app/__init__.py b/lib/lp/app/__init__.py
index 69ebb95..fa8a497 100644
--- a/lib/lp/app/__init__.py
+++ b/lib/lp/app/__init__.py
@@ -8,8 +8,6 @@ together. As such, it can import from any modules, but nothing should import
from it.
"""
-__all__ = []
-
# Zope recently changed the behaviour of items widgets with regards to missing
# values, but they kindly left this global variable for you to monkey patch if
# you want the old behaviour, just like we do.
diff --git a/lib/lp/app/browser/badge.py b/lib/lp/app/browser/badge.py
index d3fd5a6..cf4db60 100644
--- a/lib/lp/app/browser/badge.py
+++ b/lib/lp/app/browser/badge.py
@@ -11,12 +11,12 @@ Badges are shown in two main places:
__all__ = [
"Badge",
"HasBadgeBase",
- "IHasBadges",
"STANDARD_BADGES",
]
-from zope.interface import Interface, implementer
+from zope.interface import implementer
+from lp.app.browser.interfaces import IHasBadges
from lp.services.privacy.interfaces import IObjectPrivacy
@@ -109,20 +109,6 @@ STANDARD_BADGES = {
}
-class IHasBadges(Interface):
- """A method to determine visible badges.
-
- Badges are used to show connections between different content objects, for
- example a BugBranch is a link between a bug and a branch. To represent
- this link a bug has a branch badge, and the branch has a bug badge.
-
- Badges should honour the visibility of the linked objects.
- """
-
- def getVisibleBadges():
- """Return a list of `Badge` objects that the logged in user can see."""
-
-
@implementer(IHasBadges)
class HasBadgeBase:
"""The standard base implementation for badge visibility.
diff --git a/lib/lp/app/browser/interfaces.py b/lib/lp/app/browser/interfaces.py
new file mode 100644
index 0000000..32152d8
--- /dev/null
+++ b/lib/lp/app/browser/interfaces.py
@@ -0,0 +1,18 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from zope.interface import Interface
+
+
+class IHasBadges(Interface):
+ """A method to determine visible badges.
+
+ Badges are used to show connections between different content objects, for
+ example a BugBranch is a link between a bug and a branch. To represent
+ this link a bug has a branch badge, and the branch has a bug badge.
+
+ Badges should honour the visibility of the linked objects.
+ """
+
+ def getVisibleBadges():
+ """Return a list of `Badge` objects that the logged-in user can see."""
diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
index 201c948..01ed055 100644
--- a/lib/lp/app/browser/launchpadform.py
+++ b/lib/lp/app/browser/launchpadform.py
@@ -14,6 +14,8 @@ __all__ = [
"safe_action",
]
+from typing import List, Optional, Type
+
import simplejson
import transaction
from lazr.lifecycle.event import ObjectModifiedEvent
@@ -31,7 +33,7 @@ from zope.formlib.widgets import (
RadioWidget,
TextAreaWidget,
)
-from zope.interface import classImplements, implementer, providedBy
+from zope.interface import Interface, classImplements, implementer, providedBy
from zope.traversing.interfaces import ITraversable, TraversalError
from lp.services.webapp.escaping import html_escape
@@ -60,15 +62,15 @@ class LaunchpadFormView(LaunchpadView):
prefix = "field"
# The form schema
- schema = None
+ schema = None # type: Type[Interface]
# Subset of fields to use
- field_names = None
+ field_names = None # type: Optional[List[str]]
# The next URL to redirect to on successful form submission
- next_url = None
+ next_url = None # type: Optional[str]
# The cancel URL is rendered as a Cancel link in the form
# macro if set in a derived class.
- cancel_url = None
+ cancel_url = None # type: Optional[str]
# The name of the widget that will receive initial focus in the form.
# By default, the first widget will receive focus. Set this to None
@@ -87,7 +89,7 @@ class LaunchpadFormView(LaunchpadView):
# The for_input is passed through to create the fields. If this value
# is set to true in derived classes, then fields that are marked
# read only will have editable widgets created for them.
- for_input = None
+ for_input = None # type: Optional[bool]
def __init__(self, context, request):
LaunchpadView.__init__(self, context, request)
@@ -568,8 +570,13 @@ class ReturnToReferrerMixin:
else:
return canonical_url(self.context)
- next_url = _return_url
- cancel_url = _return_url
+ @property
+ def next_url(self):
+ return self._return_url
+
+ @property
+ def cancel_url(self):
+ return self._return_url
def has_structured_doc(field):
diff --git a/lib/lp/app/browser/multistep.py b/lib/lp/app/browser/multistep.py
index a6b7aee..a8df433 100644
--- a/lib/lp/app/browser/multistep.py
+++ b/lib/lp/app/browser/multistep.py
@@ -8,6 +8,7 @@ __all__ = [
"StepView",
]
+from typing import List
from zope.formlib import form
from zope.formlib.widget import CustomWidgetFactory
@@ -148,7 +149,7 @@ class StepView(LaunchpadFormView):
TextWidget, visible=False
)
- _field_names = []
+ _field_names = [] # type: List[str]
step_name = ""
main_action_label = "Continue"
next_step = None
diff --git a/lib/lp/app/browser/root.py b/lib/lp/app/browser/root.py
index 7d5acda..5dc4181 100644
--- a/lib/lp/app/browser/root.py
+++ b/lib/lp/app/browser/root.py
@@ -10,6 +10,7 @@ __all__ = [
import re
import time
+from typing import Any, List
import feedparser
import requests
@@ -55,7 +56,7 @@ class LaunchpadRootIndexView(HasAnnouncementsView, LaunchpadView):
"""An view for the default view of the LaunchpadRoot."""
page_title = "Launchpad"
- featured_projects = []
+ featured_projects = [] # type: List[Any]
featured_projects_top = None
# Used by the footer to display the lp-arcana section.
diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
index fe62ec3..ff29b5f 100644
--- a/lib/lp/app/browser/tales.py
+++ b/lib/lp/app/browser/tales.py
@@ -32,7 +32,7 @@ from zope.traversing.interfaces import (
)
from lp import _
-from lp.app.browser.badge import IHasBadges
+from lp.app.browser.interfaces import IHasBadges
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.enums import PRIVATE_INFORMATION_TYPES
from lp.app.interfaces.launchpad import (
diff --git a/lib/lp/app/browser/tests/test_vocabulary.py b/lib/lp/app/browser/tests/test_vocabulary.py
index 05e5fee..dff47ee 100644
--- a/lib/lp/app/browser/tests/test_vocabulary.py
+++ b/lib/lp/app/browser/tests/test_vocabulary.py
@@ -4,6 +4,7 @@
"""Test vocabulary adapters."""
from datetime import datetime
+from typing import List
from urllib.parse import urlencode
import pytz
@@ -23,6 +24,7 @@ from lp.app.errors import UnexpectedFormData
from lp.registry.interfaces.irc import IIrcIDSet
from lp.registry.interfaces.person import TeamMembershipPolicy
from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.model.person import Person
from lp.services.webapp.interfaces import ILaunchpadRoot
from lp.services.webapp.vocabulary import (
CountableIterator,
@@ -506,16 +508,16 @@ class TestDistributionPickerEntrySourceAdapter(TestCaseWithFactory):
@implementer(IHugeVocabulary)
class TestPersonVocabulary:
- test_persons = []
+ test_persons = [] # type: List[Person]
@classmethod
- def setTestData(cls, person_list):
+ def setTestData(cls, person_list: List[Person]):
cls.test_persons = person_list
def __init__(self, context):
self.context = context
- def toTerm(self, person):
+ def toTerm(self, person: Person):
return SimpleTerm(person, person.name, person.displayname)
def searchForTerms(self, query=None, vocab_filter=None):
diff --git a/lib/lp/app/browser/tests/test_webservice.py b/lib/lp/app/browser/tests/test_webservice.py
index 2f3850d..b1f72ad 100644
--- a/lib/lp/app/browser/tests/test_webservice.py
+++ b/lib/lp/app/browser/tests/test_webservice.py
@@ -54,7 +54,8 @@ class BaseMissingObjectWebService:
"""Base test of NotFound errors for top-level webservice objects."""
layer = DatabaseFunctionalLayer
- object_type = None
+
+ object_type = None # type: str
def test_object_not_found(self):
"""Missing top-level objects generate 404s but not OOPS."""
diff --git a/lib/lp/app/browser/webservice.py b/lib/lp/app/browser/webservice.py
index 8439f7e..0d02cb0 100644
--- a/lib/lp/app/browser/webservice.py
+++ b/lib/lp/app/browser/webservice.py
@@ -3,8 +3,6 @@
"""Adapters for registry objects for the webservice."""
-__all__ = []
-
from lazr.restful.interfaces import (
IFieldHTMLRenderer,
IReference,
diff --git a/lib/lp/app/doc/badges.rst b/lib/lp/app/doc/badges.rst
index 3e7dfcc..b2bf541 100644
--- a/lib/lp/app/doc/badges.rst
+++ b/lib/lp/app/doc/badges.rst
@@ -85,7 +85,8 @@ implementation of IHasBadges. HasBadgeBase is also a default adapter
for Interface, which just provides the privacy badge.
>>> from zope.interface import Interface, Attribute, implementer
- >>> from lp.app.browser.badge import IHasBadges, HasBadgeBase
+ >>> from lp.app.browser.interfaces import IHasBadges
+ >>> from lp.app.browser.badge import HasBadgeBase
>>> from lp.testing import verifyObject
>>> @implementer(Interface)
... class PrivateClass:
@@ -196,7 +197,7 @@ IHasBadges. Here is the sample from the branch.zcml to illustrate.
<adapter
for="lp.code.interfaces.branch.IBranch"
- provides="lp.app.browser.badge.IHasBadges"
+ provides="lp.app.browser.interfaces.IHasBadges"
factory="lp.code.browser.branchlisting.BranchBadges"
/>
diff --git a/lib/lp/app/security.py b/lib/lp/app/security.py
index 30ec73b..37ccc27 100644
--- a/lib/lp/app/security.py
+++ b/lib/lp/app/security.py
@@ -10,9 +10,10 @@ __all__ = [
]
from itertools import repeat
+from typing import Optional, Type
from zope.component import queryAdapter
-from zope.interface import implementer
+from zope.interface import Interface, implementer
from zope.security.permission import checkPermission
from lp.app.interfaces.security import IAuthorization
@@ -20,8 +21,8 @@ from lp.app.interfaces.security import IAuthorization
@implementer(IAuthorization)
class AuthorizationBase:
- permission = None
- usedfor = None
+ permission = None # type: Optional[str]
+ usedfor = None # type: Optional[Type[Interface]]
def __init__(self, obj):
self.obj = obj
diff --git a/lib/lp/app/tests/test_yuitests.py b/lib/lp/app/tests/test_yuitests.py
index 371a972..1b12917 100644
--- a/lib/lp/app/tests/test_yuitests.py
+++ b/lib/lp/app/tests/test_yuitests.py
@@ -3,8 +3,6 @@
"""Run YUI.test tests."""
-__all__ = []
-
from lp.testing import YUIUnitTestCase, build_yui_unittest_suite
from lp.testing.layers import YUITestLayer
diff --git a/lib/lp/app/utilities/celebrities.py b/lib/lp/app/utilities/celebrities.py
index 3df5989..3f54349 100644
--- a/lib/lp/app/utilities/celebrities.py
+++ b/lib/lp/app/utilities/celebrities.py
@@ -5,6 +5,8 @@
__all__ = ["LaunchpadCelebrities"]
+from typing import Set
+
from zope.component import getUtility
from zope.interface import implementer
@@ -102,7 +104,8 @@ class PersonCelebrityDescriptor(CelebrityDescriptor):
if a given person is a celebrity for special handling.
"""
- names = set() # Populated by the constructor.
+ # Populated by the constructor.
+ names = set() # type: Set[str]
def __init__(self, name):
PersonCelebrityDescriptor.names.add(name)
diff --git a/lib/lp/app/validators/__init__.py b/lib/lp/app/validators/__init__.py
index 1806143..c0fbb40 100644
--- a/lib/lp/app/validators/__init__.py
+++ b/lib/lp/app/validators/__init__.py
@@ -12,20 +12,18 @@ See README.txt for discussion
from zope.formlib.exception import (
WidgetInputErrorView as Z3WidgetInputErrorView,
)
-from zope.formlib.interfaces import IWidgetInputError
-from zope.interface import Interface, implementer
+from zope.interface import implementer
from zope.schema.interfaces import ValidationError
+from lp.app.validators.interfaces import (
+ ILaunchpadValidationError,
+ ILaunchpadWidgetInputErrorView,
+)
from lp.services.webapp.escaping import html_escape
__all__ = ["LaunchpadValidationError"]
-class ILaunchpadValidationError(IWidgetInputError):
- def snippet():
- """Render as an HTML error message, as per IWidgetInputErrorView"""
-
-
@implementer(ILaunchpadValidationError)
class LaunchpadValidationError(ValidationError):
"""A LaunchpadValidationError may be raised from a schema field
@@ -69,15 +67,6 @@ class LaunchpadValidationError(ValidationError):
return self.snippet()
-class ILaunchpadWidgetInputErrorView(Interface):
- def snippet():
- """Convert a widget input error to an html snippet
-
- If the error implements provides a snippet() method, just return it.
- Otherwise, fall back to the default Z3 mechanism
- """
-
-
@implementer(ILaunchpadWidgetInputErrorView)
class WidgetInputErrorView(Z3WidgetInputErrorView):
"""Display an input error as a snippet of text.
diff --git a/lib/lp/app/validators/interfaces.py b/lib/lp/app/validators/interfaces.py
new file mode 100644
index 0000000..ff631bd
--- /dev/null
+++ b/lib/lp/app/validators/interfaces.py
@@ -0,0 +1,18 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+from zope.formlib.interfaces import IWidgetInputError
+from zope.interface import Interface
+
+
+class ILaunchpadValidationError(IWidgetInputError):
+ def snippet():
+ """Render as an HTML error message, as per IWidgetInputErrorView"""
+
+
+class ILaunchpadWidgetInputErrorView(Interface):
+ def snippet():
+ """Convert a widget input error to an html snippet
+
+ If the error implements provides a snippet() method, just return it.
+ Otherwise, fall back to the default Z3 mechanism
+ """
diff --git a/lib/lp/app/widgets/date.py b/lib/lp/app/widgets/date.py
index 25b090d..66fb615 100644
--- a/lib/lp/app/widgets/date.py
+++ b/lib/lp/app/widgets/date.py
@@ -158,8 +158,7 @@ class DateTimeWidget(TextWidget):
return [o.strip() for o in outputs]
- # @property XXX: do as a property when we have python2.5 for tests of
- # properties
+ @property
def time_zone(self):
"""The widget time zone.
@@ -215,8 +214,6 @@ class DateTimeWidget(TextWidget):
), "DateTime widget needs a time zone."
return self.system_time_zone
- time_zone = property(time_zone, doc=time_zone.__doc__)
-
@property
def time_zone_name(self):
"""The name of the widget time zone for display in the widget."""
@@ -251,8 +248,7 @@ class DateTimeWidget(TextWidget):
else:
return None
- # @property XXX: do as a property when we have python2.5 for tests of
- # properties
+ @property
def daterange(self):
"""The javascript variable giving the allowed date range to pick.
@@ -318,8 +314,6 @@ class DateTimeWidget(TextWidget):
daterange += self.to_date.strftime("[%Y,%m,%d]]")
return daterange
- daterange = property(daterange, doc=daterange.__doc__)
-
def getInputValue(self):
"""Return the date, if it is in the allowed date range."""
value = super().getInputValue()
diff --git a/lib/lp/app/widgets/tests/test_itemswidgets.py b/lib/lp/app/widgets/tests/test_itemswidgets.py
index 8d17443..57bdae9 100644
--- a/lib/lp/app/widgets/tests/test_itemswidgets.py
+++ b/lib/lp/app/widgets/tests/test_itemswidgets.py
@@ -2,6 +2,7 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
import doctest
+from typing import Any, Type
from lazr.enum import EnumeratedType, Item
from lazr.enum._enum import DBEnumeratedType, DBItem
@@ -31,7 +32,7 @@ class ItemWidgetTestCase(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
- WIDGET_CLASS = None
+ WIDGET_CLASS = None # type: Type[Any]
SAFE_TERM = SimpleTerm("object-1", "token-1", "Safe title")
UNSAFE_TERM = SimpleTerm("object-2", "token-2", "<unsafe> title")
diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
index e2fa1e8..aeb16c9 100644
--- a/lib/lp/archivepublisher/artifactory.py
+++ b/lib/lp/archivepublisher/artifactory.py
@@ -41,14 +41,16 @@ def _path_for(
source_name: str,
source_version: str,
pub_file: IPackageReleaseFile,
-) -> Path:
+) -> ArtifactoryPath:
repository_format = archive.repository_format
if repository_format == ArchiveRepositoryFormat.DEBIAN:
path = rootpath / poolify(source_name)
elif repository_format == ArchiveRepositoryFormat.PYTHON:
path = rootpath / source_name / source_version
elif repository_format == ArchiveRepositoryFormat.CONDA:
- user_defined_fields = pub_file.binarypackagerelease.user_defined_fields
+ user_defined_fields = (
+ pub_file.binarypackagerelease.user_defined_fields # type: ignore
+ )
subdir = next(
(value for key, value in user_defined_fields if key == "subdir"),
None,
@@ -86,7 +88,7 @@ class ArtifactoryPoolEntry:
def debug(self, *args, **kwargs) -> None:
self.logger.debug(*args, **kwargs)
- def pathFor(self, component: Optional[str] = None) -> Path:
+ def pathFor(self, component: Optional[str] = None) -> ArtifactoryPath:
"""Return the path for this file in the given component."""
# For Artifactory publication, we ignore the component. There's
# only marginal benefit in having it be explicitly represented in
@@ -113,9 +115,13 @@ class ArtifactoryPoolEntry:
be set as the "launchpad.release-id" property to keep track of this.
"""
if ISourcePackageReleaseFile.providedBy(pub_file):
- return "source:%d" % pub_file.sourcepackagereleaseID
+ return "source:{:d}".format(
+ pub_file.sourcepackagereleaseID
+ ) # type: ignore
elif IBinaryPackageFile.providedBy(pub_file):
- return "binary:%d" % pub_file.binarypackagereleaseID
+ return "binary:{:d}".format(
+ pub_file.binarypackagereleaseID
+ ) # type: ignore
else:
raise AssertionError("Unsupported file: %r" % pub_file)
@@ -382,6 +388,7 @@ class ArtifactoryPool:
# the pool structure, and doing so would introduce significant
# complications in terms of having to keep track of components just
# in order to update an artifact's properties.
+ assert pub_file is not None
return _path_for(
self.archive, self.rootpath, source_name, source_version, pub_file
)
diff --git a/lib/lp/archivepublisher/customupload.py b/lib/lp/archivepublisher/customupload.py
index 6193ca4..533e488 100644
--- a/lib/lp/archivepublisher/customupload.py
+++ b/lib/lp/archivepublisher/customupload.py
@@ -113,7 +113,7 @@ class CustomUpload:
"""Base class for custom upload handlers"""
# This should be set as a class property on each subclass.
- custom_type = None
+ custom_type = None # type: str
@classmethod
def publish(cls, packageupload, libraryfilealias, logger=None):
diff --git a/lib/lp/archivepublisher/debversion.py b/lib/lp/archivepublisher/debversion.py
index 7fd1ac5..e50c6fa 100644
--- a/lib/lp/archivepublisher/debversion.py
+++ b/lib/lp/archivepublisher/debversion.py
@@ -19,7 +19,9 @@ valid_epoch = re.compile(r"^[0-9]+$")
valid_upstream = re.compile(r"^[0-9][A-Za-z0-9+:.~-]*$")
valid_revision = re.compile(r"^[A-Za-z0-9+.~]+$")
-VersionError = changelog.VersionError
+
+class VersionError(changelog.VersionError):
+ pass
class BadInputError(VersionError):
diff --git a/lib/lp/archivepublisher/diskpool.py b/lib/lp/archivepublisher/diskpool.py
index dfa3e3e..cac6989 100644
--- a/lib/lp/archivepublisher/diskpool.py
+++ b/lib/lp/archivepublisher/diskpool.py
@@ -131,7 +131,7 @@ class DiskPoolEntry:
the disk for this file.
'tempath' must be in the same filesystem as 'rootpath', it will be
- used to store the instalation candidate while it is being downloaded
+ used to store the installation candidate while it is being downloaded
from the Librarian.
Remaining files in the 'temppath' indicated installation failures and
@@ -203,9 +203,12 @@ class DiskPoolEntry:
if component in components:
return component
+ return None
+
@cachedproperty
def file_hash(self) -> str:
"""Return the SHA1 sum of this file."""
+ assert self.file_component is not None
targetpath = self.pathFor(self.file_component)
return sha1_from_path(str(targetpath))
@@ -308,6 +311,7 @@ class DiskPoolEntry:
# shuffle the symlinks, so that the one we want to delete will
# just be one of the links, and becomes safe.
targetcomponent = self.preferredComponent(remove=component)
+ assert targetcomponent is not None
self._shufflesymlinks(targetcomponent)
return self._reallyRemove(component)
@@ -336,6 +340,8 @@ class DiskPoolEntry:
def _shufflesymlinks(self, targetcomponent: str) -> None:
"""Shuffle the symlinks for filename so that targetcomponent contains
the real file and the rest are symlinks to the right place..."""
+ assert self.file_component is not None
+
if targetcomponent == self.file_component:
# We're already in the right place.
return
@@ -405,6 +411,7 @@ class DiskPoolEntry:
"""
component = self.preferredComponent()
if not self.file_component == component:
+ assert component is not None
self._shufflesymlinks(component)
@@ -427,7 +434,6 @@ class DiskPool:
self.archive = archive
self.rootpath = Path(rootpath)
self.temppath = Path(temppath) if temppath is not None else None
- self.entries = {}
self.logger = logger
def _getEntry(
@@ -437,6 +443,7 @@ class DiskPool:
pub_file: IPackageReleaseFile,
) -> DiskPoolEntry:
"""Return a new DiskPoolEntry for the given source and file."""
+ assert self.temppath is not None
return DiskPoolEntry(
self.archive,
self.rootpath,
@@ -457,6 +464,7 @@ class DiskPool:
) -> Path:
"""Return the path for the given pool file."""
if file is None:
+ assert pub_file is not None
file = pub_file.libraryfile.filename
if file is None:
raise AssertionError("Must pass either pub_file or file")
diff --git a/lib/lp/archiveuploader/dscfile.py b/lib/lp/archiveuploader/dscfile.py
index c72c28f..cc86e4e 100644
--- a/lib/lp/archiveuploader/dscfile.py
+++ b/lib/lp/archiveuploader/dscfile.py
@@ -394,14 +394,13 @@ class DSCFile(SourceUploadFile, SignableTagFile):
all exceptions that are generated while processing DSC file checks.
"""
- for error in SourceUploadFile.verify(self):
- yield error
+ yield from SourceUploadFile.verify(self)
# Check size and checksum of the DSC file itself
try:
self.checkSizeAndCheckSum()
- except UploadError as error:
- yield error
+ except UploadError as e:
+ yield e
try:
raw_files = parse_and_merge_file_lists(self._dict, changes=False)
@@ -426,8 +425,8 @@ class DSCFile(SourceUploadFile, SignableTagFile):
file_instance = DSCUploadedFile(
filepath, hashes, size, self.policy, self.logger
)
- except UploadError as error:
- yield error
+ except UploadError as e:
+ yield e
else:
files.append(file_instance)
self.files = files
@@ -463,10 +462,10 @@ class DSCFile(SourceUploadFile, SignableTagFile):
with warnings.catch_warnings():
warnings.simplefilter("error")
PkgRelation.parse_relations(field)
- except Warning as error:
+ except Warning as e:
yield UploadError(
"%s: invalid %s field; cannot be parsed by deb822: %s"
- % (self.filename, field_name, error)
+ % (self.filename, field_name, e)
)
# Verify if version declared in changesfile is the same than that
@@ -478,8 +477,7 @@ class DSCFile(SourceUploadFile, SignableTagFile):
% (self.filename, self.dsc_version, self.version)
)
- for error in self.checkFiles():
- yield error
+ yield from self.checkFiles()
def _getFileByName(self, filename):
"""Return the corresponding file reference in the policy context.
diff --git a/lib/lp/archiveuploader/tests/test_buildduploads.py b/lib/lp/archiveuploader/tests/test_buildduploads.py
index 3070282..56a20fc 100644
--- a/lib/lp/archiveuploader/tests/test_buildduploads.py
+++ b/lib/lp/archiveuploader/tests/test_buildduploads.py
@@ -25,9 +25,9 @@ from lp.testing.gpgkeys import import_public_test_keys
class TestStagedBinaryUploadBase(TestUploadProcessorBase):
name = "baz"
version = "1.0-1"
- distribution_name = None
- distroseries_name = None
- pocket = None
+ distribution_name = None # type: str
+ distroseries_name = None # type: str
+ pocket = None # type: PackagePublishingPocket
policy = "buildd"
no_mails = True
diff --git a/lib/lp/blueprints/browser/specification.py b/lib/lp/blueprints/browser/specification.py
index 82f9fb5..36c4a8e 100644
--- a/lib/lp/blueprints/browser/specification.py
+++ b/lib/lp/blueprints/browser/specification.py
@@ -39,6 +39,7 @@ __all__ = [
import os
from operator import attrgetter
from subprocess import PIPE, Popen
+from typing import List
import six
from lazr.restful.interface import copy_field, use_template
@@ -516,7 +517,7 @@ class SpecificationActionMenu(NavigationMenu, SpecificationEditLinksMixin):
usedfor = ISpecification
facet = "specifications"
- links = ("edit", "supersede", "retarget")
+ links = ["edit", "supersede", "retarget"]
class SpecificationContextMenu(ContextMenu, SpecificationEditLinksMixin):
@@ -965,7 +966,9 @@ class SpecificationInformationTypeEditView(LaunchpadFormView):
"""Return the next URL to call when this call completes."""
return None
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@property
def initial_values(self):
@@ -1043,7 +1046,7 @@ class SpecificationGoalDecideView(LaunchpadFormView):
"""
schema = Interface
- field_names = []
+ field_names = [] # type: List[str]
@property
def label(self):
@@ -1061,7 +1064,9 @@ class SpecificationGoalDecideView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class ISpecificationRetargetingSchema(Interface):
@@ -1645,7 +1650,9 @@ class SpecificationLinkBranchView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class SpecificationSetView(AppFrontPageSearchView, HasSpecificationsView):
diff --git a/lib/lp/blueprints/browser/specificationbranch.py b/lib/lp/blueprints/browser/specificationbranch.py
index 5b54cba..1ee6fe9 100644
--- a/lib/lp/blueprints/browser/specificationbranch.py
+++ b/lib/lp/blueprints/browser/specificationbranch.py
@@ -9,6 +9,8 @@ __all__ = [
"SpecificationBranchURL",
]
+from typing import List
+
from zope.interface import implementer
from lp import _
@@ -45,7 +47,7 @@ class SpecificationBranchStatusView(LaunchpadEditFormView):
"""Edit the summary of the SpecificationBranch link."""
schema = ISpecificationBranch
- field_names = []
+ field_names = [] # type: List[str]
label = _("Delete link between specification and branch")
def initialize(self):
@@ -84,7 +86,9 @@ class BranchLinkToSpecificationView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action(_("Continue"), name="continue")
def continue_action(self, action, data):
diff --git a/lib/lp/blueprints/browser/specificationsubscription.py b/lib/lp/blueprints/browser/specificationsubscription.py
index 79598e8..bb93933 100644
--- a/lib/lp/blueprints/browser/specificationsubscription.py
+++ b/lib/lp/blueprints/browser/specificationsubscription.py
@@ -9,6 +9,8 @@ __all__ = [
"SpecificationSubscriptionEditView",
]
+from typing import List
+
from lazr.delegates import delegate_to
from simplejson import dumps
from zope.component import getUtility
@@ -37,10 +39,12 @@ class SpecificationSubscriptionAddView(LaunchpadFormView):
label = "Subscribe to blueprint"
@property
- def cancel_url(self):
+ def next_url(self):
return canonical_url(self.context)
- next_url = cancel_url
+ @property
+ def cancel_url(self):
+ return self.next_url
def _subscribe(self, person, essential):
self.context.subscribe(person, self.user, essential)
@@ -75,7 +79,7 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
"""Used to unsubscribe someone from a blueprint."""
schema = ISpecificationSubscription
- field_names = []
+ field_names = [] # type: List[str]
@property
def label(self):
@@ -87,10 +91,12 @@ class SpecificationSubscriptionDeleteView(LaunchpadFormView):
page_title = label
@property
- def cancel_url(self):
+ def next_url(self):
return canonical_url(self.context.specification)
- next_url = cancel_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action("Unsubscribe", name="unsubscribe")
def unsubscribe_action(self, action, data):
@@ -116,10 +122,12 @@ class SpecificationSubscriptionEditView(LaunchpadEditFormView):
return "Modify subscription to %s" % self.context.specification.title
@property
- def cancel_url(self):
+ def next_url(self):
return canonical_url(self.context.specification)
- next_url = cancel_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action(_("Change"), name="change")
def change_action(self, action, data):
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index 4a2be23..047084a 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -25,6 +25,7 @@ __all__ = [
import csv
import io
from collections import defaultdict
+from typing import List
import pytz
from lazr.restful.utils import smartquote
@@ -401,7 +402,7 @@ class SprintDeleteView(LaunchpadFormView):
"""Form for deleting sprints."""
schema = ISprint
- field_names = []
+ field_names = [] # type: List[str]
@property
def label(self):
@@ -430,7 +431,9 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
'Review discussion topics for "%s" sprint' % self.context.title
)
- page_title = label
+ @property
+ def page_title(self):
+ return self.label
def initialize(self):
self.status_message = None
@@ -591,13 +594,13 @@ class SprintSetNavigationMenu(RegistryCollectionActionMenuBase):
"""Action menu for sprints index."""
usedfor = ISprintSet
- links = (
+ links = [
"register_team",
"register_project",
"register_sprint",
"create_account",
"view_all_sprints",
- )
+ ]
@enabled_with_permission("launchpad.View")
def register_sprint(self):
diff --git a/lib/lp/blueprints/browser/sprintattendance.py b/lib/lp/blueprints/browser/sprintattendance.py
index acfa792..2964ffd 100644
--- a/lib/lp/blueprints/browser/sprintattendance.py
+++ b/lib/lp/blueprints/browser/sprintattendance.py
@@ -133,7 +133,9 @@ class BaseSprintAttendanceAddView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
_local_timeformat = "%H:%M on %A, %Y-%m-%d"
diff --git a/lib/lp/blueprints/mail/__init__.py b/lib/lp/blueprints/mail/__init__.py
index 6b3f0d4..516532e 100644
--- a/lib/lp/blueprints/mail/__init__.py
+++ b/lib/lp/blueprints/mail/__init__.py
@@ -1,4 +1,2 @@
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__all__ = []
diff --git a/lib/lp/bugs/browser/bug.py b/lib/lp/bugs/browser/bug.py
index cbc391c..5f0a235 100644
--- a/lib/lp/bugs/browser/bug.py
+++ b/lib/lp/bugs/browser/bug.py
@@ -27,6 +27,7 @@ __all__ = [
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import re
+from typing import Type
from lazr.enum import (
EnumeratedType,
@@ -216,7 +217,7 @@ class BugSetNavigation(Navigation):
class BugContextMenu(ContextMenu):
"""Context menu of actions that can be performed upon a Bug."""
- usedfor = IBug
+ usedfor = IBug # type: Type[Interface]
links = [
'editdescription', 'markduplicate', 'visibility', 'addupstream',
'adddistro', 'subscription', 'addsubscriber', 'editsubscriptions',
@@ -400,11 +401,13 @@ class MaloneView(LaunchpadFormView):
schema = IFrontPageBugTaskSearch
field_names = ['searchtext', 'scope']
- # Test: standalone/xx-slash-malone-slash-bugs.rst
- error_message = None
-
page_title = 'Launchpad Bugs'
+ # Test: standalone/xx-slash-malone-slash-bugs.rst
+ @property
+ def error_message(self):
+ return None
+
@property
def target_css_class(self):
"""The CSS class for used in the target widget."""
@@ -753,7 +756,9 @@ class BugEditViewBase(LaunchpadEditFormView):
"""Return the next URL to call when this call completes."""
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class BugEditView(BugEditViewBase):
@@ -768,7 +773,9 @@ class BugEditView(BugEditViewBase):
"""The form label."""
return 'Edit details for bug #%d' % self.context.bug.id
- page_title = label
+ @property
+ def page_title(self):
+ return self.label
@action('Change', name='change')
def change_action(self, action, data):
@@ -840,7 +847,9 @@ class BugLockStatusEditView(LaunchpadEditFormView):
return canonical_url(self.context)
return None
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class BugMarkAsDuplicateView(BugEditViewBase):
@@ -954,7 +963,9 @@ class BugSecrecyEditView(LaunchpadFormView, BugSubscriptionPortletDetails):
return canonical_url(self.context)
return None
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@property
def initial_values(self):
diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py
index 7372c56..375af7f 100644
--- a/lib/lp/bugs/browser/bugalsoaffects.py
+++ b/lib/lp/bugs/browser/bugalsoaffects.py
@@ -8,6 +8,10 @@ __all__ = [
]
from textwrap import dedent
+from typing import (
+ Tuple,
+ Type,
+ )
from lazr.enum import (
EnumeratedType,
@@ -227,7 +231,7 @@ class BugTaskCreationStep(AlsoAffectsStep):
initial_focus_widget = 'bug_url'
step_name = 'specify_remote_bug_url'
- target_field_names = ()
+ target_field_names = () # type: Tuple[str, ...]
# This is necessary so that other views which dispatch work to this one
# have access to the newly created task.
@@ -710,7 +714,7 @@ class BugTrackerCreationStep(AlsoAffectsStep):
StrippedTextWidget, displayWidth=62)
step_name = "bugtracker_creation"
main_action_label = 'Register Bug Tracker and Add to Bug Report'
- _next_step = None
+ _next_step = None # type: Type[StepView]
def main_action(self, data):
assert self._next_step is not None, (
diff --git a/lib/lp/bugs/browser/bugbranch.py b/lib/lp/bugs/browser/bugbranch.py
index e7ea7ab..eb67251 100644
--- a/lib/lp/bugs/browser/bugbranch.py
+++ b/lib/lp/bugs/browser/bugbranch.py
@@ -10,6 +10,8 @@ __all__ = [
'BugBranchView',
]
+from typing import List
+
from lazr.restful.interfaces import IWebServiceClientRequest
from zope.component import (
adapter,
@@ -65,14 +67,16 @@ class BugBranchAddView(LaunchpadFormView):
def label(self):
return 'Add a branch to bug #%i' % self.context.bug.id
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class BugBranchDeleteView(LaunchpadEditFormView):
"""View to update a BugBranch."""
schema = IBugBranch
- field_names = []
+ field_names = [] # type: List[str]
def initialize(self):
LaunchpadEditFormView.initialize(self)
@@ -81,7 +85,9 @@ class BugBranchDeleteView(LaunchpadEditFormView):
def next_url(self):
return canonical_url(self.context.bug)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action('Remove link', name='delete')
def delete_action(self, action, data):
@@ -129,7 +135,9 @@ class BranchLinkToBugView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action(_('Continue'), name='continue')
def continue_action(self, action, data):
diff --git a/lib/lp/bugs/browser/bugnomination.py b/lib/lp/bugs/browser/bugnomination.py
index 40dddbd..d6eec68 100644
--- a/lib/lp/bugs/browser/bugnomination.py
+++ b/lib/lp/bugs/browser/bugnomination.py
@@ -10,6 +10,7 @@ __all__ = [
'BugNominationTableRowView']
import datetime
+from typing import List
import pytz
from zope.component import getUtility
@@ -204,7 +205,7 @@ class BugNominationEditView(LaunchpadFormView):
"""Browser view class for approving and declining nominations."""
schema = Interface
- field_names = []
+ field_names = [] # type: List[str]
@property
def label(self):
diff --git a/lib/lp/bugs/browser/bugsubscription.py b/lib/lp/bugs/browser/bugsubscription.py
index b28876c..401c30a 100644
--- a/lib/lp/bugs/browser/bugsubscription.py
+++ b/lib/lp/bugs/browser/bugsubscription.py
@@ -11,6 +11,8 @@ __all__ = [
'BugSubscriptionListView',
]
+from typing import List
+
from lazr.delegates import delegate_to
from lazr.restful.interfaces import (
IJSONRequestCache,
@@ -89,7 +91,9 @@ class BugSubscriptionAddView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@property
def label(self):
@@ -192,7 +196,9 @@ class BugSubscriptionSubscribeSelfView(LaunchpadFormView,
next_url = context_url
return next_url
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@cachedproperty
def _subscribers_for_current_user(self):
@@ -654,7 +660,7 @@ class BugMuteSelfView(LaunchpadFormView):
"""A view to mute a user's bug mail for a given bug."""
schema = IBugSubscription
- field_names = []
+ field_names = [] # type: List[str]
@property
def label(self):
@@ -669,7 +675,9 @@ class BugMuteSelfView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
def initialize(self):
self.is_muted = self.context.bug.isMuted(self.user)
diff --git a/lib/lp/bugs/browser/bugsubscriptionfilter.py b/lib/lp/bugs/browser/bugsubscriptionfilter.py
index e12219f..ff66f93 100644
--- a/lib/lp/bugs/browser/bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/bugsubscriptionfilter.py
@@ -106,14 +106,14 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
"""Base class for edit or create views of `IBugSubscriptionFilter`."""
schema = IBugSubscriptionFilter
- field_names = (
+ field_names = [
"description",
"statuses",
"importances",
"information_types",
"tags",
"find_all_tags",
- )
+ ]
custom_widget_description = CustomWidgetFactory(
TextWidget, displayWidth=50)
@@ -148,7 +148,9 @@ class BugSubscriptionFilterEditViewBase(LaunchpadEditFormView,
return canonical_url(
self.user, view_name="+structural-subscriptions")
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class BugSubscriptionFilterEditView(
diff --git a/lib/lp/bugs/browser/bugsupervisor.py b/lib/lp/bugs/browser/bugsupervisor.py
index 44040f2..80367de 100644
--- a/lib/lp/bugs/browser/bugsupervisor.py
+++ b/lib/lp/bugs/browser/bugsupervisor.py
@@ -55,7 +55,9 @@ class BugSupervisorEditView(LaunchpadEditFormView):
"""See `LaunchpadFormView`."""
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@action('Change', name='change')
def change_action(self, action, data):
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 7228fc7..bc11e3e 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -33,6 +33,7 @@ from datetime import (
from itertools import groupby
from operator import attrgetter
import re
+from typing import List
from urllib.parse import quote
from lazr.delegates import delegate_to
@@ -1599,7 +1600,7 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
"""Used to delete a bugtask."""
schema = IBugTask
- field_names = []
+ field_names = [] # type: List[str]
label = 'Remove bug task'
page_title = label
@@ -1611,6 +1612,13 @@ class BugTaskDeletionView(ReturnToReferrerMixin, LaunchpadFormView):
return self._next_url or self._return_url
return None
+ @property
+ def cancel_url(self):
+ # We have to explicitly define `cancel_url` as a property here
+ # to make `mypy` happy - the base classes both define `cancel_url`
+ # in a non-compatible fashion
+ return super().cancel_url
+
@action('Delete', name='delete_bugtask')
def delete_bugtask_action(self, action, data):
bugtask = self.context
diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py
index 49cfb21..a0832e9 100644
--- a/lib/lp/bugs/browser/bugtracker.py
+++ b/lib/lp/bugs/browser/bugtracker.py
@@ -488,7 +488,9 @@ class BugTrackerEditComponentView(LaunchpadEditFormView):
def next_url(self):
return canonical_url(self.context.component_group.bug_tracker)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
def updateContextFromData(self, data, context=None):
"""Link component to specified distro source package.
diff --git a/lib/lp/bugs/browser/bugwatch.py b/lib/lp/bugs/browser/bugwatch.py
index 516adad..8ee1274 100644
--- a/lib/lp/bugs/browser/bugwatch.py
+++ b/lib/lp/bugs/browser/bugwatch.py
@@ -166,7 +166,9 @@ class BugWatchEditView(LaunchpadFormView):
def next_url(self):
return canonical_url(getUtility(ILaunchBag).bug)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class BugWatchActivityPortletView(LaunchpadFormView):
@@ -194,7 +196,9 @@ class BugWatchActivityPortletView(LaunchpadFormView):
def next_url(self):
return canonical_url(getUtility(ILaunchBag).bug)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
@property
def recent_watch_activity(self):
diff --git a/lib/lp/bugs/browser/cve.py b/lib/lp/bugs/browser/cve.py
index 8b6ae28..372753d 100644
--- a/lib/lp/bugs/browser/cve.py
+++ b/lib/lp/bugs/browser/cve.py
@@ -109,7 +109,9 @@ class CveLinkView(LaunchpadFormView):
def next_url(self):
return canonical_url(self.context)
- cancel_url = next_url
+ @property
+ def cancel_url(self):
+ return self.next_url
class CveUnlinkView(CveLinkView):
@@ -124,9 +126,11 @@ class CveUnlinkView(CveLinkView):
@property
def label(self):
- return 'Bug # %s Remove link to CVE report' % self.context.bug.id
+ return 'Bug # %s Remove link to CVE report' % self.context.bug.id
- page_title = label
+ @property
+ def page_title(self):
+ return self.label
heading = 'Remove links to bug reports'
diff --git a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
index 45ec289..2981b4d 100644
--- a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
@@ -36,8 +36,8 @@ from lp.testing.views import create_initialized_view
class TestBugSubscriptionFilterBase:
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
self.owner = self.factory.makePerson(name="foo")
self.structure = self.factory.makeProduct(
owner=self.owner, name="bar")
diff --git a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
index 00151ed..7daefd9 100644
--- a/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
+++ b/lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
@@ -302,8 +302,8 @@ class FileBugViewMixin:
# Disable redirects on validation failure.
pass
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
self.target = self.factory.makeProduct()
transaction.commit()
login_person(self.target.owner)
diff --git a/lib/lp/bugs/externalbugtracker/base.py b/lib/lp/bugs/externalbugtracker/base.py
index 8cf7f0e..d1f08e9 100644
--- a/lib/lp/bugs/externalbugtracker/base.py
+++ b/lib/lp/bugs/externalbugtracker/base.py
@@ -25,7 +25,7 @@ __all__ = [
'UnsupportedBugTrackerVersion',
]
-
+from typing import Optional
from urllib.parse import (
urljoin,
urlparse,
@@ -159,7 +159,7 @@ def repost_on_redirect_hook(response, *args, **kwargs):
class ExternalBugTracker:
"""Base class for an external bug tracker."""
- batch_size = None
+ batch_size = None # type: Optional[int]
batch_query_threshold = config.checkwatches.batch_query_threshold
timeout = config.checkwatches.default_socket_timeout
comment_template = 'default_remotecomment_template.txt'
diff --git a/lib/lp/bugs/externalbugtracker/github.py b/lib/lp/bugs/externalbugtracker/github.py
index 8f45e4d..b3e60b2 100644
--- a/lib/lp/bugs/externalbugtracker/github.py
+++ b/lib/lp/bugs/externalbugtracker/github.py
@@ -7,8 +7,7 @@ __all__ = [
'BadGitHubURL',
'GitHub',
'GitHubRateLimit',
- 'IGitHubRateLimit',
- ]
+]
from contextlib import contextmanager
import http.client
@@ -21,7 +20,6 @@ from urllib.parse import (
import pytz
import requests
from zope.component import getUtility
-from zope.interface import Interface
from lp.bugs.externalbugtracker import (
BugTrackerConnectError,
@@ -31,6 +29,7 @@ from lp.bugs.externalbugtracker import (
UnparsableBugTrackerVersion,
)
from lp.bugs.externalbugtracker.base import LP_USER_AGENT
+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
from lp.bugs.interfaces.bugtask import (
BugTaskImportance,
BugTaskStatus,
@@ -56,24 +55,6 @@ class GitHubExceededRateLimit(BugWatchUpdateError):
self.host, time.ctime(self.reset))
-class IGitHubRateLimit(Interface):
- """Interface for rate-limit tracking for the GitHub Issues API."""
-
- def checkLimit(url, token=None):
- """A context manager that checks the remote host's rate limit.
-
- :param url: The URL being requested.
- :param token: If not None, an OAuth token to use as authentication
- to the remote host when asking it for the current rate limit.
- :return: A suitable `Authorization` header (from the context
- manager's `__enter__` method).
- :raises GitHubExceededRateLimit: if the rate limit was exceeded.
- """
-
- def clearCache():
- """Forget any cached rate limits."""
-
-
class GitHubRateLimit:
"""Rate-limit tracking for the GitHub Issues API."""
diff --git a/lib/lp/bugs/externalbugtracker/interfaces.py b/lib/lp/bugs/externalbugtracker/interfaces.py
new file mode 100644
index 0000000..0ce8030
--- /dev/null
+++ b/lib/lp/bugs/externalbugtracker/interfaces.py
@@ -0,0 +1,22 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from zope.interface import Interface
+
+
+class IGitHubRateLimit(Interface):
+ """Interface for rate-limit tracking for the GitHub Issues API."""
+
+ def checkLimit(url, token=None):
+ """A context manager that checks the remote host's rate limit.
+
+ :param url: The URL being requested.
+ :param token: If not None, an OAuth token to use as authentication
+ to the remote host when asking it for the current rate limit.
+ :return: A suitable `Authorization` header (from the context
+ manager's `__enter__` method).
+ :raises GitHubExceededRateLimit: if the rate limit was exceeded.
+ """
+
+ def clearCache():
+ """Forget any cached rate limits."""
diff --git a/lib/lp/bugs/externalbugtracker/tests/test_github.py b/lib/lp/bugs/externalbugtracker/tests/test_github.py
index b7644b5..85b4cc7 100644
--- a/lib/lp/bugs/externalbugtracker/tests/test_github.py
+++ b/lib/lp/bugs/externalbugtracker/tests/test_github.py
@@ -34,8 +34,8 @@ from lp.bugs.externalbugtracker.github import (
BadGitHubURL,
GitHub,
GitHubExceededRateLimit,
- IGitHubRateLimit,
)
+from lp.bugs.externalbugtracker.interfaces import IGitHubRateLimit
from lp.bugs.interfaces.bugtask import BugTaskStatus
from lp.bugs.interfaces.bugtracker import BugTrackerType
from lp.bugs.interfaces.externalbugtracker import IExternalBugTracker
diff --git a/lib/lp/bugs/interfaces/bugnotification.py b/lib/lp/bugs/interfaces/bugnotification.py
index 7d6f23a..1fffc4e 100644
--- a/lib/lp/bugs/interfaces/bugnotification.py
+++ b/lib/lp/bugs/interfaces/bugnotification.py
@@ -73,10 +73,10 @@ class IBugNotificationSet(Interface):
def getDeferredNotifications():
"""Returns the deferred notifications.
- A deferred noticiation is one that is pending but has no recipients.
+ A deferred notification is one that is pending but has no recipients.
"""
- def addNotification(self, bug, is_comment, message, recipients, activity):
+ def addNotification(bug, is_comment, message, recipients, activity):
"""Create a new `BugNotification`.
Create a new `BugNotification` object and the corresponding
diff --git a/lib/lp/bugs/model/bugtarget.py b/lib/lp/bugs/model/bugtarget.py
index 0f7f748..41fb0f9 100644
--- a/lib/lp/bugs/model/bugtarget.py
+++ b/lib/lp/bugs/model/bugtarget.py
@@ -206,6 +206,7 @@ class OfficialBugTag(Storm):
product_id = Int(name='product')
product = Reference(product_id, 'Product.id')
+ @property
def target(self):
"""See `IOfficialBugTag`."""
# A database constraint ensures that either distribution or
@@ -215,7 +216,8 @@ class OfficialBugTag(Storm):
else:
return self.product
- def _settarget(self, target):
+ @target.setter
+ def target(self, target):
"""See `IOfficialBugTag`."""
if IDistribution.providedBy(target):
self.distribution = target
@@ -225,5 +227,3 @@ class OfficialBugTag(Storm):
raise ValueError(
'The target of an OfficialBugTag must be either an '
'IDistribution instance or an IProduct instance.')
-
- target = property(target, _settarget, doc=target.__doc__)
diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
index ce372d6..f96cc46 100644
--- a/lib/lp/bugs/model/tests/test_bugtask.py
+++ b/lib/lp/bugs/model/tests/test_bugtask.py
@@ -1093,7 +1093,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
layer = DatabaseFunctionalLayer
- def setUp(self):
+ def setUp(self, *args, **kwargs):
"""Create the test setup.
We need
@@ -1104,7 +1104,7 @@ class TestBugTaskPermissionsToSetAssigneeMixin:
owners, bug supervisors, drivers
- bug tasks for the targets
"""
- super().setUp()
+ super().setUp(*args, **kwargs)
self.target_owner_member = self.factory.makePerson()
self.target_owner_team = self.factory.makeTeam(
owner=self.target_owner_member,
diff --git a/lib/lp/bugs/model/tests/test_bugtask_status.py b/lib/lp/bugs/model/tests/test_bugtask_status.py
index 781a594..f3409e0 100644
--- a/lib/lp/bugs/model/tests/test_bugtask_status.py
+++ b/lib/lp/bugs/model/tests/test_bugtask_status.py
@@ -260,8 +260,8 @@ class TestBugTaskStatusTransitionForPrivilegedUserBase:
layer = DatabaseFunctionalLayer
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
# Creation of task and target are deferred to subclasses.
self.task = None
self.person = None
@@ -281,7 +281,9 @@ class TestBugTaskStatusTransitionForPrivilegedUserBase:
with person_logged_in(self.person):
self.task.transitionToStatus(BugTaskStatus.WONTFIX, self.person)
self.assertEqual(self.task.status, BugTaskStatus.WONTFIX)
- self.task.transitionToStatus(BugTaskStatus.DOESNOTEXIST, self.person)
+ self.task.transitionToStatus(
+ BugTaskStatus.DOESNOTEXIST, self.person
+ )
self.assertEqual(self.task.status, BugTaskStatus.DOESNOTEXIST)
self.task.transitionToStatus(BugTaskStatus.EXPIRED, self.person)
self.assertEqual(self.task.status, BugTaskStatus.EXPIRED)
diff --git a/lib/lp/bugs/scripts/checkwatches/base.py b/lib/lp/bugs/scripts/checkwatches/base.py
index 0a0cfce..002daad 100644
--- a/lib/lp/bugs/scripts/checkwatches/base.py
+++ b/lib/lp/bugs/scripts/checkwatches/base.py
@@ -131,7 +131,7 @@ class WorkingBase:
self._transaction_manager = parent._transaction_manager
self.logger = parent.logger
- @property
+ @property # type: ignore
@contextmanager
def interaction(self):
"""Context manager for interaction as the given user.
@@ -149,7 +149,7 @@ class WorkingBase:
else:
yield
- @property
+ @property # type: ignore
@contextmanager
def transaction(self):
"""Context manager to ring-fence database activity.
@@ -186,7 +186,7 @@ class WorkingBase:
self._statement_logging_stop()
self._statement_logging_start()
- @property
+ @property # type: ignore
@contextmanager
def statement_logging(self):
"""Context manager to start and stop SQL statement logging.
diff --git a/lib/lp/bugs/scripts/checkwatches/core.py b/lib/lp/bugs/scripts/checkwatches/core.py
index 069fb19..f95ae89 100644
--- a/lib/lp/bugs/scripts/checkwatches/core.py
+++ b/lib/lp/bugs/scripts/checkwatches/core.py
@@ -27,6 +27,7 @@ import socket
import sys
import threading
import time
+from typing import List
from xmlrpc.client import ProtocolError
import pytz
@@ -70,7 +71,7 @@ from lp.services.scripts.logger import log as default_log
LOGIN = 'bugwatch@xxxxxxxxxxxxxxxxxx'
# A list of product names for which comments should be synchronized.
-SYNCABLE_GNOME_PRODUCTS = []
+SYNCABLE_GNOME_PRODUCTS = [] # type: List[str]
# When syncing with a remote bug tracker that reports its idea of the
# current time, this defined the maximum acceptable skew between the
diff --git a/lib/lp/bugs/scripts/debbugs.py b/lib/lp/bugs/scripts/debbugs.py
index 260b319..8c49d78 100644
--- a/lib/lp/bugs/scripts/debbugs.py
+++ b/lib/lp/bugs/scripts/debbugs.py
@@ -266,8 +266,9 @@ class Database:
raise KeyError(bug_id)
return bug
+
if __name__ == '__main__':
- for bug in Database('/srv/debzilla.no-name-yet.com/debbugs'):
+ for bug in Database('/srv/debzilla.no-name-yet.com/debbugs', None):
try:
print(bug, bug.subject)
except Exception as e:
diff --git a/lib/lp/bugs/scripts/tests/test_bugnotification.py b/lib/lp/bugs/scripts/tests/test_bugnotification.py
index 2ea216f..9434e32 100644
--- a/lib/lp/bugs/scripts/tests/test_bugnotification.py
+++ b/lib/lp/bugs/scripts/tests/test_bugnotification.py
@@ -8,6 +8,12 @@ from datetime import (
)
import re
from smtplib import SMTPException
+from typing import (
+ Any,
+ List,
+ Optional,
+ Type,
+ )
import unittest
from fixtures import FakeLogger
@@ -79,6 +85,7 @@ from lp.services.mail.helpers import (
from lp.services.mail.sendmail import set_immediate_mail_delivery
from lp.services.mail.stub import TestMailer
from lp.services.messages.interfaces.message import IMessageSet
+from lp.services.messages.model.message import Message
from lp.services.propertycache import cachedproperty
from lp.testing import (
login,
@@ -101,7 +108,7 @@ class MockBug:
duplicateof = None
information_type = InformationType.PUBLIC
- messages = []
+ messages = [] # type: List[Message]
def __init__(self, id, owner):
self.id = id
@@ -686,7 +693,12 @@ class EmailNotificationTestBase(TestCaseWithFactory):
class EmailNotificationsBugMixin:
- change_class = change_name = old = new = alt = unexpected_bytes = None
+ change_class = None # type: Optional[Type[Any]]
+ change_name = None # type: Optional[str]
+ old = None # type: Any
+ new = None # type: Any
+ alt = None # type: Any
+ unexpected_bytes = None # type: Optional[bytes]
def change(self, old, new):
self.bug.addChange(
@@ -764,7 +776,7 @@ class EmailNotificationsBugTaskMixin(EmailNotificationsBugMixin):
class EmailNotificationsAddedRemovedMixin:
- old = new = added_message = removed_message = None
+ old = new = added_message = removed_message = b""
def add(self, item):
raise NotImplementedError
diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py
index ce1829a..920762b 100644
--- a/lib/lp/bugs/security.py
+++ b/lib/lp/bugs/security.py
@@ -3,8 +3,6 @@
"""Security adapters for the bugs module."""
-__all__ = []
-
from lp.app.security import (
AnonymousAuthorization,
AuthorizationBase,
diff --git a/lib/lp/bugs/tests/externalbugtracker.py b/lib/lp/bugs/tests/externalbugtracker.py
index 11dcaab..29d038a 100644
--- a/lib/lp/bugs/tests/externalbugtracker.py
+++ b/lib/lp/bugs/tests/externalbugtracker.py
@@ -14,6 +14,11 @@ import os
import random
import re
import time
+from typing import (
+ Any,
+ Dict,
+ Tuple,
+ )
from urllib.parse import (
parse_qs,
urljoin,
@@ -522,7 +527,7 @@ class TestBugzillaXMLRPCTransport(RequestsTransport):
'add_comment',
'login_required',
'set_link',
- )
+ ) # type: Tuple[str, ...]
expired_cookie = None
@@ -836,10 +841,10 @@ class TestBugzillaAPIXMLRPCTransport(TestBugzillaXMLRPCTransport):
}
# Methods that require authentication.
- auth_required_methods = [
+ auth_required_methods = (
'add_comment',
'login_required',
- ]
+ )
# The list of users that can log in.
users = [
@@ -1292,8 +1297,8 @@ def strip_trac_comment(comment):
class TestTracXMLRPCTransport(RequestsTransport):
"""An XML-RPC transport to be used when testing Trac."""
- remote_bugs = {}
- launchpad_bugs = {}
+ remote_bugs = {} # type: Dict[str, Dict[str, Any]]
+ launchpad_bugs = {} # type: Dict[str, int]
seconds_since_epoch = None
local_timezone = 'UTC'
utc_offset = 0
diff --git a/lib/lp/bugs/tests/test_buglinktarget.py b/lib/lp/bugs/tests/test_buglinktarget.py
index 9c724a3..fbfac7f 100644
--- a/lib/lp/bugs/tests/test_buglinktarget.py
+++ b/lib/lp/bugs/tests/test_buglinktarget.py
@@ -7,8 +7,6 @@ This module will run the interface test against the CVE, Specification,
Question, and BranchMergeProposal implementations of that interface.
"""
-__all__ = []
-
import unittest
from zope.component import getUtility
diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py
index d24c992..ed4a8da 100644
--- a/lib/lp/bugs/tests/test_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugnomination.py
@@ -203,8 +203,8 @@ class CanBeNominatedForTestMixin:
layer = DatabaseFunctionalLayer
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
login('foo.bar@xxxxxxxxxxxxx')
self.eric = self.factory.makePerson(name='eric')
self.setUpTarget()
diff --git a/lib/lp/bugs/tests/test_bugsearch_conjoined.py b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
index 1407278..75a1979 100644
--- a/lib/lp/bugs/tests/test_bugsearch_conjoined.py
+++ b/lib/lp/bugs/tests/test_bugsearch_conjoined.py
@@ -3,8 +3,6 @@
"""Test for the exclude_conjoined_tasks param for BugTaskSearchParams."""
-__all__ = []
-
from storm.store import Store
from testtools.matchers import Equals
from zope.component import getUtility
diff --git a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
index 0233e02..8235450 100644
--- a/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugsupervisor_bugnomination.py
@@ -23,8 +23,8 @@ class AddNominationTestMixin:
layer = DatabaseFunctionalLayer
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
login('foo.bar@xxxxxxxxxxxxx')
self.user = self.factory.makePerson(name='ordinary-user')
self.bug_supervisor = self.factory.makePerson(name='no-ordinary-user')
diff --git a/lib/lp/bugs/tests/test_bugtarget.py b/lib/lp/bugs/tests/test_bugtarget.py
index 79fa9a1..a5df3f5 100644
--- a/lib/lp/bugs/tests/test_bugtarget.py
+++ b/lib/lp/bugs/tests/test_bugtarget.py
@@ -8,8 +8,6 @@ ProjectGroup, DistributionSourcePackage, and DistroSeries implementations
IBugTarget. It runs the bugtarget-questiontarget.rst test.
"""
-__all__ = []
-
import random
import unittest
diff --git a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
index 0b77ce3..7f00680 100644
--- a/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
+++ b/lib/lp/bugs/tests/test_bugtaskflat_triggers.py
@@ -1,8 +1,11 @@
# Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-from collections import namedtuple
from contextlib import contextmanager
+from typing import (
+ Any,
+ NamedTuple,
+ )
from testtools.matchers import MatchesStructure
from zope.component import getUtility
@@ -27,35 +30,32 @@ from lp.testing.dbuser import dbuser
from lp.testing.layers import DatabaseFunctionalLayer
-BUGTASKFLAT_COLUMNS = (
- 'bugtask',
- 'bug',
- 'datecreated',
- 'latest_patch_uploaded',
- 'date_closed',
- 'date_last_updated',
- 'duplicateof',
- 'bug_owner',
- 'fti',
- 'information_type',
- 'heat',
- 'product',
- 'productseries',
- 'distribution',
- 'distroseries',
- 'sourcepackagename',
- 'status',
- 'importance',
- 'assignee',
- 'milestone',
- 'owner',
- 'active',
- 'access_policies',
- 'access_grants',
- )
-
-BugTaskFlat = namedtuple('BugTaskFlat', BUGTASKFLAT_COLUMNS)
-
+BugTaskFlat = NamedTuple('BugTaskFlat', (
+ ('bugtask', Any),
+ ('bug', Any),
+ ('datecreated', Any),
+ ('latest_patch_uploaded', Any),
+ ('date_closed', Any),
+ ('date_last_updated', Any),
+ ('duplicateof', Any),
+ ('bug_owner', Any),
+ ('fti', Any),
+ ('information_type', Any),
+ ('heat', Any),
+ ('product', Any),
+ ('productseries', Any),
+ ('distribution', Any),
+ ('distroseries', Any),
+ ('sourcepackagename', Any),
+ ('status', Any),
+ ('importance', Any),
+ ('assignee', Any),
+ ('milestone', Any),
+ ('owner', Any),
+ ('active', Any),
+ ('access_policies', Any),
+ ('access_grants', Any),
+))
class BugTaskFlatTestMixin(TestCaseWithFactory):
@@ -84,7 +84,7 @@ class BugTaskFlatTestMixin(TestCaseWithFactory):
assert bugtask is not None
result = IStore(Bug).execute(
"SELECT %s FROM bugtaskflat WHERE bugtask = ?"
- % ', '.join(BUGTASKFLAT_COLUMNS), (bugtask,)).get_one()
+ % ', '.join(BugTaskFlat._fields), (bugtask,)).get_one()
if result is not None:
result = BugTaskFlat(*result)
return result
diff --git a/lib/lp/bugs/tests/test_bugtracker_components.py b/lib/lp/bugs/tests/test_bugtracker_components.py
index f457d7c..7735f5c 100644
--- a/lib/lp/bugs/tests/test_bugtracker_components.py
+++ b/lib/lp/bugs/tests/test_bugtracker_components.py
@@ -3,8 +3,6 @@
"""Test for components and component groups (products) in bug trackers."""
-__all__ = []
-
import transaction
from lp.testing import (
diff --git a/lib/lp/bugs/tests/test_bugwatch.py b/lib/lp/bugs/tests/test_bugwatch.py
index 9a363d5..9b3af1d 100644
--- a/lib/lp/bugs/tests/test_bugwatch.py
+++ b/lib/lp/bugs/tests/test_bugwatch.py
@@ -8,6 +8,10 @@ from datetime import (
timedelta,
)
import re
+from typing import (
+ List,
+ Optional,
+ )
from urllib.parse import urlunsplit
from lazr.lifecycle.snapshot import Snapshot
@@ -186,16 +190,16 @@ class ExtractBugTrackerAndBugTest(WithScenarios, TestCase):
layer = LaunchpadFunctionalLayer
# A URL to an unregistered bug tracker.
- base_url = None
+ base_url = None # type: str
# The bug tracker type to be tested.
bugtracker_type = None
# A sample URL to a bug in the bug tracker.
- bug_url = None
+ bug_url = None # type: str
# The bug id in the sample bug_url.
- bug_id = None
+ bug_id = None # type: Optional[str]
# True if the bug tracker is already registered in sampledata.
already_registered = False
@@ -324,7 +328,7 @@ class EmailAddressExtractBugTrackerAndBugTest(ExtractBugTrackerAndBugTest):
"""Ensure BugWatchSet.extractBugTrackerAndBug works with email addresses.
"""
- scenarios = None
+ scenarios = [] # type: List
bugtracker_type = BugTrackerType.EMAILADDRESS
bug_url = 'mailto:foo.bar@xxxxxxxxxxx'
base_url = 'mailto:foo.bar@xxxxxxxxxxx'
diff --git a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
index 71c6801..2032b2d 100644
--- a/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
+++ b/lib/lp/bugs/tests/test_bzremotecomponentfinder.py
@@ -3,8 +3,6 @@
"""Tests cronscript for retriving components from remote Bugzillas"""
-__all__ = []
-
import os
import re
diff --git a/lib/lp/bugs/tests/test_externalbugtracker.py b/lib/lp/bugs/tests/test_externalbugtracker.py
index 9805765..bb2ed10 100644
--- a/lib/lp/bugs/tests/test_externalbugtracker.py
+++ b/lib/lp/bugs/tests/test_externalbugtracker.py
@@ -3,8 +3,6 @@
"""Test related to ExternalBugtracker test infrastructure."""
-__all__ = []
-
import unittest
from lp.testing.layers import LaunchpadFunctionalLayer
diff --git a/lib/lp/bugs/tests/test_structuralsubscription.py b/lib/lp/bugs/tests/test_structuralsubscription.py
index 05464c9..b53168d 100644
--- a/lib/lp/bugs/tests/test_structuralsubscription.py
+++ b/lib/lp/bugs/tests/test_structuralsubscription.py
@@ -145,8 +145,8 @@ class FilteredStructuralSubscriptionTestBase:
def makeBugTask(self):
return self.factory.makeBugTask(target=self.target)
- def setUp(self):
- super().setUp()
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
self.ordinary_subscriber = self.factory.makePerson()
login_person(self.ordinary_subscriber)
self.target = self.makeTarget()
diff --git a/lib/lp/bugs/tests/test_yuitests.py b/lib/lp/bugs/tests/test_yuitests.py
index 88958ca..69598a4 100644
--- a/lib/lp/bugs/tests/test_yuitests.py
+++ b/lib/lp/bugs/tests/test_yuitests.py
@@ -3,8 +3,6 @@
"""Run YUI.test tests."""
-__all__ = []
-
from lp.testing import (
build_yui_unittest_suite,
YUIUnitTestCase,
diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py
index 9001fdb..98d49c1 100644
--- a/lib/lp/registry/browser/product.py
+++ b/lib/lp/registry/browser/product.py
@@ -40,6 +40,7 @@ __all__ = [
from operator import attrgetter
+from typing import Type
from urllib.parse import urlunsplit
from breezy import urlutils
@@ -1366,7 +1367,7 @@ class ProductBrandingView(BrandingChangeView):
@implementer(IProductEditMenu)
class ProductConfigureBase(ReturnToReferrerMixin, LaunchpadEditFormView):
- schema = IProduct
+ schema = IProduct # type: Type[Interface]
usage_fieldname = None
def setUpFields(self):
diff --git a/lib/lp/services/database/sqlobject/__init__.py b/lib/lp/services/database/sqlobject/__init__.py
index 381c7d2..3bafb8b 100644
--- a/lib/lp/services/database/sqlobject/__init__.py
+++ b/lib/lp/services/database/sqlobject/__init__.py
@@ -7,7 +7,29 @@
import datetime
from storm.expr import SQL
-from storm.sqlobject import * # noqa: F401,F403
+from storm.sqlobject import AND # noqa: F401
+from storm.sqlobject import BoolCol # noqa: F401
+from storm.sqlobject import CONTAINSSTRING # noqa: F401
+from storm.sqlobject import DateCol # noqa: F401
+from storm.sqlobject import DESC # noqa: F401
+from storm.sqlobject import FloatCol # noqa: F401
+from storm.sqlobject import ForeignKey # noqa: F401
+from storm.sqlobject import IN # noqa: F401
+from storm.sqlobject import IntCol # noqa: F401
+from storm.sqlobject import IntervalCol # noqa: F401
+from storm.sqlobject import LIKE # noqa: F401
+from storm.sqlobject import NOT # noqa: F401
+from storm.sqlobject import OR # noqa: F401
+from storm.sqlobject import SingleJoin # noqa: F401
+from storm.sqlobject import SQLConstant # noqa: F401
+from storm.sqlobject import SQLMultipleJoin # noqa: F401
+from storm.sqlobject import SQLObjectBase # noqa: F401
+from storm.sqlobject import SQLObjectMoreThanOneResultError # noqa: F401
+from storm.sqlobject import SQLObjectNotFound # noqa: F401
+from storm.sqlobject import SQLObjectResultSet # noqa: F401
+from storm.sqlobject import SQLRelatedJoin # noqa: F401
+from storm.sqlobject import StringCol # noqa: F401
+from storm.sqlobject import UtcDateTimeCol # noqa: F401
_sqlStringReplace = [
diff --git a/lib/lp/services/feeds/browser.py b/lib/lp/services/feeds/browser.py
index 19e76fe..45f2b9e 100644
--- a/lib/lp/services/feeds/browser.py
+++ b/lib/lp/services/feeds/browser.py
@@ -21,6 +21,11 @@ __all__ = [
'RootAnnouncementsFeedLink',
]
+from typing import (
+ Tuple,
+ Type,
+ )
+
from zope.component import getUtility
from zope.interface import implementer
from zope.publisher.interfaces import NotFound
@@ -366,7 +371,7 @@ class FeedsMixin:
ProjectBranchesFeedLink,
ProjectRevisionsFeedLink,
RootAnnouncementsFeedLink,
- )
+ ) # type: Tuple[Type[FeedLinkBase, ...]]
@property
def feed_links(self):
diff --git a/lib/lp/services/looptuner.py b/lib/lp/services/looptuner.py
index a3db4b8..27d1d50 100644
--- a/lib/lp/services/looptuner.py
+++ b/lib/lp/services/looptuner.py
@@ -368,7 +368,7 @@ class TunableLoop:
goal_seconds = 2
minimum_chunk_size = 1
- maximum_chunk_size = None # Override.
+ maximum_chunk_size = None # type: int
cooldown_time = 0
def __init__(self, log, abort_time=None):
diff --git a/lib/lp/services/mail/commands.py b/lib/lp/services/mail/commands.py
index 038f8b4..fd37fe6 100644
--- a/lib/lp/services/mail/commands.py
+++ b/lib/lp/services/mail/commands.py
@@ -60,7 +60,7 @@ class EmailCommand:
Both name the values in the args list are strings.
"""
- _numberOfArguments = None
+ _numberOfArguments = None # type: int
# Should command arguments be converted to lowercase?
case_insensitive_args = True
diff --git a/lib/lp/services/scripts/base.py b/lib/lp/services/scripts/base.py
index e7acbdb..f76f023 100644
--- a/lib/lp/services/scripts/base.py
+++ b/lib/lp/services/scripts/base.py
@@ -149,8 +149,8 @@ class LaunchpadScript:
"""
lock = None
txn = None
- usage = None
- description = None
+ usage = ""
+ description = ""
lockfilepath = None
loglevel = logging.INFO
diff --git a/lib/lp/services/webapp/breadcrumb.py b/lib/lp/services/webapp/breadcrumb.py
index eae2074..0699248 100644
--- a/lib/lp/services/webapp/breadcrumb.py
+++ b/lib/lp/services/webapp/breadcrumb.py
@@ -26,7 +26,7 @@ class Breadcrumb:
This class is intended for use as an adapter.
"""
- text = None
+ text = None # type: str
_detail = None
_url = None
inside = None
diff --git a/lib/lp/services/webapp/menu.py b/lib/lp/services/webapp/menu.py
index 627502a..6783750 100644
--- a/lib/lp/services/webapp/menu.py
+++ b/lib/lp/services/webapp/menu.py
@@ -19,6 +19,7 @@ __all__ = [
]
import types
+from typing import List
from lazr.delegates import delegate_to
from lazr.restful.utils import get_current_browser_request
@@ -192,7 +193,7 @@ MENU_ANNOTATION_KEY = 'lp.services.webapp.menu.links'
class MenuBase(UserAttributeCache):
"""Base class for facets and menus."""
- links = None
+ links = None # type: List[str]
extra_attributes = None
enable_only = ALL_LINKS
_baseclassname = 'MenuBase'
@@ -381,7 +382,7 @@ class NavigationMenu(MenuBase):
_baseclassname = 'NavigationMenu'
- title = None
+ title = None # type: str
disabled = False
def initLink(self, linkname, request_url):
diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
index 9e01c04..ee7b1fd 100644
--- a/lib/lp/services/webapp/publisher.py
+++ b/lib/lp/services/webapp/publisher.py
@@ -28,6 +28,12 @@ __all__ = [
from cgi import FieldStorage
import http.client
import re
+from typing import (
+ Any,
+ Dict,
+ Optional,
+ Type,
+ )
from urllib.parse import urlparse
from wsgiref.headers import Headers
@@ -529,7 +535,7 @@ class LaunchpadView(UserAttributeCache):
return None
# Names of feature flags which affect a view.
- related_features = ()
+ related_features = {} # type: Dict[str, bool]
@property
def related_feature_info(self):
@@ -888,7 +894,7 @@ class Navigation:
self.request = request
# Set this if you want to set a new layer before doing any traversal.
- newlayer = None
+ newlayer = None # type: Optional[Type[Any]]
def traverse(self, name):
"""Override this method to handle traversal.
diff --git a/lib/lp/services/webapp/vocabulary.py b/lib/lp/services/webapp/vocabulary.py
index 023deca..4eb92d4 100644
--- a/lib/lp/services/webapp/vocabulary.py
+++ b/lib/lp/services/webapp/vocabulary.py
@@ -22,6 +22,7 @@ __all__ = [
]
from collections import namedtuple
+from typing import Optional
import six
from storm.base import Storm
@@ -289,8 +290,8 @@ class SQLObjectVocabularyBase(FilteredVocabularyBase):
Then the vocabulary for the widget that captures a value for bar
should derive from SQLObjectVocabularyBase.
"""
- _orderBy = None
- _filter = None
+ _orderBy = None # type: Optional[str]
+ _filter = None # type: Optional[bool]
_clauseTables = None
def __init__(self, context=None):
diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
index bfc08cc..0f42d27 100644
--- a/lib/lp/testing/__init__.py
+++ b/lib/lp/testing/__init__.py
@@ -68,6 +68,10 @@ import subprocess
import sys
import tempfile
import time
+from typing import (
+ Type,
+ TYPE_CHECKING,
+ )
import unittest
from breezy import trace
@@ -176,6 +180,9 @@ from lp.testing.karma import KarmaRecorder
from lp.testing.mail_helpers import pop_notifications
+if TYPE_CHECKING:
+ from lp.testing.layers import BaseLayer
+
# The following names have been imported for the purpose of being
# exported. They are referred to here to silence lint warnings.
admin_logged_in
@@ -1032,7 +1039,7 @@ class WebServiceTestCase(TestCaseWithFactory):
class AbstractYUITestCase(TestCase):
- layer = None
+ layer = None # type: Type[BaseLayer]
suite_name = ''
# 30 seconds for the suite.
suite_timeout = 30000
diff --git a/pyproject.toml b/pyproject.toml
index 1f331da..b45aa38 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,102 @@
[tool.black]
line-length = 79
target-version = ['py35']
+
+[tool.mypy]
+python_version = "3.5"
+exclude = [
+ '/interfaces/',
+ 'interfaces\.py$',
+]
+
+[[tool.mypy.overrides]]
+module = "zope.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "twisted.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "storm.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "lazr.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "testtools.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "responses"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "transaction"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "soupmatchers"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "defusedxml.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "testscenarios.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "pystache"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "fixtures"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "breezy.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "feedparser"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "apt_inst"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "apt_pkg"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "debian.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "treq"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "gpgme"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "artifactory"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "dohq_artifactory.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "pymacaroons"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "iso8601"
+ignore_missing_imports = true
diff --git a/requirements/types.txt b/requirements/types.txt
new file mode 100644
index 0000000..d9a338e
--- /dev/null
+++ b/requirements/types.txt
@@ -0,0 +1,7 @@
+types-pytz==0.1.0
+types-simplejson==0.1.0
+types-six==0.1.9
+types-beautifulsoup4==4.9.0
+types-requests==0.1.13
+lxml-stubs==0.4.0
+types-Markdown==0.1.0