From b6bcabd11e1250f5796dcce33ddaf9a9426567b5 Mon Sep 17 00:00:00 2001 From: Ronie Martinez Date: Wed, 13 Apr 2022 21:26:33 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Linux=20using?= =?UTF-8?q?=20*.desktop=20entries=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add support for Linux using *.desktop entries * Remove print * Prevent duplicate keys * Fix: Split commands using shlex * Update README.md --- .github/workflows/python.yml | 8 ++--- README.md | 4 +-- browsers/__init__.py | 54 +++++++++++++++++++++++++--- poetry.lock | 14 +++++++- pyproject.toml | 4 ++- tests/test_detect.py | 68 ++++++++++++++++++++++++++++++------ 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a772091..0c5c6f6 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -22,12 +22,12 @@ jobs: strategy: matrix: # os: [ ubuntu-latest, macos-latest, windows-latest ] - os: [ macos-latest ] + os: [ ubuntu-latest, macos-latest ] python-version: [ '3.7', '3.8', '3.9', '3.10' ] include: -# - os: ubuntu-latest -# pip-cache: ~/.cache/pip -# poetry-cache: ~/.cache/pypoetry + - os: ubuntu-latest + pip-cache: ~/.cache/pip + poetry-cache: ~/.cache/pypoetry - os: macos-latest pip-cache: ~/Library/Caches/pip poetry-cache: ~/Library/Caches/pypoetry diff --git a/README.md b/README.md index 958ffd4..f0fe537 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ pip install pybrowsers ## TODO: - [x] Detect browser on OSX -- [ ] Detect browser on Linux/Unix +- [x] Detect browser on Linux - [ ] Detect browser on Windows -- [X] Launch browser with arguments +- [x] Launch browser with arguments - [ ] Get browser by version (support wildcards) ## Author diff --git a/browsers/__init__.py b/browsers/__init__.py index 3a3fc24..f87fc6a 100644 --- a/browsers/__init__.py +++ b/browsers/__init__.py @@ -1,6 +1,7 @@ import logging import os import plistlib +import shlex import subprocess import sys from typing import Dict, Iterator, Optional, Sequence, Tuple @@ -8,7 +9,7 @@ logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) -BROWSER_LIST = ( +OSX_BROWSER_BUNDLE_LIST = ( # browser name, bundle ID, version string ("chrome", "com.google.Chrome", "KSVersion"), ("chrome-canary", "com.google.Chrome.canary", "KSVersion"), @@ -30,9 +31,48 @@ ("brave-nightly", "com.brave.Browser.nightly", "CFBundleVersion"), ) +LINUX_DESKTOP_ENTRY_LIST = ( + ("chrome", ("google-chrome",)), + ("chromium", ("chromium",)), + ("firefox", ("firefox", "firefox_firefox")), + ("msedge", ("microsoft-edge",)), + ("opera", ("opera_opera",)), + ("opera-beta", ("opera-beta_opera-beta",)), + ("opera-developer", ("opera-developer_opera-developer",)), + ("brave", ("brave-browser", "brave_brave")), + ("brave-beta", ("brave-browser-beta",)), + ("brave-nightly", ("brave-browser-nightly",)), +) + +# $XDG_DATA_HOME and $XDG_DATA_DIRS are not always set +XDG_DATA_LOCATIONS = ( + "~/.local/share/applications", + "/usr/share/applications", + "/var/lib/snapd/desktop/applications", +) + def get_available_browsers() -> Iterator[Tuple[str, Dict]]: platform = sys.platform + if platform == "linux": + from xdg.DesktopEntry import DesktopEntry + + for browser, desktop_entries in LINUX_DESKTOP_ENTRY_LIST: + for application_dir in XDG_DATA_LOCATIONS: + # desktop entry name can be "firefox.desktop" or "firefox_firefox.desktop" + for desktop_entry in desktop_entries: + path = os.path.join(application_dir, f"{desktop_entry}.desktop") + if not os.path.isfile(path): + continue + entry = DesktopEntry(path) + executable_path = entry.getExec() + if executable_path.lower().endswith(" %u"): + executable_path = executable_path[:-3].strip() + # FIXME: --version includes the name for most browsers + version = subprocess.getoutput(f"{executable_path} --version") + info = dict(path=executable_path, display_name=entry.getName(), version=version) + yield browser, info + return if platform != "darwin": # pragma: no cover logger.info( "'%s' is currently not supported. Please open an issue or a PR at '%s'", @@ -40,7 +80,7 @@ def get_available_browsers() -> Iterator[Tuple[str, Dict]]: "https://github.com/roniemartinez/browsers", ) return - for browser, bundle_id, version_string in BROWSER_LIST: + for browser, bundle_id, version_string in OSX_BROWSER_BUNDLE_LIST: paths = subprocess.getoutput(f'mdfind "kMDItemCFBundleIdentifier == {bundle_id}"').splitlines() for path in paths: with open(os.path.join(path, "Contents/Info.plist"), "rb") as f: @@ -51,7 +91,10 @@ def get_available_browsers() -> Iterator[Tuple[str, Dict]]: def get(browser: str) -> Optional[Dict]: - return dict(get_available_browsers()).get(browser) + for key, value in get_available_browsers(): + if key == browser: + return value + return None def launch(browser: str, url: str, args: Optional[Sequence[str]] = None) -> None: @@ -65,4 +108,7 @@ def launch(browser: str, url: str, args: Optional[Sequence[str]] = None) -> None def _launch(path: str, url: str, args: Sequence[str]) -> None: # pragma: no cover - subprocess.Popen(["open", "--wait-apps", "--new", "--fresh", "-a", path, url, "--args", *args]) + command = [*shlex.split(path), url, "--args", *args] + if sys.platform == "darwin": + command = ["open", "--wait-apps", "--new", "--fresh", "-a", *command] + subprocess.Popen(command) diff --git a/poetry.lock b/poetry.lock index 632b077..d34b583 100644 --- a/poetry.lock +++ b/poetry.lock @@ -345,6 +345,14 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "pyxdg" +version = "0.27" +description = "PyXDG contains implementations of freedesktop.org standards in python." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "rich" version = "12.2.0" @@ -400,7 +408,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "630e242ec9506838eb3bf90997ede43506f702a8d5d2e138d6103cb8a86a0a7f" +content-hash = "1c1dfc8227fb718b113f9f40ac54b4ae0ae13f34290d2796a753699a682f9829" [metadata.files] atomicwrites = [ @@ -598,6 +606,10 @@ pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] +pyxdg = [ + {file = "pyxdg-0.27-py2.py3-none-any.whl", hash = "sha256:2d6701ab7c74bbab8caa6a95e0a0a129b1643cf6c298bf7c569adec06d0709a0"}, + {file = "pyxdg-0.27.tar.gz", hash = "sha256:80bd93aae5ed82435f20462ea0208fb198d8eec262e831ee06ce9ddb6b91c5a5"}, +] rich = [ {file = "rich-12.2.0-py3-none-any.whl", hash = "sha256:c50f3d253bc6a9bb9c79d61a26d510d74abdf1b16881260fab5edfc3edfb082f"}, {file = "rich-12.2.0.tar.gz", hash = "sha256:ea74bc9dad9589d8eea3e3fd0b136d8bf6e428888955f215824c2894f0da8b47"}, diff --git a/pyproject.toml b/pyproject.toml index d5a0c7a..230576d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybrowsers" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" repository = "https://github.com/roniemartinez/browsers" description = "Python library for detecting and launching browsers" authors = ["Ronie Martinez "] @@ -28,6 +28,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.7" +pyxdg = { version = "^0.27", markers = "sys_platform == 'linux'" } [tool.poetry.dev-dependencies] autoflake = "^1.3.1" @@ -83,6 +84,7 @@ addopts = """\ --cov-report=html \ -vv \ -x \ + -s \ """ [build-system] diff --git a/tests/test_detect.py b/tests/test_detect.py index aad8392..fa35537 100644 --- a/tests/test_detect.py +++ b/tests/test_detect.py @@ -1,3 +1,4 @@ +import sys from typing import Dict from unittest import mock from unittest.mock import ANY @@ -12,12 +13,18 @@ """ -def test_get_available_browsers() -> None: +@pytest.mark.parametrize( + "browser", + ( + pytest.param("chrome", id="chrome"), + pytest.param("firefox", id="firefox"), + pytest.param("safari", id="safari", marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only")), + pytest.param("msedge", id="msedge", marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only")), + ), +) +def test_get_available_browsers(browser: str) -> None: available_browsers = dict(get_available_browsers()) - assert "chrome" in available_browsers - assert "firefox" in available_browsers - assert "safari" in available_browsers - assert "msedge" in available_browsers + assert browser in available_browsers @pytest.mark.parametrize( @@ -30,7 +37,8 @@ def test_get_available_browsers() -> None: "path": "/Applications/Google Chrome.app", "version": ANY, }, - id="chrome", + marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only"), + id="chrome-osx", ), pytest.param( "firefox", @@ -39,7 +47,8 @@ def test_get_available_browsers() -> None: "path": "/Applications/Firefox.app", "version": ANY, }, - id="firefox", + marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only"), + id="firefox-osx", ), pytest.param( "safari", @@ -48,7 +57,8 @@ def test_get_available_browsers() -> None: "path": "/Applications/Safari.app", "version": ANY, }, - id="safari", + id="safari-osx", + marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only"), ), pytest.param( "msedge", @@ -57,7 +67,28 @@ def test_get_available_browsers() -> None: "path": "/Applications/Microsoft Edge.app", "version": ANY, }, - id="msedge", + id="msedge-osx", + marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only"), + ), + pytest.param( + "chrome", + { + "display_name": "Google Chrome", + "path": "/usr/bin/google-chrome-stable", + "version": ANY, + }, + marks=pytest.mark.skipif(sys.platform != "linux", reason="linux-only"), + id="chrome-linux", + ), + pytest.param( + "firefox", + { + "display_name": "Firefox Web Browser", + "path": "firefox", + "version": ANY, + }, + marks=pytest.mark.skipif(sys.platform != "linux", reason="linux-only"), + id="firefox-linux", ), ), ) @@ -65,10 +96,25 @@ def test_get(browser: str, details: Dict) -> None: assert browsers.get(browser) == details +@pytest.mark.parametrize( + "chrome_path", + ( + pytest.param( + "/Applications/Google Chrome.app", + id="osx", + marks=pytest.mark.skipif(sys.platform != "darwin", reason="osx-only"), + ), + pytest.param( + "/usr/bin/google-chrome-stable", + id="linux", + marks=pytest.mark.skipif(sys.platform != "linux", reason="linux-only"), + ), + ), +) @mock.patch.object(browsers, "_launch") -def test_launch(mock_launch: mock.MagicMock) -> None: +def test_launch(mock_launch: mock.MagicMock, chrome_path: str) -> None: browsers.launch("chrome", url="https://github.com/roniemartinez/browsers") - mock_launch.assert_called_with("/Applications/Google Chrome.app", "https://github.com/roniemartinez/browsers", []) + mock_launch.assert_called_with(chrome_path, "https://github.com/roniemartinez/browsers", []) @mock.patch.object(browsers, "_launch")