From ba69c2602d740821ec515da6c1634b4cc68ed46e Mon Sep 17 00:00:00 2001 From: ybnd Date: Tue, 12 Jul 2022 20:39:14 +0200 Subject: [PATCH 1/7] Fix Windows file dialog method typos ...and rename to make them less prone to typos --- shapeflow/util/filedialog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/shapeflow/util/filedialog.py b/shapeflow/util/filedialog.py index a552c125..42c04be2 100644 --- a/shapeflow/util/filedialog.py +++ b/shapeflow/util/filedialog.py @@ -120,13 +120,14 @@ def _call(self, command: List[str]) -> Optional[str]: class _Windows(_FileDialog): - GetOpenFileNameW: Any - GetSaveFileNameW: Any + _open_w: Any + _save_w: Any def __init__(self): from win32gui import GetOpenFileNameW, GetSaveFileNameW - self.GetOpenfileNameW = GetOpenFileNameW - self.GetSaveFileNameW = GetSaveFileNameW + self._open_w = GetOpenFileNameW + self._save_w = GetSaveFileNameW + self.ok = True def _resolve(self, method: str, kwargs: dict) -> dict: kwargs = super()._resolve(method, kwargs) @@ -137,11 +138,11 @@ def _resolve(self, method: str, kwargs: dict) -> dict: return kwargs def _load(self, **kwargs) -> Optional[str]: - file, _, _ = self.GetOpenFileNameW(**kwargs) + file, _, _ = self._open_w(**kwargs) return file def _save(self, **kwargs) -> Optional[str]: - file, _, _ = self.GetSaveFileNameW(**kwargs) + file, _, _ = self._save_w(**kwargs) return file From d5380571b32bddc660a913a8f511445dd3672a23 Mon Sep 17 00:00:00 2001 From: ybnd Date: Tue, 12 Jul 2022 21:07:57 +0200 Subject: [PATCH 2/7] Slightly more sensible file dialog resolution --- shapeflow/main.py | 6 +++++ shapeflow/util/filedialog.py | 47 +++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/shapeflow/main.py b/shapeflow/main.py index 33248484..4f803b2d 100644 --- a/shapeflow/main.py +++ b/shapeflow/main.py @@ -391,6 +391,12 @@ class _Filesystem(object): def __init__(self): self._history = History() + if not filedialog.ok: + log.error( + f"File dialog was resolved to {filedialog.__class__.__name__} " + f"but this implementation doesn't work on this system" + ) + @api.fs.select_video.expose() def select_video(self) -> Optional[str]: """Open a video selection dialog diff --git a/shapeflow/util/filedialog.py b/shapeflow/util/filedialog.py index 42c04be2..b26c18eb 100644 --- a/shapeflow/util/filedialog.py +++ b/shapeflow/util/filedialog.py @@ -46,7 +46,13 @@ def _save(self, **kwargs) -> Optional[str]: class _SubprocessTkinter(_FileDialog): - ok = True + def __init__(self): + try: + from tkinter import Tk + import tkinter.filedialog + self.ok = True + except ModuleNotFoundError: + pass def _load(self, **kwargs) -> Optional[str]: return self._call('--load', self._to_args(kwargs)) @@ -74,21 +80,16 @@ def _to_args(self, kwargs: dict) -> list: return args - -def _has_zenity(): - try: - return not sp.call(['zenity', '--version'], stdout=sp.DEVNULL) - except FileNotFoundError: - return False - - class _Zenity(_FileDialog): _map = { 'title': '--title', 'pattern': '--file-filter', } def __init__(self): - self.ok = _has_zenity() + try: + self.ok = not sp.call(['zenity', '--version'], stdout=sp.DEVNULL) + except FileNotFoundError: + pass def _load(self, **kwargs) -> Optional[str]: return self._call(self._compose(False, kwargs)) @@ -147,20 +148,22 @@ def _save(self, **kwargs) -> Optional[str]: filedialog: _FileDialog +"""Cross-platform file dialog. + +Tries to use `zenity `_ +on Linux if available, because ``tkinter`` looks fugly in GNOME don't @ me. + +Uses win32gui dialogs on Windows. + +Falls back to ``tkinter`` to support any other platform. +For MacOS this may mean that you have to install tkinter via +``brew install tkinter-python`` for these dialogs to work. +""" if os.name != "nt": - # try using zenity by default filedialog = _Zenity() - """Cross-platform file dialog. - - Tries to use `zenity `_ - if available, because ``tkinter`` looks fugly in GNOME don't @ me. - - Defaults to ``tkinter`` to support basically any platform. - """ - # if zenity doesn't work (e.g. it's not installed), - # default to tkinter. - if not filedialog.ok: - filedialog = _SubprocessTkinter() else: filedialog = _Windows() + +if not filedialog.ok: + filedialog = _SubprocessTkinter() \ No newline at end of file From 2128d70e0026d76fcc2d948d4ea91094121d6c3e Mon Sep 17 00:00:00 2001 From: ybnd Date: Tue, 12 Jul 2022 21:09:58 +0200 Subject: [PATCH 3/7] Fix typo in Tkinter file dialog script --- shapeflow/util/tk-filedialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shapeflow/util/tk-filedialog.py b/shapeflow/util/tk-filedialog.py index 2863d16d..c5feff40 100644 --- a/shapeflow/util/tk-filedialog.py +++ b/shapeflow/util/tk-filedialog.py @@ -22,9 +22,9 @@ path = None if args.load or (not args.load and not args.save): - out = tkinter.filedialog.askopenfilename(**d) + path = tkinter.filedialog.askopenfilename(**d) elif args.save: - out = tkinter.filedialog.asksaveasfilename(**d) + path = tkinter.filedialog.asksaveasfilename(**d) if isinstance(path, str): print(path) # can be read with `out, err = p.communicate()` From 53d199c9642aa5454b35f2dd6b11fabe0072e7fc Mon Sep 17 00:00:00 2001 From: ybnd Date: Sat, 6 Aug 2022 00:23:38 +0200 Subject: [PATCH 4/7] Document Homebrew tkinter install --- docs/source/troubleshooting.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst index ed3a298d..632e6659 100644 --- a/docs/source/troubleshooting.rst +++ b/docs/source/troubleshooting.rst @@ -117,8 +117,15 @@ Application runs, but something’s gone wrong * Refresh the page +* On MacOS, file/directory selection windows don't appear (e.g. when selecting a file to load or a directory to save to) + + * ``tkinter`` `doesn't work out of the box on MacOS `_, try installing it via `Homebrew `_:: + + brew install python-tk .. _shapeflow-releases: https://github.com/ybnd/shapeflow/releases .. _add-path-win10: https://www.architectryan.com/2018/03/17/add-to-the-path-on-windows-10/ .. _cairo: https://www.cairographics.org/manual/ -.. _cairo-windows: https://github.com/preshing/cairo-windows \ No newline at end of file +.. _cairo-windows: https://github.com/preshing/cairo-windows +.. _tk_macos: https://www.python.org/download/mac/tcltk/ +.. _brew: https://brew.sh/ \ No newline at end of file From 240aeed2d7e4ed3afcc4c56530d286688433402c Mon Sep 17 00:00:00 2001 From: ybnd Date: Sat, 6 Aug 2022 00:25:51 +0200 Subject: [PATCH 5/7] Add to changelog --- docs/source/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 290e581e..009d5c24 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +0.4.5 +----- + +* Fixed some issues with file/directory selection dialogs on Windows and MacOS + 0.4.4 ----- From 5e1b3a526639b487699e0ce20e22651b1a2858bb Mon Sep 17 00:00:00 2001 From: ybnd Date: Sat, 6 Aug 2022 01:05:21 +0200 Subject: [PATCH 6/7] Update file dialog tests --- shapeflow/util/filedialog.py | 6 +-- test/test_util.py | 89 ++++++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/shapeflow/util/filedialog.py b/shapeflow/util/filedialog.py index b26c18eb..71311de7 100644 --- a/shapeflow/util/filedialog.py +++ b/shapeflow/util/filedialog.py @@ -125,9 +125,9 @@ class _Windows(_FileDialog): _save_w: Any def __init__(self): - from win32gui import GetOpenFileNameW, GetSaveFileNameW - self._open_w = GetOpenFileNameW - self._save_w = GetSaveFileNameW + import win32gui + self._open_w = win32gui.GetOpenFileNameW + self._save_w = win32gui.GetSaveFileNameW self.ok = True def _resolve(self, method: str, kwargs: dict) -> dict: diff --git a/test/test_util.py b/test/test_util.py index b36182d6..2232f04d 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,35 +1,51 @@ import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock import os import json import tkinter import tkinter.filedialog import subprocess +import sys +import platform +if platform.system() == "Windows": + import win32gui + _win32gui = win32gui -from shapeflow.util.filedialog import _SubprocessTkinter, _Zenity +win32gui = MagicMock() +win32gui.GetOpenFileNameW = MagicMock(return_value=(b'...', None, None)) +win32gui.GetSaveFileNameW = MagicMock(return_value=(b'...', None, None)) +sys.modules["win32gui"] = win32gui + +from shapeflow.util.filedialog import _SubprocessTkinter, _Zenity, _Windows from shapeflow.util.from_venv import _VenvCall, _WindowsVenvCall, from_venv class FileDialogTest(unittest.TestCase): - kw = [ - 'title', - 'pattern', - 'pattern_description', - ] - kwargs = {k:k for k in kw} + kwargs = { + 'title': 'title', + 'pattern': 'pattern1 pattern2 pattern3', + 'pattern_description': 'pattern_description', + } + + @classmethod + def tearDownClass(cls) -> None: + if platform.system() == "Windows": + sys.modules["win32gui"] = _win32gui + @patch('subprocess.Popen') def test_tkinter_load(self, Popen): Popen.return_value.communicate.return_value = (b'...', 0) _SubprocessTkinter().load(**self.kwargs) - self.assertCountEqual( + self.assertSequenceEqual( [ 'python', 'shapeflow/util/tk-filedialog.py', '--load', - '--title', 'title', '--pattern', 'pattern', - '--pattern_description', 'pattern_description' + '--pattern', 'pattern1 pattern2 pattern3', + '--pattern_description', 'pattern_description', + '--title', 'title', ], Popen.call_args[0][0] ) @@ -39,11 +55,12 @@ def test_tkinter_save(self, Popen): Popen.return_value.communicate.return_value = (b'...', 0) _SubprocessTkinter().save(**self.kwargs) - self.assertCountEqual( + self.assertSequenceEqual( [ 'python', 'shapeflow/util/tk-filedialog.py', '--save', - '--title', 'title', '--pattern', 'pattern', - '--pattern_description', 'pattern_description' + '--pattern', 'pattern1 pattern2 pattern3', + '--pattern_description', 'pattern_description', + '--title', 'title', ], Popen.call_args[0][0] ) @@ -53,23 +70,27 @@ def test_tkinter_save(self, Popen): def test_zenity_load(self, Popen): Popen.return_value.communicate.return_value = (b'...', 0) _Zenity().load(**self.kwargs) - - c = 'zenity --file-selection --title title --file-filter pattern' - - self.assertCountEqual( - c.split(' '), + self.assertSequenceEqual( + [ + 'zenity', '--file-selection', + '--file-filter', 'pattern1 pattern2 pattern3', + '--title', 'title', + ], Popen.call_args[0][0] ) + @patch('subprocess.Popen') def test_zenity_save(self, Popen): Popen.return_value.communicate.return_value = (b'...', 0) _Zenity().save(**self.kwargs) - c = 'zenity --file-selection --save --title title --file-filter pattern' - - self.assertCountEqual( - c.split(' '), + self.assertSequenceEqual( + [ + 'zenity', '--file-selection', '--save', + '--file-filter', 'pattern1 pattern2 pattern3', + '--title', 'title', + ], Popen.call_args[0][0] ) @@ -98,6 +119,28 @@ def test_zenity_save_cancel(self, Popen): with self.assertRaises(ValueError): _Zenity().save(**self.kwargs) + def test_windows_load(self): + _Windows().load(**self.kwargs) + + self.assertDictEqual( + { + "Title": "title", + "Filter": "pattern_description\0pattern1;pattern2;pattern3\0" + }, + win32gui.GetOpenFileNameW.call_args.kwargs + ) + + def test_windows_save(self): + _Windows().save(**self.kwargs) + + self.assertDictEqual( + { + "Title": "title", + "Filter": "pattern_description\0pattern1;pattern2;pattern3\0" + }, + win32gui.GetSaveFileNameW.call_args.kwargs + ) + ENV = '.venv-name' PYTHON = 'python4.2.0' From 9f9f0db8c3d9391eaf678dd4c6698009c59e9e25 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 25 Aug 2022 17:34:03 +0200 Subject: [PATCH 7/7] Fix open directory on MacOS --- .gitignore | 1 + shapeflow/cli.py | 8 ++++---- shapeflow/util/__init__.py | 9 ++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 3aaf4afd..072ff793 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ deploy_*.py build/ ui/dist/ .venv/ +.venv .local .~*.md .ploy diff --git a/shapeflow/cli.py b/shapeflow/cli.py index f0204f08..636eb591 100644 --- a/shapeflow/cli.py +++ b/shapeflow/cli.py @@ -628,7 +628,7 @@ def command(self) -> None: self._reclutter() def _declutter(self) -> None: - if os.name == 'nt': + if sys.platform == 'win32' or sys.platform == 'cygwin': # Pre-emptively create __pycache__ so we can hide it now. if not os.path.isdir('__pycache__'): os.mkdir('__pycache__') @@ -636,7 +636,7 @@ def _declutter(self) -> None: for file in self.CLUTTER + glob.glob('.*'): if Path(file).exists(): os.system(f'attrib +h "{file}"') - elif os.name == 'darwin': + elif sys.platform == 'darwin': for file in self.CLUTTER: if Path(file).exists(): os.system(f'chflags hidden "{file}"') @@ -645,11 +645,11 @@ def _declutter(self) -> None: f.write('\n'.join(self.CLUTTER)) def _reclutter(self) -> None: - if os.name == 'nt': + if sys.platform == 'win32' or sys.platform == 'cygwin': for file in self.CLUTTER + glob.glob('.*'): if Path(file).exists(): os.system(f'attrib -h "{file}"') - elif os.name == 'darwin': + elif sys.platform == 'darwin': for file in self.CLUTTER: if Path(file).exists(): os.system(f'chflags nohidden "{file}"') diff --git a/shapeflow/util/__init__.py b/shapeflow/util/__init__.py index 77f3f854..6acb5bdd 100644 --- a/shapeflow/util/__init__.py +++ b/shapeflow/util/__init__.py @@ -8,9 +8,8 @@ from pathlib import Path import json from logging import Logger -from distutils.util import strtobool -from functools import wraps, lru_cache -from typing import Any, Generator, Optional, Union +from functools import wraps +from typing import Generator, Optional, Union from collections import namedtuple import threading import queue @@ -277,9 +276,9 @@ def open_path(path: str) -> None: if os.path.isfile(path): path = os.path.dirname(os.path.realpath(path)) - if os.name == 'nt': # Windows + if sys.platform == 'win32': os.startfile(path) # type: ignore - elif os.name == 'darwin': # MacOS + elif sys.platform == 'darwin': # MacOS subprocess.Popen(['open', path]) else: # Something else, probably has xdg-open subprocess.Popen(['xdg-open', path])