diff --git a/gentoopm/basepm/pkg.py b/gentoopm/basepm/pkg.py index 5cb70ad..8af28d6 100644 --- a/gentoopm/basepm/pkg.py +++ b/gentoopm/basepm/pkg.py @@ -1,4 +1,5 @@ # (c) 2011-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later import os.path @@ -14,6 +15,7 @@ from .atom import PMAtom, PMPackageKey from .environ import PMPackageEnvironment +from gentoopm.basepm.upstream import PMUpstream PMPackageState = EnumTuple("PMPackageState", "installable", "installed") @@ -417,6 +419,13 @@ def maintainers(self): """ pass + @property + @abstractmethod + def upstream(self) -> PMUpstream: + """ + Get the package upstream metadata. + """ + @abstractproperty def repo_masked(self): """ diff --git a/gentoopm/basepm/upstream.py b/gentoopm/basepm/upstream.py new file mode 100644 index 0000000..f9e8294 --- /dev/null +++ b/gentoopm/basepm/upstream.py @@ -0,0 +1,190 @@ +# (c) 2024 Anna +# SPDX-License-Identifier: GPL-2.0-or-later + +import typing +from abc import abstractmethod +from collections.abc import Sequence +from enum import Enum + +from ..util import ( + ABCObject, + StringCompat +) + + +class PMUpstreamMaintainerStatus(Enum): + """ + Maintainer status enumeration. + """ + + UNKNOWN = None + ACTIVE = "active" + INACTIVE = "inactive" + + +class PMUpstreamMaintainer(ABCObject, StringCompat): + """ + A base class for an upstream maintainer. + """ + + _name: str + _email: typing.Optional[str] + _status: PMUpstreamMaintainerStatus + + def __new__(cls, name: str, email: typing.Optional[str] = None, + status: PMUpstreamMaintainerStatus = PMUpstreamMaintainerStatus.UNKNOWN): + """ + Instantiate the actual string. Requires other props prepared + beforehand. + + @param email: maintainer's e-mail address + @param name: maintainer's name + @param status: maintainer's activity status + """ + + parts = [name] + if email is not None: + parts.append(f"<{email}>") + + ret = StringCompat.__new__(cls, " ".join(parts)) + ret._name = name + ret._email = email + ret._status = status + return ret + + @property + def name(self) -> str: + """ + Maintainer's name. + """ + return self._name + + @property + def email(self) -> typing.Optional[str]: + """ + Maintainer's e-mail address. + """ + return self._email + + @property + def status(self) -> PMUpstreamMaintainerStatus: + """ + Maintainer's activity status. + """ + return self._status + + +class PMUpstreamDoc(ABCObject, StringCompat): + """ + A base class for a link to the upstream documentation. + """ + + _url: str + _lang: str + + def __new__(cls, url: str, lang: str = "en"): + """ + Instantiate the actual string. Requires other props prepared + beforehand. + + @param url: documentation URL + @param lang: documentation language + """ + + ret = StringCompat.__new__(cls, url) + ret._url = url + ret._lang = lang + return ret + + @property + def url(self) -> str: + """ + Documentation URL. + """ + return self._url + + @property + def lang(self) -> str: + """ + Documentation language. + """ + return self._lang + + +class PMUpstreamRemoteID(ABCObject, StringCompat): + """ + A base class for an upstream remote-id. + """ + + _name: str + _site: str + + def __new__(cls, name: str, site: str): + """ + Instantiate the actual string. Requires other props prepared + beforehand. + + @param name: package's identifier on the site + @param site: type of package identification tracker + """ + + ret = StringCompat.__new__(cls, f"{site}: {name}") + ret._name = name + ret._site = site + return ret + + @property + def name(self) -> str: + """ + Package's identifier on the site. + """ + return self._name + + @property + def site(self) -> str: + """ + Type of package identification tracker. + """ + return self._site + + +class PMUpstream(ABCObject): + """ + An abstract class representing upstream metadata. + """ + + @property + def bugs_to(self) -> typing.Optional[str]: + """ + Location where to report bugs (may also be an email address prefixed + with "mailto:"). + """ + raise NotImplementedError + + @property + def changelog(self) -> typing.Optional[str]: + """ + URL where the upstream changelog can be found. + """ + raise NotImplementedError + + @property + def docs(self) -> Sequence[PMUpstreamDoc]: + """ + URLs where the location of the upstream documentation can be found. + """ + raise NotImplementedError + + @property + def maintainers(self) -> Sequence[PMUpstreamMaintainer]: + """ + Upstream maintainers. + """ + raise NotImplementedError + + @property + @abstractmethod + def remote_ids(self) -> Sequence[PMUpstreamRemoteID]: + """ + Package's identifiers on third-party trackers. + """ diff --git a/gentoopm/pkgcorepm/pkg.py b/gentoopm/pkgcorepm/pkg.py index 3c37b34..b8c8d5b 100644 --- a/gentoopm/pkgcorepm/pkg.py +++ b/gentoopm/pkgcorepm/pkg.py @@ -1,6 +1,11 @@ # (c) 2011-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later +from collections.abc import Iterable, Sequence + +from pkgcore.ebuild.repo_objs import Upstream + from ..basepm.pkg import ( PMPackage, PMPackageDescription, @@ -12,6 +17,10 @@ PMPackageMaintainer, ) from ..basepm.pkgset import PMPackageSet, PMFilteredPackageSet +from gentoopm.basepm.upstream import ( + PMUpstream, + PMUpstreamRemoteID, +) from ..util import SpaceSepTuple, SpaceSepFrozenSet from .atom import PkgCoreAtom, PkgCorePackageKey @@ -168,6 +177,20 @@ def __str__(self): return "=%s" % s +class PkgCoreUpstreamRemoteID(PMUpstreamRemoteID): + def __new__(cls, remote_id: Upstream): + return PMUpstreamRemoteID.__new__(cls, remote_id.name, remote_id.type) + + +class PkgCoreUpstream(PMUpstream): + def __init__(self, remote_ids: Iterable[Upstream]): + self._remote_ids = remote_ids + + @property + def remote_ids(self) -> Sequence[PkgCoreUpstreamRemoteID]: + return tuple(PkgCoreUpstreamRemoteID(r) for r in self._remote_ids) + + class PkgCoreInstallablePackage(PkgCorePackage, PMInstallablePackage): @property def inherits(self): @@ -228,6 +251,10 @@ def restrict(self): def maintainers(self): return PkgCoreMaintainerTuple(self._pkg.maintainers) + @property + def upstream(self) -> PkgCoreUpstream: + return PkgCoreUpstream(self._pkg.upstreams) + @property def repo_masked(self): for m in self._pkg.repo.masked: diff --git a/gentoopm/portagepm/pkg.py b/gentoopm/portagepm/pkg.py index f01ae8b..b1b475d 100644 --- a/gentoopm/portagepm/pkg.py +++ b/gentoopm/portagepm/pkg.py @@ -1,8 +1,11 @@ # (c) 2017-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later import errno -import os.path +import typing +from collections.abc import Sequence +from pathlib import Path from portage.versions import cpv_getkey from portage.xml.metadata import MetaDataXML @@ -19,6 +22,12 @@ PMPackageMaintainer, ) from ..basepm.pkgset import PMPackageSet, PMFilteredPackageSet +from ..basepm.upstream import ( + PMUpstream, + PMUpstreamDoc, + PMUpstreamMaintainer, + PMUpstreamRemoteID, +) from ..util import SpaceSepTuple, SpaceSepFrozenSet from .atom import ( @@ -292,12 +301,90 @@ def __lt__(self, other): return False +class PortageUpstreamDoc(PMUpstreamDoc): + def __new__(cls, doc: Sequence[str]): + return PMUpstreamDoc.__new__(cls, doc[0], doc[1] or "en") + + +class PortageUpstreamMaintainer(PMUpstreamMaintainer): + def __new__(cls, m): + return PMUpstreamMaintainer.__new__(cls, m.name, m.email, m.status) + + +class PortageUpstreamRemoteID(PMUpstreamRemoteID): + def __new__(cls, remote_id: Sequence[str]): + return PMUpstreamRemoteID.__new__(cls, remote_id[0], remote_id[1]) + + +class PortageUpstream(PMUpstream): + def __init__(self, meta: MetaDataXML): + self._bugs_to: typing.Optional[str] = None + self._changelog: typing.Optional[str] = None + self._docs: list[PortageUpstreamDoc] = [] + self._maintainers: list[PortageUpstreamMaintainer] = [] + self._remote_ids: list[PortageUpstreamRemoteID] = [] + + if meta is None: + return + + upstreams = meta.upstream() + if len(upstreams) == 0: + return + + upstream = upstreams[-1] + if len(upstream.bugtrackers) != 0: + self._bugs_to = upstream.bugtrackers[-1] + if len(upstream.changelogs) != 0: + self._changelog = upstream.changelogs[-1] + + for doc in upstream.docs: + self._docs.append(PortageUpstreamDoc(doc)) + + for maintainer in filter(lambda m: m.name, upstream.maintainers): + self._maintainers.append(PortageUpstreamMaintainer(maintainer)) + + for remote_id in filter(all, upstream.remoteids): + self._remote_ids.append(PortageUpstreamRemoteID(remote_id)) + + @property + def bugs_to(self) -> typing.Optional[str]: + return self._bugs_to + + @property + def changelog(self) -> typing.Optional[str]: + return self._changelog + + @property + def docs(self) -> Sequence[PortageUpstreamDoc]: + return tuple(self._docs) + + @property + def maintainers(self) -> Sequence[PortageUpstreamMaintainer]: + return tuple(self._maintainers) + + @property + def remote_ids(self) -> Sequence[PortageUpstreamRemoteID]: + return tuple(self._remote_ids) + + class PortageCPV(PortageDBCPV, PMInstallablePackage): def __init__(self, cpv, dbapi, tree, repo_prio): PortageDBCPV.__init__(self, cpv, dbapi) self._tree = tree self._repo_prio = repo_prio + @property + def _metadata(self) -> typing.Optional[MetaDataXML]: + # yes, seriously, the only API portage has is direct parser + # for the XML file + xml_path = Path(self.path).parent / "metadata.xml" + try: + return MetaDataXML(xml_path, None) + except (IOError, OSError) as e: + if e.errno == errno.ENOENT: + return None + raise e + @property def path(self): return self._dbapi.findname(self._cpv, self._tree) @@ -307,19 +394,15 @@ def repository(self): return self._dbapi.getRepositoryName(self._tree) @property - def maintainers(self): - # yes, seriously, the only API portage has is direct parser - # for the XML file - xml_path = os.path.join(os.path.dirname(self.path), "metadata.xml") - try: - meta = MetaDataXML(xml_path, None) - except (IOError, OSError) as e: - if e.errno == errno.ENOENT: - return () - raise - + def maintainers(self) -> Sequence[PortagePackageMaintainer]: + if (meta := self._metadata) is None: + return tuple() return tuple(PortagePackageMaintainer(m) for m in meta.maintainers()) + @property + def upstream(self) -> PortageUpstream: + return PortageUpstream(self._metadata) + @property def repo_masked(self): raise NotImplementedError(".repo_masked is not implemented for Portage") diff --git a/gentoopm/tests/test_pkg.py b/gentoopm/tests/test_pkg.py index c79ca2a..1f2f116 100644 --- a/gentoopm/tests/test_pkg.py +++ b/gentoopm/tests/test_pkg.py @@ -1,4 +1,5 @@ # (c) 2011-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later import pytest @@ -125,6 +126,46 @@ def test_no_maintainers(subslotted_pkg): assert list(subslotted_pkg.maintainers) == [] +def test_upstream(stack_pkg): + assert (upstream := stack_pkg.upstream) is not None + try: + assert upstream.bugs_to == "https://bugs.example.com/enter_bug.cgi" + assert upstream.changelog == "https://example.com/changelog.txt" + except NotImplementedError: + pytest.skip("upstream.bugs_to not implemented") + + +def test_upstream_docs(stack_pkg): + assert (upstream := stack_pkg.upstream) is not None + try: + assert [(d.url, d.lang) for d in upstream.docs] == [ + ("https://docs.example.com/en/", "en"), + ("https://docs.example.com/pl/", "pl"), + ] + except NotImplementedError: + pytest.skip("upstream.docs not implemented") + + +def test_upstream_maintainers(stack_pkg): + assert (upstream := stack_pkg.upstream) is not None + try: + assert [(d.name, d.email, d.status) for d in upstream.maintainers] == [ + ("Alice", None, "active"), + ("Bob", "bob@example.com", "inactive"), + ("Carol", "carol@example.com", None), + ] + except NotImplementedError: + pytest.skip("upstream.maintainers not implemented") + + +def test_upstream_remote_id(stack_pkg): + assert (upstream := stack_pkg.upstream) is not None + assert [(r.site, r.name) for r in upstream.remote_ids] == [ + ("github", "projg2/gentoopm"), + ("pypi", "gentoopm") + ] + + def test_repo_masked(pm): pkg = pm.stack.select(PackageNames.pmasked) try: diff --git a/test-root/usr/portage/a/single/metadata.xml b/test-root/usr/portage/a/single/metadata.xml index dca3c39..a10d4b9 100644 --- a/test-root/usr/portage/a/single/metadata.xml +++ b/test-root/usr/portage/a/single/metadata.xml @@ -9,4 +9,23 @@ test2@example.com Michał Górny + + https://bugs.example.com/enter_bug.cgi + https://example.com/changelog.txt + https://docs.example.com/en/ + https://docs.example.com/pl/ + + Alice + + + Bob + bob@example.com + + + Carol + carol@example.com + + projg2/gentoopm + gentoopm +