-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathsetup.py
executable file
·283 lines (235 loc) · 10.5 KB
/
setup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#!/usr/bin/env python
# This file is part of PyPop
# Copyright (C) 2003-2007.
# The Regents of the University of California (Regents)
# All Rights Reserved.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
# IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT,
# INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
# LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
# DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
# REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE. THE SOFTWARE AND ACCOMPANYING
# DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS
# IS". REGENTS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT,
# UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
import os
import sys
from distutils.command import clean
from glob import glob
from pathlib import Path
from sysconfig import get_config_var
import tomli
from setuptools import setup
from setuptools.command.build_ext import build_ext as _build_ext
from setuptools.command.build_py import build_py as _build_py
from setuptools.extension import Extension
class CleanCommand(clean.clean):
"""Customized clean command - removes in_place extension files if they exist"""
def run(self):
DIR = Path(__file__).resolve().parent / "src"
# generate glob pattern from extension name and suffix
ext_files = [
DIR
/ "PyPop"
/ str(
ext.name.split("PyPop.").pop()
+ ("*.pyd" if sys.platform == "win32" else "*.so")
)
for ext in extensions
]
for ext_file in ext_files:
# FIXME: use `glob.glob` for the moment, Path.glob not working
for the_ext_file in glob(str(ext_file)): # noqa: PTH207
if Path(the_ext_file).exists():
print(f"Removing in-place extension {the_ext_file}")
Path(the_ext_file).unlink()
clean.clean.run(self)
class CustomBuildExt(_build_ext):
def finalize_options(self):
super().finalize_options()
# look for libraries in _PREFIX
prefix = Path(get_config_var("prefix"))
self.library_dirs += [str(prefix / "lib")]
self.include_dirs += [str(prefix / "include")]
# also look in LIBRARY_PATH, CPATH (needed for macports etc.)
if "LIBRARY_PATH" in os.environ:
self.library_dirs += (
os.environ["LIBRARY_PATH"].rstrip(os.pathsep).split(os.pathsep)
)
if "CPATH" in os.environ:
self.include_dirs += (
os.environ["CPATH"].rstrip(os.pathsep).split(os.pathsep)
)
class CustomBuildPy(_build_py):
def run(self):
# do standard build process
super().run()
# FIXME: bit of a hack to make sure that we mirror metadata
# from pyproject and don't hardcode it in PyPop/__init__.py
# read pyproject.toml
with Path("pyproject.toml").open("rb") as f:
pyproject_data = tomli.load(f)
# extract relevant metadata
pkgname = pyproject_data["project"]["name"]
version_scheme = pyproject_data["tool"]["setuptools_scm"]["version_scheme"]
# append additional metadata to _metadata.py
metadata_content = f"""# auto-generated by setup.py
__pkgname__ = "{pkgname}"
__version_scheme__ = "{version_scheme}"
"""
# use setuptools' temp build directory
build_lib = self.get_finalized_command("build").build_lib
# write in temp build directory (to get included in wheel)
wheel_metadata_path = Path(build_lib) / "PyPop" / "_metadata.py"
print("writing metadata to be included in wheel", wheel_metadata_path)
with wheel_metadata_path.open("w") as f:
f.write(metadata_content)
# and write local directory (to be used during installation)
# FIXME: this is a bit messy
source_metadata_path = Path("src") / "PyPop" / "_metadata.py"
print("writing metadata for source", source_metadata_path)
with source_metadata_path.open("w") as f:
f.write(metadata_content)
# FIXME: need to delay this import because _metadata.py may
# not have been created yet
from src.PyPop.citation import convert_citation_formats
# if not running from a CIBUILDWHEEL environment variable
# we also need to create the citations
if os.environ.get("CIBUILDWHEEL") != "1":
# source citation path (single-source of truth)
citation_path = "CITATION.cff"
# then copy CITATION.cff to temp build directory
convert_citation_formats(build_lib, citation_path)
# convert extensions defined in `toml_path` to extensions
# FIXME: this is only necessary while we are building for Python
# that doesn't support `ext-modules` within pyproject.toml
def add_more_ext_modules_from_toml(toml_path, extensions):
with open(toml_path, "rb") as f:
config = tomli.load(f)
ext_modules_config = (
config.get("tool", {}).get("setuptools", {}).get("ext-modules", [])
)
# existing extensions names
existing_extensions = [ext.name for ext in extensions]
ext_modules = extensions
print("extensions in setup.py:", existing_extensions)
print("parsing extensions in:", toml_path)
for ext in ext_modules_config:
if ext["name"] not in existing_extensions:
print("appending extension configuration for:", ext["name"])
# translate TOML keys to kwargs for Extension
kwargs = {k.replace("-", "_"): v for k, v in ext.items() if k != "name"}
ext_modules.append(
Extension(
name=ext["name"],
**kwargs, # dynamically unpack keyword arguments
)
)
else:
print("skipping extension configuration:", ext, "already exists")
return ext_modules
# function to parse pyproject.toml and extract metadata for older Python versions
def add_metadata_from_pyproject(toml_path):
# load the pyproject.toml file
with open(toml_path, encoding="utf-8") as f:
pyproject_data = tomli.load(f)
project_data = pyproject_data.get("project", {})
setuptools_data = pyproject_data.get("tool", {}).get("setuptools", {})
scm_data = pyproject_data.get("tool", {}).get("setuptools_scm", {})
dynamic_data = setuptools_data.get("dynamic", {})
# map fields
metadata = {
"name": project_data.get("name"),
"description": project_data.get("description"),
"license": project_data.get("license", {}).get("text"),
"author": ", ".join(
[author.get("name", "") for author in project_data.get("authors", [])]
),
"maintainer": ", ".join(
[
maintainer.get("name", "")
for maintainer in project_data.get("maintainers", [])
]
),
"keywords": project_data.get("keywords", []),
"classifiers": project_data.get("classifiers", []),
"install_requires": project_data.get("dependencies", []),
"extras_require": {
"test": project_data.get("optional-dependencies", {}).get("test", [])
},
"project_urls": project_data.get("urls", {}),
"packages": setuptools_data.get("packages", {})
.get("find", {})
.get("include", []),
"package_dir": {
"": setuptools_data.get("packages", {})
.get("find", {})
.get("where", ["src"])[0]
},
"package_data": setuptools_data.get("package-data", {}),
"include_package_data": True,
"entry_points": {
"console_scripts": [
f"{script}={entry}"
for script, entry in project_data.get("scripts", {}).items()
]
},
}
# handle dynamic fields like readme and version
if "readme" in project_data.get("dynamic", []):
readme_config = dynamic_data.get("readme", {})
readme_file = readme_config.get("file", "README.md")
mime_type = readme_config.get("content-type", "text/markdown")
try:
with open(readme_file, encoding="utf-8") as f:
metadata["long_description"] = f.read()
metadata["long_description_content_type"] = mime_type
except FileNotFoundError:
print(
f"Warning: Readme file '{readme_file}' not found. Skipping long description."
)
if "version" in project_data.get("dynamic", []):
# Use setuptools_scm to handle the dynamic version
metadata["use_scm_version"] = {
"write_to": scm_data.get("write_to", "src/PyPop/_version.py"),
"version_scheme": scm_data.get("version_scheme", "post-release"),
}
# Filter out None values
return {k: v for k, v in metadata.items() if v is not None}
# extension configuration moved to extensions.toml
# if there are any extensions that can't be converted to TOML, add them here
extensions = []
# check for older Python versions
# FIXME: can drop this when we drop support for Python 3.6 wheels
if sys.version_info < (3, 7): # noqa: UP036
# populate metadata tags to send to `setup()` for backwards-compatibility
metadata = add_metadata_from_pyproject("pyproject.toml")
else:
# otherwise use on pyproject.toml directly, don't need to send metadata to `setup()`
metadata = {}
setup(
ext_modules=add_more_ext_modules_from_toml("extensions.toml", extensions),
cmdclass={
# custom clean command to remove extension files
"clean": CleanCommand,
# enable the custom build for citations
"build_py": CustomBuildPy,
# customize the build extension to read environment variables
"build_ext": CustomBuildExt,
},
**metadata, # add metadata only for older Python or where applicable
)