launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32748
Re: [Merge] ~enriqueesanchz/launchpad:add-external-package into launchpad:master
Diff comments:
> diff --git a/lib/lp/bugs/browser/structuralsubscription.py b/lib/lp/bugs/browser/structuralsubscription.py
> index ec278f7..f4874ef 100644
> --- a/lib/lp/bugs/browser/structuralsubscription.py
> +++ b/lib/lp/bugs/browser/structuralsubscription.py
> @@ -284,9 +285,11 @@ class StructuralSubscriptionView(LaunchpadFormView):
> def userIsDriver(self):
> """Has the current user driver permissions?"""
> # We only want to look at this if the target is a
> - # distribution source package, in order to maintain
> - # compatibility with the obsolete bug contacts feature.
> - if IDistributionSourcePackage.providedBy(self.context):
> + # distribution source packagfe or external package, in order to
thanks!
> + # maintain compatibility with the obsolete bug contacts feature.
> + if IDistributionSourcePackage.providedBy(
> + self.context
> + ) or IExternalPackage.providedBy(self.context):
> return check_permission(
> "launchpad.Driver", self.context.distribution
> )
> diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
> index 4549f0a..5306398 100644
> --- a/lib/lp/bugs/model/tests/test_bugtask.py
> +++ b/lib/lp/bugs/model/tests/test_bugtask.py
> @@ -3581,6 +3615,44 @@ class TestValidateTarget(TestCaseWithFactory, ValidateTargetMixin):
> dsp,
> )
>
> + def test_externalpackage_task_is_allowed(self):
> + # An External task can coexist with a task for its Distribution.
done!
> + d = self.factory.makeDistribution()
> + task = self.factory.makeBugTask(target=d)
> + externalpackage = self.factory.makeExternalPackage(distribution=d)
> + validate_target(task.bug, externalpackage)
> +
> + def test_externalpackage_task_with_dsp_is_allowed(self):
> + # An External task can coexist with a task for a
> + # DistributionSourcePackage with the same name.
> + d = self.factory.makeDistribution()
> + dsp = self.factory.makeDistributionSourcePackage(distribution=d)
> + task = self.factory.makeBugTask(target=dsp)
> + externalpackage = self.factory.makeExternalPackage(distribution=d)
> + validate_target(task.bug, externalpackage)
> +
> + def test_different_externalpackage_tasks_are_allowed(self):
> + # An ExternalPackage task can also coexist with a task for another one.
> + externalpackage = self.factory.makeExternalPackage()
> + task = self.factory.makeBugTask(target=externalpackage)
> + externalpackage = self.factory.makeExternalPackage(
> + distribution=externalpackage.distribution
> + )
> + validate_target(task.bug, externalpackage)
> +
> + def test_same_externalpackage_task_is_forbidden(self):
> + # But an ExternalPackage task cannot coexist with a task for itself.
> + externalpackage = self.factory.makeExternalPackage()
> + task = self.factory.makeBugTask(target=externalpackage)
> + self.assertRaisesWithContent(
> + IllegalTarget,
> + "A fix for this bug has already been requested for %s"
> + % (externalpackage.displayname),
> + validate_target,
> + task.bug,
> + externalpackage,
> + )
> +
> def test_illegal_information_type_disallowed(self):
> # The bug's current information_type must be permitted by the
> # new target.
> diff --git a/lib/lp/registry/interfaces/externalpackage.py b/lib/lp/registry/interfaces/externalpackage.py
> new file mode 100644
> index 0000000..e5ec7f4
> --- /dev/null
> +++ b/lib/lp/registry/interfaces/externalpackage.py
> @@ -0,0 +1,157 @@
> +# Copyright 2009, 2025 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""External package interfaces."""
> +
> +__all__ = [
> + "IExternalPackage",
> + "ExternalPackageType",
> +]
> +
> +from lazr.enum import DBEnumeratedType, DBItem
> +from lazr.restful.declarations import exported, exported_as_webservice_entry
> +from lazr.restful.fields import Reference
> +from zope.interface import Attribute
> +from zope.schema import TextLine
> +
> +from lp import _
> +from lp.app.interfaces.launchpad import IHeadingContext
> +from lp.bugs.interfaces.bugtarget import IBugTarget, IHasOfficialBugTags
> +from lp.registry.interfaces.distribution import IDistribution
> +from lp.registry.interfaces.role import IHasDrivers
> +
> +
> +@exported_as_webservice_entry(as_of="beta")
> +class IExternalPackageView(
> + IHeadingContext,
> + IBugTarget,
> + IHasOfficialBugTags,
> + IHasDrivers,
> +):
> + """`IExternalPackage` attributes that require launchpad.View."""
> +
> + packagetype = Attribute("The package type")
> +
> + channel = Attribute("The package channel")
> +
> + display_channel = TextLine(title=_("Display channel name"), readonly=True)
> +
> + distribution = exported(
> + Reference(IDistribution, title=_("The distribution."))
> + )
> + sourcepackagename = Attribute("The source package name.")
> +
> + name = exported(
> + TextLine(title=_("The source package name as text"), readonly=True)
> + )
> + display_name = exported(
> + TextLine(title=_("Display name for this package."), readonly=True)
> + )
> + displayname = Attribute("Display name (deprecated)")
> + title = exported(
> + TextLine(title=_("Title for this package."), readonly=True)
> + )
> +
> + drivers = Attribute("The drivers for the distribution.")
> +
> + def __eq__(other):
> + """IExternalPackage comparison method.
> +
> + Distro sourcepackages compare equal only if their fields compare equal.
> + """
> +
> + def __ne__(other):
> + """IExternalPackage comparison method.
> +
> + External packages compare not equal if either of their
> + fields compare not equal.
> + """
> +
> +
> +@exported_as_webservice_entry(as_of="beta")
> +class IExternalPackage(
> + IExternalPackageView,
> +):
> + """Represents an ExternalPackage in a distribution.
> +
> + Create IExternalPackage by invoking `IDistribution.getExternalPackage()`.
> + """
> +
> +
> +class ExternalPackageType(DBEnumeratedType):
I'm not sure as `SourcePackageType` exists and the types are `dpkg`, `rpm`, `ebuild` and `ci_build`. We can also reuse that enum but I don't know if that makes sense since it is used for the publishing history and releases and we don't want those things to support `snaps`, `charms`, etc. right?
> + """Bug Task Status
done!
> +
> + The various possible states for a bugfix in a specific place.
> + """
> +
> + UNKNOWN = DBItem(
> + 0,
> + """
> + Unknown
> +
> + Unknown external package
> + """,
> + )
> +
> + SNAP = DBItem(
> + 1,
> + """
> + Snap
> +
> + Snap external package
> + """,
> + )
> +
> + CHARM = DBItem(
> + 2,
> + """
> + Charm
> +
> + Charm external package
> + """,
> + )
> +
> + ROCK = DBItem(
> + 3,
> + """
> + Rock
> +
> + Rock external package
> + """,
> + )
> +
> + PYTHON = DBItem(
> + 4,
> + """
> + Python
> +
> + Python external package
> + """,
> + )
> +
> + CONDA = DBItem(
> + 5,
> + """
> + Conda
> +
> + Conda external package
> + """,
> + )
> +
> + CARGO = DBItem(
> + 6,
> + """
> + Cargo
> +
> + Cargo external package
> + """,
> + )
> +
> + MAVEN = DBItem(
> + 7,
> + """
> + Maven
> +
> + Maven external package
> + """,
> + )
> diff --git a/lib/lp/registry/model/externalpackage.py b/lib/lp/registry/model/externalpackage.py
> new file mode 100644
> index 0000000..a92c19f
> --- /dev/null
> +++ b/lib/lp/registry/model/externalpackage.py
> @@ -0,0 +1,152 @@
> +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
done!
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Classes to represent external packages in a distribution."""
> +
> +__all__ = [
> + "ExternalPackage",
> +]
> +
> +from zope.interface import implementer
> +
> +from lp.bugs.model.bugtarget import BugTargetBase
> +from lp.bugs.model.structuralsubscription import (
> + StructuralSubscriptionTargetMixin,
> +)
> +from lp.registry.interfaces.distribution import IDistribution
> +from lp.registry.interfaces.externalpackage import (
> + ExternalPackageType,
> + IExternalPackage,
> +)
> +from lp.registry.interfaces.sourcepackagename import ISourcePackageName
> +from lp.registry.model.hasdrivers import HasDriversMixin
> +from lp.services.channels import channel_list_to_string, channel_string_to_list
> +from lp.services.propertycache import cachedproperty
> +
> +
> +@implementer(IExternalPackage)
> +class ExternalPackage(
> + BugTargetBase,
> + HasDriversMixin,
> + StructuralSubscriptionTargetMixin,
> +):
> + """This is a "Magic External Package". It is not a Storm model, but instead
> + it represents a package with a particular name, type and channel in a
> + particular distribution.
> + """
> +
> + def __init__(
> + self,
> + distribution: IDistribution,
> + sourcepackagename: ISourcePackageName,
> + packagetype: ExternalPackageType,
> + channel: (str, tuple, list),
> + ) -> "ExternalPackage":
> + self.distribution = distribution
> + self.sourcepackagename = sourcepackagename
> + self.packagetype = packagetype
> + self.channel = self.validate_channel(channel)
> +
> + def __repr__(self) -> str:
> + return f"<{self.__class__.__name__} '{self.display_name}'>"
> +
> + def validate_channel(self, channel: (str, tuple, list)) -> tuple:
> + if channel is None:
> + return None
> +
> + if not isinstance(channel, (str, tuple, list)):
> + raise ValueError("Channel must be a str, tuple or list")
> +
> + return channel_string_to_list(channel)
> +
> + @property
> + def name(self) -> str:
> + """See `IExternalPackage`."""
> + return self.sourcepackagename.name
> +
> + @property
> + def display_channel(self) -> str:
> + """See `IExternalPackage`."""
> + if not self.channel:
> + return None
> +
> + return channel_list_to_string(*self.channel)
> +
> + @cachedproperty
> + def display_name(self) -> str:
> + """See `IExternalPackage`."""
> + if self.channel:
> + return "%s - %s @%s in %s" % (
> + self.sourcepackagename.name,
> + self.packagetype,
> + self.display_channel,
> + self.distribution.display_name,
> + )
> +
> + return "%s - %s in %s" % (
> + self.sourcepackagename.name,
> + self.packagetype,
> + self.distribution.display_name,
> + )
> +
> + # There are different places of launchpad codebase where they use different
> + # display names
> + @property
> + def displayname(self) -> str:
I don't like `displayname = display_name` as I didn't notice this at first when I was looking at that class.
> + """See `IExternalPackage`."""
> + return self.display_name
> +
> + @property
> + def bugtargetdisplayname(self) -> str:
> + """See `IExternalPackage`."""
> + return self.display_name
> +
> + @property
> + def bugtargetname(self) -> str:
> + """See `IExternalPackage`."""
> + return self.display_name
> +
> + @property
> + def title(self) -> str:
> + """See `IExternalPackage`."""
> + return self.display_name
> +
> + def __eq__(self, other: "ExternalPackage") -> str:
> + """See `IExternalPackage`."""
> + return (
> + (IExternalPackage.providedBy(other))
> + and (self.distribution.id == other.distribution.id)
> + and (self.sourcepackagename.id == other.sourcepackagename.id)
> + and (self.packagetype == other.packagetype)
> + and (self.channel == other.channel)
> + )
> +
> + def __hash__(self) -> int:
> + """Return the combined attributes hash."""
> + return hash(
> + (
> + self.distribution,
> + self.sourcepackagename,
> + self.packagetype,
> + self.display_channel,
> + )
> + )
> +
> + @property
> + def drivers(self) -> list:
> + """See `IHasDrivers`."""
> + return self.distribution.drivers
> +
> + @property
> + def official_bug_tags(self) -> list:
> + """See `IHasBugs`."""
> + return self.distribution.official_bug_tags
> +
> + @property
> + def pillar(self) -> IDistribution:
> + """See `IBugTarget`."""
> + return self.distribution
> +
> + def _getOfficialTagClause(self):
> + """See `IBugTarget`."""
> + return self.distribution._getOfficialTagClause()
> diff --git a/lib/lp/services/channels.py b/lib/lp/services/channels.py
> index 2d36856..508a44e 100644
> --- a/lib/lp/services/channels.py
> +++ b/lib/lp/services/channels.py
> @@ -33,9 +33,19 @@ def channel_string_to_list(channel):
>
> :raises ValueError: If the channel string is invalid.
> """
> - components = channel.split(CHANNEL_COMPONENTS_DELIMITER)
> + if isinstance(channel, str):
> + components = channel.split(CHANNEL_COMPONENTS_DELIMITER)
> + else:
> + components = channel
> +
> if len(components) == 3:
> track, risk, branch = components
> + if not _is_risk(risk):
Good catch! Addressed, thanks! I was thinking on changing the less that I needed and lost track of improving it.
Now it is more readable and efficient!
> + raise ValueError("No valid risk provided: %r" % channel)
> + if _is_risk(branch):
> + raise ValueError(
> + "Branch name cannot match a risk name: %r" % channel
> + )
> elif len(components) == 2:
> # Identify risk to determine if this is track/risk or risk/branch.
> if _is_risk(components[0]):
> diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
> index d23c481..a940fed 100644
> --- a/lib/lp/testing/factory.py
> +++ b/lib/lp/testing/factory.py
> @@ -5585,6 +5586,28 @@ class LaunchpadObjectFactory(ObjectFactory):
> )
> return dsp
>
> + def makeExternalPackage(
> + self,
> + sourcepackagename=None,
> + packagetype=None,
> + channel=None,
> + distribution=None,
> + ):
> + if sourcepackagename is None or isinstance(sourcepackagename, str):
> + sourcepackagename = self.getOrMakeSourcePackageName(
> + sourcepackagename
> + )
> + if distribution is None:
> + distribution = self.makeDistribution()
> + if packagetype is None:
> + packagetype = ExternalPackageType.SNAP
> + if channel is None:
> + channel = "12.1/stable"
done!
> +
> + return distribution.getExternalPackage(
> + sourcepackagename, packagetype, channel
> + )
> +
> def makeEmailMessage(
> self,
> body=None,
--
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/488673
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-external-package into launchpad:master.
References