1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3
4"""Code coverage measurement for Python"""
5
6# Distutils setup for coverage.py
7# This file is used unchanged under all versions of Python, 2.x and 3.x.
8
9import os
10import sys
11
12# Setuptools has to be imported before distutils, or things break.
13from setuptools import setup
14from distutils.core import Extension                # pylint: disable=wrong-import-order
15from distutils.command.build_ext import build_ext   # pylint: disable=wrong-import-order
16from distutils import errors                        # pylint: disable=wrong-import-order
17
18
19# Get or massage our metadata.  We exec coverage/version.py so we can avoid
20# importing the product code into setup.py.
21
22classifiers = """\
23Environment :: Console
24Intended Audience :: Developers
25License :: OSI Approved :: Apache Software License
26Operating System :: OS Independent
27Programming Language :: Python
28Programming Language :: Python :: 2
29Programming Language :: Python :: 2.7
30Programming Language :: Python :: 3
31Programming Language :: Python :: 3.5
32Programming Language :: Python :: 3.6
33Programming Language :: Python :: 3.7
34Programming Language :: Python :: 3.8
35Programming Language :: Python :: 3.9
36Programming Language :: Python :: Implementation :: CPython
37Programming Language :: Python :: Implementation :: PyPy
38Topic :: Software Development :: Quality Assurance
39Topic :: Software Development :: Testing
40"""
41
42cov_ver_py = os.path.join(os.path.split(__file__)[0], "coverage/version.py")
43with open(cov_ver_py) as version_file:
44    # __doc__ will be overwritten by version.py.
45    doc = __doc__
46    # Keep pylint happy.
47    __version__ = __url__ = version_info = ""
48    # Execute the code in version.py.
49    exec(compile(version_file.read(), cov_ver_py, 'exec'))
50
51with open("README.rst") as readme:
52    long_description = readme.read().replace("https://coverage.readthedocs.io", __url__)
53
54with open("CONTRIBUTORS.txt", "rb") as contributors:
55    paras = contributors.read().split(b"\n\n")
56    num_others = len(paras[-1].splitlines())
57    num_others += 1                 # Count Gareth Rees, who is mentioned in the top paragraph.
58
59classifier_list = classifiers.splitlines()
60
61if version_info[3] == 'alpha':
62    devstat = "3 - Alpha"
63elif version_info[3] in ['beta', 'candidate']:
64    devstat = "4 - Beta"
65else:
66    assert version_info[3] == 'final'
67    devstat = "5 - Production/Stable"
68classifier_list.append("Development Status :: " + devstat)
69
70# Create the keyword arguments for setup()
71
72setup_args = dict(
73    name='coverage',
74    version=__version__,
75
76    packages=[
77        'coverage',
78    ],
79
80    package_data={
81        'coverage': [
82            'htmlfiles/*.*',
83            'fullcoverage/*.*',
84        ]
85    },
86
87    entry_points={
88        # Install a script as "coverage", and as "coverage[23]", and as
89        # "coverage-2.7" (or whatever).
90        'console_scripts': [
91            'coverage = coverage.cmdline:main',
92            'coverage%d = coverage.cmdline:main' % sys.version_info[:1],
93            'coverage-%d.%d = coverage.cmdline:main' % sys.version_info[:2],
94        ],
95    },
96
97    extras_require={
98        # Enable pyproject.toml support.
99        'toml': ['toml'],
100    },
101
102    # We need to get HTML assets from our htmlfiles directory.
103    zip_safe=False,
104
105    author='Ned Batchelder and {} others'.format(num_others),
106    author_email='ned@nedbatchelder.com',
107    description=doc,
108    long_description=long_description,
109    long_description_content_type='text/x-rst',
110    keywords='code coverage testing',
111    license='Apache 2.0',
112    classifiers=classifier_list,
113    url="https://github.com/nedbat/coveragepy",
114    project_urls={
115        'Documentation': __url__,
116        'Funding': (
117            'https://tidelift.com/subscription/pkg/pypi-coverage'
118            '?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi'
119        ),
120        'Issues': 'https://github.com/nedbat/coveragepy/issues',
121    },
122    python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4",
123)
124
125# A replacement for the build_ext command which raises a single exception
126# if the build fails, so we can fallback nicely.
127
128ext_errors = (
129    errors.CCompilerError,
130    errors.DistutilsExecError,
131    errors.DistutilsPlatformError,
132)
133if sys.platform == 'win32':
134    # distutils.msvc9compiler can raise an IOError when failing to
135    # find the compiler
136    ext_errors += (IOError,)
137
138
139class BuildFailed(Exception):
140    """Raise this to indicate the C extension wouldn't build."""
141    def __init__(self):
142        Exception.__init__(self)
143        self.cause = sys.exc_info()[1]      # work around py 2/3 different syntax
144
145
146class ve_build_ext(build_ext):
147    """Build C extensions, but fail with a straightforward exception."""
148
149    def run(self):
150        """Wrap `run` with `BuildFailed`."""
151        try:
152            build_ext.run(self)
153        except errors.DistutilsPlatformError:
154            raise BuildFailed()
155
156    def build_extension(self, ext):
157        """Wrap `build_extension` with `BuildFailed`."""
158        try:
159            # Uncomment to test compile failure handling:
160            #   raise errors.CCompilerError("OOPS")
161            build_ext.build_extension(self, ext)
162        except ext_errors:
163            raise BuildFailed()
164        except ValueError as err:
165            # this can happen on Windows 64 bit, see Python issue 7511
166            if "'path'" in str(err):    # works with both py 2/3
167                raise BuildFailed()
168            raise
169
170# There are a few reasons we might not be able to compile the C extension.
171# Figure out if we should attempt the C extension or not.
172
173compile_extension = True
174
175if sys.platform.startswith('java'):
176    # Jython can't compile C extensions
177    compile_extension = False
178
179if '__pypy__' in sys.builtin_module_names:
180    # Pypy can't compile C extensions
181    compile_extension = False
182
183if compile_extension:
184    setup_args.update(dict(
185        ext_modules=[
186            Extension(
187                "coverage.tracer",
188                sources=[
189                    "coverage/ctracer/datastack.c",
190                    "coverage/ctracer/filedisp.c",
191                    "coverage/ctracer/module.c",
192                    "coverage/ctracer/tracer.c",
193                ],
194            ),
195        ],
196        cmdclass={
197            'build_ext': ve_build_ext,
198        },
199    ))
200
201
202def main():
203    """Actually invoke setup() with the arguments we built above."""
204    # For a variety of reasons, it might not be possible to install the C
205    # extension.  Try it with, and if it fails, try it without.
206    try:
207        setup(**setup_args)
208    except BuildFailed as exc:
209        msg = "Couldn't install with extension module, trying without it..."
210        exc_msg = "%s: %s" % (exc.__class__.__name__, exc.cause)
211        print("**\n** %s\n** %s\n**" % (msg, exc_msg))
212
213        del setup_args['ext_modules']
214        setup(**setup_args)
215
216if __name__ == '__main__':
217    main()
218