# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
import re
from extra_platforms import ALL_PLATFORMS
from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager
from ..version import parse_version
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from ..package import Package
[docs]
class Zef(PackageManager):
"""Zef, the module manager of the Raku language.
A package is a Raku distribution, identified by the bare name its identity
string opens with. Raku names are colon-separated (`JSON::Fast`) and the
identity appends its own colon-prefixed fields to them
(`JSON::Fast:ver<0.20>:auth<zef:timo>`), so every pattern here matches the
name lazily up to the literal `:ver<` rather than splitting on colons.
```{note}
Two distributions may share a name and differ only by the `auth` field, and
several versions of one may be installed side by side. mpm keys a package on
its id alone, so both listings are reduced here to one entry per name,
keeping the highest version. That reduction is what makes this a class
rather than a bundled definition.
```
```{note}
Search compounds the same problem rather than avoiding it: it answers with a
pipe-delimited table carrying one row per *(distribution, version)* pair, so
a single module comes back once per release it has ever published.
```
```{note}
No `outdated`: zef reports no staleness of its own. `upgrade` covers both the
bulk and the single-package cases natively, so neither is synthesized.
```
Documentation: [zef README](https://github.com/ugexe/zef#readme).
"""
name = "Zef"
homepage_url = "https://github.com/ugexe/zef"
platforms = ALL_PLATFORMS
version_regexes = (r"^(?P<version>\d+\.\d+\.\d+)",)
r"""Search the bare version zef prints on its own.
```{code-block} shell-session
$ zef --version
1.1.3
```
"""
_INSTALLED_REGEXP = re.compile(
r"^(?P<package_id>.+?):ver<(?P<installed_version>[^>]+)>",
)
"""One installed distribution, as its identity string. The `===> Found via`
banner opening the listing carries no `:ver<` and so cannot match.
"""
_SEARCH_REGEXP = re.compile(
r"^\d+[ \t]*\|[^|]*\|[ \t]*(?P<package_id>.+?):ver<(?P<latest_version>[^>]+)>"
r"[^|]*\|[ \t]*(?P<description>.*?)[ \t]*$",
)
"""One search hit: the leading row number, the repository it came from, the
identity, then the description. Anchoring on the row number skips the two
rule lines and the column header, none of which open with a digit.
"""
def _newest_per_name(
self,
rows: Iterable[tuple[str, str, str | None]],
installed: bool,
) -> Iterator[Package]:
"""Reduce rows to one package per name, keeping the highest version."""
best: dict[str, tuple[str, str | None]] = {}
for package_id, version, description in rows:
held = best.get(package_id)
if held is None or parse_version(version) > parse_version(held[0]):
best[package_id] = (version, description)
for package_id, (version, description) in best.items():
yield self.package(
id=package_id,
description=description,
**{"installed_version" if installed else "latest_version": version},
)
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
```{code-block} shell-session
$ zef list --installed
===> Found via inst#/opt/homebrew/Cellar/rakudo-star/2026.07/share/perl6/site
App::Prove6:ver<0.0.18>:auth<zef:leont>
Config::TOML:ver<0.1.3>:auth<zef:raku-community-modules>
Config:ver<3.0.4>:auth<cpan:TYIL>:api<3>
Crane:ver<0.1.2>:auth<zef:raku-community-modules>
Digest:ver<1.1.0>:auth<zef:grondilu>
```
"""
output = self.run_cli("list", "--installed")
rows = (
(match.group("package_id"), match.group("installed_version"), None)
for match in map(self._INSTALLED_REGEXP.match, output.splitlines())
if match
)
yield from self._newest_per_name(rows, installed=True)
[docs]
@search_capabilities(extended_support=False, exact_support=False)
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
```{caution}
Search does not support extended or exact matching.
```
```{code-block} shell-session
$ zef search JSON::Fast
===> Found 50 results
-------------------------------------------------------------------------------
ID|From |Package |Description
-------------------------------------------------------------------------------
0 |Zef::Repository::Ecosystems<fez>|JSON::Fast:ver<0.20>:auth<zef:timo> |A naive, fast json parser and serializer
1 |Zef::Repository::Ecosystems<fez>|JSON::Fast:ver<0.20.1>:auth<zef:timo>|A naive, fast json parser and serializer
```
"""
output = self.run_cli("search", query)
rows = (
(
match.group("package_id"),
match.group("latest_version"),
match.group("description") or None,
)
for match in map(self._SEARCH_REGEXP.match, output.splitlines())
if match
)
yield from self._newest_per_name(rows, installed=False)
[docs]
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
```{code-block} shell-session
$ zef install JSON::Fast::Hyper
===> Testing: hyperize:ver<0.0.4>:auth<zef:lizmat>
===> Testing [OK] for hyperize:ver<0.0.4>:auth<zef:lizmat>
===> Testing: JSON::Fast::Hyper:ver<0.0.11>:auth<zef:lizmat>
===> Testing [OK] for JSON::Fast::Hyper:ver<0.0.11>:auth<zef:lizmat>
===> Installing: hyperize:ver<0.0.4>:auth<zef:lizmat>
===> Installing: JSON::Fast::Hyper:ver<0.0.11>:auth<zef:lizmat>
```
"""
return self.run_cli("install", package_id)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generate the CLI to upgrade all packages.
```{code-block} shell-session
$ zef upgrade
```
"""
return self.build_cli("upgrade")
[docs]
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generate the CLI to upgrade one package.
```{code-block} shell-session
$ zef upgrade JSON::Fast
```
"""
return self.build_cli("upgrade", package_id)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
```{code-block} shell-session
$ zef uninstall JSON::Fast::Hyper
===> Uninstalled from inst#/opt/homebrew/Cellar/rakudo-star/2026.07/share/perl6/site
JSON::Fast::Hyper:ver<0.0.11>:auth<zef:lizmat>
```
"""
return self.run_cli("uninstall", package_id)
[docs]
def sync(self) -> None:
"""Sync package metadata.
```{code-block} shell-session
$ zef update
```
"""
self.run_cli("update")