codekingpro/portable-devtools
114k
1import os2import operator3import sys4import contextlib5import itertools6import unittest7from distutils.errors import DistutilsError, DistutilsOptionError8from distutils import log9from unittest import TestLoader10 11from pkg_resources import (12 resource_listdir,13 resource_exists,14 normalize_path,15 working_set,16 evaluate_marker,17 add_activation_listener,18 require,19)20from .._importlib import metadata21from setuptools import Command22from setuptools.extern.more_itertools import unique_everseen23from setuptools.extern.jaraco.functools import pass_none24 25 26class ScanningLoader(TestLoader):27 def __init__(self):28 TestLoader.__init__(self)29 self._visited = set()30 31 def loadTestsFromModule(self, module, pattern=None):32 """Return a suite of all tests cases contained in the given module33 34 If the module is a package, load tests from all the modules in it.35 If the module has an ``additional_tests`` function, call it and add36 the return value to the tests.37 """38 if module in self._visited:39 return None40 self._visited.add(module)41 42 tests = []43 tests.append(TestLoader.loadTestsFromModule(self, module))44 45 if hasattr(module, "additional_tests"):46 tests.append(module.additional_tests())47 48 if hasattr(module, '__path__'):49 for file in resource_listdir(module.__name__, ''):50 if file.endswith('.py') and file != '__init__.py':51 submodule = module.__name__ + '.' + file[:-3]52 else:53 if resource_exists(module.__name__, file + '/__init__.py'):54 submodule = module.__name__ + '.' + file55 else:56 continue57 tests.append(self.loadTestsFromName(submodule))58 59 if len(tests) != 1:60 return self.suiteClass(tests)61 else:62 return tests[0] # don't create a nested suite for only one return63 64 65# adapted from jaraco.classes.properties:NonDataProperty66class NonDataProperty:67 def __init__(self, fget):68 self.fget = fget69 70 def __get__(self, obj, objtype=None):71 if obj is None:72 return self73 return self.fget(obj)74 75 76class test(Command):77 """Command to run unit tests after in-place build"""78 79 description = "run unit tests after in-place build (deprecated)"80 81 user_options = [82 ('test-module=', 'm', "Run 'test_suite' in specified module"),83 (84 'test-suite=',85 's',86 "Run single test, case or suite (e.g. 'module.test_suite')",87 ),88 ('test-runner=', 'r', "Test runner to use"),89 ]90 91 def initialize_options(self):92 self.test_suite = None93 self.test_module = None94 self.test_loader = None95 self.test_runner = None96 97 def finalize_options(self):98 if self.test_suite and self.test_module:99 msg = "You may specify a module or a suite, but not both"100 raise DistutilsOptionError(msg)101 102 if self.test_suite is None:103 if self.test_module is None:104 self.test_suite = self.distribution.test_suite105 else:106 self.test_suite = self.test_module + ".test_suite"107 108 if self.test_loader is None:109 self.test_loader = getattr(self.distribution, 'test_loader', None)110 if self.test_loader is None:111 self.test_loader = "setuptools.command.test:ScanningLoader"112 if self.test_runner is None:113 self.test_runner = getattr(self.distribution, 'test_runner', None)114 115 @NonDataProperty116 def test_args(self):117 return list(self._test_args())118 119 def _test_args(self):120 if not self.test_suite:121 yield 'discover'122 if self.verbose:123 yield '--verbose'124 if self.test_suite:125 yield self.test_suite126 127 def with_project_on_sys_path(self, func):128 """129 Backward compatibility for project_on_sys_path context.130 """131 with self.project_on_sys_path():132 func()133 134 @contextlib.contextmanager135 def project_on_sys_path(self, include_dists=()):136 self.run_command('egg_info')137 138 # Build extensions in-place139 self.reinitialize_command('build_ext', inplace=1)140 self.run_command('build_ext')141 142 ei_cmd = self.get_finalized_command("egg_info")143 144 old_path = sys.path[:]145 old_modules = sys.modules.copy()146 147 try:148 project_path = normalize_path(ei_cmd.egg_base)149 sys.path.insert(0, project_path)150 working_set.__init__()151 add_activation_listener(lambda dist: dist.activate())152 require('%s==%s' % (ei_cmd.egg_name, ei_cmd.egg_version))153 with self.paths_on_pythonpath([project_path]):154 yield155 finally:156 sys.path[:] = old_path157 sys.modules.clear()158 sys.modules.update(old_modules)159 working_set.__init__()160 161 @staticmethod162 @contextlib.contextmanager163 def paths_on_pythonpath(paths):164 """165 Add the indicated paths to the head of the PYTHONPATH environment166 variable so that subprocesses will also see the packages at167 these paths.168 169 Do this in a context that restores the value on exit.170 """171 nothing = object()172 orig_pythonpath = os.environ.get('PYTHONPATH', nothing)173 current_pythonpath = os.environ.get('PYTHONPATH', '')174 try:175 prefix = os.pathsep.join(unique_everseen(paths))176 to_join = filter(None, [prefix, current_pythonpath])177 new_path = os.pathsep.join(to_join)178 if new_path:179 os.environ['PYTHONPATH'] = new_path180 yield181 finally:182 if orig_pythonpath is nothing:183 os.environ.pop('PYTHONPATH', None)184 else:185 os.environ['PYTHONPATH'] = orig_pythonpath186 187 @staticmethod188 def install_dists(dist):189 """190 Install the requirements indicated by self.distribution and191 return an iterable of the dists that were built.192 """193 ir_d = dist.fetch_build_eggs(dist.install_requires)194 tr_d = dist.fetch_build_eggs(dist.tests_require or [])195 er_d = dist.fetch_build_eggs(196 v197 for k, v in dist.extras_require.items()198 if k.startswith(':') and evaluate_marker(k[1:])199 )200 return itertools.chain(ir_d, tr_d, er_d)201 202 def run(self):203 self.announce(204 "WARNING: Testing via this command is deprecated and will be "205 "removed in a future version. Users looking for a generic test "206 "entry point independent of test runner are encouraged to use "207 "tox.",208 log.WARN,209 )210 211 installed_dists = self.install_dists(self.distribution)212 213 cmd = ' '.join(self._argv)214 if self.dry_run:215 self.announce('skipping "%s" (dry run)' % cmd)216 return217 218 self.announce('running "%s"' % cmd)219 220 paths = map(operator.attrgetter('location'), installed_dists)221 with self.paths_on_pythonpath(paths):222 with self.project_on_sys_path():223 self.run_tests()224 225 def run_tests(self):226 test = unittest.main(227 None,228 None,229 self._argv,230 testLoader=self._resolve_as_ep(self.test_loader),231 testRunner=self._resolve_as_ep(self.test_runner),232 exit=False,233 )234 if not test.result.wasSuccessful():235 msg = 'Test failed: %s' % test.result236 self.announce(msg, log.ERROR)237 raise DistutilsError(msg)238 239 @property240 def _argv(self):241 return ['unittest'] + self.test_args242 243 @staticmethod244 @pass_none245 def _resolve_as_ep(val):246 """247 Load the indicated attribute value, called, as a as if it were248 specified as an entry point.249 """250 return metadata.EntryPoint(value=val, name=None, group=None).load()()251 