Source code for meta_package_manager.managers.mas

# 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 json

from extra_platforms import MACOS

from ..base import PackageManager
from ..capabilities import search_capabilities, version_not_implemented

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator

    from ..base import Package


[docs] class MAS(PackageManager): name = "Mac AppStore" homepage_url = "https://github.com/mas-cli/mas" platforms = MACOS requirement = ">=7.0.0" """`7.0.0 <https://github.com/mas-cli/mas/releases/tag/v7.0.0>`_ introduces the ``--json`` flag on ``config``, ``list``, ``lookup``/``info``, ``outdated`` & ``search``. Parsing structured JSON output is the supported programmatic interface: it sidesteps the column-alignment ambiguities of the tabular output (app names containing parentheses or extra whitespace would break the previous regex-based parser). """ version_cli_options = ("version",) """ .. code-block:: shell-session $ mas version 7.0.0 """ @staticmethod def _parse_json_stream(output: str) -> Iterator[dict]: """Parse mas ``--json`` output as a stream of concatenated JSON objects, one per app. .. note:: ``mas`` emits one JSON object per record but does not escape control characters (``U+0000``-``U+001F``, ``U+2028``) inside string fields like app names and descriptions, so splitting by lines breaks any record whose fields contain a real ``\\n`` or ``U+2028``. Walking the buffer with :py:meth:`json.JSONDecoder.raw_decode` instead lets each object terminate at its own closing brace, and ``strict=False`` permits the embedded control characters that the upstream output actually contains. Upstream bug: https://github.com/mas-cli/mas/issues/1248 """ decoder = json.JSONDecoder(strict=False) idx = 0 end = len(output) while idx < end: while idx < end and output[idx].isspace(): idx += 1 if idx >= end: break obj, idx = decoder.raw_decode(output, idx) yield obj @property def installed(self) -> Iterator[Package]: """Fetch installed packages. .. code-block:: shell-session $ mas list --json {"adamID":1569813296,"bundleID":"com.1password.1password-safari","name":"1Password for Safari","version":"2.3.5",...} {"adamID":1295203466,"bundleID":"com.microsoft.rdc.macos","name":"Microsoft Remote Desktop","version":"10.7.6",...} {"adamID":409183694,"bundleID":"com.apple.iWork.Keynote","name":"Keynote","version":"12.0",...} """ output = self.run_cli("list", "--json") for app in self._parse_json_stream(output): yield self.package( id=str(app["adamID"]), name=app["name"], installed_version=app["version"], ) @property def outdated(self) -> Iterator[Package]: """Fetch outdated packages. .. code-block:: shell-session $ mas outdated --json {"adamID":409183694,"name":"Keynote","newVersion":"12.0","version":"11.0",...} {"adamID":1176895641,"name":"Spark","newVersion":"2.11.21","version":"2.11.20",...} """ output = self.run_cli("outdated", "--json") for app in self._parse_json_stream(output): yield self.package( id=str(app["adamID"]), name=app["name"], installed_version=app["version"], latest_version=app["newVersion"], )
[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. So we returns the best subset of results and let :py:meth:`meta_package_manager.base.PackageManager.refiltered_search` refine them. .. code-block:: shell-session $ mas search python --json {"adamID":689176796,"name":"Python Runner","version":"1.3",...} {"adamID":630736088,"name":"Learning Python","version":"1.0",...} {"adamID":945397020,"name":"Run Python","version":"1.0",...} {"adamID":1164498373,"name":"PythonGames","version":"1.0",...} {"adamID":1400050251,"name":"Pythonic","version":"1.0.0",...} """ output = self.run_cli("search", query, "--json") for app in self._parse_json_stream(output): yield self.package( id=str(app["adamID"]), name=app["name"], latest_version=app["version"], )
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. .. code-block:: shell-session $ mas install 945397020 """ return self.run_cli("install", package_id)
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages (default) or only the one provided as parameter. .. code-block:: shell-session $ mas upgrade """ return self.build_cli("upgrade")
[docs] @version_not_implemented def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade all packages (default) or only the one provided as parameter. .. code-block:: shell-session $ mas upgrade 945397020 """ return self.build_cli("upgrade", package_id)
[docs] def remove(self, package_id: str) -> str: """Removes a package. ``mas`` 4.1.0+ requests root privileges itself when not already running as root, so we don't pre-wrap the call in ``sudo``. This matches how ``install`` and ``upgrade`` are already invoked. .. code-block:: shell-session $ mas uninstall 1494051017 Password: Uninstalled '/Applications/SimpleLogin.app' to '/Users/kde/.Trash/SimpleLogin.app' """ return self.run_cli("uninstall", package_id)