diff --git a/gentoopm/basepm/pkg.py b/gentoopm/basepm/pkg.py index 5cb70ad..927b285 100644 --- a/gentoopm/basepm/pkg.py +++ b/gentoopm/basepm/pkg.py @@ -1,7 +1,9 @@ # (c) 2011-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later import os.path +import typing from abc import abstractmethod, abstractproperty from ..util import ( @@ -14,6 +16,7 @@ from .atom import PMAtom, PMPackageKey from .environ import PMPackageEnvironment +from gentoopm.basepm.upstream import PMUpstream PMPackageState = EnumTuple("PMPackageState", "installable", "installed") @@ -417,6 +420,13 @@ def maintainers(self): """ pass + @property + @abstractmethod + def upstream(self) -> typing.Optional[PMUpstream]: + """ + Get the package upstream metadata (or ``None`` if unavailable). + """ + @abstractproperty def repo_masked(self): """ diff --git a/gentoopm/basepm/upstream.py b/gentoopm/basepm/upstream.py new file mode 100644 index 0000000..c9af204 --- /dev/null +++ b/gentoopm/basepm/upstream.py @@ -0,0 +1,59 @@ +# (c) 2024 Anna +# SPDX-License-Identifier: GPL-2.0-or-later + +import typing +from collections.abc import Iterable +from dataclasses import dataclass +from enum import Enum, auto + + +class PMUpstreamMaintainerStatus(Enum): + """ + Maintainer status enumeration. + """ + + NONE = auto() + ACTIVE = auto() + INACTIVE = auto() + + +@dataclass(frozen=True, order=True) +class PMUpstreamMaintainer: + """ + Representation of an upstream maintainer. + """ + + name: str + email: typing.Optional[str] = None + status: PMUpstreamMaintainerStatus = PMUpstreamMaintainerStatus.NONE + + def __str__(self) -> str: + if self.name and self.email: + return f"{self.name} <{self.email}>" + return self.name + + +@dataclass(frozen=True) +class PMUpstreamRemoteID: + """ + Representation of a remote-id. + """ + + name: str + site: str + + def __str__(self) -> str: + return f"{self.name}: {self.site}" + + +@dataclass +class PMUpstream: + """ + Representation of upstream metadata. + """ + + bugs_to: typing.Optional[str] = None + changelog: typing.Optional[str] = None + doc: typing.Optional[str] = None + maintainers: typing.Optional[Iterable[PMUpstreamMaintainer]] = None + remote_ids: typing.Optional[Iterable[PMUpstreamRemoteID]] = None diff --git a/gentoopm/pkgcorepm/pkg.py b/gentoopm/pkgcorepm/pkg.py index 3c37b34..95f45fe 100644 --- a/gentoopm/pkgcorepm/pkg.py +++ b/gentoopm/pkgcorepm/pkg.py @@ -1,6 +1,9 @@ # (c) 2011-2024 Michał Górny +# (c) 2024 Anna # SPDX-License-Identifier: GPL-2.0-or-later +import typing + from ..basepm.pkg import ( PMPackage, PMPackageDescription, @@ -12,6 +15,11 @@ PMPackageMaintainer, ) from ..basepm.pkgset import PMPackageSet, PMFilteredPackageSet +from gentoopm.basepm.upstream import ( + PMUpstream, + PMUpstreamMaintainer, + PMUpstreamRemoteID, +) from ..util import SpaceSepTuple, SpaceSepFrozenSet from .atom import PkgCoreAtom, PkgCorePackageKey @@ -228,6 +236,13 @@ def restrict(self): def maintainers(self): return PkgCoreMaintainerTuple(self._pkg.maintainers) + @property + def upstream(self) -> typing.Optional[PMUpstream]: + result = PMUpstream() + result.remote_ids = tuple(PMUpstreamRemoteID(r.type, r.name) + for r in self._pkg.upstreams) + return result + @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..12ed6d4 100644 --- a/gentoopm/portagepm/pkg.py +++ b/gentoopm/portagepm/pkg.py @@ -1,8 +1,12 @@ # (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 Iterable +from pathlib import Path from portage.versions import cpv_getkey from portage.xml.metadata import MetaDataXML @@ -18,6 +22,11 @@ PMUseFlag, PMPackageMaintainer, ) +from gentoopm.basepm.upstream import ( + PMUpstream, + PMUpstreamMaintainer, + PMUpstreamRemoteID, +) from ..basepm.pkgset import PMPackageSet, PMFilteredPackageSet from ..util import SpaceSepTuple, SpaceSepFrozenSet @@ -298,6 +307,18 @@ def __init__(self, cpv, dbapi, tree, repo_prio): 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 +328,38 @@ 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) -> typing.Optional[Iterable[PortagePackageMaintainer]]: + if (meta := self._metadata) is None: + return None return tuple(PortagePackageMaintainer(m) for m in meta.maintainers()) + @property + def upstream(self) -> typing.Optional[PMUpstream]: + if (meta := self._metadata) is None: + return None + + upstreams = meta.upstream() + if len(upstreams) == 0: + return None + upstream = upstreams[0] + + result = PMUpstream() + if len(upstream.bugtrackers) != 0: + result.bugs_to = upstream.bugtrackers[0] + if len(upstream.changelogs) != 0: + result.changelog = upstream.changelogs[0] + if len(upstream.docs) != 0: + result.doc = upstream.docs[0] + + result.maintainers = tuple(PMUpstreamMaintainer(m.name, m.email) + for m in upstream.maintainers + if m.name) or None + + result.remote_ids = tuple(PMUpstreamRemoteID(attr, value) + for value, attr in upstream.remoteids + if attr and value) or None + return result + @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..9364b06 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,14 @@ def test_no_maintainers(subslotted_pkg): assert list(subslotted_pkg.maintainers) == [] +def test_upstream_remote_id(stack_pkg): + assert (upstream := stack_pkg.upstream) is not None + assert [(r.name, r.site) 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..714223c 100644 --- a/test-root/usr/portage/a/single/metadata.xml +++ b/test-root/usr/portage/a/single/metadata.xml @@ -9,4 +9,8 @@ test2@example.com Michał Górny + + projg2/gentoopm + gentoopm +