← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:stormify-worlddata into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:stormify-worlddata into launchpad:master.

Commit message:
Convert lp.services.worlddata to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/448428

I tried to do this back in 2020, but at the time there was too much test fallout; it worked out to be easier to start with classes that were closer to being leaves in the data model.  Now it's viable.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-worlddata into launchpad:master.
diff --git a/lib/lp/answers/model/question.py b/lib/lp/answers/model/question.py
index 2665579..a561d0f 100644
--- a/lib/lp/answers/model/question.py
+++ b/lib/lp/answers/model/question.py
@@ -899,11 +899,9 @@ class QuestionSet:
     def getQuestionLanguages(self):
         """See `IQuestionSet`"""
         return set(
-            Language.select(
-                "Language.id = Question.language",
-                clauseTables=["Question"],
-                distinct=True,
-            )
+            IStore(Language)
+            .find(Language, Question.language == Language.id)
+            .config(distinct=True)
         )
 
     def getMostActiveProjects(self, limit=5):
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index 1692c07..6e245b7 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -1167,9 +1167,8 @@ class Distribution(
         )
 
         if not mirrors and country is not None:
-            continent = country.continent
             query = And(
-                Country.continentID == continent.id,
+                Country.continent == country.continent,
                 DistributionMirror.country == Country.id,
                 base_query,
             )
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index 9f09770..1e424b8 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -510,7 +510,7 @@ class ProductSeries(
                 Language.id != english.id,
             )
 
-            ordered_results = query.order_by(["Language.englishname"])
+            ordered_results = query.order_by(Language.englishname)
 
             for language, pofile in ordered_results:
                 psl = ProductSeriesLanguage(self, language, pofile=pofile)
@@ -554,7 +554,7 @@ class ProductSeries(
                 Language.id != english.id,
             ).group_by(Language)
 
