99import re
1010import os
1111import os .path
12+ import platform
1213import site
1314import sys
15+ import textwrap
16+
17+ from typing import Any
1418
1519from setuptools import Extension , errors , setup
1620from setuptools .command .build_ext import build_ext # pylint: disable=wrong-import-order
1721
18- # Get or massage our metadata. We exec coverage/version.py so we can avoid
19- # importing the product code into setup.py.
20-
21- # PYVERSIONS
22- classifiers = """\
23- Environment :: Console
24- Intended Audience :: Developers
25- Operating System :: OS Independent
26- Programming Language :: Python
27- Programming Language :: Python :: 3
28- Programming Language :: Python :: 3.10
29- Programming Language :: Python :: 3.11
30- Programming Language :: Python :: 3.12
31- Programming Language :: Python :: 3.13
32- Programming Language :: Python :: 3.14
33- Programming Language :: Python :: 3.15
34- Programming Language :: Python :: Free Threading :: 3 - Stable
35- Programming Language :: Python :: Implementation :: CPython
36- Programming Language :: Python :: Implementation :: PyPy
37- Topic :: Software Development :: Quality Assurance
38- Topic :: Software Development :: Testing
39- """
40-
41- cov_ver_py = os .path .join (os .path .split (__file__ )[0 ], "coverage/version.py" )
42- with open (cov_ver_py , encoding = "utf-8" ) as version_file :
43- # __doc__ will be overwritten by version.py.
44- doc = __doc__
45- # Keep pylint happy.
46- __version__ = __url__ = version_info = ""
47- # Execute the code in version.py.
48- exec (compile (version_file .read (), cov_ver_py , "exec" , dont_inherit = True ))
49-
50- with open ("README.rst" , encoding = "utf-8" ) as readme :
51- readme_text = readme .read ()
52-
53- temp_url = __url__ .replace ("readthedocs" , "@@" )
54- assert "@@" not in readme_text
55- long_description = (
56- readme_text .replace ("https://coverage.readthedocs.io/en/latest" , temp_url )
57- .replace ("https://coverage.readthedocs.io" , temp_url )
58- .replace ("@@" , "readthedocs" )
59- )
60-
61- with open ("CONTRIBUTORS.txt" , "rb" ) as contributors :
62- paras = contributors .read ().split (b"\n \n " )
63- num_others = len (paras [- 1 ].splitlines ())
64- num_others += 1 # Count Gareth Rees, who is mentioned in the top paragraph.
65-
66- classifier_list = classifiers .splitlines ()
6722
68- if version_info [3 ] == "alpha" :
69- devstat = "3 - Alpha"
70- elif version_info [3 ] in ["beta" , "candidate" ]:
71- devstat = "4 - Beta"
72- else :
73- assert version_info [3 ] == "final"
74- devstat = "5 - Production/Stable"
75- classifier_list .append (f"Development Status :: { devstat } " )
76-
77-
78- def do_make_pth ():
23+ def get_version_data () -> dict [str , Any ]:
24+ """Get the globals from coverage/version.py."""
25+ # We exec coverage/version.py so we can avoid importing the product code into setup.py.
26+ module_globals : dict [str , Any ] = {}
27+ cov_ver_py = os .path .join (os .path .split (__file__ )[0 ], "coverage/version.py" )
28+ with open (cov_ver_py , encoding = "utf-8" ) as version_file :
29+ # Execute the code in version.py.
30+ exec (compile (version_file .read (), cov_ver_py , "exec" , dont_inherit = True ), module_globals )
31+ return module_globals
32+
33+
34+ def get_long_description (url : str ) -> str :
35+ """Massage README.rst to get the long description."""
36+ with open ("README.rst" , encoding = "utf-8" ) as readme :
37+ readme_text = readme .read ()
38+
39+ url = url .replace ("readthedocs" , "@@" )
40+ assert "@@" not in readme_text
41+ long_description = (
42+ readme_text .replace ("https://coverage.readthedocs.io/en/latest" , url )
43+ .replace ("https://coverage.readthedocs.io" , url )
44+ .replace ("@@" , "readthedocs" )
45+ )
46+ return long_description
47+
48+
49+ def count_contributors () -> int :
50+ """Read CONTRIBUTORS.txt to count how many people have helped."""
51+ with open ("CONTRIBUTORS.txt" , "rb" ) as contributors :
52+ paras = contributors .read ().split (b"\n \n " )
53+ num_others = len (paras [- 1 ].splitlines ())
54+ num_others += 1 # Count Gareth Rees, who is mentioned in the top paragraph.
55+ return num_others
56+
57+
58+ def get_classifiers (version_info : tuple [int , int , int , str , int ]) -> list [str ]:
59+ """Build the list of classifiers"""
60+ # PYVERSIONS
61+ classifier_list = textwrap .dedent ("""\
62+ Environment :: Console
63+ Intended Audience :: Developers
64+ Operating System :: OS Independent
65+ Programming Language :: Python
66+ Programming Language :: Python :: 3
67+ Programming Language :: Python :: 3.10
68+ Programming Language :: Python :: 3.11
69+ Programming Language :: Python :: 3.12
70+ Programming Language :: Python :: 3.13
71+ Programming Language :: Python :: 3.14
72+ Programming Language :: Python :: 3.15
73+ Programming Language :: Python :: Free Threading :: 3 - Stable
74+ Programming Language :: Python :: Implementation :: CPython
75+ Programming Language :: Python :: Implementation :: PyPy
76+ Topic :: Software Development :: Quality Assurance
77+ Topic :: Software Development :: Testing
78+ """ ).splitlines ()
79+
80+ if version_info [3 ] == "alpha" :
81+ devstat = "3 - Alpha"
82+ elif version_info [3 ] in ["beta" , "candidate" ]:
83+ devstat = "4 - Beta"
84+ else :
85+ assert version_info [3 ] == "final"
86+ devstat = "5 - Production/Stable"
87+ classifier_list .append (f"Development Status :: { devstat } " )
88+
89+ return classifier_list
90+
91+
92+ def make_pth_file () -> None :
7993 """Make the packaged .pth file used for measuring subprocess coverage."""
8094
8195 with open ("coverage/pth_file.py" , encoding = "utf-8" ) as f :
@@ -90,13 +104,65 @@ def do_make_pth():
90104 pth_file .write (f"import sys; exec({ code !r} )\n " )
91105
92106
93- do_make_pth ()
107+ # A replacement for the build_ext command which raises a single exception
108+ # if the build fails, so we can fallback nicely.
109+
110+ ext_errors = (
111+ errors .CCompilerError ,
112+ errors .ExecError ,
113+ errors .PlatformError ,
114+ )
115+ if sys .platform == "win32" :
116+ # IOError can be raised when failing to find the compiler
117+ ext_errors += (IOError ,)
118+
119+
120+ class BuildFailed (Exception ):
121+ """Raise this to indicate the C extension wouldn't build."""
122+
123+ def __init__ (self ) -> None :
124+ Exception .__init__ (self )
125+ self .cause = sys .exc_info ()[1 ] # work around py 2/3 different syntax
126+
127+
128+ class ve_build_ext (build_ext ): # type: ignore[misc]
129+ """Build C extensions, but fail with a straightforward exception."""
130+
131+ def run (self ) -> None :
132+ """Wrap `run` with `BuildFailed`."""
133+ try :
134+ build_ext .run (self )
135+ except errors .PlatformError as exc :
136+ raise BuildFailed () from exc
137+
138+ def build_extension (self , ext : Any ) -> None :
139+ """Wrap `build_extension` with `BuildFailed`."""
140+ if self .compiler .compiler_type == "msvc" :
141+ ext .extra_compile_args = (ext .extra_compile_args or []) + [
142+ "/std:c11" ,
143+ "/experimental:c11atomics" ,
144+ ]
145+ try :
146+ # Uncomment to test compile failure handling:
147+ # raise errors.CCompilerError("OOPS")
148+ build_ext .build_extension (self , ext )
149+ except ext_errors as exc :
150+ raise BuildFailed () from exc
151+ except ValueError as err :
152+ # this can happen on Windows 64 bit, see Python issue 7511
153+ if "'path'" in str (err ): # works with both py 2/3
154+ raise BuildFailed () from err
155+ raise
156+
157+
158+ version_data = get_version_data ()
159+ make_pth_file ()
94160
95161# Create the keyword arguments for setup()
96162
97163setup_args = dict (
98164 name = "coverage" ,
99- version = __version__ ,
165+ version = version_data [ " __version__" ] ,
100166 packages = [
101167 "coverage" ,
102168 ],
@@ -128,20 +194,23 @@ def do_make_pth():
128194 # Enable pyproject.toml support.
129195 "toml" : ['tomli; python_full_version<="3.11.0a6"' ],
130196 },
197+ cmdclass = {
198+ "build_ext" : ve_build_ext ,
199+ },
131200 # We need to get HTML assets from our htmlfiles directory.
132201 zip_safe = False ,
133- author = f"Ned Batchelder and { num_others } others" ,
202+ author = f"Ned Batchelder and { count_contributors () } others" ,
134203 author_email = "ned@nedbatchelder.com" ,
135- description = doc ,
136- long_description = long_description ,
204+ description = __doc__ ,
205+ long_description = get_long_description ( url = version_data [ "__url__" ]) ,
137206 long_description_content_type = "text/x-rst" ,
138207 keywords = "code coverage testing" ,
139208 license = "Apache-2.0" ,
140209 license_files = ["LICENSE.txt" ],
141- classifiers = classifier_list ,
210+ classifiers = get_classifiers ( version_data [ "version_info" ]) ,
142211 url = "https://github.com/coveragepy/coveragepy" ,
143212 project_urls = {
144- "Documentation" : __url__ ,
213+ "Documentation" : version_data [ " __url__" ] ,
145214 "Funding" : (
146215 "https://tidelift.com/subscription/pkg/pypi-coverage"
147216 + "?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=pypi"
@@ -153,65 +222,14 @@ def do_make_pth():
153222 python_requires = ">=3.10" , # minimum of PYVERSIONS
154223)
155224
156- # A replacement for the build_ext command which raises a single exception
157- # if the build fails, so we can fallback nicely.
158-
159- ext_errors = (
160- errors .CCompilerError ,
161- errors .ExecError ,
162- errors .PlatformError ,
163- )
164- if sys .platform == "win32" :
165- # IOError can be raised when failing to find the compiler
166- ext_errors += (IOError ,)
167-
168-
169- class BuildFailed (Exception ):
170- """Raise this to indicate the C extension wouldn't build."""
171-
172- def __init__ (self ):
173- Exception .__init__ (self )
174- self .cause = sys .exc_info ()[1 ] # work around py 2/3 different syntax
175-
176-
177- class ve_build_ext (build_ext ):
178- """Build C extensions, but fail with a straightforward exception."""
179-
180- def run (self ):
181- """Wrap `run` with `BuildFailed`."""
182- try :
183- build_ext .run (self )
184- except errors .PlatformError as exc :
185- raise BuildFailed () from exc
186-
187- def build_extension (self , ext ):
188- """Wrap `build_extension` with `BuildFailed`."""
189- if self .compiler .compiler_type == "msvc" :
190- ext .extra_compile_args = (ext .extra_compile_args or []) + [
191- "/std:c11" ,
192- "/experimental:c11atomics" ,
193- ]
194- try :
195- # Uncomment to test compile failure handling:
196- # raise errors.CCompilerError("OOPS")
197- build_ext .build_extension (self , ext )
198- except ext_errors as exc :
199- raise BuildFailed () from exc
200- except ValueError as err :
201- # this can happen on Windows 64 bit, see Python issue 7511
202- if "'path'" in str (err ): # works with both py 2/3
203- raise BuildFailed () from err
204- raise
205-
206-
207225# There are a few reasons we might not be able to compile the C extension.
208226# Figure out if we should attempt the C extension or not. Define
209227# COVERAGE_DISABLE_EXTENSION in the build environment to explicitly disable the
210228# extension.
211229
212230compile_extension = os .getenv ("COVERAGE_DISABLE_EXTENSION" , None ) is None
213231
214- if "__pypy__" in sys . builtin_module_names :
232+ if platform . python_implementation () == "PyPy" :
215233 # Pypy can't compile C extensions
216234 compile_extension = False
217235
@@ -229,14 +247,11 @@ def build_extension(self, ext):
229247 ],
230248 ),
231249 ],
232- cmdclass = {
233- "build_ext" : ve_build_ext ,
234- },
235250 ),
236251 )
237252
238253
239- def main ():
254+ def main () -> None :
240255 """Actually invoke setup() with the arguments we built above."""
241256 # For a variety of reasons, it might not be possible to install the C
242257 # extension. Try it with, and if it fails, try it without.
0 commit comments