Skip to content

Commit

Permalink
✨ Add support for Linux using *.desktop entries (#6)
Browse files Browse the repository at this point in the history
* ✨ Add support for Linux using *.desktop entries

* Remove print

* Prevent duplicate keys

* Fix: Split commands using shlex

* Update README.md
  • Loading branch information
roniemartinez authored Apr 13, 2022
1 parent 2d362b9 commit b6bcabd
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 23 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 50 additions & 4 deletions browsers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import logging
import os
import plistlib
import shlex
import subprocess
import sys
from typing import Dict, Iterator, Optional, Sequence, Tuple

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"),
Expand All @@ -30,17 +31,56 @@
("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'",
platform,
"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:
Expand All @@ -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:
Expand All @@ -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)
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <ronmarti18@gmail.com>"]
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -83,6 +84,7 @@ addopts = """\
--cov-report=html \
-vv \
-x \
-s \
"""

[build-system]
Expand Down
68 changes: 57 additions & 11 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Dict
from unittest import mock
from unittest.mock import ANY
Expand All @@ -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(
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -57,18 +67,54 @@ 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",
),
),
)
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")
Expand Down

0 comments on commit b6bcabd

Please sign in to comment.