Skip to content

Commit 833b436

Browse files
committed
refactor: re-organize setup.py
1 parent f46457a commit 833b436

File tree

2 files changed

+139
-124
lines changed

2 files changed

+139
-124
lines changed

setup.py

Lines changed: 138 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,87 @@
99
import re
1010
import os
1111
import os.path
12+
import platform
1213
import site
1314
import sys
15+
import textwrap
16+
17+
from typing import Any
1418

1519
from setuptools import Extension, errors, setup
1620
from 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

97163
setup_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

212230
compile_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.

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ deps =
123123
setenv =
124124
{[testenv]setenv}
125125
PYTHONWARNDEFAULTENCODING=
126-
TYPEABLE=coverage tests
126+
TYPEABLE=coverage tests setup.py
127127

128128
commands =
129129
# PYVERSIONS

0 commit comments

Comments
 (0)