From 8954f6174caf3fd71e1ee4eff4a9c138db015b94 Mon Sep 17 00:00:00 2001 From: Ben Rowland Date: Tue, 13 Jun 2023 14:37:06 +0100 Subject: [PATCH] 477 Move clear_comtypes_cache to be a callable module (#478) * move clear_comtypes_cache to be a callable module This commit modifies the clear_comtypes_cache.py script so that it is inside the main comtypes module (renamed as just clear_cache) so that is can be called more easily as "py -m comtypes.clear_cache". The main function of the script is also exported using the "console_scripts" entry point so that the script also goes into the standard Python "Scripts" folder as before, but now as a .exe instead of a .py script, which makes it easier to run if systems are set to open .py files instead of running them. This version also includes a test case using the 3rd party package pyfakefs. Currently, this is not desired to avoid the requirement of 3rd party packages in comtypes, but is included here for potential use if the position changes. A subsequent commit will modify the tests to use unittest.patch instead, which is an inferior technical solution but avoids a 3rd party package. * modify clear_cache tests to not use pyfakefs This commit updates the test for comtypes.clear_cache to not use any 3rd party packages, instead relying on mocking the shutil.rmtree function which is used to do the actual cache deletion. * change quotes in print string * style changes based on review by @junkmd * Apply suggestions from code review Co-authored-by: Jun Komoda <45822440+junkmd@users.noreply.github.com> --------- Co-authored-by: Ben Rowland Co-authored-by: Jun Komoda <45822440+junkmd@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- clear_comtypes_cache.py | 57 ------------------------------- comtypes/clear_cache.py | 53 ++++++++++++++++++++++++++++ comtypes/test/test_clear_cache.py | 25 ++++++++++++++ setup.py | 12 +++---- 5 files changed, 84 insertions(+), 65 deletions(-) delete mode 100644 clear_comtypes_cache.py create mode 100644 comtypes/clear_cache.py create mode 100644 comtypes/test/test_clear_cache.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 555c8aca6..472b88c1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ Those `.py` files act like ”caches”. If there are some problems with the developing code base, partial or non-executable modules might be created in `.../comtypes/gen/...`. Importing them will cause some error. -If that happens, you should run `python -m clear_comtypes_cache` to clear those caches. +If that happens, you should run `python -m comtypes.clear_cache` to clear those caches. The command will delete the entire `.../comtypes/gen` directory. Importing `comtypes.gen.client` will restore the directory and `__init__.py` file. diff --git a/clear_comtypes_cache.py b/clear_comtypes_cache.py deleted file mode 100644 index bd743cd55..000000000 --- a/clear_comtypes_cache.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import sys -import shutil - -def get_next_cache_dir(): - work_dir = os.getcwd() - try: - # change working directory to avoid import from local folder - # during installation process - os.chdir(os.path.dirname(sys.executable)) - import comtypes.client - return comtypes.client._code_cache._find_gen_dir() - except ImportError: - return None - finally: - os.chdir(work_dir) - - -def _remove(directory): - shutil.rmtree(directory) - print('Removed directory "%s"' % directory) - - -def remove_directory(directory, silent): - if directory: - if silent: - _remove(directory) - else: - try: - confirm = raw_input('Remove comtypes cache directories? (y/n): ') - except NameError: - confirm = input('Remove comtypes cache directories? (y/n): ') - if confirm.lower() == 'y': - _remove(directory) - else: - print('Directory "%s" NOT removed' % directory) - return False - return True - - -if len(sys.argv) > 1 and "-y" in sys.argv[1:]: - silent = True -else: - silent = False - - -# First iteration may get folder with restricted rights. -# Second iteration always gets temp cache folder (writable for all). -directory = get_next_cache_dir() -removed = remove_directory(directory, silent) - -if removed: - directory = get_next_cache_dir() - - # do not request the second confirmation - # if the first folder was already removed - remove_directory(directory, silent=removed) diff --git a/comtypes/clear_cache.py b/comtypes/clear_cache.py new file mode 100644 index 000000000..f0e10ced6 --- /dev/null +++ b/comtypes/clear_cache.py @@ -0,0 +1,53 @@ +import argparse +import contextlib +import os +import sys +from shutil import rmtree # TESTS ASSUME USE OF RMTREE + + +# if supporting Py>=3.11 only, this might be `contextlib.chdir`. +# https://docs.python.org/3/library/contextlib.html#contextlib.chdir +@contextlib.contextmanager +def chdir(path): + """Context manager to change the current working directory.""" + work_dir = os.getcwd() + os.chdir(path) + yield + os.chdir(work_dir) + + +def main(): + parser = argparse.ArgumentParser( + prog="py -m comtypes.clear_cache", description="Removes comtypes cache folders." + ) + parser.add_argument( + "-y", help="Pre-approve deleting all folders", action="store_true" + ) + args = parser.parse_args() + + if not args.y: + confirm = input("Remove comtypes cache directories? (y/n): ") + if confirm.lower() != "y": + print("Cache directories NOT removed") + return + + # change cwd to avoid import from local folder during installation process + with chdir(os.path.dirname(sys.executable)): + try: + import comtypes.client + except ImportError: + print("Could not import comtypes", file=sys.stderr) + sys.exit(1) + + # there are two possible locations for the cache folder (in the comtypes + # folder in site-packages if that is writable, otherwise in APPDATA) + # fortunately, by deleting the first location returned by _find_gen_dir() + # we make it un-writable, so calling it again gives us the APPDATA location + for _ in range(2): + dir_path = comtypes.client._find_gen_dir() + rmtree(dir_path) + print(f'Removed directory "{dir_path}"') + + +if __name__ == "__main__": + main() diff --git a/comtypes/test/test_clear_cache.py b/comtypes/test/test_clear_cache.py new file mode 100644 index 000000000..e3cf81475 --- /dev/null +++ b/comtypes/test/test_clear_cache.py @@ -0,0 +1,25 @@ +""" +Test for the ``comtypes.clear_cache`` module. +""" +import contextlib +import runpy +from unittest.mock import patch, call +from unittest import TestCase + +from comtypes.client import _find_gen_dir + + +class ClearCacheTestCase(TestCase): + # we patch sys.stdout so unittest doesn't show the print statements + + @patch("sys.argv", ["clear_cache.py", "-y"]) + @patch("shutil.rmtree") + def test_clear_cache(self, mock_rmtree): + with contextlib.redirect_stdout(None): + runpy.run_module("comtypes.clear_cache", {}, "__main__") + + # because we don't actually delete anything, _find_gen_dir() will + # give the same answer every time we call it + self.assertEqual( + mock_rmtree.call_args_list, [call(_find_gen_dir()) for _ in range(2)] + ) diff --git a/setup.py b/setup.py index 03adfcef4..57cc5ce5b 100644 --- a/setup.py +++ b/setup.py @@ -110,14 +110,10 @@ def run(self): install.run(self) # Custom script we run at the end of installing if not self.dry_run and not self.root: - filename = os.path.join( - self.install_scripts, "clear_comtypes_cache.py") - if not os.path.isfile(filename): - raise RuntimeError("Can't find '%s'" % (filename,)) print("Executing post install script...") - print('"' + sys.executable + '" "' + filename + '" -y') + print(f'"{sys.executable}" -m comtypes.clear_cache -y') try: - subprocess.check_call([sys.executable, filename, '-y']) + subprocess.check_call([sys.executable, "-m", "comtypes.clear_cache", '-y']) except subprocess.CalledProcessError: print("Failed to run post install script!") @@ -145,7 +141,9 @@ def run(self): ]}, classifiers=classifiers, - scripts=["clear_comtypes_cache.py"], + entry_points={ + "console_scripts": ["clear_comtypes_cache=comtypes.clear_cache:main"] + }, cmdclass={ 'test': test,