From 70abe62d26ebd82edf8558979e12354267ebe6c7 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Sun, 28 Apr 2024 21:53:28 +0200 Subject: [PATCH 1/8] Fix android detection when python4android is present Use the builtin mActivity of python4android to get hold of our main activity, which is capable of generating a proper application context. --- src/platformdirs/android.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index fefafd3..65e838f 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -6,7 +6,7 @@ import re import sys from functools import lru_cache -from typing import cast +from typing import TYPE_CHECKING, cast from .api import PlatformDirsABC @@ -119,14 +119,31 @@ def site_runtime_dir(self) -> str: @lru_cache(maxsize=1) def _android_folder() -> str | None: """:return: base folder for the Android OS or None if it cannot be found""" - try: - # First try to get a path to android app via pyjnius - from jnius import autoclass # noqa: PLC0415 - - context = autoclass("android.content.Context") - result: str | None = context.getFilesDir().getParentFile().getAbsolutePath() - except Exception: # noqa: BLE001 - # if fails find an android folder looking a path on the sys.path + result: str | None = None + # type checker isn't happy with our "import android", just don't do this when type checking + # see https://stackoverflow.com/a/61394121 + if not TYPE_CHECKING: + try: + # First try to get a path to android app using python4android (if available)... + from android import mActivity # noqa: PLC0415 + + context = cast("android.content.Context", mActivity.getApplicationContext()) # noqa: F821 + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + try: + # ...and fall back to using plain pyjnius, if python4android isn't + # available or doesn't deliver any useful result... + from jnius import autoclass # noqa: PLC0415 + + context = autoclass("android.content.Context") + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + # and if that fails, too, find an android folder looking at path on the sys.path + # warning: only works for apps installed under /data, not adopted storage etc. pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path): From 3222282d46cc14d03dc9a6c22608b20ef9d1b600 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 13:52:37 +0200 Subject: [PATCH 2/8] Add pcre implementation covering android adopted storage --- src/platformdirs/android.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index 65e838f..67b5673 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -151,6 +151,16 @@ def _android_folder() -> str | None: break else: result = None + if result is None: + # one last try: find an android folder looking at path on the sys.path + # taking adopted storage paths into acount + pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") + for path in sys.path: + if pattern.match(path): + result = path.split("/files")[0] + break + else: + result = None return result From bfcc35270dcdb621db72ef72084654a245da5c51 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 13:53:06 +0200 Subject: [PATCH 3/8] Implement tests --- tests/test_android.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/test_android.py b/tests/test_android.py index 57ac1b5..40689fd 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -67,10 +67,13 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No assert result == expected -def test_android_folder_from_jnius(mocker: MockerFixture) -> None: +def test_android_folder_from_jnius(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: from platformdirs import PlatformDirs # noqa: PLC0415 from platformdirs.android import _android_folder # noqa: PLC0415 + mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__('sys').modules, 'android') + _android_folder.cache_clear() if PlatformDirs is Android: @@ -92,16 +95,42 @@ def test_android_folder_from_jnius(mocker: MockerFixture) -> None: assert _android_folder() is result assert autoclass.call_count == 1 +def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: + from platformdirs import PlatformDirs # noqa: PLC0415 + from platformdirs.android import _android_folder # noqa: PLC0415 + + mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__('sys').modules, 'jnius') + + _android_folder.cache_clear() + + getAbsolutePath = MagicMock(return_value="/A") # pragma: no cover + getParentFile = MagicMock(getAbsolutePath=getAbsolutePath) # pragma: no cover + getFilesDir = MagicMock(getParentFile=MagicMock(return_value=getParentFile)) # pragma: no cover + getApplicationContext = MagicMock(getFilesDir = MagicMock(return_value=getFilesDir)) # pragma: no cover + mActivity = MagicMock(getApplicationContext=MagicMock(return_value=getApplicationContext)) # pragma: no cover + mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=mActivity)}) # pragma: no cover + + result = _android_folder() + assert result == "/A" + assert getAbsolutePath.call_count == 1 + + assert _android_folder() is result + assert getAbsolutePath.call_count == 1 @pytest.mark.parametrize( "path", [ "/data/user/1/a/files", "/data/data/a/files", + "/mnt/expand/8e06fc2f-a86a-44e8-81ce-109e0eedd5ed/user/1/a/files", ], ) def test_android_folder_from_sys_path(mocker: MockerFixture, path: str, monkeypatch: pytest.MonkeyPatch) -> None: - mocker.patch.dict(sys.modules, {"jnius": MagicMock(autoclass=MagicMock(side_effect=ModuleNotFoundError))}) + mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__('sys').modules, 'jnius') + mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__('sys').modules, 'android') from platformdirs.android import _android_folder # noqa: PLC0415 From 9ea77af87e72d7c19314f2f386faa545c1009e78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 11:53:46 +0000 Subject: [PATCH 4/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_android.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_android.py b/tests/test_android.py index 40689fd..26f957b 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -72,7 +72,7 @@ def test_android_folder_from_jnius(mocker: MockerFixture, monkeypatch: pytest.Mo from platformdirs.android import _android_folder # noqa: PLC0415 mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) - monkeypatch.delitem(__import__('sys').modules, 'android') + monkeypatch.delitem(__import__("sys").modules, "android") _android_folder.cache_clear() @@ -95,19 +95,19 @@ def test_android_folder_from_jnius(mocker: MockerFixture, monkeypatch: pytest.Mo assert _android_folder() is result assert autoclass.call_count == 1 + def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: - from platformdirs import PlatformDirs # noqa: PLC0415 from platformdirs.android import _android_folder # noqa: PLC0415 mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) - monkeypatch.delitem(__import__('sys').modules, 'jnius') + monkeypatch.delitem(__import__("sys").modules, "jnius") _android_folder.cache_clear() getAbsolutePath = MagicMock(return_value="/A") # pragma: no cover getParentFile = MagicMock(getAbsolutePath=getAbsolutePath) # pragma: no cover getFilesDir = MagicMock(getParentFile=MagicMock(return_value=getParentFile)) # pragma: no cover - getApplicationContext = MagicMock(getFilesDir = MagicMock(return_value=getFilesDir)) # pragma: no cover + getApplicationContext = MagicMock(getFilesDir=MagicMock(return_value=getFilesDir)) # pragma: no cover mActivity = MagicMock(getApplicationContext=MagicMock(return_value=getApplicationContext)) # pragma: no cover mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=mActivity)}) # pragma: no cover @@ -118,6 +118,7 @@ def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.Monk assert _android_folder() is result assert getAbsolutePath.call_count == 1 + @pytest.mark.parametrize( "path", [ @@ -128,9 +129,9 @@ def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.Monk ) def test_android_folder_from_sys_path(mocker: MockerFixture, path: str, monkeypatch: pytest.MonkeyPatch) -> None: mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) - monkeypatch.delitem(__import__('sys').modules, 'jnius') + monkeypatch.delitem(__import__("sys").modules, "jnius") mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) - monkeypatch.delitem(__import__('sys').modules, 'android') + monkeypatch.delitem(__import__("sys").modules, "android") from platformdirs.android import _android_folder # noqa: PLC0415 From 9cd764c613a14c6a1a3f740769fa3143edb13563 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 13:57:32 +0200 Subject: [PATCH 5/8] Make stupid linter happy --- src/platformdirs/android.py | 4 ++-- tests/test_android.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index 67b5673..68a4652 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -117,7 +117,7 @@ def site_runtime_dir(self) -> str: @lru_cache(maxsize=1) -def _android_folder() -> str | None: +def _android_folder() -> str | None: # noqa: C901, PLR0912 """:return: base folder for the Android OS or None if it cannot be found""" result: str | None = None # type checker isn't happy with our "import android", just don't do this when type checking @@ -153,7 +153,7 @@ def _android_folder() -> str | None: result = None if result is None: # one last try: find an android folder looking at path on the sys.path - # taking adopted storage paths into acount + # taking adopted storage paths into account pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path): diff --git a/tests/test_android.py b/tests/test_android.py index 26f957b..046a9af 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -104,19 +104,19 @@ def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.Monk _android_folder.cache_clear() - getAbsolutePath = MagicMock(return_value="/A") # pragma: no cover - getParentFile = MagicMock(getAbsolutePath=getAbsolutePath) # pragma: no cover - getFilesDir = MagicMock(getParentFile=MagicMock(return_value=getParentFile)) # pragma: no cover - getApplicationContext = MagicMock(getFilesDir=MagicMock(return_value=getFilesDir)) # pragma: no cover - mActivity = MagicMock(getApplicationContext=MagicMock(return_value=getApplicationContext)) # pragma: no cover - mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=mActivity)}) # pragma: no cover + abspath = MagicMock(return_value="/A") # pragma: no cover + pfile = MagicMock(getAbsolutePath=abspath) # pragma: no cover + fdir = MagicMock(getParentFile=MagicMock(return_value=pfile)) # pragma: no cover + appc = MagicMock(getFilesDir=MagicMock(return_value=fdir)) # pragma: no cover + act = MagicMock(getApplicationContext=MagicMock(return_value=appc)) # pragma: no cover + mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=act)}) # pragma: no cover result = _android_folder() assert result == "/A" - assert getAbsolutePath.call_count == 1 + assert abspath.call_count == 1 assert _android_folder() is result - assert getAbsolutePath.call_count == 1 + assert abspath.call_count == 1 @pytest.mark.parametrize( From 4ba637652748a2226543849fa7cb724a16a706a2 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 17:34:55 +0200 Subject: [PATCH 6/8] Change casing once again --- tests/test_android.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_android.py b/tests/test_android.py index 046a9af..7166ee4 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -104,19 +104,19 @@ def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.Monk _android_folder.cache_clear() - abspath = MagicMock(return_value="/A") # pragma: no cover - pfile = MagicMock(getAbsolutePath=abspath) # pragma: no cover - fdir = MagicMock(getParentFile=MagicMock(return_value=pfile)) # pragma: no cover - appc = MagicMock(getFilesDir=MagicMock(return_value=fdir)) # pragma: no cover - act = MagicMock(getApplicationContext=MagicMock(return_value=appc)) # pragma: no cover - mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=act)}) # pragma: no cover + get_absolute_path = MagicMock(return_value="/A") + get_parent_file = MagicMock(getAbsolutePath=get_absolute_path) + get_files_dir = MagicMock(getParentFile=MagicMock(return_value=get_parent_file)) + get_application_context = MagicMock(getFilesDir=MagicMock(return_value=get_files_dir)) + m_activity = MagicMock(getApplicationContext=MagicMock(return_value=get_application_context)) + mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=m_activity)}) result = _android_folder() assert result == "/A" - assert abspath.call_count == 1 + assert get_absolute_path.call_count == 1 assert _android_folder() is result - assert abspath.call_count == 1 + assert get_absolute_path.call_count == 1 @pytest.mark.parametrize( From c13e13091f578edb18cba25b524db08771025d52 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 21:18:26 +0200 Subject: [PATCH 7/8] Realign comment --- src/platformdirs/android.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index 68a4652..08d7c1b 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -133,8 +133,8 @@ def _android_folder() -> str | None: # noqa: C901, PLR0912 result = None if result is None: try: - # ...and fall back to using plain pyjnius, if python4android isn't - # available or doesn't deliver any useful result... + # ...and fall back to using plain pyjnius, if python4android isn't available or doesn't deliver any useful + # result... from jnius import autoclass # noqa: PLC0415 context = autoclass("android.content.Context") From 0e71fa932dcfd3955d3079a7b7d6621ec7eb26d2 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Tue, 14 May 2024 21:25:35 +0200 Subject: [PATCH 8/8] Realign other comments, too --- src/platformdirs/android.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index 08d7c1b..afd3141 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -120,8 +120,8 @@ def site_runtime_dir(self) -> str: def _android_folder() -> str | None: # noqa: C901, PLR0912 """:return: base folder for the Android OS or None if it cannot be found""" result: str | None = None - # type checker isn't happy with our "import android", just don't do this when type checking - # see https://stackoverflow.com/a/61394121 + # type checker isn't happy with our "import android", just don't do this when type checking see + # https://stackoverflow.com/a/61394121 if not TYPE_CHECKING: try: # First try to get a path to android app using python4android (if available)... @@ -152,8 +152,8 @@ def _android_folder() -> str | None: # noqa: C901, PLR0912 else: result = None if result is None: - # one last try: find an android folder looking at path on the sys.path - # taking adopted storage paths into account + # one last try: find an android folder looking at path on the sys.path taking adopted storage paths into + # account pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path):