Home | History | Annotate | Download | only in altgraph
      1 """
      2 Shared setup file for simple python packages. Uses a setup.cfg that
      3 is the same as the distutils2 project, unless noted otherwise.
      4 
      5 It exists for two reasons:
      6 1) This makes it easier to reuse setup.py code between my own
      7    projects
      8 
      9 2) Easier migration to distutils2 when that catches on.
     10 
     11 Additional functionality:
     12 
     13 * Section metadata:
     14     requires-test:  Same as 'tests_require' option for setuptools.
     15 
     16 """
     17 
     18 import sys
     19 import os
     20 import re
     21 import platform
     22 from fnmatch import fnmatch
     23 import os
     24 import sys
     25 import time
     26 import tempfile
     27 import tarfile
     28 try:
     29     import urllib.request as urllib
     30 except ImportError:
     31     import urllib
     32 from distutils import log
     33 try:
     34     from hashlib import md5
     35 
     36 except ImportError:
     37     from md5 import md5
     38 
     39 if sys.version_info[0] == 2:
     40     from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
     41 else:
     42     from configparser import RawConfigParser, NoOptionError, NoSectionError
     43 
     44 ROOTDIR = os.path.dirname(os.path.abspath(__file__))
     45 
     46 
     47 #
     48 #
     49 #
     50 # Parsing the setup.cfg and converting it to something that can be
     51 # used by setuptools.setup()
     52 #
     53 #
     54 #
     55 
     56 def eval_marker(value):
     57     """
     58     Evaluate an distutils2 environment marker.
     59 
     60     This code is unsafe when used with hostile setup.cfg files,
     61     but that's not a problem for our own files.
     62     """
     63     value = value.strip()
     64 
     65     class M:
     66         def __init__(self, **kwds):
     67             for k, v in kwds.items():
     68                 setattr(self, k, v)
     69 
     70     variables = {
     71         'python_version': '%d.%d'%(sys.version_info[0], sys.version_info[1]),
     72         'python_full_version': sys.version.split()[0],
     73         'os': M(
     74             name=os.name,
     75         ),
     76         'sys': M(
     77             platform=sys.platform,
     78         ),
     79         'platform': M(
     80             version=platform.version(),
     81             machine=platform.machine(),
     82         ),
     83     }
     84 
     85     return bool(eval(value, variables, variables))
     86 
     87 
     88     return True
     89 
     90 def _opt_value(cfg, into, section, key, transform = None):
     91     try:
     92         v = cfg.get(section, key)
     93         if transform != _as_lines and ';' in v:
     94             v, marker = v.rsplit(';', 1)
     95             if not eval_marker(marker):
     96                 return
     97 
     98             v = v.strip()
     99 
    100         if v:
    101             if transform:
    102                 into[key] = transform(v.strip())
    103             else:
    104                 into[key] = v.strip()
    105 
    106     except (NoOptionError, NoSectionError):
    107         pass
    108 
    109 def _as_bool(value):
    110     if value.lower() in ('y', 'yes', 'on'):
    111         return True
    112     elif value.lower() in ('n', 'no', 'off'):
    113         return False
    114     elif value.isdigit():
    115         return bool(int(value))
    116     else:
    117         raise ValueError(value)
    118 
    119 def _as_list(value):
    120     return value.split()
    121 
    122 def _as_lines(value):
    123     result = []
    124     for v in value.splitlines():
    125         if ';' in v:
    126             v, marker = v.rsplit(';', 1)
    127             if not eval_marker(marker):
    128                 continue
    129 
    130             v = v.strip()
    131             if v:
    132                 result.append(v)
    133         else:
    134             result.append(v)
    135     return result
    136 
    137 def _map_requirement(value):
    138     m = re.search(r'(\S+)\s*(?:\((.*)\))?', value)
    139     name = m.group(1)
    140     version = m.group(2)
    141 
    142     if version is None:
    143         return name
    144 
    145     else:
    146         mapped = []
    147         for v in version.split(','):
    148             v = v.strip()
    149             if v[0].isdigit():
    150                 # Checks for a specific version prefix
    151                 m = v.rsplit('.', 1)
    152                 mapped.append('>=%s,<%s.%s'%(
    153                     v, m[0], int(m[1])+1))
    154 
    155             else:
    156                 mapped.append(v)
    157         return '%s %s'%(name, ','.join(mapped),)
    158 
    159 def _as_requires(value):
    160     requires = []
    161     for req in value.splitlines():
    162         if ';' in req:
    163             req, marker = v.rsplit(';', 1)
    164             if not eval_marker(marker):
    165                 continue
    166             req = req.strip()
    167 
    168         if not req:
    169             continue
    170         requires.append(_map_requirement(req))
    171     return requires
    172 
    173 def parse_setup_cfg():
    174     cfg = RawConfigParser()
    175     r = cfg.read([os.path.join(ROOTDIR, 'setup.cfg')])
    176     if len(r) != 1:
    177         print("Cannot read 'setup.cfg'")
    178         sys.exit(1)
    179 
    180     metadata = dict(
    181             name        = cfg.get('metadata', 'name'),
    182             version     = cfg.get('metadata', 'version'),
    183             description = cfg.get('metadata', 'description'),
    184     )
    185 
    186     _opt_value(cfg, metadata, 'metadata', 'license')
    187     _opt_value(cfg, metadata, 'metadata', 'maintainer')
    188     _opt_value(cfg, metadata, 'metadata', 'maintainer_email')
    189     _opt_value(cfg, metadata, 'metadata', 'author')
    190     _opt_value(cfg, metadata, 'metadata', 'author_email')
    191     _opt_value(cfg, metadata, 'metadata', 'url')
    192     _opt_value(cfg, metadata, 'metadata', 'download_url')
    193     _opt_value(cfg, metadata, 'metadata', 'classifiers', _as_lines)
    194     _opt_value(cfg, metadata, 'metadata', 'platforms', _as_list)
    195     _opt_value(cfg, metadata, 'metadata', 'packages', _as_list)
    196     _opt_value(cfg, metadata, 'metadata', 'keywords', _as_list)
    197 
    198     try:
    199         v = cfg.get('metadata', 'requires-dist')
    200 
    201     except (NoOptionError, NoSectionError):
    202         pass
    203 
    204     else:
    205         requires = _as_requires(v)
    206         if requires:
    207             metadata['install_requires'] = requires
    208 
    209     try:
    210         v = cfg.get('metadata', 'requires-test')
    211 
    212     except (NoOptionError, NoSectionError):
    213         pass
    214 
    215     else:
    216         requires = _as_requires(v)
    217         if requires:
    218             metadata['tests_require'] = requires
    219 
    220 
    221     try:
    222         v = cfg.get('metadata', 'long_description_file')
    223     except (NoOptionError, NoSectionError):
    224         pass
    225 
    226     else:
    227         parts = []
    228         for nm in v.split():
    229             fp = open(nm, 'rU')
    230             parts.append(fp.read())
    231             fp.close()
    232 
    233         metadata['long_description'] = '\n\n'.join(parts)
    234 
    235 
    236     try:
    237         v = cfg.get('metadata', 'zip-safe')
    238     except (NoOptionError, NoSectionError):
    239         pass
    240 
    241     else:
    242         metadata['zip_safe'] = _as_bool(v)
    243 
    244     try:
    245         v = cfg.get('metadata', 'console_scripts')
    246     except (NoOptionError, NoSectionError):
    247         pass
    248 
    249     else:
    250         if 'entry_points' not in metadata:
    251             metadata['entry_points'] = {}
    252 
    253         metadata['entry_points']['console_scripts'] = v.splitlines()
    254 
    255     if sys.version_info[:2] <= (2,6):
    256         try:
    257             metadata['tests_require'] += ", unittest2"
    258         except KeyError:
    259             metadata['tests_require'] = "unittest2"
    260 
    261     return metadata
    262 
    263 
    264 #
    265 #
    266 #
    267 # Bootstrapping setuptools/distribute, based on
    268 # a heavily modified version of distribute_setup.py
    269 #
    270 #
    271 #
    272 
    273 
    274 SETUPTOOLS_PACKAGE='setuptools'
    275 
    276 
    277 try:
    278     import subprocess
    279 
    280     def _python_cmd(*args):
    281         args = (sys.executable,) + args
    282         return subprocess.call(args) == 0
    283 
    284 except ImportError:
    285     def _python_cmd(*args):
    286         args = (sys.executable,) + args
    287         new_args = []
    288         for a in args:
    289             new_args.append(a.replace("'", "'\"'\"'"))
    290         os.system(' '.join(new_args)) == 0
    291 
    292 
    293 try:
    294     import json
    295 
    296     def get_pypi_src_download(package):
    297         url = 'https://pypi.python.org/pypi/%s/json'%(package,)
    298         fp = urllib.urlopen(url)
    299         try:
    300             try:
    301                 data = fp.read()
    302 
    303             finally:
    304                 fp.close()
    305         except urllib.error:
    306             raise RuntimeError("Cannot determine download link for %s"%(package,))
    307 
    308         pkgdata = json.loads(data.decode('utf-8'))
    309         if 'urls' not in pkgdata:
    310             raise RuntimeError("Cannot determine download link for %s"%(package,))
    311 
    312         for info in pkgdata['urls']:
    313             if info['packagetype'] == 'sdist' and info['url'].endswith('tar.gz'):
    314                 return (info.get('md5_digest'), info['url'])
    315 
    316         raise RuntimeError("Cannot determine downlink link for %s"%(package,))
    317 
    318 except ImportError:
    319     # Python 2.5 compatibility, no JSON in stdlib but luckily JSON syntax is
    320     # simular enough to Python's syntax to be able to abuse the Python compiler
    321 
    322     import _ast as ast
    323 
    324     def get_pypi_src_download(package):
    325         url = 'https://pypi.python.org/pypi/%s/json'%(package,)
    326         fp = urllib.urlopen(url)
    327         try:
    328             try:
    329                 data = fp.read()
    330 
    331             finally:
    332                 fp.close()
    333         except urllib.error:
    334             raise RuntimeError("Cannot determine download link for %s"%(package,))
    335 
    336 
    337         a = compile(data, '-', 'eval', ast.PyCF_ONLY_AST)
    338         if not isinstance(a, ast.Expression):
    339             raise RuntimeError("Cannot determine download link for %s"%(package,))
    340 
    341         a = a.body
    342         if not isinstance(a, ast.Dict):
    343             raise RuntimeError("Cannot determine download link for %s"%(package,))
    344 
    345         for k, v in zip(a.keys, a.values):
    346             if not isinstance(k, ast.Str):
    347                 raise RuntimeError("Cannot determine download link for %s"%(package,))
    348 
    349             k = k.s
    350             if k == 'urls':
    351                 a = v
    352                 break
    353         else:
    354             raise RuntimeError("PyPI JSON for %s doesn't contain URLs section"%(package,))
    355 
    356         if not isinstance(a, ast.List):
    357             raise RuntimeError("Cannot determine download link for %s"%(package,))
    358 
    359         for info in v.elts:
    360             if not isinstance(info, ast.Dict):
    361                 raise RuntimeError("Cannot determine download link for %s"%(package,))
    362             url = None
    363             packagetype = None
    364             chksum = None
    365 
    366             for k, v in zip(info.keys, info.values):
    367                 if not isinstance(k, ast.Str):
    368                     raise RuntimeError("Cannot determine download link for %s"%(package,))
    369 
    370                 if k.s == 'url':
    371                     if not isinstance(v, ast.Str):
    372                         raise RuntimeError("Cannot determine download link for %s"%(package,))
    373                     url = v.s
    374 
    375                 elif k.s == 'packagetype':
    376                     if not isinstance(v, ast.Str):
    377                         raise RuntimeError("Cannot determine download link for %s"%(package,))
    378                     packagetype = v.s
    379 
    380                 elif k.s == 'md5_digest':
    381                     if not isinstance(v, ast.Str):
    382                         raise RuntimeError("Cannot determine download link for %s"%(package,))
    383                     chksum = v.s
    384 
    385             if url is not None and packagetype == 'sdist' and url.endswith('.tar.gz'):
    386                 return (chksum, url)
    387 
    388         raise RuntimeError("Cannot determine download link for %s"%(package,))
    389 
    390 def _build_egg(egg, tarball, to_dir):
    391     # extracting the tarball
    392     tmpdir = tempfile.mkdtemp()
    393     log.warn('Extracting in %s', tmpdir)
    394     old_wd = os.getcwd()
    395     try:
    396         os.chdir(tmpdir)
    397         tar = tarfile.open(tarball)
    398         _extractall(tar)
    399         tar.close()
    400 
    401         # going in the directory
    402         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
    403         os.chdir(subdir)
    404         log.warn('Now working in %s', subdir)
    405 
    406         # building an egg
    407         log.warn('Building a %s egg in %s', egg, to_dir)
    408         _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
    409 
    410     finally:
    411         os.chdir(old_wd)
    412     # returning the result
    413     log.warn(egg)
    414     if not os.path.exists(egg):
    415         raise IOError('Could not build the egg.')
    416 
    417 
    418 def _do_download(to_dir, packagename=SETUPTOOLS_PACKAGE):
    419     tarball = download_setuptools(packagename, to_dir)
    420     version = tarball.split('-')[-1][:-7]
    421     egg = os.path.join(to_dir, '%s-%s-py%d.%d.egg'
    422                        % (packagename, version, sys.version_info[0], sys.version_info[1]))
    423     if not os.path.exists(egg):
    424         _build_egg(egg, tarball, to_dir)
    425     sys.path.insert(0, egg)
    426     import setuptools
    427     setuptools.bootstrap_install_from = egg
    428 
    429 
    430 def use_setuptools():
    431     # making sure we use the absolute path
    432     return _do_download(os.path.abspath(os.curdir))
    433 
    434 def download_setuptools(packagename, to_dir):
    435     # making sure we use the absolute path
    436     to_dir = os.path.abspath(to_dir)
    437     try:
    438         from urllib.request import urlopen
    439     except ImportError:
    440         from urllib2 import urlopen
    441 
    442     chksum, url = get_pypi_src_download(packagename)
    443     tgz_name = os.path.basename(url)
    444     saveto = os.path.join(to_dir, tgz_name)
    445 
    446     src = dst = None
    447     if not os.path.exists(saveto):  # Avoid repeated downloads
    448         try:
    449             log.warn("Downloading %s", url)
    450             src = urlopen(url)
    451             # Read/write all in one block, so we don't create a corrupt file
    452             # if the download is interrupted.
    453             data = src.read()
    454 
    455             if chksum is not None:
    456                 data_sum = md5(data).hexdigest()
    457                 if data_sum != chksum:
    458                     raise RuntimeError("Downloading %s failed: corrupt checksum"%(url,))
    459 
    460 
    461             dst = open(saveto, "wb")
    462             dst.write(data)
    463         finally:
    464             if src:
    465                 src.close()
    466             if dst:
    467                 dst.close()
    468     return os.path.realpath(saveto)
    469 
    470 
    471 
    472 def _extractall(self, path=".", members=None):
    473     """Extract all members from the archive to the current working
    474        directory and set owner, modification time and permissions on
    475        directories afterwards. `path' specifies a different directory
    476        to extract to. `members' is optional and must be a subset of the
    477        list returned by getmembers().
    478     """
    479     import copy
    480     import operator
    481     from tarfile import ExtractError
    482     directories = []
    483 
    484     if members is None:
    485         members = self
    486 
    487     for tarinfo in members:
    488         if tarinfo.isdir():
    489             # Extract directories with a safe mode.
    490             directories.append(tarinfo)
    491             tarinfo = copy.copy(tarinfo)
    492             tarinfo.mode = 448 # decimal for oct 0700
    493         self.extract(tarinfo, path)
    494 
    495     # Reverse sort directories.
    496     if sys.version_info < (2, 4):
    497         def sorter(dir1, dir2):
    498             return cmp(dir1.name, dir2.name)
    499         directories.sort(sorter)
    500         directories.reverse()
    501     else:
    502         directories.sort(key=operator.attrgetter('name'), reverse=True)
    503 
    504     # Set correct owner, mtime and filemode on directories.
    505     for tarinfo in directories:
    506         dirpath = os.path.join(path, tarinfo.name)
    507         try:
    508             self.chown(tarinfo, dirpath)
    509             self.utime(tarinfo, dirpath)
    510             self.chmod(tarinfo, dirpath)
    511         except ExtractError:
    512             e = sys.exc_info()[1]
    513             if self.errorlevel > 1:
    514                 raise
    515             else:
    516                 self._dbg(1, "tarfile: %s" % e)
    517 
    518 
    519 #
    520 #
    521 #
    522 # Definitions of custom commands
    523 #
    524 #
    525 #
    526 
    527 try:
    528     import setuptools
    529 
    530 except ImportError:
    531     use_setuptools()
    532 
    533 from setuptools import setup
    534 
    535 try:
    536     from distutils.core import PyPIRCCommand
    537 except ImportError:
    538     PyPIRCCommand = None # Ancient python version
    539 
    540 from distutils.core import Command
    541 from distutils.errors  import DistutilsError
    542 from distutils import log
    543 
    544 if PyPIRCCommand is None:
    545     class upload_docs (Command):
    546         description = "upload sphinx documentation"
    547         user_options = []
    548 
    549         def initialize_options(self):
    550             pass
    551 
    552         def finalize_options(self):
    553             pass
    554 
    555         def run(self):
    556             raise DistutilsError("not supported on this version of python")
    557 
    558 else:
    559     class upload_docs (PyPIRCCommand):
    560         description = "upload sphinx documentation"
    561         user_options = PyPIRCCommand.user_options
    562 
    563         def initialize_options(self):
    564             PyPIRCCommand.initialize_options(self)
    565             self.username = ''
    566             self.password = ''
    567 
    568 
    569         def finalize_options(self):
    570             PyPIRCCommand.finalize_options(self)
    571             config = self._read_pypirc()
    572             if config != {}:
    573                 self.username = config['username']
    574                 self.password = config['password']
    575 
    576 
    577         def run(self):
    578             import subprocess
    579             import shutil
    580             import zipfile
    581             import os
    582             import urllib
    583             import StringIO
    584             from base64 import standard_b64encode
    585             import httplib
    586             import urlparse
    587 
    588             # Extract the package name from distutils metadata
    589             meta = self.distribution.metadata
    590             name = meta.get_name()
    591 
    592             # Run sphinx
    593             if os.path.exists('doc/_build'):
    594                 shutil.rmtree('doc/_build')
    595             os.mkdir('doc/_build')
    596 
    597             p = subprocess.Popen(['make', 'html'],
    598                 cwd='doc')
    599             exit = p.wait()
    600             if exit != 0:
    601                 raise DistutilsError("sphinx-build failed")
    602 
    603             # Collect sphinx output
    604             if not os.path.exists('dist'):
    605                 os.mkdir('dist')
    606             zf = zipfile.ZipFile('dist/%s-docs.zip'%(name,), 'w',
    607                     compression=zipfile.ZIP_DEFLATED)
    608 
    609             for toplevel, dirs, files in os.walk('doc/_build/html'):
    610                 for fn in files:
    611                     fullname = os.path.join(toplevel, fn)
    612                     relname = os.path.relpath(fullname, 'doc/_build/html')
    613 
    614                     print ("%s -> %s"%(fullname, relname))
    615 
    616                     zf.write(fullname, relname)
    617 
    618             zf.close()
    619 
    620             # Upload the results, this code is based on the distutils
    621             # 'upload' command.
    622             content = open('dist/%s-docs.zip'%(name,), 'rb').read()
    623 
    624             data = {
    625                 ':action': 'doc_upload',
    626                 'name': name,
    627                 'content': ('%s-docs.zip'%(name,), content),
    628             }
    629             auth = "Basic " + standard_b64encode(self.username + ":" +
    630                  self.password)
    631 
    632 
    633             boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
    634             sep_boundary = '\n--' + boundary
    635             end_boundary = sep_boundary + '--'
    636             body = StringIO.StringIO()
    637             for key, value in data.items():
    638                 if not isinstance(value, list):
    639                     value = [value]
    640 
    641                 for value in value:
    642                     if isinstance(value, tuple):
    643                         fn = ';filename="%s"'%(value[0])
    644                         value = value[1]
    645                     else:
    646                         fn = ''
    647 
    648                     body.write(sep_boundary)
    649                     body.write('\nContent-Disposition: form-data; name="%s"'%key)
    650                     body.write(fn)
    651                     body.write("\n\n")
    652                     body.write(value)
    653 
    654             body.write(end_boundary)
    655             body.write('\n')
    656             body = body.getvalue()
    657 
    658             self.announce("Uploading documentation to %s"%(self.repository,), log.INFO)
    659 
    660             schema, netloc, url, params, query, fragments = \
    661                     urlparse.urlparse(self.repository)
    662 
    663 
    664             if schema == 'http':
    665                 http = httplib.HTTPConnection(netloc)
    666             elif schema == 'https':
    667                 http = httplib.HTTPSConnection(netloc)
    668             else:
    669                 raise AssertionError("unsupported schema "+schema)
    670 
    671             data = ''
    672             loglevel = log.INFO
    673             try:
    674                 http.connect()
    675                 http.putrequest("POST", url)
    676                 http.putheader('Content-type',
    677                     'multipart/form-data; boundary=%s'%boundary)
    678                 http.putheader('Content-length', str(len(body)))
    679                 http.putheader('Authorization', auth)
    680                 http.endheaders()
    681                 http.send(body)
    682             except socket.error:
    683                 e = socket.exc_info()[1]
    684                 self.announce(str(e), log.ERROR)
    685                 return
    686 
    687             r = http.getresponse()
    688             if r.status in (200, 301):
    689                 self.announce('Upload succeeded (%s): %s' % (r.status, r.reason),
    690                     log.INFO)
    691             else:
    692                 self.announce('Upload failed (%s): %s' % (r.status, r.reason),
    693                     log.ERROR)
    694 
    695                 print ('-'*75)
    696                 print (r.read())
    697                 print ('-'*75)
    698 
    699 
    700 def recursiveGlob(root, pathPattern):
    701     """
    702     Recursively look for files matching 'pathPattern'. Return a list
    703     of matching files/directories.
    704     """
    705     result = []
    706 
    707     for rootpath, dirnames, filenames in os.walk(root):
    708         for fn in filenames:
    709             if fnmatch(fn, pathPattern):
    710                 result.append(os.path.join(rootpath, fn))
    711     return result
    712 
    713 
    714 def importExternalTestCases(unittest,
    715         pathPattern="test_*.py", root=".", package=None):
    716     """
    717     Import all unittests in the PyObjC tree starting at 'root'
    718     """
    719 
    720     testFiles = recursiveGlob(root, pathPattern)
    721     testModules = map(lambda x:x[len(root)+1:-3].replace('/', '.'), testFiles)
    722     if package is not None:
    723         testModules = [(package + '.' + m) for m in testModules]
    724 
    725     suites = []
    726 
    727     for modName in testModules:
    728         try:
    729             module = __import__(modName)
    730         except ImportError:
    731             print("SKIP %s: %s"%(modName, sys.exc_info()[1]))
    732             continue
    733 
    734         if '.' in modName:
    735             for elem in modName.split('.')[1:]:
    736                 module = getattr(module, elem)
    737 
    738         s = unittest.defaultTestLoader.loadTestsFromModule(module)
    739         suites.append(s)
    740 
    741     return unittest.TestSuite(suites)
    742 
    743 
    744 
    745 class test (Command):
    746     description = "run test suite"
    747     user_options = [
    748         ('verbosity=', None, "print what tests are run"),
    749     ]
    750 
    751     def initialize_options(self):
    752         self.verbosity='1'
    753 
    754     def finalize_options(self):
    755         if isinstance(self.verbosity, str):
    756             self.verbosity = int(self.verbosity)
    757 
    758 
    759     def cleanup_environment(self):
    760         ei_cmd = self.get_finalized_command('egg_info')
    761         egg_name = ei_cmd.egg_name.replace('-', '_')
    762 
    763         to_remove =  []
    764         for dirname in sys.path:
    765             bn = os.path.basename(dirname)
    766             if bn.startswith(egg_name + "-"):
    767                 to_remove.append(dirname)
    768 
    769         for dirname in to_remove:
    770             log.info("removing installed %r from sys.path before testing"%(
    771                 dirname,))
    772             sys.path.remove(dirname)
    773 
    774     def add_project_to_sys_path(self):
    775         from pkg_resources import normalize_path, add_activation_listener
    776         from pkg_resources import working_set, require
    777 
    778         self.reinitialize_command('egg_info')
    779         self.run_command('egg_info')
    780         self.reinitialize_command('build_ext', inplace=1)
    781         self.run_command('build_ext')
    782 
    783 
    784         # Check if this distribution is already on sys.path
    785         # and remove that version, this ensures that the right
    786         # copy of the package gets tested.
    787 
    788         self.__old_path = sys.path[:]
    789         self.__old_modules = sys.modules.copy()
    790 
    791 
    792         ei_cmd = self.get_finalized_command('egg_info')
    793         sys.path.insert(0, normalize_path(ei_cmd.egg_base))
    794         sys.path.insert(1, os.path.dirname(__file__))
    795 
    796         # Strip the namespace packages defined in this distribution
    797         # from sys.modules, needed to reset the search path for
    798         # those modules.
    799 
    800         nspkgs = getattr(self.distribution, 'namespace_packages')
    801         if nspkgs is not None:
    802             for nm in nspkgs:
    803                 del sys.modules[nm]
    804 
    805         # Reset pkg_resources state:
    806         add_activation_listener(lambda dist: dist.activate())
    807         working_set.__init__()
    808         require('%s==%s'%(ei_cmd.egg_name, ei_cmd.egg_version))
    809 
    810     def remove_from_sys_path(self):
    811         from pkg_resources import working_set
    812         sys.path[:] = self.__old_path
    813         sys.modules.clear()
    814         sys.modules.update(self.__old_modules)
    815         working_set.__init__()
    816 
    817 
    818     def run(self):
    819         import unittest
    820 
    821         # Ensure that build directory is on sys.path (py3k)
    822 
    823         self.cleanup_environment()
    824         self.add_project_to_sys_path()
    825 
    826         try:
    827             meta = self.distribution.metadata
    828             name = meta.get_name()
    829             test_pkg = name + "_tests"
    830             suite = importExternalTestCases(unittest,
    831                     "test_*.py", test_pkg, test_pkg)
    832 
    833             runner = unittest.TextTestRunner(verbosity=self.verbosity)
    834             result = runner.run(suite)
    835 
    836             # Print out summary. This is a structured format that
    837             # should make it easy to use this information in scripts.
    838             summary = dict(
    839                 count=result.testsRun,
    840                 fails=len(result.failures),
    841                 errors=len(result.errors),
    842                 xfails=len(getattr(result, 'expectedFailures', [])),
    843                 xpass=len(getattr(result, 'expectedSuccesses', [])),
    844                 skip=len(getattr(result, 'skipped', [])),
    845             )
    846             print("SUMMARY: %s"%(summary,))
    847 
    848         finally:
    849             self.remove_from_sys_path()
    850 
    851 #
    852 #
    853 #
    854 #  And finally run the setuptools main entry point.
    855 #
    856 #
    857 #
    858 
    859 metadata = parse_setup_cfg()
    860 
    861 setup(
    862     cmdclass=dict(
    863         upload_docs=upload_docs,
    864         test=test,
    865     ),
    866     **metadata
    867 )
    868