spin 0.8__tar.gz → 0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spin-0.8 → spin-0.9}/LICENSE +1 -1
- {spin-0.8/spin.egg-info → spin-0.9}/PKG-INFO +35 -9
- {spin-0.8 → spin-0.9}/README.md +30 -5
- {spin-0.8 → spin-0.9}/pyproject.toml +4 -3
- {spin-0.8 → spin-0.9}/spin/__init__.py +1 -1
- {spin-0.8 → spin-0.9}/spin/__main__.py +35 -15
- {spin-0.8 → spin-0.9}/spin/cmds/meson.py +272 -35
- spin-0.9/spin/tests/conftest.py +20 -0
- spin-0.9/spin/tests/test_build_cmds.py +145 -0
- spin-0.9/spin/tests/test_cli.py +17 -0
- spin-0.9/spin/tests/test_editable.py +21 -0
- spin-0.9/spin/tests/testutil.py +32 -0
- {spin-0.8 → spin-0.9/spin.egg-info}/PKG-INFO +35 -9
- {spin-0.8 → spin-0.9}/spin.egg-info/SOURCES.txt +6 -1
- {spin-0.8 → spin-0.9}/spin.egg-info/requires.txt +3 -2
- {spin-0.8 → spin-0.9}/setup.cfg +0 -0
- {spin-0.8 → spin-0.9}/spin/cmds/__init__.py +0 -0
- {spin-0.8 → spin-0.9}/spin/cmds/build.py +0 -0
- {spin-0.8 → spin-0.9}/spin/cmds/pip.py +0 -0
- {spin-0.8 → spin-0.9}/spin/cmds/util.py +0 -0
- {spin-0.8 → spin-0.9}/spin/color_format.py +0 -0
- {spin-0.8 → spin-0.9}/spin/containers.py +0 -0
- {spin-0.8 → spin-0.9}/spin/sectioned_help.py +0 -0
- {spin-0.8 → spin-0.9}/spin/tests/test_meson.py +0 -0
- {spin-0.8 → spin-0.9}/spin.egg-info/dependency_links.txt +0 -0
- {spin-0.8 → spin-0.9}/spin.egg-info/entry_points.txt +0 -0
- {spin-0.8 → spin-0.9}/spin.egg-info/top_level.txt +0 -0
{spin-0.8 → spin-0.9}/LICENSE
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9
|
|
4
4
|
Summary: Developer tool for scientific Python libraries
|
|
5
5
|
Maintainer-email: Scientific Python <spin@discuss.scientific-python.org>
|
|
6
6
|
License: BSD 3-Clause License
|
|
7
7
|
|
|
8
|
-
Copyright (c) 2021--
|
|
8
|
+
Copyright (c) 2021--2024, Scientific Python project
|
|
9
9
|
All rights reserved.
|
|
10
10
|
|
|
11
11
|
Redistribution and use in source and binary forms, with or without
|
|
@@ -53,24 +53,28 @@ License-File: LICENSE
|
|
|
53
53
|
Requires-Dist: click
|
|
54
54
|
Requires-Dist: tomli; python_version < "3.11"
|
|
55
55
|
Requires-Dist: colorama; platform_system == "Windows"
|
|
56
|
+
Requires-Dist: importlib_metadata>=7
|
|
56
57
|
Provides-Extra: lint
|
|
57
|
-
Requires-Dist: pre-commit==3.
|
|
58
|
+
Requires-Dist: pre-commit==3.7.0; extra == "lint"
|
|
58
59
|
Provides-Extra: dev
|
|
59
|
-
Requires-Dist: changelist==0.
|
|
60
|
+
Requires-Dist: changelist==0.5; extra == "dev"
|
|
60
61
|
|
|
61
62
|
# 💫 Scientific Python INcantations (`spin`)
|
|
62
63
|
|
|
63
64
|
## A developer tool for scientific Python libraries
|
|
64
65
|
|
|
65
66
|
Developers need to memorize a whole bunch of magic command-line incantations.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
The
|
|
67
|
+
These incantations may also change over time.
|
|
68
|
+
Often, Makefiles are used to provide aliases, but Makefiles can be convoluted, are not written in Python, and are hard to extend.
|
|
69
|
+
The goal of `spin` is therefore to provide a simple, user-friendly, extendable interface for common development tasks.
|
|
69
70
|
It comes with a few common build commands out the box, but can easily be customized per project.
|
|
70
71
|
|
|
71
72
|
As a curiosity: the impetus behind developing the tool was the mass migration of scientific Python libraries (SciPy, scikit-image, and NumPy, etc.) to Meson, after distutils was deprecated.
|
|
72
73
|
When many of the build and installation commands changed, it made sense to abstract away the nuisance of having to re-learn them.
|
|
73
74
|
|
|
75
|
+
_Note:_ We now have experimental builds for editable installs.
|
|
76
|
+
Most of the Meson commands listed below should work "out of the box" for those.
|
|
77
|
+
|
|
74
78
|
## Installation
|
|
75
79
|
|
|
76
80
|
```
|
|
@@ -152,7 +156,6 @@ Available as `spin.cmds.meson.*`.
|
|
|
152
156
|
docs 📖 Build Sphinx documentation
|
|
153
157
|
gdb 👾 Execute a Python snippet with GDB
|
|
154
158
|
lldb 👾 Execute a Python snippet with LLDB
|
|
155
|
-
install 💽 Build and install package using pip.
|
|
156
159
|
```
|
|
157
160
|
|
|
158
161
|
### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
|
|
@@ -207,6 +210,29 @@ def example():
|
|
|
207
210
|
print(config["tool.spin"])
|
|
208
211
|
```
|
|
209
212
|
|
|
213
|
+
### Argument overrides
|
|
214
|
+
|
|
215
|
+
Default arguments can be overridden for any command.
|
|
216
|
+
The custom command above, e.g., has the following signature:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
@click.command()
|
|
220
|
+
@click.option("-f", "--flag")
|
|
221
|
+
@click.option("-t", "--test", default="not set")
|
|
222
|
+
def example(flag, test, default_kwd=None):
|
|
223
|
+
"""🧪 Example custom command.
|
|
224
|
+
...
|
|
225
|
+
"""
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Use the `[tool.spin.kwargs]` section to override default values for
|
|
229
|
+
click options or function keywords:
|
|
230
|
+
|
|
231
|
+
```toml
|
|
232
|
+
[tool.spin.kwargs]
|
|
233
|
+
".spin/cmds.py:example" = {"test" = "default override", "default_kwd" = 3}
|
|
234
|
+
```
|
|
235
|
+
|
|
210
236
|
### Advanced: adding arguments to built-in commands
|
|
211
237
|
|
|
212
238
|
Instead of rewriting a command from scratch, a project may want to add a flag to a built-in `spin` command, or perhaps do some pre- or post-processing.
|
|
@@ -284,7 +310,7 @@ fc-cache -f -v
|
|
|
284
310
|
`spin` tests are invoked using:
|
|
285
311
|
|
|
286
312
|
```
|
|
287
|
-
nox -s
|
|
313
|
+
nox -s test
|
|
288
314
|
```
|
|
289
315
|
|
|
290
316
|
## History
|
{spin-0.8 → spin-0.9}/README.md
RENAMED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
## A developer tool for scientific Python libraries
|
|
4
4
|
|
|
5
5
|
Developers need to memorize a whole bunch of magic command-line incantations.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
The
|
|
6
|
+
These incantations may also change over time.
|
|
7
|
+
Often, Makefiles are used to provide aliases, but Makefiles can be convoluted, are not written in Python, and are hard to extend.
|
|
8
|
+
The goal of `spin` is therefore to provide a simple, user-friendly, extendable interface for common development tasks.
|
|
9
9
|
It comes with a few common build commands out the box, but can easily be customized per project.
|
|
10
10
|
|
|
11
11
|
As a curiosity: the impetus behind developing the tool was the mass migration of scientific Python libraries (SciPy, scikit-image, and NumPy, etc.) to Meson, after distutils was deprecated.
|
|
12
12
|
When many of the build and installation commands changed, it made sense to abstract away the nuisance of having to re-learn them.
|
|
13
13
|
|
|
14
|
+
_Note:_ We now have experimental builds for editable installs.
|
|
15
|
+
Most of the Meson commands listed below should work "out of the box" for those.
|
|
16
|
+
|
|
14
17
|
## Installation
|
|
15
18
|
|
|
16
19
|
```
|
|
@@ -92,7 +95,6 @@ Available as `spin.cmds.meson.*`.
|
|
|
92
95
|
docs 📖 Build Sphinx documentation
|
|
93
96
|
gdb 👾 Execute a Python snippet with GDB
|
|
94
97
|
lldb 👾 Execute a Python snippet with LLDB
|
|
95
|
-
install 💽 Build and install package using pip.
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
|
|
@@ -147,6 +149,29 @@ def example():
|
|
|
147
149
|
print(config["tool.spin"])
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
### Argument overrides
|
|
153
|
+
|
|
154
|
+
Default arguments can be overridden for any command.
|
|
155
|
+
The custom command above, e.g., has the following signature:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
@click.command()
|
|
159
|
+
@click.option("-f", "--flag")
|
|
160
|
+
@click.option("-t", "--test", default="not set")
|
|
161
|
+
def example(flag, test, default_kwd=None):
|
|
162
|
+
"""🧪 Example custom command.
|
|
163
|
+
...
|
|
164
|
+
"""
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use the `[tool.spin.kwargs]` section to override default values for
|
|
168
|
+
click options or function keywords:
|
|
169
|
+
|
|
170
|
+
```toml
|
|
171
|
+
[tool.spin.kwargs]
|
|
172
|
+
".spin/cmds.py:example" = {"test" = "default override", "default_kwd" = 3}
|
|
173
|
+
```
|
|
174
|
+
|
|
150
175
|
### Advanced: adding arguments to built-in commands
|
|
151
176
|
|
|
152
177
|
Instead of rewriting a command from scratch, a project may want to add a flag to a built-in `spin` command, or perhaps do some pre- or post-processing.
|
|
@@ -224,7 +249,7 @@ fc-cache -f -v
|
|
|
224
249
|
`spin` tests are invoked using:
|
|
225
250
|
|
|
226
251
|
```
|
|
227
|
-
nox -s
|
|
252
|
+
nox -s test
|
|
228
253
|
```
|
|
229
254
|
|
|
230
255
|
## History
|
|
@@ -25,7 +25,8 @@ classifiers = [
|
|
|
25
25
|
dependencies = [
|
|
26
26
|
"click",
|
|
27
27
|
"tomli; python_version < '3.11'",
|
|
28
|
-
"colorama; platform_system == 'Windows'"
|
|
28
|
+
"colorama; platform_system == 'Windows'",
|
|
29
|
+
"importlib_metadata >= 7"
|
|
29
30
|
]
|
|
30
31
|
dynamic = ['version']
|
|
31
32
|
|
|
@@ -33,8 +34,8 @@ dynamic = ['version']
|
|
|
33
34
|
spin = "spin.__main__:main"
|
|
34
35
|
|
|
35
36
|
[project.optional-dependencies]
|
|
36
|
-
lint = ["pre-commit == 3.
|
|
37
|
-
dev = ["changelist == 0.
|
|
37
|
+
lint = ["pre-commit == 3.7.0"]
|
|
38
|
+
dev = ["changelist == 0.5"]
|
|
38
39
|
|
|
39
40
|
[project.urls]
|
|
40
41
|
homepage = "https://github.com/scientific-python/spin"
|
|
@@ -25,10 +25,6 @@ def main():
|
|
|
25
25
|
if (len(sys.argv) == 2) and (sys.argv[1] == "help"):
|
|
26
26
|
sys.argv[1] = "--help"
|
|
27
27
|
|
|
28
|
-
def error(message):
|
|
29
|
-
print(f"Error: {message}", file=sys.stderr)
|
|
30
|
-
sys.exit(1)
|
|
31
|
-
|
|
32
28
|
def load_toml(filename):
|
|
33
29
|
if not os.path.exists(filename):
|
|
34
30
|
return None
|
|
@@ -36,7 +32,7 @@ def main():
|
|
|
36
32
|
try:
|
|
37
33
|
return tomllib.load(f)
|
|
38
34
|
except tomllib.TOMLDecodeError:
|
|
39
|
-
|
|
35
|
+
print(f"Error: cannot parse [{filename}]", file=sys.stderr)
|
|
40
36
|
|
|
41
37
|
toml_config = collections.ChainMap()
|
|
42
38
|
toml_config.maps.extend(
|
|
@@ -50,18 +46,28 @@ def main():
|
|
|
50
46
|
)
|
|
51
47
|
|
|
52
48
|
# Basic configuration validation
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
version_query = len(sys.argv) == 2 and (sys.argv[1] == "--version")
|
|
50
|
+
|
|
51
|
+
spin_config = {}
|
|
52
|
+
if not version_query:
|
|
53
|
+
if "tool.spin" in toml_config:
|
|
54
|
+
spin_config = toml_config["tool.spin"]
|
|
55
|
+
if "tool.spin.commands" not in toml_config:
|
|
56
|
+
print(
|
|
57
|
+
"Error: configuration is missing section [tool.spin.commands]\n",
|
|
58
|
+
file=sys.stderr,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
print(
|
|
62
|
+
"Error: need valid configuration in [.spin.toml], [spin.toml], or [pyproject.toml]\n"
|
|
63
|
+
"See https://github.com/scientific-python/spin/blob/main/README.md\n",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
59
66
|
|
|
60
|
-
spin_config = toml_config["tool.spin"]
|
|
61
67
|
proj_name = (
|
|
62
68
|
toml_config.get("project.name")
|
|
63
69
|
or spin_config.get("package")
|
|
64
|
-
or "unknown project"
|
|
70
|
+
or "[unknown project]"
|
|
65
71
|
)
|
|
66
72
|
|
|
67
73
|
@click.group(help=f"Developer tool for {proj_name}", cls=SectionedHelpGroup)
|
|
@@ -72,7 +78,7 @@ def main():
|
|
|
72
78
|
ctx.meta["commands"] = ctx.command.section_commands
|
|
73
79
|
ctx.show_default = True
|
|
74
80
|
|
|
75
|
-
config_cmds = spin_config
|
|
81
|
+
config_cmds = spin_config.get("commands", [])
|
|
76
82
|
# Commands can be provided as a list, or as a dictionary
|
|
77
83
|
# so that they can be sorted into sections
|
|
78
84
|
if isinstance(config_cmds, list):
|
|
@@ -88,6 +94,7 @@ def main():
|
|
|
88
94
|
"spin.python": _cmds.meson.python,
|
|
89
95
|
"spin.shell": _cmds.meson.shell,
|
|
90
96
|
}
|
|
97
|
+
cmd_default_kwargs = toml_config.get("tool.spin.kwargs", {})
|
|
91
98
|
|
|
92
99
|
for section, cmds in config_cmds.items():
|
|
93
100
|
for cmd in cmds:
|
|
@@ -127,6 +134,18 @@ def main():
|
|
|
127
134
|
print(f"!! Could not load command `{func}` from file `{path}`.\n")
|
|
128
135
|
continue
|
|
129
136
|
|
|
137
|
+
default_kwargs = cmd_default_kwargs.get(cmd)
|
|
138
|
+
import functools
|
|
139
|
+
|
|
140
|
+
if default_kwargs:
|
|
141
|
+
callback = cmd_func.callback
|
|
142
|
+
cmd_func.callback = functools.partial(callback, **default_kwargs)
|
|
143
|
+
|
|
144
|
+
# Also override option defaults
|
|
145
|
+
for option in cmd_func.params:
|
|
146
|
+
if option.name in default_kwargs:
|
|
147
|
+
option.default = default_kwargs[option.name]
|
|
148
|
+
|
|
130
149
|
commands[cmd] = cmd_func
|
|
131
150
|
|
|
132
151
|
group.add_command(commands[cmd], section=section)
|
|
@@ -134,7 +153,8 @@ def main():
|
|
|
134
153
|
try:
|
|
135
154
|
group()
|
|
136
155
|
except Exception as e:
|
|
137
|
-
|
|
156
|
+
print(f"Error: {e}; aborting.", file=sys.stderr)
|
|
157
|
+
sys.exit(1)
|
|
138
158
|
|
|
139
159
|
|
|
140
160
|
if __name__ == "__main__":
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
+
import copy
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
5
|
+
import re
|
|
4
6
|
import shutil
|
|
5
7
|
import sys
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
6
10
|
|
|
7
11
|
import click
|
|
8
12
|
|
|
@@ -10,6 +14,14 @@ from .util import get_commands, get_config
|
|
|
10
14
|
from .util import run as _run
|
|
11
15
|
|
|
12
16
|
install_dir = "build-install"
|
|
17
|
+
build_dir = "build"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GcovReportFormat(str, Enum):
|
|
21
|
+
html = "html"
|
|
22
|
+
xml = "xml"
|
|
23
|
+
text = "text"
|
|
24
|
+
sonarqube = "sonarqube"
|
|
13
25
|
|
|
14
26
|
|
|
15
27
|
# Allow specification of meson binary in configuration
|
|
@@ -25,6 +37,32 @@ def _meson_cli():
|
|
|
25
37
|
return [meson_cli]
|
|
26
38
|
|
|
27
39
|
|
|
40
|
+
def _editable_install_path(distname):
|
|
41
|
+
import importlib_metadata
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
dist = importlib_metadata.Distribution.from_name(distname)
|
|
45
|
+
except importlib_metadata.PackageNotFoundError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if getattr(dist.origin.dir_info, "editable", False):
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
return dist.origin.url.removeprefix("file:///")
|
|
51
|
+
else:
|
|
52
|
+
return dist.origin.url.removeprefix("file://")
|
|
53
|
+
else:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_editable_install(distname):
|
|
58
|
+
return _editable_install_path(distname) is not None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_editable_install_of_same_source(distname):
|
|
62
|
+
editable_path = _editable_install_path(distname)
|
|
63
|
+
return editable_path and os.path.samefile(_editable_install_path(distname), ".")
|
|
64
|
+
|
|
65
|
+
|
|
28
66
|
def _set_pythonpath(quiet=False):
|
|
29
67
|
"""Set first entry of PYTHONPATH to site packages directory.
|
|
30
68
|
|
|
@@ -35,6 +73,29 @@ def _set_pythonpath(quiet=False):
|
|
|
35
73
|
site_packages = _get_site_packages()
|
|
36
74
|
env = os.environ
|
|
37
75
|
|
|
76
|
+
cfg = get_config()
|
|
77
|
+
distname = cfg.get("project.name", None)
|
|
78
|
+
if distname:
|
|
79
|
+
if _is_editable_install(distname):
|
|
80
|
+
if _is_editable_install_of_same_source(distname):
|
|
81
|
+
if not (quiet):
|
|
82
|
+
click.secho(
|
|
83
|
+
"Editable install of same source directory detected; not setting PYTHONPATH",
|
|
84
|
+
fg="bright_red",
|
|
85
|
+
)
|
|
86
|
+
return ""
|
|
87
|
+
else:
|
|
88
|
+
# Ignoring the quiet flag, because picking up the wrong package is problematic
|
|
89
|
+
click.secho(
|
|
90
|
+
f"Warning! Editable install of `{distname}`, from a different source location, detected.",
|
|
91
|
+
fg="bright_red",
|
|
92
|
+
)
|
|
93
|
+
click.secho("Spin commands will pick up that version.", fg="bright_red")
|
|
94
|
+
click.secho(
|
|
95
|
+
f"Try removing the other installation by switching to its source and running `pip uninstall {distname}`.",
|
|
96
|
+
fg="bright_red",
|
|
97
|
+
)
|
|
98
|
+
|
|
38
99
|
if "PYTHONPATH" in env:
|
|
39
100
|
env["PYTHONPATH"] = f"{site_packages}{os.pathsep}{env['PYTHONPATH']}"
|
|
40
101
|
else:
|
|
@@ -49,6 +110,15 @@ def _set_pythonpath(quiet=False):
|
|
|
49
110
|
|
|
50
111
|
|
|
51
112
|
def _get_site_packages():
|
|
113
|
+
try:
|
|
114
|
+
cfg = get_config()
|
|
115
|
+
distname = cfg.get("project.name", None)
|
|
116
|
+
if _is_editable_install_of_same_source(distname):
|
|
117
|
+
return ""
|
|
118
|
+
except RuntimeError:
|
|
119
|
+
# Probably not running in click
|
|
120
|
+
pass
|
|
121
|
+
|
|
52
122
|
candidate_paths = []
|
|
53
123
|
for root, dirs, _files in os.walk(install_dir):
|
|
54
124
|
for subdir in dirs:
|
|
@@ -103,14 +173,67 @@ def _meson_version_configured():
|
|
|
103
173
|
pass
|
|
104
174
|
|
|
105
175
|
|
|
176
|
+
def _meson_coverage_configured() -> bool:
|
|
177
|
+
try:
|
|
178
|
+
build_options_fn = os.path.join(
|
|
179
|
+
"build", "meson-info", "intro-buildoptions.json"
|
|
180
|
+
)
|
|
181
|
+
with open(build_options_fn) as f:
|
|
182
|
+
build_options = json.load(f)
|
|
183
|
+
for b in build_options:
|
|
184
|
+
if (b["name"] == "b_coverage") and (b["value"] is True):
|
|
185
|
+
return True
|
|
186
|
+
except:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
|
|
193
|
+
requirements = { # https://github.com/mesonbuild/meson/blob/6e381714c7cb15877e2bcaa304b93c212252ada3/docs/markdown/Unit-tests.md?plain=1#L49-L62
|
|
194
|
+
GcovReportFormat.html: ["Gcovr/GenHTML", "lcov"],
|
|
195
|
+
GcovReportFormat.xml: ["Gcovr (version 3.3 or higher)"],
|
|
196
|
+
GcovReportFormat.text: ["Gcovr (version 3.3 or higher)"],
|
|
197
|
+
GcovReportFormat.sonarqube: ["Gcovr (version 4.2 or higher)"],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# First check the presence of a valid build
|
|
201
|
+
if not (os.path.exists(build_dir)):
|
|
202
|
+
raise click.ClickException(
|
|
203
|
+
f"`{build_dir}` folder not found, cannot generate coverage reports. "
|
|
204
|
+
"Generate coverage artefacts by running `spin test --gcov`"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
debug_files = Path(build_dir).rglob("*.gcno")
|
|
208
|
+
if len(list(debug_files)) == 0:
|
|
209
|
+
raise click.ClickException(
|
|
210
|
+
"Debug build not found, cannot generate coverage reports.\n\n"
|
|
211
|
+
"Please rebuild using `spin build --clean --gcov` first."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Verify the tools are installed prior to the build
|
|
215
|
+
p = _run(["ninja", "-C", build_dir, "-t", "targets", "all"], output=False)
|
|
216
|
+
if f"coverage-{coverage_type.value}" not in p.stdout.decode("ascii"):
|
|
217
|
+
raise click.ClickException(
|
|
218
|
+
f"coverage-{coverage_type.value} is not supported... "
|
|
219
|
+
f"Ensure the following are installed: {', '.join(requirements[coverage_type])} "
|
|
220
|
+
"and rerun `spin test --gcov`"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
106
224
|
@click.command()
|
|
107
225
|
@click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int)
|
|
108
226
|
@click.option("--clean", is_flag=True, help="Clean build directory before build")
|
|
109
227
|
@click.option(
|
|
110
228
|
"-v", "--verbose", is_flag=True, help="Print detailed build and installation output"
|
|
111
229
|
)
|
|
230
|
+
@click.option(
|
|
231
|
+
"--gcov",
|
|
232
|
+
is_flag=True,
|
|
233
|
+
help="Enable C code coverage using `gcov`. Use `spin test --gcov` to generate reports.",
|
|
234
|
+
)
|
|
112
235
|
@click.argument("meson_args", nargs=-1)
|
|
113
|
-
def build(meson_args, jobs=None, clean=False, verbose=False, quiet=False):
|
|
236
|
+
def build(meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=False):
|
|
114
237
|
"""🔧 Build package with Meson/ninja and install
|
|
115
238
|
|
|
116
239
|
MESON_ARGS are passed through e.g.:
|
|
@@ -128,8 +251,22 @@ def build(meson_args, jobs=None, clean=False, verbose=False, quiet=False):
|
|
|
128
251
|
|
|
129
252
|
CFLAGS="-O0 -g" spin build
|
|
130
253
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
254
|
+
cfg = get_config()
|
|
255
|
+
distname = cfg.get("project.name", None)
|
|
256
|
+
if distname and _is_editable_install_of_same_source(distname):
|
|
257
|
+
if not quiet:
|
|
258
|
+
click.secho(
|
|
259
|
+
"Editable install of same source detected; skipping build",
|
|
260
|
+
fg="bright_red",
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
meson_args = list(meson_args)
|
|
265
|
+
|
|
266
|
+
if gcov:
|
|
267
|
+
meson_args = meson_args + ["-Db_coverage=true"]
|
|
268
|
+
|
|
269
|
+
setup_cmd = _meson_cli() + ["setup", build_dir, "--prefix=/usr"] + meson_args
|
|
133
270
|
|
|
134
271
|
if clean:
|
|
135
272
|
print(f"Removing `{build_dir}`")
|
|
@@ -149,7 +286,9 @@ def build(meson_args, jobs=None, clean=False, verbose=False, quiet=False):
|
|
|
149
286
|
# Build dir has been configured; check if it was configured by
|
|
150
287
|
# current version of Meson
|
|
151
288
|
|
|
152
|
-
if _meson_version() != _meson_version_configured()
|
|
289
|
+
if (_meson_version() != _meson_version_configured()) or (
|
|
290
|
+
gcov and not _meson_coverage_configured()
|
|
291
|
+
):
|
|
153
292
|
_run(setup_cmd + ["--reconfigure"], output=not quiet)
|
|
154
293
|
|
|
155
294
|
# Any other conditions that warrant a reconfigure?
|
|
@@ -213,10 +352,30 @@ Which tests to run. Can be a module, function, class, or method:
|
|
|
213
352
|
"-c",
|
|
214
353
|
"--coverage",
|
|
215
354
|
is_flag=True,
|
|
216
|
-
help="Generate a coverage report of executed tests. An HTML copy of the report is written to `build/coverage`.",
|
|
355
|
+
help="Generate a Python coverage report of executed tests. An HTML copy of the report is written to `build/coverage`.",
|
|
356
|
+
)
|
|
357
|
+
@click.option(
|
|
358
|
+
"--gcov",
|
|
359
|
+
is_flag=True,
|
|
360
|
+
help="Generate a C coverage report in `build/meson-logs/coveragereport`.",
|
|
361
|
+
)
|
|
362
|
+
@click.option(
|
|
363
|
+
"--gcov-format",
|
|
364
|
+
type=click.Choice(GcovReportFormat),
|
|
365
|
+
default="html",
|
|
366
|
+
help=f"Format of the gcov report. Can be one of {', '.join(e.value for e in GcovReportFormat)}.",
|
|
217
367
|
)
|
|
218
368
|
@click.pass_context
|
|
219
|
-
def test(
|
|
369
|
+
def test(
|
|
370
|
+
ctx,
|
|
371
|
+
pytest_args,
|
|
372
|
+
n_jobs,
|
|
373
|
+
tests,
|
|
374
|
+
verbose,
|
|
375
|
+
coverage=False,
|
|
376
|
+
gcov=None,
|
|
377
|
+
gcov_format=None,
|
|
378
|
+
):
|
|
220
379
|
"""🔧 Run tests
|
|
221
380
|
|
|
222
381
|
PYTEST_ARGS are passed through directly to pytest, e.g.:
|
|
@@ -253,16 +412,28 @@ def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False):
|
|
|
253
412
|
|
|
254
413
|
spin test -j auto
|
|
255
414
|
|
|
415
|
+
To run a test inside of LLDB:
|
|
416
|
+
|
|
417
|
+
spin lldb -- python -mspin test ...
|
|
418
|
+
|
|
256
419
|
For more, see `pytest --help`.
|
|
257
420
|
""" # noqa: E501
|
|
258
421
|
cfg = get_config()
|
|
422
|
+
distname = cfg.get("project.name", None)
|
|
423
|
+
|
|
424
|
+
if gcov and distname and _is_editable_install_of_same_source(distname):
|
|
425
|
+
click.secho(
|
|
426
|
+
"Error: cannot generate coverage report for editable installs",
|
|
427
|
+
fg="bright_red",
|
|
428
|
+
)
|
|
429
|
+
raise SystemExit(-1)
|
|
259
430
|
|
|
260
431
|
build_cmd = _get_configured_command("build")
|
|
261
432
|
if build_cmd:
|
|
262
433
|
click.secho(
|
|
263
434
|
"Invoking `build` prior to running tests:", bold=True, fg="bright_green"
|
|
264
435
|
)
|
|
265
|
-
ctx.invoke(build_cmd)
|
|
436
|
+
ctx.invoke(build_cmd, gcov=bool(gcov))
|
|
266
437
|
|
|
267
438
|
package = cfg.get("tool.spin.package", None)
|
|
268
439
|
if (not pytest_args) and (not tests):
|
|
@@ -274,6 +445,8 @@ def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False):
|
|
|
274
445
|
sys.exit(1)
|
|
275
446
|
|
|
276
447
|
site_path = _set_pythonpath()
|
|
448
|
+
if site_path:
|
|
449
|
+
print(f'$ export PYTHONPATH="{site_path}"')
|
|
277
450
|
|
|
278
451
|
# Sanity check that library built properly
|
|
279
452
|
#
|
|
@@ -313,12 +486,54 @@ def test(ctx, pytest_args, n_jobs, tests, verbose, coverage=False):
|
|
|
313
486
|
f"--cov={package}",
|
|
314
487
|
]
|
|
315
488
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
489
|
+
if sys.version_info[:2] >= (3, 11):
|
|
490
|
+
cmd = [sys.executable, "-P", "-m", "pytest"]
|
|
491
|
+
else:
|
|
492
|
+
cmd = ["pytest"]
|
|
493
|
+
|
|
494
|
+
cwd = os.getcwd()
|
|
495
|
+
pytest_p = _run(
|
|
496
|
+
cmd + ([f"--rootdir={site_path}"] if site_path else []) + list(pytest_args),
|
|
319
497
|
cwd=site_path,
|
|
320
|
-
replace=True,
|
|
321
498
|
)
|
|
499
|
+
os.chdir(cwd)
|
|
500
|
+
|
|
501
|
+
if gcov:
|
|
502
|
+
# Verify the tools are present
|
|
503
|
+
click.secho(
|
|
504
|
+
"Verifying gcov dependencies...",
|
|
505
|
+
bold=True,
|
|
506
|
+
fg="bright_yellow",
|
|
507
|
+
)
|
|
508
|
+
_check_coverage_tool_installation(gcov_format)
|
|
509
|
+
|
|
510
|
+
# Generate report
|
|
511
|
+
click.secho(
|
|
512
|
+
f"Generating {gcov_format.value} coverage report...",
|
|
513
|
+
bold=True,
|
|
514
|
+
fg="bright_yellow",
|
|
515
|
+
)
|
|
516
|
+
p = _run(
|
|
517
|
+
[
|
|
518
|
+
"ninja",
|
|
519
|
+
"-C",
|
|
520
|
+
build_dir,
|
|
521
|
+
f"coverage-{gcov_format.value.lower()}",
|
|
522
|
+
],
|
|
523
|
+
output=False,
|
|
524
|
+
)
|
|
525
|
+
coverage_folder = click.style(
|
|
526
|
+
re.search(r"file://(.*)", p.stdout.decode("utf-8")).group(1),
|
|
527
|
+
bold=True,
|
|
528
|
+
fg="bright_yellow",
|
|
529
|
+
)
|
|
530
|
+
click.secho(
|
|
531
|
+
f"Coverage report generated successfully and written to {coverage_folder}",
|
|
532
|
+
bold=True,
|
|
533
|
+
fg="bright_green",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
raise SystemExit(pytest_p.returncode)
|
|
322
537
|
|
|
323
538
|
|
|
324
539
|
@click.command()
|
|
@@ -392,7 +607,8 @@ def ipython(ctx, ipython_args):
|
|
|
392
607
|
ctx.invoke(build_cmd)
|
|
393
608
|
|
|
394
609
|
p = _set_pythonpath()
|
|
395
|
-
|
|
610
|
+
if p:
|
|
611
|
+
print(f'💻 Launching IPython with PYTHONPATH="{p}"')
|
|
396
612
|
_run(["ipython", "--ignore-cwd"] + list(ipython_args), replace=True)
|
|
397
613
|
|
|
398
614
|
|
|
@@ -417,9 +633,11 @@ def shell(ctx, shell_args=[]):
|
|
|
417
633
|
ctx.invoke(build_cmd)
|
|
418
634
|
|
|
419
635
|
p = _set_pythonpath()
|
|
636
|
+
if p:
|
|
637
|
+
print(f'💻 Launching shell with PYTHONPATH="{p}"')
|
|
638
|
+
|
|
420
639
|
shell = os.environ.get("SHELL", "sh")
|
|
421
640
|
cmd = [shell] + list(shell_args)
|
|
422
|
-
print(f'💻 Launching shell with PYTHONPATH="{p}"')
|
|
423
641
|
print("⚠ Change directory to avoid importing source instead of built package")
|
|
424
642
|
print("⚠ Ensure that your ~/.shellrc does not unset PYTHONPATH")
|
|
425
643
|
_run(cmd, replace=True)
|
|
@@ -443,6 +661,9 @@ def python(ctx, python_args):
|
|
|
443
661
|
ctx.invoke(build_cmd)
|
|
444
662
|
|
|
445
663
|
p = _set_pythonpath()
|
|
664
|
+
if p:
|
|
665
|
+
print(f'🐍 Launching Python with PYTHONPATH="{p}"')
|
|
666
|
+
|
|
446
667
|
v = sys.version_info
|
|
447
668
|
if (v.major < 3) or (v.major == 3 and v.minor < 11):
|
|
448
669
|
print("We're sorry, but this feature only works on Python 3.11 and greater 😢")
|
|
@@ -460,9 +681,7 @@ def python(ctx, python_args):
|
|
|
460
681
|
print("import sys; del(sys.path[0])")
|
|
461
682
|
sys.exit(-1)
|
|
462
683
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
_run(["/usr/bin/env", "python", "-P"] + list(python_args), replace=True)
|
|
684
|
+
_run([sys.executable, "-P"] + list(python_args), replace=True)
|
|
466
685
|
|
|
467
686
|
|
|
468
687
|
@click.command(context_settings={"ignore_unknown_options": True})
|
|
@@ -496,15 +715,28 @@ def run(ctx, args):
|
|
|
496
715
|
|
|
497
716
|
is_posix = sys.platform in ("linux", "darwin")
|
|
498
717
|
shell = len(args) == 1
|
|
718
|
+
cmd_args = copy.copy(args)
|
|
499
719
|
if shell:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
args = ["bash", "-c", args]
|
|
720
|
+
cmd_args = args[0]
|
|
721
|
+
if not is_posix:
|
|
722
|
+
# On Windows, we're going to try to use bash
|
|
723
|
+
cmd_args = ["bash", "-c", cmd_args]
|
|
505
724
|
|
|
506
725
|
_set_pythonpath(quiet=True)
|
|
507
|
-
_run(
|
|
726
|
+
p = _run(cmd_args, echo=False, shell=shell, sys_exit=False)
|
|
727
|
+
|
|
728
|
+
# Is the user trying to run a Python script, without calling the Python interpreter?
|
|
729
|
+
executable = args[0]
|
|
730
|
+
if (
|
|
731
|
+
(p.returncode != 0)
|
|
732
|
+
and args[0].endswith(".py")
|
|
733
|
+
and os.path.exists(executable)
|
|
734
|
+
and (not os.access(executable, os.X_OK))
|
|
735
|
+
):
|
|
736
|
+
click.secho(
|
|
737
|
+
f"Did you mean to call `spin run python {' '.join(args)}`?", fg="bright_red"
|
|
738
|
+
)
|
|
739
|
+
sys.exit(p.returncode)
|
|
508
740
|
|
|
509
741
|
|
|
510
742
|
@click.command()
|
|
@@ -562,15 +794,15 @@ def docs(ctx, sphinx_target, clean, first_build, jobs, sphinx_gallery_plot):
|
|
|
562
794
|
|
|
563
795
|
if clean:
|
|
564
796
|
doc_dirs = [
|
|
565
|
-
"./
|
|
566
|
-
"./
|
|
567
|
-
"./
|
|
568
|
-
"./
|
|
797
|
+
f"./{doc_dir}/build/",
|
|
798
|
+
f"./{doc_dir}/source/api/",
|
|
799
|
+
f"./{doc_dir}/source/auto_examples/",
|
|
800
|
+
f"./{doc_dir}/source/jupyterlite_contents/",
|
|
569
801
|
]
|
|
570
|
-
for
|
|
571
|
-
if os.path.isdir(
|
|
572
|
-
print(f"Removing {
|
|
573
|
-
shutil.rmtree(
|
|
802
|
+
for target_dir in doc_dirs:
|
|
803
|
+
if os.path.isdir(target_dir):
|
|
804
|
+
print(f"Removing {target_dir!r}")
|
|
805
|
+
shutil.rmtree(target_dir)
|
|
574
806
|
|
|
575
807
|
build_cmd = _get_configured_command("build")
|
|
576
808
|
|
|
@@ -596,11 +828,16 @@ def docs(ctx, sphinx_target, clean, first_build, jobs, sphinx_gallery_plot):
|
|
|
596
828
|
f"$ export SPHINXOPTS={os.environ['SPHINXOPTS']}", bold=True, fg="bright_blue"
|
|
597
829
|
)
|
|
598
830
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
831
|
+
if site_path:
|
|
832
|
+
os.environ["PYTHONPATH"] = (
|
|
833
|
+
f'{site_path}{os.sep}:{os.environ.get("PYTHONPATH", "")}'
|
|
834
|
+
)
|
|
835
|
+
click.secho(
|
|
836
|
+
f"$ export PYTHONPATH={os.environ['PYTHONPATH']}",
|
|
837
|
+
bold=True,
|
|
838
|
+
fg="bright_blue",
|
|
839
|
+
)
|
|
840
|
+
_run(["make", "-C", doc_dir, sphinx_target], replace=True)
|
|
604
841
|
|
|
605
842
|
|
|
606
843
|
@click.command()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from spin import util
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def pre_post_test():
|
|
10
|
+
# Pre-test code
|
|
11
|
+
cwd = os.getcwd()
|
|
12
|
+
os.chdir("example_pkg")
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
yield
|
|
16
|
+
finally:
|
|
17
|
+
# Post test code
|
|
18
|
+
os.chdir(cwd)
|
|
19
|
+
util.run(["git", "clean", "-xdf"], cwd="example_pkg")
|
|
20
|
+
os.chdir(cwd)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from testutil import skip_on_windows, skip_unless_linux, spin, stdout
|
|
9
|
+
|
|
10
|
+
from spin.cmds.util import run
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_basic_build():
|
|
14
|
+
"""Does the package build?"""
|
|
15
|
+
spin("build")
|
|
16
|
+
|
|
17
|
+
assert Path("build").exists(), "`build` folder not created after `spin build`"
|
|
18
|
+
assert Path(
|
|
19
|
+
"build-install"
|
|
20
|
+
).exists(), "`build-install` folder not created after `spin build`"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_debug_builds():
|
|
24
|
+
"""Does spin generate gcov debug output files?"""
|
|
25
|
+
spin("build", "--gcov")
|
|
26
|
+
|
|
27
|
+
debug_files = Path(".").rglob("*.gcno")
|
|
28
|
+
assert len(list(debug_files)) != 0, "debug files not generated for gcov build"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_coverage_builds():
|
|
32
|
+
"""Does gcov test generate coverage files?"""
|
|
33
|
+
spin("test", "--gcov")
|
|
34
|
+
|
|
35
|
+
coverage_files = Path(".").rglob("*.gcda")
|
|
36
|
+
assert len(list(coverage_files)) != 0, "coverage files not generated for gcov build"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize(
|
|
40
|
+
"report_type,output_file",
|
|
41
|
+
[
|
|
42
|
+
("html", Path("coveragereport/index.html")),
|
|
43
|
+
("xml", Path("coverage.xml")),
|
|
44
|
+
("text", Path("coverage.txt")),
|
|
45
|
+
("sonarqube", Path("sonarqube.xml")),
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
def test_coverage_reports(report_type, output_file):
|
|
49
|
+
"""Does gcov test generate coverage reports?"""
|
|
50
|
+
spin("test", "--gcov", f"--gcov-format={report_type}")
|
|
51
|
+
|
|
52
|
+
coverage_report = Path("./build/meson-logs", output_file)
|
|
53
|
+
assert (
|
|
54
|
+
coverage_report.exists()
|
|
55
|
+
), f"coverage report not generated for gcov build ({report_type})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_expand_pythonpath():
|
|
59
|
+
"""Does an $ENV_VAR get expanded in `spin run`?"""
|
|
60
|
+
output = spin("run", "echo $PYTHONPATH")
|
|
61
|
+
assert any(
|
|
62
|
+
p in stdout(output) for p in ("site-packages", "dist-packages")
|
|
63
|
+
), f"Expected value of $PYTHONPATH, got {stdout(output)} instead"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_run_stdout():
|
|
67
|
+
"""Ensure `spin run` only includes command output on stdout."""
|
|
68
|
+
p = spin(
|
|
69
|
+
"run",
|
|
70
|
+
sys.executable,
|
|
71
|
+
"-c",
|
|
72
|
+
"import sys; del sys.path[0]; import example_pkg; print(example_pkg.__version__)",
|
|
73
|
+
)
|
|
74
|
+
assert (
|
|
75
|
+
stdout(p) == "0.0.0dev0"
|
|
76
|
+
), f"`spin run` stdout did not yield version, but {stdout(p)}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Detecting whether a file is executable is not that easy on Windows,
|
|
80
|
+
# as it seems to take into consideration whether that file is associated as an executable.
|
|
81
|
+
@skip_on_windows
|
|
82
|
+
def test_recommend_run_python():
|
|
83
|
+
"""If `spin run file.py` is called, is `spin run python file.py` recommended?"""
|
|
84
|
+
with tempfile.NamedTemporaryFile(suffix=".py") as f:
|
|
85
|
+
p = spin("run", f.name, sys_exit=False)
|
|
86
|
+
assert "Did you mean to call" in stdout(
|
|
87
|
+
p
|
|
88
|
+
), "Failed to recommend `python run python file.py`"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_test():
|
|
92
|
+
"""Does the test command run?"""
|
|
93
|
+
spin("test")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_test_with_pythonpath():
|
|
97
|
+
"""Does `spin test` work when PYTHONPATH is set?"""
|
|
98
|
+
spin("test", env={**os.environ, "PYTHONPATH": "/tmp"})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_sdist():
|
|
102
|
+
spin("sdist")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_example():
|
|
106
|
+
spin("example")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_docs():
|
|
110
|
+
run(["pip", "install", "--quiet", "sphinx"])
|
|
111
|
+
spin("docs")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_spin_install():
|
|
115
|
+
cwd = os.getcwd()
|
|
116
|
+
spin("install")
|
|
117
|
+
with tempfile.TemporaryDirectory() as d:
|
|
118
|
+
try:
|
|
119
|
+
os.chdir(d)
|
|
120
|
+
p = run(
|
|
121
|
+
[
|
|
122
|
+
sys.executable,
|
|
123
|
+
"-c",
|
|
124
|
+
"import example_pkg; print(example_pkg.__version__)",
|
|
125
|
+
],
|
|
126
|
+
stdout=subprocess.PIPE,
|
|
127
|
+
)
|
|
128
|
+
assert stdout(p) == "0.0.0dev0"
|
|
129
|
+
finally:
|
|
130
|
+
os.chdir(cwd)
|
|
131
|
+
run(["pip", "uninstall", "-y", "--quiet", "example_pkg"])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@skip_unless_linux
|
|
135
|
+
def test_gdb():
|
|
136
|
+
p = spin(
|
|
137
|
+
"gdb",
|
|
138
|
+
"-c",
|
|
139
|
+
'import example_pkg; example_pkg.echo("hi")',
|
|
140
|
+
"--",
|
|
141
|
+
"--eval",
|
|
142
|
+
"run",
|
|
143
|
+
"--batch",
|
|
144
|
+
)
|
|
145
|
+
assert "hi" in stdout(p)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from testutil import spin, stdout
|
|
2
|
+
|
|
3
|
+
import spin as libspin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_get_version():
|
|
7
|
+
p = spin("--version")
|
|
8
|
+
assert stdout(p) == f"spin {libspin.__version__}"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_arg_override():
|
|
12
|
+
p = spin("example")
|
|
13
|
+
assert "--test is: default override" in stdout(p)
|
|
14
|
+
assert "Default kwd is: 3" in stdout(p)
|
|
15
|
+
|
|
16
|
+
p = spin("example", "-t", 6)
|
|
17
|
+
assert "--test is: 6" in stdout(p)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from testutil import spin, stdout
|
|
3
|
+
|
|
4
|
+
from spin.cmds.util import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def editable_install():
|
|
9
|
+
run(["pip", "install", "--quiet", "--no-build-isolation", "-e", "."])
|
|
10
|
+
yield
|
|
11
|
+
run(["pip", "uninstall", "--quiet", "-y", "example_pkg"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_detect_editable(editable_install):
|
|
15
|
+
assert "Editable install of same source detected" in stdout(
|
|
16
|
+
spin("build")
|
|
17
|
+
), "Failed to detect and warn about editable install"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_editable_tests(editable_install):
|
|
21
|
+
spin("test")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from spin.cmds.util import run
|
|
7
|
+
|
|
8
|
+
skip_on_windows = pytest.mark.skipif(
|
|
9
|
+
sys.platform.startswith("win"), reason="Skipped; platform is Windows"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
skip_unless_linux = pytest.mark.skipif(
|
|
13
|
+
not sys.platform.startswith("linux"), reason="Skipped; platform not Linux"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def spin(*args, **user_kwargs):
|
|
18
|
+
args = (str(el) for el in args)
|
|
19
|
+
default_kwargs = {
|
|
20
|
+
"stdout": subprocess.PIPE,
|
|
21
|
+
"stderr": subprocess.PIPE,
|
|
22
|
+
"sys_exit": True,
|
|
23
|
+
}
|
|
24
|
+
return run(["spin"] + list(args), **{**default_kwargs, **user_kwargs})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stdout(p):
|
|
28
|
+
return p.stdout.decode("utf-8").strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stderr(p):
|
|
32
|
+
return p.stderr.decode("utf-8").strip()
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9
|
|
4
4
|
Summary: Developer tool for scientific Python libraries
|
|
5
5
|
Maintainer-email: Scientific Python <spin@discuss.scientific-python.org>
|
|
6
6
|
License: BSD 3-Clause License
|
|
7
7
|
|
|
8
|
-
Copyright (c) 2021--
|
|
8
|
+
Copyright (c) 2021--2024, Scientific Python project
|
|
9
9
|
All rights reserved.
|
|
10
10
|
|
|
11
11
|
Redistribution and use in source and binary forms, with or without
|
|
@@ -53,24 +53,28 @@ License-File: LICENSE
|
|
|
53
53
|
Requires-Dist: click
|
|
54
54
|
Requires-Dist: tomli; python_version < "3.11"
|
|
55
55
|
Requires-Dist: colorama; platform_system == "Windows"
|
|
56
|
+
Requires-Dist: importlib_metadata>=7
|
|
56
57
|
Provides-Extra: lint
|
|
57
|
-
Requires-Dist: pre-commit==3.
|
|
58
|
+
Requires-Dist: pre-commit==3.7.0; extra == "lint"
|
|
58
59
|
Provides-Extra: dev
|
|
59
|
-
Requires-Dist: changelist==0.
|
|
60
|
+
Requires-Dist: changelist==0.5; extra == "dev"
|
|
60
61
|
|
|
61
62
|
# 💫 Scientific Python INcantations (`spin`)
|
|
62
63
|
|
|
63
64
|
## A developer tool for scientific Python libraries
|
|
64
65
|
|
|
65
66
|
Developers need to memorize a whole bunch of magic command-line incantations.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
The
|
|
67
|
+
These incantations may also change over time.
|
|
68
|
+
Often, Makefiles are used to provide aliases, but Makefiles can be convoluted, are not written in Python, and are hard to extend.
|
|
69
|
+
The goal of `spin` is therefore to provide a simple, user-friendly, extendable interface for common development tasks.
|
|
69
70
|
It comes with a few common build commands out the box, but can easily be customized per project.
|
|
70
71
|
|
|
71
72
|
As a curiosity: the impetus behind developing the tool was the mass migration of scientific Python libraries (SciPy, scikit-image, and NumPy, etc.) to Meson, after distutils was deprecated.
|
|
72
73
|
When many of the build and installation commands changed, it made sense to abstract away the nuisance of having to re-learn them.
|
|
73
74
|
|
|
75
|
+
_Note:_ We now have experimental builds for editable installs.
|
|
76
|
+
Most of the Meson commands listed below should work "out of the box" for those.
|
|
77
|
+
|
|
74
78
|
## Installation
|
|
75
79
|
|
|
76
80
|
```
|
|
@@ -152,7 +156,6 @@ Available as `spin.cmds.meson.*`.
|
|
|
152
156
|
docs 📖 Build Sphinx documentation
|
|
153
157
|
gdb 👾 Execute a Python snippet with GDB
|
|
154
158
|
lldb 👾 Execute a Python snippet with LLDB
|
|
155
|
-
install 💽 Build and install package using pip.
|
|
156
159
|
```
|
|
157
160
|
|
|
158
161
|
### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
|
|
@@ -207,6 +210,29 @@ def example():
|
|
|
207
210
|
print(config["tool.spin"])
|
|
208
211
|
```
|
|
209
212
|
|
|
213
|
+
### Argument overrides
|
|
214
|
+
|
|
215
|
+
Default arguments can be overridden for any command.
|
|
216
|
+
The custom command above, e.g., has the following signature:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
@click.command()
|
|
220
|
+
@click.option("-f", "--flag")
|
|
221
|
+
@click.option("-t", "--test", default="not set")
|
|
222
|
+
def example(flag, test, default_kwd=None):
|
|
223
|
+
"""🧪 Example custom command.
|
|
224
|
+
...
|
|
225
|
+
"""
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Use the `[tool.spin.kwargs]` section to override default values for
|
|
229
|
+
click options or function keywords:
|
|
230
|
+
|
|
231
|
+
```toml
|
|
232
|
+
[tool.spin.kwargs]
|
|
233
|
+
".spin/cmds.py:example" = {"test" = "default override", "default_kwd" = 3}
|
|
234
|
+
```
|
|
235
|
+
|
|
210
236
|
### Advanced: adding arguments to built-in commands
|
|
211
237
|
|
|
212
238
|
Instead of rewriting a command from scratch, a project may want to add a flag to a built-in `spin` command, or perhaps do some pre- or post-processing.
|
|
@@ -284,7 +310,7 @@ fc-cache -f -v
|
|
|
284
310
|
`spin` tests are invoked using:
|
|
285
311
|
|
|
286
312
|
```
|
|
287
|
-
nox -s
|
|
313
|
+
nox -s test
|
|
288
314
|
```
|
|
289
315
|
|
|
290
316
|
## History
|
|
@@ -17,4 +17,9 @@ spin/cmds/build.py
|
|
|
17
17
|
spin/cmds/meson.py
|
|
18
18
|
spin/cmds/pip.py
|
|
19
19
|
spin/cmds/util.py
|
|
20
|
-
spin/tests/
|
|
20
|
+
spin/tests/conftest.py
|
|
21
|
+
spin/tests/test_build_cmds.py
|
|
22
|
+
spin/tests/test_cli.py
|
|
23
|
+
spin/tests/test_editable.py
|
|
24
|
+
spin/tests/test_meson.py
|
|
25
|
+
spin/tests/testutil.py
|
{spin-0.8 → spin-0.9}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|