diff --git a/README.md b/README.md index fdfbb3bc..c9a1b047 100644 --- a/README.md +++ b/README.md @@ -310,7 +310,15 @@ Below stand further descriptions for each available (default) option : // Set it to `false` to allow compatibility with non-Unicode locales. "use_unicode": true }, - { "type": "Packages" }, + { + "type": "Packages", + // Set to `true` to sum up all installed package counts. + "combine_total": false, + // Set to `false` not to join all packages tool counts on the same line. + "one_line": true, + // Set to `true` to include tools with no installed package. + "show_zeros": false + }, { "type": "Temperature", // The character to display between the temperature value and the unit (as '°' in 53.2°C). diff --git a/apparmor.profile b/apparmor.profile index 4ade673f..a7f5e210 100644 --- a/apparmor.profile +++ b/apparmor.profile @@ -73,15 +73,18 @@ profile archey4 /usr/{,local/}bin/archey{,4} { # [Packages] entry /{,usr/}bin/ls rix, /{,usr/}bin/apk PUx, + #/{,usr/}bin/apt PUx, /{,usr/}bin/dnf PUx, /{,usr/}bin/dpkg PUx, /{,usr/}bin/emerge PUx, + /usr/{,local/}bin/flatpak PUx, /{,usr/}bin/nix-env PUx, /{,usr/}bin/pacman PUx, /{,usr/}bin/pacstall PUx, /{,usr/}bin/pkgin PUx, /{,usr/}bin/port PUx, /{,usr/}bin/rpm PUx, + /usr/{,local/}bin/snap PUx, /{,usr/}bin/yum PUx, /{,usr/}bin/zypper PUx, diff --git a/archey/entries/packages.py b/archey/entries/packages.py index 8633ddbf..b3f09b27 100644 --- a/archey/entries/packages.py +++ b/archey/entries/packages.py @@ -26,7 +26,8 @@ def get_homebrew_cellar_path() -> str: {"cmd": ("dnf", "list", "installed"), "skew": 1}, {"cmd": ("dpkg", "--get-selections")}, {"cmd": ("emerge", "-ep", "world"), "skew": 5}, - {"cmd": ("ls", "-1", get_homebrew_cellar_path())}, # Homebrew. + {"cmd": ("flatpak", "list"), "skew": 1}, + {"cmd": ("ls", "-1", get_homebrew_cellar_path()), "name": "homebrew"}, {"cmd": ("nix-env", "-q")}, {"cmd": ("pacman", "-Q")}, {"cmd": ("pacstall", "-L")}, @@ -39,7 +40,8 @@ def get_homebrew_cellar_path() -> str: {"cmd": ("pkgin", "list")}, {"cmd": ("port", "installed"), "skew": 1}, {"cmd": ("rpm", "-qa")}, - {"cmd": ("ls", "-1", "/var/log/packages/")}, # SlackWare. + {"cmd": ("ls", "-1", "/var/log/packages/"), "name": "slackware"}, + {"cmd": ("snap", "list", "--all"), "skew": 1}, {"cmd": ("yum", "list", "installed"), "skew": 2}, {"cmd": ("zypper", "search", "-i"), "skew": 5}, ) @@ -53,6 +55,8 @@ class Packages(Entry): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.value = {} + for packages_tool in PACKAGES_TOOLS: packages_tool = typing.cast(dict, packages_tool) if ( @@ -77,17 +81,40 @@ def __init__(self, *args, **kwargs): continue # Here we *may* use `\n` as `universal_newlines` has been set. - if self.value: - self.value += results.count("\n") - else: - self.value = results.count("\n") + count = results.count("\n") # If any, deduct output skew present due to the packages tool itself. if "skew" in packages_tool: - self.value -= packages_tool["skew"] + count -= packages_tool["skew"] - # For DPKG only, remove any not purged package. - if packages_tool["cmd"][0] == "dpkg": - self.value -= results.count("deinstall") + pkg_tool_name = packages_tool.get("name", packages_tool["cmd"][0]) - # Let's just loop over, in case there are multiple package managers. + # For DPKG only, remove any not purged package. + if pkg_tool_name == "dpkg": + count -= results.count("deinstall") + + self.value[pkg_tool_name] = count + + def output(self, output) -> None: + """Adds the entry to `output` after pretty-formatting packages tool counts""" + if not self.value: + # Fall back on the default behavior if no temperatures were detected. + super().output(output) + return + + if self.options.get("combine_total"): + output.append(self.name, str(sum(self.value.values()))) + return + + entries = [] + for pkg_tool_name, count in self.value.items(): + if count > 0 or self.options.get("show_zeros"): + entries.append(f"({pkg_tool_name}) {count}") + + if self.options.get("one_line", True): + # One-line output is enabled : Join the results ! + output.append(self.name, ", ".join(entries)) + else: + # One-line output has been disabled, add one entry per item. + for entry in entries: + output.append(self.name, entry) diff --git a/archey/test/entries/test_archey_packages.py b/archey/test/entries/test_archey_packages.py index 09d2d504..04676404 100644 --- a/archey/test/entries/test_archey_packages.py +++ b/archey/test/entries/test_archey_packages.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import DEFAULT as DEFAULT_SENTINEL -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from archey.configuration import DEFAULT_CONFIG from archey.distributions import Distributions @@ -41,7 +41,7 @@ def test_match_with_apk(self, check_output_mock): """Simple test for the APK packages manager""" check_output_mock.side_effect = self._check_output_side_effect("apk") - self.assertEqual(Packages().value, 8) + self.assertDictEqual(Packages().value, {"apk": 8}) @patch( "archey.entries.packages.check_output", @@ -57,7 +57,7 @@ def test_match_with_dnf(self, check_output_mock): """Simple test for the DNF packages manager""" check_output_mock.side_effect = self._check_output_side_effect("dnf") - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"dnf": 4}) @patch( "archey.entries.packages.check_output", @@ -75,7 +75,7 @@ def test_match_with_dpkg(self, check_output_mock): """Simple test for the DPKG packages manager""" check_output_mock.side_effect = self._check_output_side_effect("dpkg") - self.assertEqual(Packages().value, 6) + self.assertDictEqual(Packages().value, {"dpkg": 6}) @patch( "archey.entries.packages.check_output", @@ -97,7 +97,23 @@ def test_match_with_emerge(self, check_output_mock): """Simple test for the Emerge packages manager""" check_output_mock.side_effect = self._check_output_side_effect("emerge") - self.assertEqual(Packages().value, 5) + self.assertDictEqual(Packages().value, {"emerge": 5}) + + @patch( + "archey.entries.packages.check_output", + return_value="""\ +Name Application ID Version Branch Origin Installation +Discord com.discordapp.Discord 0.0.35 stable flathub system +Xournal++ com.github.xournalpp.xournalpp 1.2.2 stable flathub system +draw.io com.jgraph.drawio.desktop 22.0.2 stable flathub system +Extension Manager com.mattjakeman.ExtensionManager 0.4.2 stable flathub system +""", + ) + def test_match_with_flatpak(self, check_output_mock): + """Simple test for the Flatpak packages manager""" + check_output_mock.side_effect = self._check_output_side_effect("flatpak") + + self.assertDictEqual(Packages().value, {"flatpak": 4}) @patch( "archey.entries.packages.check_output", @@ -112,7 +128,7 @@ def test_match_with_nix_env(self, check_output_mock): """Simple test for the Emerge packages manager""" check_output_mock.side_effect = self._check_output_side_effect("nix-env") - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"nix-env": 4}) @patch( "archey.entries.packages.check_output", @@ -127,7 +143,7 @@ def test_match_with_pacman(self, check_output_mock): """Simple test for the Pacman packages manager""" check_output_mock.side_effect = self._check_output_side_effect("pacman") - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"pacman": 4}) @patch( "archey.entries.packages.check_output", @@ -147,7 +163,7 @@ def test_match_with_pkg_info(self, check_output_mock): """Simple test for the OpenBSD `pkg_*` package manager""" check_output_mock.side_effect = self._check_output_side_effect("pkg_info") - self.assertEqual(Packages().value, 9) + self.assertDictEqual(Packages().value, {"pkg_info": 9}) @patch("archey.entries.packages.Distributions.get_local", return_value=Distributions.FREEBSD) @patch( @@ -167,7 +183,7 @@ def test_match_with_pkg(self, check_output_mock, _): """Simple test for the FreeBSD `pkg` package manager""" check_output_mock.side_effect = self._check_output_side_effect("pkg") - self.assertEqual(Packages().value, 8) + self.assertDictEqual(Packages().value, {"pkg": 8}) @patch( "archey.entries.packages.check_output", @@ -191,7 +207,7 @@ def test_match_with_pkgin(self, check_output_mock): """Simple test for the (NetBSD) `pkgin` package manager""" check_output_mock.side_effect = self._check_output_side_effect("pkgin") - self.assertEqual(Packages().value, 13) + self.assertDictEqual(Packages().value, {"pkgin": 13}) @patch( "archey.entries.packages.check_output", @@ -217,7 +233,7 @@ def test_match_with_macports(self, check_output_mock): """Simple test for the MacPorts CLI client (`port`) package manager""" check_output_mock.side_effect = self._check_output_side_effect("port") - self.assertEqual(Packages().value, 14) + self.assertDictEqual(Packages().value, {"port": 14}) @patch( "archey.entries.packages.check_output", @@ -232,7 +248,7 @@ def test_match_with_rpm(self, check_output_mock): """Simple test for the RPM packages manager""" check_output_mock.side_effect = self._check_output_side_effect("rpm") - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"rpm": 4}) @patch( "archey.entries.packages.check_output", @@ -249,7 +265,7 @@ def test_match_with_yum(self, check_output_mock): """Simple test for the Yum packages manager""" check_output_mock.side_effect = self._check_output_side_effect("yum") - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"yum": 4}) @patch( "archey.entries.packages.check_output", @@ -270,13 +286,13 @@ def test_match_with_zypper(self, check_output_mock): """Simple test for the Zypper packages manager""" check_output_mock.side_effect = self._check_output_side_effect("zypper") - self.assertEqual(Packages().value, 5) + self.assertDictEqual(Packages().value, {"zypper": 5}) @patch( "archey.entries.packages.PACKAGES_TOOLS", new=( - {"cmd": ("pkg_tool_1")}, - {"cmd": ("pkg_tool_2"), "skew": 2}, + {"cmd": ("pkg_tool_1",), "name": "acae_loot_42"}, + {"cmd": ("pkg_tool_2",), "skew": 2}, ), ) @patch( @@ -296,23 +312,68 @@ def test_match_with_zypper(self, check_output_mock): ) def test_multiple_package_managers(self, _): """Simple test for multiple packages managers""" - self.assertEqual(Packages().value, 4) + self.assertDictEqual(Packages().value, {"acae_loot_42": 2, "pkg_tool_2": 2}) - @patch("archey.entries.packages.check_output") @HelperMethods.patch_clean_configuration - def test_no_packages_manager(self, check_output_mock): - """No packages manager is available at the moment...""" - check_output_mock.side_effect = self._check_output_side_effect() + def test_various_output_configuration(self): + """Test `output` overloading based on user preferences combination""" + packages_instance_mock = HelperMethods.entry_mock(Packages) + output_mock = MagicMock() - packages = Packages() + packages_instance_mock.value = {"pkg_tool_0": 0, "pkg_tool_18": 18, "pkg_tool_42": 42} - output_mock = MagicMock() - packages.output(output_mock) + with self.subTest("Single-line fully-combined output."): + packages_instance_mock.options["combine_total"] = True + + Packages.output(packages_instance_mock, output_mock) + output_mock.append.assert_called_once_with("Packages", "60") + + output_mock.reset_mock() + + with self.subTest("Single-line combined output (without zero counts)."): + packages_instance_mock.options["combine_total"] = False + packages_instance_mock.options["one_line"] = True + packages_instance_mock.options["show_zeros"] = False + + Packages.output(packages_instance_mock, output_mock) + output_mock.append.assert_called_once_with( + "Packages", "(pkg_tool_18) 18, (pkg_tool_42) 42" + ) + + output_mock.reset_mock() + + with self.subTest("Single-line combined output (with zero counts)."): + packages_instance_mock.options["show_zeros"] = True + + Packages.output(packages_instance_mock, output_mock) + output_mock.append.assert_called_once_with( + "Packages", "(pkg_tool_0) 0, (pkg_tool_18) 18, (pkg_tool_42) 42" + ) + + output_mock.reset_mock() + + with self.subTest("Multi-lines output (with zero counts)."): + packages_instance_mock.options["one_line"] = False + + Packages.output(packages_instance_mock, output_mock) + self.assertEqual(output_mock.append.call_count, 3) + output_mock.append.assert_has_calls( + [ + call("Packages", "(pkg_tool_0) 0"), + call("Packages", "(pkg_tool_18) 18"), + call("Packages", "(pkg_tool_42) 42"), + ] + ) + + output_mock.reset_mock() + + with self.subTest("No available packages tool."): + packages_instance_mock.value = {} - self.assertIsNone(packages.value) - self.assertEqual( - output_mock.append.call_args[0][1], DEFAULT_CONFIG["default_strings"]["not_detected"] - ) + Packages.output(packages_instance_mock, output_mock) + output_mock.append.assert_called_once_with( + "Packages", DEFAULT_CONFIG["default_strings"]["not_detected"] + ) @staticmethod def _check_output_side_effect(pkg_manager_cmd=None): diff --git a/config.json b/config.json index d858dbf6..08d64481 100644 --- a/config.json +++ b/config.json @@ -30,7 +30,12 @@ "type": "Terminal", "use_unicode": true }, - { "type": "Packages" }, + { + "type": "Packages", + "combine_total": false, + "one_line": true, + "show_zeros": false + }, { "type": "Temperature", "char_before_unit": " ",