From a17f9d96a84de4e24d23a17d509a177311f82b53 Mon Sep 17 00:00:00 2001 From: Stefan Pascu Date: Tue, 21 May 2024 13:57:13 -0400 Subject: [PATCH 1/2] removed unnecessary imports --- .gitignore | 1 + pyproject.toml | 3 +- src/briefcase/commands/base.py | 2 + src/briefcase/integrations/__init__.py | 2 + src/briefcase/integrations/android_sdk.py | 5 ++- src/briefcase/integrations/base.py | 2 + src/briefcase/integrations/files.py | 30 +++++++++++++ src/briefcase/integrations/java.py | 5 ++- tests/integrations/conftest.py | 2 + tests/integrations/files/__init__.py | 0 .../integrations/files/test_Files__rename.py | 45 +++++++++++++++++++ .../integrations/files/test_Files__verify.py | 11 +++++ 12 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/briefcase/integrations/files.py create mode 100644 tests/integrations/files/__init__.py create mode 100644 tests/integrations/files/test_Files__rename.py create mode 100644 tests/integrations/files/test_Files__verify.py diff --git a/.gitignore b/.gitignore index 9ae452b64..fc06cb89e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ venv*/ .eggs/ .tox/ /local +__pycache__/ /pip-wheel-metadata # IntelliJ Idea family of suites diff --git a/pyproject.toml b/pyproject.toml index 7fe18ed54..b44ce0df4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,8 +88,9 @@ dependencies = [ "python-dateutil >= 2.9.0.post0", # transitive dependency (beeware/briefcase#1428) "requests >= 2.28, < 3.0", "rich >= 12.6, < 14.0", + "tenacity >= 8.0, < 9.0", "tomli >= 2.0, < 3.0; python_version <= '3.10'", - "tomli_w >= 1.0, < 2.0", + "tomli_w >= 1.0, < 2.0" ] [project.optional-dependencies] diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 160108308..6ea9befb7 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -38,6 +38,7 @@ ) from briefcase.integrations.base import ToolCache from briefcase.integrations.download import Download +from briefcase.integrations.files import Files from briefcase.integrations.subprocess import Subprocess from briefcase.platforms import get_output_formats, get_platforms @@ -188,6 +189,7 @@ def __init__( # Immediately add tools that must be always available Subprocess.verify(tools=self.tools) Download.verify(tools=self.tools) + Files.verify(tools=self.tools) if not is_clone: self.validate_locale() diff --git a/src/briefcase/integrations/__init__.py b/src/briefcase/integrations/__init__.py index 2a789b054..7d5b28e5c 100644 --- a/src/briefcase/integrations/__init__.py +++ b/src/briefcase/integrations/__init__.py @@ -3,6 +3,7 @@ cookiecutter, docker, download, + files, flatpak, git, java, @@ -20,6 +21,7 @@ "cookiecutter", "docker", "download", + "files", "flatpak", "git", "java", diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index ad3d2d684..673fae1af 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -426,8 +426,9 @@ def install(self): self.tools.shutil.rmtree(self.cmdline_tools_path) # Rename the top level zip content to the final name - (self.cmdline_tools_path.parent / "cmdline-tools").rename( - self.cmdline_tools_path + self.tools.files.path_rename( + old_path=(self.cmdline_tools_path.parent / "cmdline-tools"), + new_path=self.cmdline_tools_path, ) # Zip file no longer needed once unpacked. diff --git a/src/briefcase/integrations/base.py b/src/briefcase/integrations/base.py index a359b2301..306180cfb 100644 --- a/src/briefcase/integrations/base.py +++ b/src/briefcase/integrations/base.py @@ -31,6 +31,7 @@ from briefcase.integrations.android_sdk import AndroidSDK from briefcase.integrations.docker import Docker, DockerAppContext from briefcase.integrations.download import Download + from briefcase.integrations.files import Files from briefcase.integrations.flatpak import Flatpak from briefcase.integrations.java import JDK from briefcase.integrations.linuxdeploy import LinuxDeploy @@ -149,6 +150,7 @@ class ToolCache(Mapping): app_context: Subprocess | DockerAppContext docker: Docker download: Download + files: Files flatpak: Flatpak git: git_ java: JDK diff --git a/src/briefcase/integrations/files.py b/src/briefcase/integrations/files.py new file mode 100644 index 000000000..0b85afece --- /dev/null +++ b/src/briefcase/integrations/files.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + +from tenacity import retry, stop_after_attempt, wait_fixed + +from briefcase.integrations.base import Tool, ToolCache + + +class Files(Tool): + name = "files" + full_name = "Files" + + @classmethod + def verify_install(cls, tools: ToolCache, **kwargs) -> Files: + """Make files available in tool cache.""" + # short circuit since already verified and available + if hasattr(tools, "files"): + return tools.files + + tools.files = Files(tools=tools) + return tools.files + + @retry(wait=wait_fixed(0.2), stop=stop_after_attempt(25)) + def path_rename(self, old_path: Path, new_path: object): + """Using tenacity for a retry policy on pathlib rename. + + Windows does not like renaming a dir in a path with an opened file. + """ + old_path.rename(new_path) diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py index 5a3874e6e..022db623f 100644 --- a/src/briefcase/integrations/java.py +++ b/src/briefcase/integrations/java.py @@ -305,7 +305,10 @@ def install(self): java_unpack_path = ( self.tools.base_path / f"jdk-{self.JDK_RELEASE}+{self.JDK_BUILD}" ) - java_unpack_path.rename(self.tools.base_path / self.JDK_INSTALL_DIR_NAME) + self.tools.files.path_rename( + old_path=java_unpack_path, + new_path=self.tools.base_path / self.JDK_INSTALL_DIR_NAME, + ) def uninstall(self): """Uninstall a JDK.""" diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 15c0fcc38..ef34c8758 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -10,6 +10,7 @@ from briefcase.console import Log from briefcase.integrations.base import ToolCache from briefcase.integrations.download import Download +from briefcase.integrations.files import Files from briefcase.integrations.subprocess import Subprocess from tests.utils import DummyConsole @@ -38,6 +39,7 @@ def mock_tools(tmp_path) -> ToolCache: # Make Download and Subprocess always available Download.verify(tools=mock_tools) + Files.verify(tools=mock_tools) Subprocess.verify(tools=mock_tools) return mock_tools diff --git a/tests/integrations/files/__init__.py b/tests/integrations/files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integrations/files/test_Files__rename.py b/tests/integrations/files/test_Files__rename.py new file mode 100644 index 000000000..c82269ec0 --- /dev/null +++ b/tests/integrations/files/test_Files__rename.py @@ -0,0 +1,45 @@ +import os +import sys +import threading +import time + +import pytest +import tenacity + + +def test_rename_path(mock_tools, tmp_path): + def openclose(filepath): + handler = filepath.open(encoding="UTF-8") + time.sleep(1) + handler.close() + + (tmp_path / "orig-dir-1").mkdir() + tl = tmp_path / "orig-dir-1/orig-file" + tl.touch() + file_access_thread = threading.Thread(target=openclose, args=(tl,)) + rename_thread = threading.Thread( + target=mock_tools.files.path_rename, + args=(tmp_path / "orig-dir-1", tmp_path / "new-dir-1"), + ) + + file_access_thread.start() + rename_thread.start() + + rename_thread.join() + + assert "new-dir-1" in os.listdir(tmp_path) + + +@pytest.mark.xfail( + sys.platform == "win32", + raises=tenacity.RetryError, + reason="Windows can't rename folder in filepath when the file is open", +) +def test_rename_path_fail(mock_tools, tmp_path, monkeypatch): + (tmp_path / "orig-dir-2").mkdir() + (tmp_path / "orig-dir-2/orig-file").touch() + + with (tmp_path / "orig-dir-2/orig-file").open(encoding="UTF-8"): + + monkeypatch.setattr(tenacity.nap.time, "sleep", lambda x: True) + mock_tools.files.path_rename(tmp_path / "orig-dir-2", tmp_path / "new-dir-2") diff --git a/tests/integrations/files/test_Files__verify.py b/tests/integrations/files/test_Files__verify.py new file mode 100644 index 000000000..5f8facfcb --- /dev/null +++ b/tests/integrations/files/test_Files__verify.py @@ -0,0 +1,11 @@ +from briefcase.integrations.files import Files + + +def test_short_circuit(mock_tools): + """Tool is not created if already cached.""" + mock_tools.files = "tool" + + tool = Files.verify(mock_tools) + + assert tool == "tool" + assert tool == mock_tools.files From 9d7f14697e07c6f4390f341504667f6173b62e9d Mon Sep 17 00:00:00 2001 From: Stefan Pascu Date: Tue, 21 May 2024 14:11:36 -0400 Subject: [PATCH 2/2] Created 1780.bugfix.rst --- changes/1780.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1780.bugfix.rst diff --git a/changes/1780.bugfix.rst b/changes/1780.bugfix.rst new file mode 100644 index 000000000..8b176a17c --- /dev/null +++ b/changes/1780.bugfix.rst @@ -0,0 +1 @@ +Added Files integration using tenacity for path dir rename retry