Skip to content

Commit

Permalink
pkg: Support upstream metadata
Browse files Browse the repository at this point in the history
Fully supported only for Portage, pkgcore can return only remote-ids.

"X | None" type hints and walrus operator require Python 3.10.
  • Loading branch information
CyberTailor committed Jan 17, 2024
1 parent 597b022 commit 97f1ba9
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 11 deletions.
10 changes: 10 additions & 0 deletions gentoopm/basepm/pkg.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# (c) 2011-2024 Michał Górny <[email protected]>
# (c) 2024 Anna <[email protected]>
# SPDX-License-Identifier: GPL-2.0-or-later

import os.path
import typing
from abc import abstractmethod, abstractproperty

from ..util import (
Expand All @@ -14,6 +16,7 @@

from .atom import PMAtom, PMPackageKey
from .environ import PMPackageEnvironment
from gentoopm.basepm.upstream import PMUpstream

PMPackageState = EnumTuple("PMPackageState", "installable", "installed")

Expand Down Expand Up @@ -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):
"""
Expand Down
59 changes: 59 additions & 0 deletions gentoopm/basepm/upstream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# (c) 2024 Anna <[email protected]>
# 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
15 changes: 15 additions & 0 deletions gentoopm/pkgcorepm/pkg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# (c) 2011-2024 Michał Górny <[email protected]>
# (c) 2024 Anna <[email protected]>
# SPDX-License-Identifier: GPL-2.0-or-later

import typing

from ..basepm.pkg import (
PMPackage,
PMPackageDescription,
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
62 changes: 51 additions & 11 deletions gentoopm/portagepm/pkg.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# (c) 2017-2024 Michał Górny <[email protected]>
# (c) 2024 Anna <[email protected]>
# 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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions gentoopm/tests/test_pkg.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# (c) 2011-2024 Michał Górny <[email protected]>
# (c) 2024 Anna <[email protected]>
# SPDX-License-Identifier: GPL-2.0-or-later

import pytest
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions test-root/usr/portage/a/single/metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
<email>[email protected]</email>
<name>Michał Górny</name>
</maintainer>
<upstream>
<remote-id type="github">projg2/gentoopm</remote-id>
<remote-id type="pypi">gentoopm</remote-id>
</upstream>
</pkgmetadata>

0 comments on commit 97f1ba9

Please sign in to comment.