-            ordered_results = query.order_by(["Language.englishname"])
+            ordered_results = query.order_by(Language.englishname)
 
             for (
                 language,
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index 428c718..1e91273 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -20,7 +20,7 @@ from lp.answers.enums import QUESTION_STATUS_DEFAULT_SEARCH
 from lp.answers.interfaces.faqcollection import IFAQCollection
 from lp.answers.interfaces.questioncollection import ISearchableByQuestionOwner
 from lp.answers.model.faq import FAQ, FAQSearch
-from lp.answers.model.question import QuestionTargetSearch
+from lp.answers.model.question import Question, QuestionTargetSearch
 from lp.app.enums import ServiceUsage
 from lp.app.errors import NotFoundError
 from lp.blueprints.enums import SprintSpecificationStatus
@@ -63,7 +63,7 @@ from lp.services.database.constants import UTC_NOW
 from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import SQLBase, sqlvalues
+from lp.services.database.sqlbase import SQLBase
 from lp.services.database.sqlobject import (
     AND,
     BoolCol,
@@ -367,15 +367,14 @@ class ProjectGroup(
     def getQuestionLanguages(self):
         """See `IQuestionCollection`."""
         return set(
-            Language.select(
-                """
-            Language.id = Question.language AND
-            Question.product = Product.id AND
-            Product.project = %s"""
-                % sqlvalues(self.id),
-                clauseTables=["Question", "Product"],
-                distinct=True,
+            IStore(Language)
+            .find(
+                Language,
+                Question.language == Language.id,
+                Question.product == Product.id,
+                Product.projectgroup == self.id,
             )
+            .config(distinct=True)
         )
 
     @property
diff --git a/lib/lp/registry/templates/person-editlanguages.pt b/lib/lp/registry/templates/person-editlanguages.pt
index 72f346b..466122e 100644
--- a/lib/lp/registry/templates/person-editlanguages.pt
+++ b/lib/lp/registry/templates/person-editlanguages.pt
@@ -48,10 +48,10 @@
           </dt>
           <tal:languages define="languages country/languages">
             <dd class="sprite language"
-              tal:condition="languages"
+              tal:condition="not: languages/is_empty"
               tal:repeat="lang country/languages"
               tal:content="lang/englishname"/>
-            <dd tal:condition="not: languages">
+            <dd tal:condition="languages/is_empty">
               No languages are currently registered as being spoken in <span
               tal:replace="country/name">country</span>.  If you have this
               information, please see if it has already been submitted in the
diff --git a/lib/lp/services/worlddata/model/country.py b/lib/lp/services/worlddata/model/country.py
index c02d2a9..e94438b 100644
--- a/lib/lp/services/worlddata/model/country.py
+++ b/lib/lp/services/worlddata/model/country.py
@@ -3,17 +3,13 @@
 
 __all__ = ["Country", "CountrySet", "Continent"]
 
+from storm.locals import Int, Reference, ReferenceSet, Unicode
 from zope.interface import implementer
 
 from lp.app.errors import NotFoundError
 from lp.services.database.constants import DEFAULT
 from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import (
-    ForeignKey,
-    SQLRelatedJoin,
-    StringCol,
-)
+from lp.services.database.stormbase import StormBase
 from lp.services.worlddata.interfaces.country import (
     IContinent,
     ICountry,
@@ -22,28 +18,25 @@ from lp.services.worlddata.interfaces.country import (
 
 
 @implementer(ICountry)
-class Country(SQLBase):
+class Country(StormBase):
     """A country."""
 
-    _table = "Country"
+    __storm_table__ = "Country"
 
     # default to listing newest first
-    _defaultOrder = "name"
+    __storm_order__ = "name"
 
     # db field names
-    name = StringCol(dbName="name", unique=True, notNull=True)
-    iso3166code2 = StringCol(dbName="iso3166code2", unique=True, notNull=True)
-    iso3166code3 = StringCol(dbName="iso3166code3", unique=True, notNull=True)
-    title = StringCol(dbName="title", notNull=False, default=DEFAULT)
-    description = StringCol(dbName="description")
-    continent = ForeignKey(
-        dbName="continent", foreignKey="Continent", default=None
-    )
-    languages = SQLRelatedJoin(
-        "Language",
-        joinColumn="country",
-        otherColumn="language",
-        intermediateTable="SpokenIn",
+    id = Int(primary=True)
+    name = Unicode(name="name", allow_none=False)
+    iso3166code2 = Unicode(name="iso3166code2", allow_none=False)
+    iso3166code3 = Unicode(name="iso3166code3", allow_none=False)
+    title = Unicode(name="title", allow_none=True, default=DEFAULT)
+    description = Unicode(name="description")
+    continent_id = Int(name="continent", default=None)
+    continent = Reference(continent_id, "Continent.id")
+    languages = ReferenceSet(
+        id, "SpokenIn.country_id", "SpokenIn.language_id", "Language.id"
     )
 
 
@@ -52,13 +45,15 @@ class CountrySet:
     """A set of countries"""
 
     def __getitem__(self, iso3166code2):
-        country = Country.selectOneBy(iso3166code2=iso3166code2)
+        country = (
+            IStore(Country).find(Country, iso3166code2=iso3166code2).one()
+        )
         if country is None:
             raise NotFoundError(iso3166code2)
         return country
 
     def __iter__(self):
-        yield from Country.select()
+        yield from IStore(Country).find(Country)
 
     def getByName(self, name):
         """See `ICountrySet`."""
@@ -74,11 +69,12 @@ class CountrySet:
 
 
 @implementer(IContinent)
-class Continent(SQLBase):
+class Continent(StormBase):
     """See IContinent."""
 
-    _table = "Continent"
-    _defaultOrder = ["name", "id"]
+    __storm_table__ = "Continent"
+    __storm_order__ = ["name", "id"]
 
-    name = StringCol(unique=True, notNull=True)
-    code = StringCol(unique=True, notNull=True)
+    id = Int(primary=True)
+    name = Unicode(allow_none=False)
+    code = Unicode(allow_none=False)
diff --git a/lib/lp/services/worlddata/model/language.py b/lib/lp/services/worlddata/model/language.py
index c3cd90a..a0cb647 100644
--- a/lib/lp/services/worlddata/model/language.py
+++ b/lib/lp/services/worlddata/model/language.py
@@ -7,8 +7,8 @@ __all__ = [
     "LanguageSet",
 ]
 
-import six
-from storm.expr import And, Count, Desc, Join, LeftJoin, Or
+from storm.expr import And, Count, Desc, Is, Join, LeftJoin, Or
+from storm.locals import Bool, Int, Unicode
 from storm.references import ReferenceSet
 from zope.interface import implementer
 
@@ -17,14 +17,7 @@ from lp.registry.model.karma import KarmaCache, KarmaCategory
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import IStandbyStore, IStore
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import (
-    BoolCol,
-    IntCol,
-    SQLObjectNotFound,
-    SQLRelatedJoin,
-    StringCol,
-)
+from lp.services.database.stormbase import StormBase
 from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.worlddata.interfaces.language import (
     ILanguage,
@@ -34,16 +27,17 @@ from lp.services.worlddata.interfaces.language import (
 
 
 @implementer(ILanguage)
-class Language(SQLBase):
-    _table = "Language"
-
-    code = StringCol(dbName="code", notNull=True, unique=True)
-    uuid = StringCol(dbName="uuid", notNull=False, default=None)
-    nativename = StringCol(dbName="nativename")
-    englishname = StringCol(dbName="englishname")
-    pluralforms = IntCol(dbName="pluralforms")
-    pluralexpression = StringCol(dbName="pluralexpression")
-    visible = BoolCol(dbName="visible", notNull=True)
+class Language(StormBase):
+    __storm_table__ = "Language"
+
+    id = Int(primary=True)
+    code = Unicode(name="code", allow_none=False)
+    uuid = Unicode(name="uuid", allow_none=True, default=None)
+    nativename = Unicode(name="nativename")
+    englishname = Unicode(name="englishname", allow_none=False)
+    pluralforms = Int(name="pluralforms")
+    pluralexpression = Unicode(name="pluralexpression")
+    visible = Bool(name="visible", allow_none=False)
     direction = DBEnum(
         name="direction",
         allow_none=False,
@@ -52,19 +46,38 @@ class Language(SQLBase):
     )
 
     translation_teams = ReferenceSet(
-        "<primary key>",
-        "Translator.language_id",
-        "Translator.translator_id",
-        "Person.id",
+        "id", "Translator.language_id", "Translator.translator_id", "Person.id"
     )
 
-    _countries = SQLRelatedJoin(
-        "Country",
-        joinColumn="language",
-        otherColumn="country",
-        intermediateTable="SpokenIn",
+    _countries = ReferenceSet(
+        "id", "SpokenIn.language_id", "SpokenIn.country_id", "Country.id"
     )
 
+    def __init__(
+        self,
+        code,
+        englishname,
+        nativename=None,
+        pluralforms=None,
+        pluralexpression=None,
+        visible=True,
+        direction=TextDirection.LTR,
+    ):
+        super().__init__()
+        self.code = code
+        self.englishname = englishname
+        self.nativename = nativename
+        self.pluralforms = pluralforms
+        self.pluralexpression = pluralexpression
+        self.visible = visible
+        self.direction = direction
+
+    def addCountry(self, country):
+        self._countries.add(country)
+
+    def removeCountry(self, country):
+        self._countries.remove(country)
+
     # Define a read/write property `countries` so it can be passed
     # to language administration `LaunchpadFormView`.
     def _getCountries(self):
@@ -199,7 +212,11 @@ class LanguageSet:
 
     @property
     def _visible_languages(self):
-        return Language.select("visible IS TRUE", orderBy="englishname")
+        return (
+            IStore(Language)
+            .find(Language, Is(Language.visible, True))
+            .order_by(Language.englishname)
+        )
 
     @property
     def common_languages(self):
@@ -220,7 +237,7 @@ class LanguageSet:
             IStore(Language)
             .find(
                 Language,
-                Language.visible == True if only_visible else True,
+                Is(Language.visible, True) if only_visible else True,
             )
             .order_by(Language.englishname)
         )
@@ -257,7 +274,9 @@ class LanguageSet:
 
     def __iter__(self):
         """See `ILanguageSet`."""
-        return iter(Language.select(orderBy="englishname"))
+        return iter(
+            IStore(Language).find(Language).order_by(Language.englishname)
+        )
 
     def __getitem__(self, code):
         """See `ILanguageSet`."""
@@ -270,10 +289,7 @@ class LanguageSet:
 
     def get(self, language_id):
         """See `ILanguageSet`."""
-        try:
-            return Language.get(language_id)
-        except SQLObjectNotFound:
-            return None
+        return IStore(Language).get(Language, language_id)
 
     def getLanguageByCode(self, code):
         """See `ILanguageSet`."""
@@ -284,7 +300,7 @@ class LanguageSet:
 
     def keys(self):
         """See `ILanguageSet`."""
-        return [language.code for language in Language.select()]
+        return list(IStore(Language).find(Language.code))
 
     def canonicalise_language_code(self, code):
         """See `ILanguageSet`."""
@@ -307,7 +323,7 @@ class LanguageSet:
         direction=TextDirection.LTR,
     ):
         """See `ILanguageSet`."""
-        return Language(
+        language = Language(
             code=code,
             englishname=englishname,
             nativename=nativename,
@@ -316,11 +332,12 @@ class LanguageSet:
             visible=visible,
             direction=direction,
         )
+        return IStore(Language).add(language)
 
     def search(self, text):
         """See `ILanguageSet`."""
         if text:
-            text = six.ensure_text(text).lower()
+            text = text.lower()
             results = (
                 IStandbyStore(Language)
                 .find(
diff --git a/lib/lp/services/worlddata/model/spokenin.py b/lib/lp/services/worlddata/model/spokenin.py
index 61d5dc9..26c426a 100644
--- a/lib/lp/services/worlddata/model/spokenin.py
+++ b/lib/lp/services/worlddata/model/spokenin.py
@@ -3,23 +3,24 @@
 
 __all__ = ["SpokenIn"]
 
+from storm.locals import Int, Reference
 from zope.interface import implementer
 
-from lp.services.database.sqlbase import SQLBase
-from lp.services.database.sqlobject import ForeignKey
+from lp.services.database.stormbase import StormBase
 from lp.services.worlddata.interfaces.spokenin import ISpokenIn
 
 
 @implementer(ISpokenIn)
-class SpokenIn(SQLBase):
+class SpokenIn(StormBase):
     """A way of telling which languages are spoken in which countries.
 
     This table maps a language which is SpokenIn a country.
     """
 
-    _table = "SpokenIn"
+    __storm_table__ = "SpokenIn"
 
-    country = ForeignKey(dbName="country", notNull=True, foreignKey="Country")
-    language = ForeignKey(
-        dbName="language", notNull=True, foreignKey="Language"
-    )
+    id = Int(primary=True)
+    country_id = Int(name="country", allow_none=False)
+    country = Reference(country_id, "Country.id")
+    language_id = Int(name="language", allow_none=False)
+    language = Reference(language_id, "Language.id")
diff --git a/lib/lp/services/worlddata/vocabularies.py b/lib/lp/services/worlddata/vocabularies.py
index 72db756..c20f49b 100644
--- a/lib/lp/services/worlddata/vocabularies.py
+++ b/lib/lp/services/worlddata/vocabularies.py
@@ -11,7 +11,7 @@ from zope.component import getUtility
 from zope.interface import alsoProvides
 from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
 
-from lp.services.webapp.vocabulary import SQLObjectVocabularyBase
+from lp.services.webapp.vocabulary import StormVocabularyBase
 from lp.services.worlddata.interfaces.language import ILanguage, ILanguageSet
 from lp.services.worlddata.interfaces.timezone import ITimezoneNameVocabulary
 from lp.services.worlddata.model.country import Country
@@ -74,31 +74,32 @@ def TimezoneNameVocabulary(context=None):
     return _timezone_vocab
 
 
-# Country.name may have non-ASCII characters, so we can't use
-# NamedSQLObjectVocabulary here.
-
-
-class CountryNameVocabulary(SQLObjectVocabularyBase):
+# Country.name is the English name of the country rather than an identifier,
+# so we don't use it as the token and thus don't use NamedStormVocabulary
+# here.
+class CountryNameVocabulary(StormVocabularyBase):
     """A vocabulary for country names."""
 
     _table = Country
-    _orderBy = "name"
+    _order_by = "name"
 
     def toTerm(self, obj):
         return SimpleTerm(obj, obj.id, obj.name)
 
 
-class LanguageVocabulary(SQLObjectVocabularyBase):
+class LanguageVocabulary(StormVocabularyBase):
     """All the languages known by Launchpad."""
 
     _table = Language
-    _orderBy = "englishname"
+    _order_by = "englishname"
 
     def __contains__(self, language):
         """See `IVocabulary`."""
-        assert ILanguage.providedBy(language), (
-            "'in LanguageVocabulary' requires ILanguage as left operand, "
-            "got %s instead." % type(language)
+        assert ILanguage.providedBy(
+            language
+        ), "'in %s' requires ILanguage as left operand, got %s instead." % (
+            self.__class__.__name__,
+            type(language),
         )
         return super().__contains__(language)
 
diff --git a/lib/lp/translations/model/pofile.py b/lib/lp/translations/model/pofile.py
index 594eac3..14626b1 100644
--- a/lib/lp/translations/model/pofile.py
+++ b/lib/lp/translations/model/pofile.py
@@ -866,7 +866,7 @@ class POFile(StormBase, POFileMixIn):
         )
         params = {
             "potemplate": quote(self.potemplate.id),
-            "language": quote(self.language),
+            "language": quote(self.language.id),
             "flag": side_traits.flag_name,
             "other_flag": side_traits.other_side_traits.flag_name,
             "has_msgstrs": complete_plural_clause_this_side,
@@ -946,7 +946,7 @@ class POFile(StormBase, POFileMixIn):
             ]
         )
         params = {
-            "language": quote(self.language),
+            "language": quote(self.language.id),
             "potemplate": quote(self.potemplate.id),
             "flag": flag_name,
             "suggestion_nonempty": suggestion_nonempty,
@@ -1309,7 +1309,7 @@ class POFile(StormBase, POFileMixIn):
         template_id = quote(self.potemplate.id)
         params = {
             "flag": flag_name,
-            "language": quote(self.language),
+            "language": quote(self.language.id),
             "template": template_id,
         }
         query = (
diff --git a/lib/lp/translations/templates/language-index.pt b/lib/lp/translations/templates/language-index.pt
index 96685b1..ef31980 100644
--- a/lib/lp/translations/templates/language-index.pt
+++ b/lib/lp/translations/templates/language-index.pt
@@ -88,7 +88,7 @@
       <div class="yui-u">
         <div class="portlet">
           <h2>Countries</h2>
-          <tal:has_countries condition="context/countries">
+          <tal:has_countries condition="not:context/countries/is_empty">
             <p>
               <tal:language replace="view/language_name">
                 Espa&ntilde;ol
@@ -100,7 +100,7 @@
                 tal:content="country/name">Spain</li>
             </ul>
           </tal:has_countries>
-          <tal:has_not_countries condition="not:context/countries">
+          <tal:has_not_countries condition="context/countries/is_empty">
             <p class="helpwanted">
               <tal:language replace="view/language_name">
                 Espa&ntilde;ol
diff --git a/lib/lp/translations/utilities/translation_import.py b/lib/lp/translations/utilities/translation_import.py
index a27c53c..2749fae 100644
--- a/lib/lp/translations/utilities/translation_import.py
+++ b/lib/lp/translations/utilities/translation_import.py
@@ -143,7 +143,7 @@ class ExistingPOFileInDatabase:
         substitutions = {
             "translation_columns": ", ".join(translations),
             "translation_joins": "\n".join(msgstr_joins),
-            "language": quote(self.pofile.language),
+            "language": quote(self.pofile.language.id),
             "potemplate": quote(self.pofile.potemplate.id),
             "flag": self._getFlagName(),
         }
diff --git a/lib/lp/translations/vocabularies.py b/lib/lp/translations/vocabularies.py
index fcf3aec..a564444 100644
--- a/lib/lp/translations/vocabularies.py
+++ b/lib/lp/translations/vocabularies.py
@@ -23,7 +23,7 @@ from lp.services.webapp.vocabulary import (
     NamedStormVocabulary,
     StormVocabularyBase,
 )
-from lp.services.worlddata.interfaces.language import ILanguage
+from lp.services.worlddata.model.language import Language
 from lp.services.worlddata.vocabularies import LanguageVocabulary
 from lp.translations.enums import LanguagePackType
 from lp.translations.model.languagepack import LanguagePack
@@ -40,39 +40,7 @@ class TranslatableLanguageVocabulary(LanguageVocabulary):
     excluding English and non-visible languages.
     """
 
-    def __contains__(self, language):
-        """See `IVocabulary`.
-
-        This vocabulary excludes English and languages that are not visible.
-        """
-        assert ILanguage.providedBy(language), (
-            "'in TranslatableLanguageVocabulary' requires ILanguage as "
-            "left operand, got %s instead." % type(language)
-        )
-        if language.code == "en":
-            return False
-        return language.visible == True and super().__contains__(language)
-
-    def __iter__(self):
-        """See `IVocabulary`.
-
-        Iterate languages that are visible and not English.
-        """
-        languages = self._table.select(
-            "Language.code != 'en' AND Language.visible = True",
-            orderBy=self._orderBy,
-        )
-        for language in languages:
-            yield self.toTerm(language)
-
-    def getTermByToken(self, token):
-        """See `IVocabulary`."""
-        if token == "en":
-            raise LookupError(token)
-        term = super().getTermByToken(token)
-        if not term.value.visible:
-            raise LookupError(token)
-        return term
+    _clauses = [Language.code != "en", Is(Language.visible, True)]
 
 
 class TranslationGroupVocabulary(NamedStormVocabulary):