From a2d87aa59f1003a55c31705eaf23221f2e04df5b Mon Sep 17 00:00:00 2001 From: shx2 Date: Thu, 10 Nov 2016 11:05:51 +0200 Subject: [PATCH] New functions: uninstall(), is_installed() Plus some minor changes: - added unittests to test reset() functionality - fixed a minor bug in __init__.py where py2 module was import first when running on py3 - prevent pyc files from being written during unittests, to protect later tests from finding pyc files written by earlier tests NOTE: reset() and uninstall() don't fully work in py2, and their tests currently fail on py2, until issue#2 is resolved (https://github.com/elfsternberg/polyloader/issues/2) --- polyloader/__init__.py | 9 +++---- polyloader/_python2.py | 22 ++++++++++++++++ polyloader/_python3.py | 24 +++++++++++++++++ tests_py2/test_polyloader.py | 50 ++++++++++++++++++++++++++++++++++++ tests_py3/test_polyloader.py | 42 ++++++++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 5 deletions(-) diff --git a/polyloader/__init__.py b/polyloader/__init__.py index 55d8878..113619f 100644 --- a/polyloader/__init__.py +++ b/polyloader/__init__.py @@ -5,10 +5,9 @@ __author__ = 'Kenneth M. "Elf" Sternberg' __email__ = 'elf.sternberg@gmail.com' __version__ = '0.1.0' -if sys.version_info[0:2] >= (2, 6): - from ._python2 import install, reset - if sys.version_info[0] >= 3: - from ._python3 import install, reset + from ._python3 import install, uninstall, is_installed, reset +elif sys.version_info[0:2] >= (2, 6): + from ._python2 import install, uninstall, is_installed, reset -__all__ = ['install', 'reset'] +__all__ = ['install', 'uninstall', 'is_installed', 'reset'] diff --git a/polyloader/_python2.py b/polyloader/_python2.py index 09ed6d1..6c6614c 100644 --- a/polyloader/_python2.py +++ b/polyloader/_python2.py @@ -111,6 +111,21 @@ class PolyFinder(object): return cls._loader_handlers += [Loader(suf, compiler) for suf in suffixes] + @classmethod + def _uninstall(cls, suffixes): + if isinstance(suffixes, basestring): + suffixes = [suffixes] + suffixes = set(suffixes) + overlap = suffixes.intersection(set([suf[0] for suf in imp.get_suffixes()])) + if overlap: + raise RuntimeError("Removing a native Python extensions is not permitted.") + + cls._loader_handlers = [ loader for loader in cls._loader_handlers if loader.suffix not in suffixes ] + + @classmethod + def _is_installed(cls, suffix): + return any( loader.suffix == suffix for loader in cls._loader_handlers ) + @classmethod def getmodulename(cls, path): filename = os.path.basename(path) @@ -174,6 +189,13 @@ def install(compiler, suffixes): PolyFinder._installed = True PolyFinder._install(compiler, suffixes) +def uninstall(suffixes): + if not PolyFinder._installed: + return + PolyFinder._uninstall(suffixes) + +def is_installed(suffix): + return PolyFinder._is_installed(suffix) def reset(): PolyFinder._loader_handlers = [] diff --git a/polyloader/_python3.py b/polyloader/_python3.py index 379b636..e21d9ff 100644 --- a/polyloader/_python3.py +++ b/polyloader/_python3.py @@ -152,6 +152,25 @@ class PolyFileFinder(FileFinder): dict(_compiler = compiler)) cls._custom_loaders += [(EXS + suffix, newloader) for suffix in suffixset] + @classmethod + def _uninstall(cls, suffixes): + if not suffixes: + return + if isinstance(suffixes, str): + suffixes = [suffixes] + suffixset = set(suffixes) + overlap = suffixset.intersection(set([suf[0] for suf in cls._native_loaders])) + if overlap: + raise RuntimeError("Removing a native Python extensions is not permitted.") + + exs_suffixset = set( EXS + suffix for suffix in suffixset ) + cls._custom_loaders = [ (suffix, loader) for suffix, loader in cls._custom_loaders if suffix not in exs_suffixset ] + + @classmethod + def _is_installed(cls, suffix): + suffix = EXS + suffix + return any( suf == suffix for suf, loader in cls._custom_loaders ) + @classmethod def getmodulename(cls, path): filename = os.path.basename(path) @@ -268,6 +287,11 @@ def install(compiler, suffixes): PolyFileFinder._install(compiler, suffixes) +def uninstall(suffixes): + PolyFileFinder._uninstall(suffixes) + +def is_installed(suffix): + return PolyFileFinder._is_installed(suffix) def reset(): PolyFileFinder._custom_loaders = [] diff --git a/tests_py2/test_polyloader.py b/tests_py2/test_polyloader.py index b042191..d946ce9 100644 --- a/tests_py2/test_polyloader.py +++ b/tests_py2/test_polyloader.py @@ -11,6 +11,7 @@ Tests for `polyloader` module. import polyloader import copy import sys +import pytest # Note that these compilers don't actually load much out of the # source files. That's not the point. The point is to show that the @@ -28,6 +29,8 @@ class ImportEnvironment(object): self.meta_path = copy.copy(sys.meta_path) self.modules = copy.copy(sys.modules) self.path_importer_cache = copy.copy(sys.path_importer_cache) + self.dont_write_bytecode = sys.dont_write_bytecode + sys.dont_write_bytecode = True return sys def __exit__(self, type, value, traceback): @@ -36,6 +39,7 @@ class ImportEnvironment(object): sys.meta_path = self.meta_path sys.modules = self.modules sys.path_importer_cache = self.path_importer_cache + sys.dont_write_bytecode = self.dont_write_bytecode class Compiler: @@ -99,6 +103,52 @@ class Test_Polymorph_Module(object): assert(result2 == "Success for 2: Test Two") assert(result3 == "Success for 3: Test Three") +class Test_Polymorph_Reset(object): + def test_reset_after_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("2"), ['2']) + polyloader.install(compiler("3"), ['3']) + from tests_py2.polytestmix.test3 import result as result3 + polyloader.reset() + with pytest.raises(ImportError): + from tests_py2.polytestmix.test2 import result as result2 + def test_reset_before_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("3"), ['3']) + polyloader.reset() + with pytest.raises(ImportError): + from tests_py2.polytestmix.test3 import result as result3 + +class Test_Polymorph_Uninstall(object): + def test_uninstall_after_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("2"), ['2']) + polyloader.install(compiler("3"), ['3']) + import tests_py2.polytestmix.test3 + assert(polyloader.is_installed('2')) + assert(polyloader.is_installed('3')) + polyloader.uninstall('2') + assert(not polyloader.is_installed('2')) + assert(polyloader.is_installed('3')) + with pytest.raises(ImportError): + import tests_py2.polytestmix.test2 + import tests_py2.polytestmix.test1 + def test_uninstall_before_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("2"), ['2']) + polyloader.install(compiler("3"), ['3']) + assert(polyloader.is_installed('2')) + assert(polyloader.is_installed('3')) + polyloader.uninstall('2') + polyloader.uninstall('3') + assert(not polyloader.is_installed('2')) + assert(not polyloader.is_installed('3')) + with pytest.raises(ImportError): + import tests_py2.polytestmix.test2 + with pytest.raises(ImportError): + import tests_py2.polytestmix.test3 + import tests_py2.polytestmix.test1 + class Test_Polymorph_Iterator(object): ''' The Django Compatibility test: Can we load arbitrary modules from a package? ''' def test_iterator(self): diff --git a/tests_py3/test_polyloader.py b/tests_py3/test_polyloader.py index 2f882aa..c549e34 100644 --- a/tests_py3/test_polyloader.py +++ b/tests_py3/test_polyloader.py @@ -11,6 +11,7 @@ Tests for `polyloader` module. import polyloader import copy import sys +import pytest # Note that these compilers don't actually load much out of the # source files. That's not the point. The point is to show that the @@ -28,6 +29,8 @@ class ImportEnvironment(object): self.meta_path = copy.copy(sys.meta_path) self.modules = copy.copy(sys.modules) self.path_importer_cache = copy.copy(sys.path_importer_cache) + self.dont_write_bytecode = sys.dont_write_bytecode + sys.dont_write_bytecode = True return sys def __exit__(self, type, value, traceback): @@ -36,6 +39,7 @@ class ImportEnvironment(object): sys.meta_path = self.meta_path sys.modules = self.modules sys.path_importer_cache = self.path_importer_cache + sys.dont_write_bytecode = self.dont_write_bytecode class Compiler: @@ -99,6 +103,44 @@ class Test_Polymorph_Module(object): assert(result2 == "Success for 2: Test Two") assert(result3 == "Success for 3: Test Three") +class Test_Polymorph_Reset(object): + def test_reset_after_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("2"), ['2']) + polyloader.install(compiler("3"), ['3']) + from tests_py2.polytestmix.test3 import result as result3 + polyloader.reset() + with pytest.raises(ImportError): + from tests_py2.polytestmix.test2 import result as result2 + def test_reset_before_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("3"), ['3']) + polyloader.reset() + with pytest.raises(ImportError): + from tests_py2.polytestmix.test3 import result as result3 + +class Test_Polymorph_Uninstall(object): + def test_uninstall_after_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("3"), ['3']) + polyloader.install(compiler("2"), ['2']) + import tests_py2.polytestmix.test3 + polyloader.uninstall('2') + with pytest.raises(ImportError): + import tests_py2.polytestmix.test2 + import tests_py2.polytestmix.test1 + def test_uninstall_before_import(self): + with ImportEnvironment() as sys: + polyloader.install(compiler("3"), ['3']) + polyloader.install(compiler("2"), ['2']) + polyloader.uninstall('3') + polyloader.uninstall('2') + with pytest.raises(ImportError): + import tests_py2.polytestmix.test3 + with pytest.raises(ImportError): + import tests_py2.polytestmix.test2 + import tests_py2.polytestmix.test1 + class Test_Polymorph_Iterator(object): ''' The Django Compatibility test: Can we load arbitrary modules from a package? ''' def test_iterator(self):