spin 0.11__tar.gz → 0.12__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.
Files changed (31) hide show
  1. {spin-0.11/spin.egg-info → spin-0.12}/PKG-INFO +28 -13
  2. {spin-0.11 → spin-0.12}/README.md +26 -11
  3. {spin-0.11 → spin-0.12}/pyproject.toml +13 -5
  4. {spin-0.11 → spin-0.12}/spin/__init__.py +1 -1
  5. {spin-0.11 → spin-0.12}/spin/__main__.py +47 -10
  6. spin-0.12/spin/cmds/build.py +15 -0
  7. {spin-0.11 → spin-0.12}/spin/cmds/meson.py +99 -48
  8. spin-0.12/spin/cmds/meta.py +60 -0
  9. {spin-0.11 → spin-0.12}/spin/cmds/pip.py +7 -1
  10. {spin-0.11 → spin-0.12}/spin/cmds/util.py +5 -2
  11. {spin-0.11 → spin-0.12}/spin/color_format.py +1 -1
  12. {spin-0.11 → spin-0.12}/spin/tests/test_build_cmds.py +28 -1
  13. spin-0.12/spin/tests/test_meson.py +109 -0
  14. {spin-0.11 → spin-0.12}/spin/tests/testutil.py +4 -0
  15. {spin-0.11 → spin-0.12/spin.egg-info}/PKG-INFO +28 -13
  16. {spin-0.11 → spin-0.12}/spin.egg-info/SOURCES.txt +1 -0
  17. {spin-0.11 → spin-0.12}/spin.egg-info/requires.txt +1 -1
  18. spin-0.11/spin/cmds/build.py +0 -9
  19. spin-0.11/spin/tests/test_meson.py +0 -116
  20. {spin-0.11 → spin-0.12}/LICENSE +0 -0
  21. {spin-0.11 → spin-0.12}/setup.cfg +0 -0
  22. {spin-0.11 → spin-0.12}/spin/cmds/__init__.py +0 -0
  23. {spin-0.11 → spin-0.12}/spin/containers.py +0 -0
  24. {spin-0.11 → spin-0.12}/spin/sectioned_help.py +0 -0
  25. {spin-0.11 → spin-0.12}/spin/tests/conftest.py +0 -0
  26. {spin-0.11 → spin-0.12}/spin/tests/test_cli.py +0 -0
  27. {spin-0.11 → spin-0.12}/spin/tests/test_editable.py +0 -0
  28. {spin-0.11 → spin-0.12}/spin/tests/test_test.py +0 -0
  29. {spin-0.11 → spin-0.12}/spin.egg-info/dependency_links.txt +0 -0
  30. {spin-0.11 → spin-0.12}/spin.egg-info/entry_points.txt +0 -0
  31. {spin-0.11 → spin-0.12}/spin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spin
3
- Version: 0.11
3
+ Version: 0.12
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
@@ -55,7 +55,7 @@ Requires-Dist: tomli; python_version < "3.11"
55
55
  Requires-Dist: colorama; platform_system == "Windows"
56
56
  Requires-Dist: importlib_metadata>=7
57
57
  Provides-Extra: lint
58
- Requires-Dist: pre-commit==3.7.1; extra == "lint"
58
+ Requires-Dist: pre-commit==3.8.0; extra == "lint"
59
59
  Provides-Extra: dev
60
60
  Requires-Dist: changelist==0.5; extra == "dev"
61
61
 
@@ -147,15 +147,15 @@ python -m spin
147
147
  Available as `spin.cmds.meson.*`.
148
148
 
149
149
  ```
150
- build 🔧 Build package with Meson/ninja
151
- ipython 💻 Launch IPython shell with PYTHONPATH set
152
- python 🐍 Launch Python shell with PYTHONPATH set
153
- shell 💻 Launch shell with PYTHONPATH set
154
- test 🔧 Run pytest
155
- run 🏁 Run a shell command with PYTHONPATH set
156
- docs 📖 Build Sphinx documentation
157
- gdb 👾 Execute a Python snippet with GDB
158
- lldb 👾 Execute a Python snippet with LLDB
150
+ build 🔧 Build package with Meson/ninja
151
+ ipython 💻 Launch IPython shell with PYTHONPATH set
152
+ python 🐍 Launch Python shell with PYTHONPATH set
153
+ shell 💻 Launch shell with PYTHONPATH set
154
+ test 🔧 Run pytest
155
+ run 🏁 Run a shell command with PYTHONPATH set
156
+ docs 📖 Build Sphinx documentation
157
+ gdb 👾 Execute a Python snippet with GDB
158
+ lldb 👾 Execute a Python snippet with LLDB
159
159
  ```
160
160
 
161
161
  ### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
@@ -163,7 +163,7 @@ Available as `spin.cmds.meson.*`.
163
163
  Available as `spin.cmds.build.*`:
164
164
 
165
165
  ```
166
- sdist 📦 Build a source distribution in `dist/`
166
+ sdist 📦 Build a source distribution in `dist/`
167
167
  ```
168
168
 
169
169
  ### [pip](https://pip.pypa.io) (Package Installer for Python)
@@ -174,7 +174,15 @@ development workflow.
174
174
  Available as `spin.cmds.pip.*`:
175
175
 
176
176
  ```
177
- install 💽 Build and install package using pip.
177
+ install 💽 Build and install package using pip.
178
+ ```
179
+
180
+ ### Meta (commands that operate on commands)
181
+
182
+ Available as `spin.cmds.meta.*`:
183
+
184
+ ```
185
+ introspect 🔍 Print a command's location and source code
178
186
  ```
179
187
 
180
188
  ## 🧪 Custom commands
@@ -310,6 +318,13 @@ fc-cache -f -v
310
318
  nox -s test
311
319
  ```
312
320
 
321
+ Other examples:
322
+
323
+ ```
324
+ nox -s test -- -v
325
+ nox -s test -- -v spin/tests/test_meson.py
326
+ ```
327
+
313
328
  ## History
314
329
 
315
330
  The `dev.py` tool was [proposed for SciPy](https://github.com/scipy/scipy/issues/15489) by Ralf Gommers and [implemented](https://github.com/scipy/scipy/pull/15959) by Sayantika Banik, Eduardo Naufel Schettino, and Ralf Gommers (also see [Sayantika's blog post](https://labs.quansight.org/blog/the-evolution-of-the-scipy-developer-cli)).
@@ -86,15 +86,15 @@ python -m spin
86
86
  Available as `spin.cmds.meson.*`.
87
87
 
88
88
  ```
89
- build 🔧 Build package with Meson/ninja
90
- ipython 💻 Launch IPython shell with PYTHONPATH set
91
- python 🐍 Launch Python shell with PYTHONPATH set
92
- shell 💻 Launch shell with PYTHONPATH set
93
- test 🔧 Run pytest
94
- run 🏁 Run a shell command with PYTHONPATH set
95
- docs 📖 Build Sphinx documentation
96
- gdb 👾 Execute a Python snippet with GDB
97
- lldb 👾 Execute a Python snippet with LLDB
89
+ build 🔧 Build package with Meson/ninja
90
+ ipython 💻 Launch IPython shell with PYTHONPATH set
91
+ python 🐍 Launch Python shell with PYTHONPATH set
92
+ shell 💻 Launch shell with PYTHONPATH set
93
+ test 🔧 Run pytest
94
+ run 🏁 Run a shell command with PYTHONPATH set
95
+ docs 📖 Build Sphinx documentation
96
+ gdb 👾 Execute a Python snippet with GDB
97
+ lldb 👾 Execute a Python snippet with LLDB
98
98
  ```
99
99
 
100
100
  ### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
@@ -102,7 +102,7 @@ Available as `spin.cmds.meson.*`.
102
102
  Available as `spin.cmds.build.*`:
103
103
 
104
104
  ```
105
- sdist 📦 Build a source distribution in `dist/`
105
+ sdist 📦 Build a source distribution in `dist/`
106
106
  ```
107
107
 
108
108
  ### [pip](https://pip.pypa.io) (Package Installer for Python)
@@ -113,7 +113,15 @@ development workflow.
113
113
  Available as `spin.cmds.pip.*`:
114
114
 
115
115
  ```
116
- install 💽 Build and install package using pip.
116
+ install 💽 Build and install package using pip.
117
+ ```
118
+
119
+ ### Meta (commands that operate on commands)
120
+
121
+ Available as `spin.cmds.meta.*`:
122
+
123
+ ```
124
+ introspect 🔍 Print a command's location and source code
117
125
  ```
118
126
 
119
127
  ## 🧪 Custom commands
@@ -249,6 +257,13 @@ fc-cache -f -v
249
257
  nox -s test
250
258
  ```
251
259
 
260
+ Other examples:
261
+
262
+ ```
263
+ nox -s test -- -v
264
+ nox -s test -- -v spin/tests/test_meson.py
265
+ ```
266
+
252
267
  ## History
253
268
 
254
269
  The `dev.py` tool was [proposed for SciPy](https://github.com/scipy/scipy/issues/15489) by Ralf Gommers and [implemented](https://github.com/scipy/scipy/pull/15959) by Sayantika Banik, Eduardo Naufel Schettino, and Ralf Gommers (also see [Sayantika's blog post](https://labs.quansight.org/blog/the-evolution-of-the-scipy-developer-cli)).
@@ -34,12 +34,15 @@ dynamic = ['version']
34
34
  spin = "spin.__main__:main"
35
35
 
36
36
  [project.optional-dependencies]
37
- lint = ["pre-commit == 3.7.1"]
37
+ lint = ["pre-commit == 3.8.0"]
38
38
  dev = ["changelist == 0.5"]
39
39
 
40
40
  [project.urls]
41
41
  homepage = "https://github.com/scientific-python/spin"
42
42
 
43
+ [tool.changelist]
44
+ ignored_user_logins = ["dependabot[bot]", "pre-commit-ci[bot]", "web-flow"]
45
+
43
46
  [tool.setuptools.dynamic.version]
44
47
  attr = 'spin.__version__'
45
48
 
@@ -47,13 +50,13 @@ attr = 'spin.__version__'
47
50
  include = ["spin*"]
48
51
 
49
52
  [tool.pytest.ini_options]
53
+ minversion = "8.0"
54
+ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
55
+ xfail_strict = true
50
56
  filterwarnings = [
51
57
  "ignore:cannot collect 'test' because it is not a function:",
52
58
  ]
53
-
54
- [tool.ruff]
55
- line-length = 88
56
- target-version = "py38"
59
+ log_cli_level = "info"
57
60
 
58
61
  [tool.ruff.lint]
59
62
  select = [
@@ -63,6 +66,11 @@ select = [
63
66
  "W",
64
67
  "B",
65
68
  "I",
69
+ "C4",
70
+ "ISC",
71
+ "PGH",
66
72
  "UP",
73
+ "FURB",
74
+ "PYI",
67
75
  ]
68
76
  ignore = ["B006", "C901", "E501", "E722"]
@@ -6,7 +6,7 @@ For usage information see `python -m spin`.
6
6
  Read more at https://github.com/scientific-python/spin
7
7
  """
8
8
 
9
- __version__ = "0.11"
9
+ __version__ = "0.12"
10
10
 
11
11
  from .cmds import util
12
12
 
@@ -2,9 +2,11 @@ import collections
2
2
  import importlib
3
3
  import importlib.util
4
4
  import os
5
+ import pathlib
5
6
  import sys
6
7
  import textwrap
7
8
  import traceback
9
+ from typing import Union
8
10
 
9
11
  import click
10
12
 
@@ -19,8 +21,26 @@ if sys.version_info >= (3, 11):
19
21
  else:
20
22
  import tomli as tomllib
21
23
 
24
+
22
25
  click.Context.formatter_class = ColorHelpFormatter
23
26
 
27
+ config_filenames = (
28
+ ".spin.toml",
29
+ "spin.toml",
30
+ "pyproject.toml",
31
+ )
32
+
33
+
34
+ def _detect_config_dir(path: pathlib.Path) -> Union[pathlib.Path, None]:
35
+ path = path.resolve()
36
+ files = os.listdir(path)
37
+ if any(f in files for f in config_filenames):
38
+ return path
39
+ elif path.parent != path:
40
+ return _detect_config_dir(path.parent)
41
+ else:
42
+ return None
43
+
24
44
 
25
45
  def main():
26
46
  # Alias `spin help` to `spin --help`
@@ -38,15 +58,25 @@ def main():
38
58
 
39
59
  toml_config = collections.ChainMap()
40
60
  toml_config.maps.extend(
41
- DotDict(cfg)
42
- for filename in (
43
- ".spin.toml",
44
- "spin.toml",
45
- "pyproject.toml",
46
- )
47
- if (cfg := load_toml(filename))
61
+ DotDict(cfg) for filename in config_filenames if (cfg := load_toml(filename))
48
62
  )
49
63
 
64
+ if not toml_config:
65
+ click.secho(
66
+ f"Could not load configuration from one of: {', '.join(config_filenames)}",
67
+ file=sys.stderr,
68
+ fg="red",
69
+ )
70
+ config_dir = _detect_config_dir(pathlib.Path("."))
71
+ if config_dir:
72
+ print()
73
+ print(
74
+ "Are you running `spin` from the correct directory? Perhaps you'd like to\n"
75
+ )
76
+ click.secho(f" $ cd {os.path.relpath(config_dir, '.')}\n")
77
+ print("and try again.")
78
+ sys.exit(1)
79
+
50
80
  # Basic configuration validation
51
81
  version_query = len(sys.argv) == 2 and (sys.argv[1] == "--version")
52
82
 
@@ -55,15 +85,18 @@ def main():
55
85
  if "tool.spin" in toml_config:
56
86
  spin_config = toml_config["tool.spin"]
57
87
  if "tool.spin.commands" not in toml_config:
58
- print(
59
- "Error: configuration is missing section [tool.spin.commands]\n",
88
+ click.secho(
89
+ "Error: configuration is missing section [tool.spin.commands]\n"
90
+ "See https://github.com/scientific-python/spin/blob/main/README.md\n",
60
91
  file=sys.stderr,
92
+ fg="red",
61
93
  )
62
94
  else:
63
- print(
95
+ click.secho(
64
96
  "Error: need valid configuration in [.spin.toml], [spin.toml], or [pyproject.toml]\n"
65
97
  "See https://github.com/scientific-python/spin/blob/main/README.md\n",
66
98
  file=sys.stderr,
99
+ fg="red",
67
100
  )
68
101
 
69
102
  proj_name = (
@@ -132,10 +165,14 @@ def main():
132
165
 
133
166
  try:
134
167
  cmd_func = getattr(mod, func)
168
+ cmd_func.module = mod # metadata for use by `introspect` command
135
169
  except AttributeError:
136
170
  print(f"!! Could not load command `{func}` from file `{path}`.\n")
137
171
  continue
138
172
 
173
+ # Save command definition for use in `introspect`
174
+ cmd_func.spec = cmd
175
+
139
176
  default_kwargs = cmd_default_kwargs.get(cmd)
140
177
  import functools
141
178
 
@@ -0,0 +1,15 @@
1
+ import click
2
+
3
+ from .util import run
4
+
5
+
6
+ @click.command()
7
+ @click.argument("pyproject-build-args", metavar="", nargs=-1)
8
+ def sdist(pyproject_build_args):
9
+ """📦 Build a source distribution in `dist/`
10
+
11
+ Extra arguments are passed to `pyproject-build`, e.g.
12
+
13
+ spin sdist -- -x -n
14
+ """
15
+ run(["pyproject-build", ".", "--sdist"] + list(pyproject_build_args))
@@ -13,9 +13,6 @@ import click
13
13
  from .util import get_commands, get_config
14
14
  from .util import run as _run
15
15
 
16
- install_dir = "build-install"
17
- build_dir = "build"
18
-
19
16
 
20
17
  class GcovReportFormat(str, Enum):
21
18
  html = "html"
@@ -68,16 +65,15 @@ def _is_editable_install_of_same_source(distname):
68
65
  return editable_path and os.path.samefile(_editable_install_path(distname), ".")
69
66
 
70
67
 
71
- def _set_pythonpath(quiet=False):
68
+ def _set_pythonpath(build_dir, quiet=False):
72
69
  """Set first entry of PYTHONPATH to site packages directory.
73
70
 
71
+ For editable installs, leave the PYTHONPATH alone.
72
+
74
73
  Returns
75
74
  -------
76
75
  site_packages
77
76
  """
78
- site_packages = _get_site_packages()
79
- env = os.environ
80
-
81
77
  cfg = get_config()
82
78
  distname = cfg.get("project.name", None)
83
79
  if distname:
@@ -101,6 +97,9 @@ def _set_pythonpath(quiet=False):
101
97
  fg="bright_red",
102
98
  )
103
99
 
100
+ site_packages = _get_site_packages(build_dir)
101
+ env = os.environ
102
+
104
103
  if "PYTHONPATH" in env:
105
104
  env["PYTHONPATH"] = f"{site_packages}{os.pathsep}{env['PYTHONPATH']}"
106
105
  else:
@@ -114,7 +113,12 @@ def _set_pythonpath(quiet=False):
114
113
  return site_packages
115
114
 
116
115
 
117
- def _get_site_packages():
116
+ def _get_install_dir(build_dir):
117
+ return f"{build_dir}-install"
118
+
119
+
120
+ def _get_site_packages(build_dir):
121
+ install_dir = _get_install_dir(build_dir)
118
122
  try:
119
123
  cfg = get_config()
120
124
  distname = cfg.get("project.name", None)
@@ -168,9 +172,9 @@ def _meson_version():
168
172
  pass
169
173
 
170
174
 
171
- def _meson_version_configured():
175
+ def _meson_version_configured(build_dir):
172
176
  try:
173
- meson_info_fn = os.path.join("build", "meson-info", "meson-info.json")
177
+ meson_info_fn = os.path.join(build_dir, "meson-info", "meson-info.json")
174
178
  with open(meson_info_fn) as f:
175
179
  meson_info = json.load(f)
176
180
  return meson_info["meson_version"]["full"]
@@ -194,7 +198,7 @@ def _meson_coverage_configured() -> bool:
194
198
  return False
195
199
 
196
200
 
197
- def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
201
+ def _check_coverage_tool_installation(coverage_type: GcovReportFormat, build_dir):
198
202
  requirements = { # https://github.com/mesonbuild/meson/blob/6e381714c7cb15877e2bcaa304b93c212252ada3/docs/markdown/Unit-tests.md?plain=1#L49-L62
199
203
  GcovReportFormat.html: ["Gcovr/GenHTML", "lcov"],
200
204
  GcovReportFormat.xml: ["Gcovr (version 3.3 or higher)"],
@@ -226,6 +230,16 @@ def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
226
230
  )
227
231
 
228
232
 
233
+ build_dir_option = click.option(
234
+ "-C",
235
+ "--build-dir",
236
+ default="build",
237
+ show_envvar=True,
238
+ envvar="SPIN_BUILD_DIR",
239
+ help="Meson build directory; package is installed into './{build-dir}-install'.",
240
+ )
241
+
242
+
229
243
  @click.command()
230
244
  @click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int)
231
245
  @click.option("--clean", is_flag=True, help="Clean build directory before build")
@@ -238,12 +252,21 @@ def _check_coverage_tool_installation(coverage_type: GcovReportFormat):
238
252
  help="Enable C code coverage using `gcov`. Use `spin test --gcov` to generate reports.",
239
253
  )
240
254
  @click.argument("meson_args", nargs=-1)
255
+ @build_dir_option
241
256
  def build(
242
- *, meson_args, jobs=None, clean=False, verbose=False, gcov=False, quiet=False
257
+ *,
258
+ meson_args,
259
+ jobs=None,
260
+ clean=False,
261
+ verbose=False,
262
+ gcov=False,
263
+ quiet=False,
264
+ build_dir=None,
243
265
  ):
244
266
  """🔧 Build package with Meson/ninja
245
267
 
246
- The package is installed to `build-install`.
268
+ The package is installed to `build-install` (unless a different
269
+ build directory is specified with `-C`).
247
270
 
248
271
  MESON_ARGS are passed through e.g.:
249
272
 
@@ -257,7 +280,23 @@ def build(
257
280
  or set CFLAGS appropriately:
258
281
 
259
282
  CFLAGS="-O0 -g" spin build
283
+
284
+ Build into a different build/build-install directory via the
285
+ `-C/--build-dir` flag:
286
+
287
+ spin build -C build-for-feature-x
288
+
289
+ This feature is useful in combination with a shell alias, e.g.:
290
+
291
+ $ alias spin-clang="SPIN_BUILD_DIR=build-clang CC=clang spin"
292
+
293
+ Which can then be used to build (`spin-clang build`), to test (`spin-clang test ...`), etc.
294
+
260
295
  """
296
+ abs_build_dir = os.path.abspath(build_dir)
297
+ install_dir = _get_install_dir(build_dir)
298
+ abs_install_dir = os.path.abspath(install_dir)
299
+
261
300
  cfg = get_config()
262
301
  distname = cfg.get("project.name", None)
263
302
  if distname and _is_editable_install_of_same_source(distname):
@@ -283,7 +322,7 @@ def build(
283
322
  if os.path.isdir(install_dir):
284
323
  shutil.rmtree(install_dir)
285
324
 
286
- if not (os.path.exists(build_dir) and _meson_version_configured()):
325
+ if not (os.path.exists(build_dir) and _meson_version_configured(build_dir)):
287
326
  p = _run(setup_cmd, sys_exit=False, output=not quiet)
288
327
  if p.returncode != 0:
289
328
  raise RuntimeError(
@@ -293,7 +332,7 @@ def build(
293
332
  # Build dir has been configured; check if it was configured by
294
333
  # current version of Meson
295
334
 
296
- if (_meson_version() != _meson_version_configured()) or (
335
+ if (_meson_version() != _meson_version_configured(build_dir)) or (
297
336
  gcov and not _meson_coverage_configured()
298
337
  ):
299
338
  _run(setup_cmd + ["--reconfigure"], output=not quiet)
@@ -301,6 +340,9 @@ def build(
301
340
  # Any other conditions that warrant a reconfigure?
302
341
 
303
342
  compile_flags = ["-v"] if verbose else []
343
+ if jobs:
344
+ compile_flags += ["-j", str(jobs)]
345
+
304
346
  p = _run(
305
347
  _meson_cli() + ["compile"] + compile_flags + ["-C", build_dir],
306
348
  sys_exit=True,
@@ -314,7 +356,9 @@ def build(
314
356
  "-C",
315
357
  build_dir,
316
358
  "--destdir",
317
- f"../{install_dir}",
359
+ install_dir
360
+ if os.path.isabs(install_dir)
361
+ else os.path.relpath(abs_install_dir, abs_build_dir),
318
362
  ],
319
363
  output=(not quiet) and verbose,
320
364
  )
@@ -334,7 +378,7 @@ def _get_configured_command(command_name):
334
378
  metavar="N_JOBS",
335
379
  default="1",
336
380
  help=(
337
- "Number of parallel jobs for testing. " "Can be set to `auto` to use all cores."
381
+ "Number of parallel jobs for testing. Can be set to `auto` to use all cores."
338
382
  ),
339
383
  )
340
384
  @click.option(
@@ -372,6 +416,7 @@ Which tests to run. Can be a module, function, class, or method:
372
416
  default="html",
373
417
  help=f"Format of the gcov report. Can be one of {', '.join(e.value for e in GcovReportFormat)}.",
374
418
  )
419
+ @build_dir_option
375
420
  @click.pass_context
376
421
  def test(
377
422
  ctx,
@@ -383,6 +428,7 @@ def test(
383
428
  coverage=False,
384
429
  gcov=None,
385
430
  gcov_format=None,
431
+ build_dir=None,
386
432
  ):
387
433
  """🔧 Run tests
388
434
 
@@ -420,10 +466,6 @@ def test(
420
466
 
421
467
  spin test -j auto
422
468
 
423
- To run a test inside of LLDB:
424
-
425
- spin lldb -- python -mspin test ...
426
-
427
469
  For more, see `pytest --help`.
428
470
  """ # noqa: E501
429
471
  cfg = get_config()
@@ -469,13 +511,11 @@ def test(
469
511
  "Invoking `build` prior to running tests:", bold=True, fg="bright_green"
470
512
  )
471
513
  if gcov is not None:
472
- ctx.invoke(build_cmd, gcov=bool(gcov))
514
+ ctx.invoke(build_cmd, build_dir=build_dir, gcov=bool(gcov))
473
515
  else:
474
- ctx.invoke(build_cmd)
516
+ ctx.invoke(build_cmd, build_dir=build_dir)
475
517
 
476
- site_path = _set_pythonpath()
477
- if site_path:
478
- print(f'$ export PYTHONPATH="{site_path}"')
518
+ site_path = _set_pythonpath(build_dir)
479
519
 
480
520
  # Sanity check that library built properly
481
521
  #
@@ -520,6 +560,7 @@ def test(
520
560
  else:
521
561
  cmd = ["pytest"]
522
562
 
563
+ install_dir = _get_install_dir(build_dir)
523
564
  if not os.path.exists(install_dir):
524
565
  os.mkdir(install_dir)
525
566
 
@@ -534,7 +575,7 @@ def test(
534
575
  bold=True,
535
576
  fg="bright_yellow",
536
577
  )
537
- _check_coverage_tool_installation(gcov_format)
578
+ _check_coverage_tool_installation(gcov_format, build_dir)
538
579
 
539
580
  # Generate report
540
581
  click.secho(
@@ -568,8 +609,9 @@ def test(
568
609
  @click.command()
569
610
  @click.option("--code", "-c", help="Python program passed in as a string")
570
611
  @click.argument("gdb_args", nargs=-1)
612
+ @build_dir_option
571
613
  @click.pass_context
572
- def gdb(ctx, *, code, gdb_args):
614
+ def gdb(ctx, *, code, gdb_args, build_dir):
573
615
  """👾 Execute code through GDB
574
616
 
575
617
  spin gdb -c 'import numpy as np; print(np.__version__)'
@@ -595,9 +637,9 @@ def gdb(ctx, *, code, gdb_args):
595
637
  click.secho(
596
638
  "Invoking `build` prior to invoking gdb:", bold=True, fg="bright_green"
597
639
  )
598
- ctx.invoke(build_cmd)
640
+ ctx.invoke(build_cmd, build_dir=build_dir)
599
641
 
600
- _set_pythonpath()
642
+ _set_pythonpath(build_dir)
601
643
  gdb_args = list(gdb_args)
602
644
 
603
645
  if gdb_args and gdb_args[0].endswith(".py"):
@@ -620,8 +662,9 @@ def gdb(ctx, *, code, gdb_args):
620
662
 
621
663
  @click.command()
622
664
  @click.argument("ipython_args", nargs=-1)
665
+ @build_dir_option
623
666
  @click.pass_context
624
- def ipython(ctx, *, ipython_args):
667
+ def ipython(ctx, *, ipython_args, build_dir):
625
668
  """💻 Launch IPython shell with PYTHONPATH set
626
669
 
627
670
  IPYTHON_ARGS are passed through directly to IPython, e.g.:
@@ -633,9 +676,9 @@ def ipython(ctx, *, ipython_args):
633
676
  click.secho(
634
677
  "Invoking `build` prior to invoking ipython:", bold=True, fg="bright_green"
635
678
  )
636
- ctx.invoke(build_cmd)
679
+ ctx.invoke(build_cmd, build_dir=build_dir)
637
680
 
638
- p = _set_pythonpath()
681
+ p = _set_pythonpath(build_dir)
639
682
  if p:
640
683
  print(f'💻 Launching IPython with PYTHONPATH="{p}"')
641
684
  _run(["ipython", "--ignore-cwd"] + list(ipython_args), replace=True)
@@ -643,8 +686,9 @@ def ipython(ctx, *, ipython_args):
643
686
 
644
687
  @click.command()
645
688
  @click.argument("shell_args", nargs=-1)
689
+ @build_dir_option
646
690
  @click.pass_context
647
- def shell(ctx, shell_args=[]):
691
+ def shell(ctx, shell_args=[], build_dir=None):
648
692
  """💻 Launch shell with PYTHONPATH set
649
693
 
650
694
  SHELL_ARGS are passed through directly to the shell, e.g.:
@@ -659,9 +703,9 @@ def shell(ctx, shell_args=[]):
659
703
  click.secho(
660
704
  "Invoking `build` prior to invoking shell:", bold=True, fg="bright_green"
661
705
  )
662
- ctx.invoke(build_cmd)
706
+ ctx.invoke(build_cmd, build_dir=build_dir)
663
707
 
664
- p = _set_pythonpath()
708
+ p = _set_pythonpath(build_dir)
665
709
  if p:
666
710
  print(f'💻 Launching shell with PYTHONPATH="{p}"')
667
711
 
@@ -674,8 +718,9 @@ def shell(ctx, shell_args=[]):
674
718
 
675
719
  @click.command()
676
720
  @click.argument("python_args", nargs=-1)
721
+ @build_dir_option
677
722
  @click.pass_context
678
- def python(ctx, *, python_args):
723
+ def python(ctx, *, python_args, build_dir):
679
724
  """🐍 Launch Python shell with PYTHONPATH set
680
725
 
681
726
  PYTHON_ARGS are passed through directly to Python, e.g.:
@@ -687,9 +732,9 @@ def python(ctx, *, python_args):
687
732
  click.secho(
688
733
  "Invoking `build` prior to invoking Python:", bold=True, fg="bright_green"
689
734
  )
690
- ctx.invoke(build_cmd)
735
+ ctx.invoke(build_cmd, build_dir=build_dir)
691
736
 
692
- p = _set_pythonpath()
737
+ p = _set_pythonpath(build_dir)
693
738
  if p:
694
739
  print(f'🐍 Launching Python with PYTHONPATH="{p}"')
695
740
 
@@ -714,9 +759,10 @@ def python(ctx, *, python_args):
714
759
 
715
760
 
716
761
  @click.command(context_settings={"ignore_unknown_options": True})
762
+ @build_dir_option
717
763
  @click.argument("args", nargs=-1)
718
764
  @click.pass_context
719
- def run(ctx, *, args):
765
+ def run(ctx, *, args, build_dir=None):
720
766
  """🏁 Run a shell command with PYTHONPATH set
721
767
 
722
768
  \b
@@ -740,7 +786,7 @@ def run(ctx, *, args):
740
786
  # Redirect spin generated output
741
787
  with contextlib.redirect_stdout(sys.stderr):
742
788
  # Also ask build to be quiet
743
- ctx.invoke(build_cmd, quiet=True)
789
+ ctx.invoke(build_cmd, build_dir=build_dir, quiet=True)
744
790
 
745
791
  is_posix = sys.platform in ("linux", "darwin")
746
792
  shell = len(args) == 1
@@ -751,7 +797,7 @@ def run(ctx, *, args):
751
797
  # On Windows, we're going to try to use bash
752
798
  cmd_args = ["bash", "-c", cmd_args]
753
799
 
754
- _set_pythonpath(quiet=True)
800
+ _set_pythonpath(build_dir, quiet=True)
755
801
  p = _run(cmd_args, echo=False, shell=shell, sys_exit=False)
756
802
 
757
803
  # Is the user trying to run a Python script, without calling the Python interpreter?
@@ -789,6 +835,7 @@ def run(ctx, *, args):
789
835
  help="Sphinx gallery: enable/disable plots",
790
836
  )
791
837
  @click.option("--jobs", "-j", default="auto", help="Number of parallel build jobs")
838
+ @build_dir_option
792
839
  @click.pass_context
793
840
  def docs(
794
841
  ctx,
@@ -799,6 +846,7 @@ def docs(
799
846
  jobs,
800
847
  sphinx_gallery_plot,
801
848
  clean_dirs=None,
849
+ build_dir=None,
802
850
  ):
803
851
  """📖 Build Sphinx documentation
804
852
 
@@ -853,12 +901,14 @@ def docs(
853
901
  click.secho(
854
902
  "Invoking `build` prior to building docs:", bold=True, fg="bright_green"
855
903
  )
856
- ctx.invoke(build_cmd)
904
+ ctx.invoke(build_cmd, build_dir=build_dir)
857
905
 
858
906
  try:
859
- site_path = _get_site_packages()
907
+ site_path = _get_site_packages(build_dir)
860
908
  except FileNotFoundError:
861
- print("No built numpy found; run `spin build` first.")
909
+ cfg = get_config()
910
+ distname = cfg.get("project.name", None)
911
+ print(f"{distname} build not found; run `spin build` or `spin install` first.")
862
912
  sys.exit(1)
863
913
 
864
914
  opts = os.environ.get("SPHINXOPTS", "-W")
@@ -889,8 +939,9 @@ def docs(
889
939
  @click.command()
890
940
  @click.option("--code", "-c", help="Python program passed in as a string")
891
941
  @click.argument("lldb_args", nargs=-1)
942
+ @build_dir_option
892
943
  @click.pass_context
893
- def lldb(ctx, *, code, lldb_args):
944
+ def lldb(ctx, *, code, lldb_args, build_dir=None):
894
945
  """👾 Execute code through LLDB
895
946
 
896
947
  spin lldb -c 'import numpy as np; print(np.__version__)'
@@ -918,9 +969,9 @@ def lldb(ctx, *, code, lldb_args):
918
969
  click.secho(
919
970
  "Invoking `build` prior to invoking lldb:", bold=True, fg="bright_green"
920
971
  )
921
- ctx.invoke(build_cmd)
972
+ ctx.invoke(build_cmd, build_dir=build_dir)
922
973
 
923
- _set_pythonpath()
974
+ _set_pythonpath(build_dir)
924
975
  lldb_args = list(lldb_args)
925
976
 
926
977
  if code:
@@ -0,0 +1,60 @@
1
+ import inspect
2
+
3
+ import click
4
+
5
+ from .util import get_commands, get_config
6
+
7
+
8
+ def _highlight(src):
9
+ from pygments import highlight
10
+ from pygments.formatters import TerminalFormatter
11
+ from pygments.lexers import PythonLexer
12
+
13
+ return highlight(src, PythonLexer(), TerminalFormatter())
14
+
15
+
16
+ @click.command()
17
+ @click.argument("cmd", nargs=1)
18
+ def introspect(*, cmd):
19
+ """🔍 Print a command's location and source code."""
20
+ cmds_by_section = get_commands()
21
+ overrides = get_config().get("tool.spin.kwargs")
22
+
23
+ cmds = {}
24
+ for section in cmds_by_section:
25
+ cmds.update({cmd.name: cmd for cmd in cmds_by_section[section]})
26
+
27
+ if cmd not in cmds:
28
+ raise SystemExit(f"Command `{cmd}` not found. Exiting.")
29
+
30
+ cmd_func = cmds[cmd]
31
+ try:
32
+ code = inspect.getsource(cmd_func.callback)
33
+ except TypeError:
34
+ # Perhaps a partial, try again
35
+ code = inspect.getsource(cmd_func.callback.func)
36
+
37
+ try:
38
+ code = _highlight(code)
39
+ except ImportError:
40
+ pass
41
+
42
+ print(code)
43
+
44
+ click.secho(
45
+ f"The `{cmd}` command is defined as `{cmd_func.spec}`.",
46
+ bold=True,
47
+ fg="magenta",
48
+ )
49
+
50
+ click.secho(
51
+ f"The code is in `{cmd_func.module.__file__}`.", bold=True, fg="magenta"
52
+ )
53
+
54
+ if cmd_func.spec in overrides:
55
+ click.secho(
56
+ "\nThe function has the following keyword overrides defined:\n",
57
+ bold=True,
58
+ fg="magenta",
59
+ )
60
+ print(" ", overrides[cmd_func.spec], "\n")
@@ -9,7 +9,13 @@ from .util import run as _run
9
9
  default=True,
10
10
  help="Install in editable mode.",
11
11
  )
12
- @click.option("-v", "--verbose", is_flag=True, help="Print detailed build output.")
12
+ @click.option(
13
+ "-v/-q",
14
+ "--verbose/--quiet",
15
+ is_flag=True,
16
+ default=True,
17
+ help="Print detailed build output.",
18
+ )
13
19
  @click.option(
14
20
  "--verbose-import/--no-verbose-import",
15
21
  is_flag=True,
@@ -1,15 +1,18 @@
1
+ from __future__ import (
2
+ annotations, # noqa: F401 # TODO: remove once only >3.8 is supported
3
+ )
4
+
1
5
  import os
2
6
  import shlex
3
7
  import subprocess
4
8
  import sys
5
- from typing import Optional
6
9
 
7
10
  import click
8
11
 
9
12
 
10
13
  def run(
11
14
  cmd: list[str],
12
- cwd: Optional[str] = None, # in 3.10 and up: str | None
15
+ cwd: str | None = None, # in 3.10 and up: str | None
13
16
  replace: bool = False,
14
17
  sys_exit: bool = True,
15
18
  output: bool = True,
@@ -36,7 +36,7 @@ class ColorHelpFormatter(click.formatting.HelpFormatter):
36
36
  },
37
37
  default={"bold": True, "fg": "cyan"},
38
38
  )
39
- val_fmt = RegexpFormatter({r"\[default: .*?\]": {"dim": True}})
39
+ val_fmt = RegexpFormatter({r"\[(env var: .*?; )?default: .*?\]": {"dim": True}})
40
40
  items = [(key_fmt(key), val_fmt(val)) for (key, val) in items]
41
41
  super().write_dl(items)
42
42
 
@@ -5,7 +5,14 @@ import tempfile
5
5
  from pathlib import Path
6
6
 
7
7
  import pytest
8
- from testutil import skip_on_windows, skip_unless_linux, skip_unless_macos, spin, stdout
8
+ from testutil import (
9
+ skip_on_windows,
10
+ skip_py_lt_311,
11
+ skip_unless_linux,
12
+ skip_unless_macos,
13
+ spin,
14
+ stdout,
15
+ )
9
16
 
10
17
  from spin.cmds.util import run
11
18
 
@@ -147,3 +154,23 @@ def test_lldb():
147
154
  "--batch",
148
155
  )
149
156
  assert "hi" in stdout(p)
157
+
158
+
159
+ @skip_py_lt_311 # python command does not run on older pythons
160
+ def test_parallel_builds():
161
+ spin("build")
162
+ spin("build", "-C", "parallel/build")
163
+ p = spin("python", "--", "-c", "import example_pkg; print(example_pkg.__file__)")
164
+ example_pkg_path = stdout(p).split("\n")[-1]
165
+ p = spin(
166
+ "python",
167
+ "-C",
168
+ "parallel/build",
169
+ "--",
170
+ "-c",
171
+ "import example_pkg; print(example_pkg.__file__)",
172
+ )
173
+ example_pkg_parallel_path = stdout(p).split("\n")[-1]
174
+ assert "build-install" in example_pkg_path
175
+ assert "parallel/build-install" in example_pkg_parallel_path
176
+ assert "parallel/build-install" not in example_pkg_path
@@ -0,0 +1,109 @@
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ from os.path import join as pjoin
5
+ from os.path import normpath
6
+
7
+ import pytest
8
+
9
+ from spin.cmds import meson
10
+ from spin.containers import DotDict
11
+
12
+
13
+ def make_paths(root, paths):
14
+ for p in paths:
15
+ os.makedirs(pjoin(root, p.lstrip("/")))
16
+
17
+
18
+ def test_path_discovery():
19
+ version = sys.version_info
20
+ X, Y = version.major, version.minor
21
+
22
+ # With multiple site-packages, choose the one that matches the
23
+ # current Python version
24
+ with tempfile.TemporaryDirectory() as d:
25
+ build_dir = pjoin(d, "./build")
26
+ install_dir = pjoin(d, "./build-install")
27
+
28
+ make_paths(
29
+ install_dir,
30
+ [
31
+ f"/usr/lib64/python{X}.{Y}/site-packages",
32
+ f"/usr/lib64/python{X}.{Y + 1}/site-packages",
33
+ f"/usr/lib64/python{X}.{Y + 2}/site-packages",
34
+ ],
35
+ )
36
+ assert normpath(
37
+ f"/usr/lib64/python{X}.{Y}/site-packages"
38
+ ) in meson._get_site_packages(build_dir)
39
+
40
+ # Debian uses dist-packages
41
+ with tempfile.TemporaryDirectory() as d:
42
+ build_dir = pjoin(d, "./build")
43
+ install_dir = pjoin(d, "./build-install")
44
+
45
+ make_paths(
46
+ install_dir,
47
+ [
48
+ f"/usr/lib64/python{X}.{Y}/dist-packages",
49
+ ],
50
+ )
51
+ assert normpath(
52
+ f"/usr/lib64/python{X}.{Y}/dist-packages"
53
+ ) in meson._get_site_packages(build_dir)
54
+
55
+ # If there is no version information in site-packages,
56
+ # use whatever site-packages can be found
57
+ with tempfile.TemporaryDirectory() as d:
58
+ build_dir = pjoin(d, "./build")
59
+ install_dir = pjoin(d, "./build-install")
60
+
61
+ make_paths(install_dir, ["/Python3/site-packages"])
62
+ assert normpath("/Python3/site-packages") in meson._get_site_packages(build_dir)
63
+
64
+ # Raise if no site-package directory present
65
+ with tempfile.TemporaryDirectory() as d:
66
+ install_dir = pjoin(d, "-install")
67
+
68
+ with pytest.raises(FileNotFoundError):
69
+ meson._get_site_packages(build_dir)
70
+
71
+ # If there are multiple site-package paths, but without version information,
72
+ # refuse the temptation to guess
73
+ with tempfile.TemporaryDirectory() as d:
74
+ build_dir = pjoin(d, "./build")
75
+ install_dir = pjoin(d, "./build-install")
76
+ make_paths(
77
+ install_dir, ["/Python3/x/site-packages", "/Python3/y/site-packages"]
78
+ )
79
+ with pytest.raises(FileNotFoundError):
80
+ meson._get_site_packages(build_dir)
81
+
82
+ # Multiple site-package paths found, but none that matches our Python
83
+ with tempfile.TemporaryDirectory() as d:
84
+ build_dir = pjoin(d, "./build")
85
+ install_dir = pjoin(d, "./build-install")
86
+
87
+ make_paths(
88
+ install_dir,
89
+ [
90
+ f"/usr/lib64/python{X}.{Y + 1}/site-packages",
91
+ f"/usr/lib64/python{X}.{Y + 2}/site-packages",
92
+ ],
93
+ )
94
+ with pytest.raises(FileNotFoundError):
95
+ meson._get_site_packages(build_dir)
96
+
97
+
98
+ def test_meson_cli_discovery(monkeypatch):
99
+ config0 = DotDict({"tool": {"spin": {"meson": {"cli": "~/envs/py311/bin/meson"}}}})
100
+ config1 = DotDict(
101
+ {"tool": {"spin": {"meson": {"cli": "~/envs/py311/bin/meson.py"}}}}
102
+ )
103
+
104
+ with monkeypatch.context() as m:
105
+ m.setattr(meson, "get_config", lambda: config0)
106
+ assert meson._meson_cli()[0] == os.path.expanduser("~/envs/py311/bin/meson")
107
+
108
+ m.setattr(meson, "get_config", lambda: config1)
109
+ assert meson._meson_cli()[0] == sys.executable
@@ -17,6 +17,10 @@ skip_unless_macos = pytest.mark.skipif(
17
17
  not sys.platform.startswith("darwin"), reason="Skipped; platform not macOS"
18
18
  )
19
19
 
20
+ skip_py_lt_311 = pytest.mark.skipif(
21
+ sys.version_info[:2] < (3, 11), reason="Skipped; Python < 3.11"
22
+ )
23
+
20
24
 
21
25
  def spin(*args, **user_kwargs):
22
26
  args = (str(el) for el in args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spin
3
- Version: 0.11
3
+ Version: 0.12
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
@@ -55,7 +55,7 @@ Requires-Dist: tomli; python_version < "3.11"
55
55
  Requires-Dist: colorama; platform_system == "Windows"
56
56
  Requires-Dist: importlib_metadata>=7
57
57
  Provides-Extra: lint
58
- Requires-Dist: pre-commit==3.7.1; extra == "lint"
58
+ Requires-Dist: pre-commit==3.8.0; extra == "lint"
59
59
  Provides-Extra: dev
60
60
  Requires-Dist: changelist==0.5; extra == "dev"
61
61
 
@@ -147,15 +147,15 @@ python -m spin
147
147
  Available as `spin.cmds.meson.*`.
148
148
 
149
149
  ```
150
- build 🔧 Build package with Meson/ninja
151
- ipython 💻 Launch IPython shell with PYTHONPATH set
152
- python 🐍 Launch Python shell with PYTHONPATH set
153
- shell 💻 Launch shell with PYTHONPATH set
154
- test 🔧 Run pytest
155
- run 🏁 Run a shell command with PYTHONPATH set
156
- docs 📖 Build Sphinx documentation
157
- gdb 👾 Execute a Python snippet with GDB
158
- lldb 👾 Execute a Python snippet with LLDB
150
+ build 🔧 Build package with Meson/ninja
151
+ ipython 💻 Launch IPython shell with PYTHONPATH set
152
+ python 🐍 Launch Python shell with PYTHONPATH set
153
+ shell 💻 Launch shell with PYTHONPATH set
154
+ test 🔧 Run pytest
155
+ run 🏁 Run a shell command with PYTHONPATH set
156
+ docs 📖 Build Sphinx documentation
157
+ gdb 👾 Execute a Python snippet with GDB
158
+ lldb 👾 Execute a Python snippet with LLDB
159
159
  ```
160
160
 
161
161
  ### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
@@ -163,7 +163,7 @@ Available as `spin.cmds.meson.*`.
163
163
  Available as `spin.cmds.build.*`:
164
164
 
165
165
  ```
166
- sdist 📦 Build a source distribution in `dist/`
166
+ sdist 📦 Build a source distribution in `dist/`
167
167
  ```
168
168
 
169
169
  ### [pip](https://pip.pypa.io) (Package Installer for Python)
@@ -174,7 +174,15 @@ development workflow.
174
174
  Available as `spin.cmds.pip.*`:
175
175
 
176
176
  ```
177
- install 💽 Build and install package using pip.
177
+ install 💽 Build and install package using pip.
178
+ ```
179
+
180
+ ### Meta (commands that operate on commands)
181
+
182
+ Available as `spin.cmds.meta.*`:
183
+
184
+ ```
185
+ introspect 🔍 Print a command's location and source code
178
186
  ```
179
187
 
180
188
  ## 🧪 Custom commands
@@ -310,6 +318,13 @@ fc-cache -f -v
310
318
  nox -s test
311
319
  ```
312
320
 
321
+ Other examples:
322
+
323
+ ```
324
+ nox -s test -- -v
325
+ nox -s test -- -v spin/tests/test_meson.py
326
+ ```
327
+
313
328
  ## History
314
329
 
315
330
  The `dev.py` tool was [proposed for SciPy](https://github.com/scipy/scipy/issues/15489) by Ralf Gommers and [implemented](https://github.com/scipy/scipy/pull/15959) by Sayantika Banik, Eduardo Naufel Schettino, and Ralf Gommers (also see [Sayantika's blog post](https://labs.quansight.org/blog/the-evolution-of-the-scipy-developer-cli)).
@@ -15,6 +15,7 @@ spin.egg-info/top_level.txt
15
15
  spin/cmds/__init__.py
16
16
  spin/cmds/build.py
17
17
  spin/cmds/meson.py
18
+ spin/cmds/meta.py
18
19
  spin/cmds/pip.py
19
20
  spin/cmds/util.py
20
21
  spin/tests/conftest.py
@@ -11,4 +11,4 @@ tomli
11
11
  changelist==0.5
12
12
 
13
13
  [lint]
14
- pre-commit==3.7.1
14
+ pre-commit==3.8.0
@@ -1,9 +0,0 @@
1
- import click
2
-
3
- from .util import run
4
-
5
-
6
- @click.command()
7
- def sdist():
8
- """📦 Build a source distribution in `dist/`"""
9
- run(["pyproject-build", ".", "--sdist"])
@@ -1,116 +0,0 @@
1
- import os
2
- import sys
3
- import tempfile
4
- from os.path import join as pjoin
5
- from os.path import normpath
6
-
7
- import pytest
8
-
9
- from spin.cmds import meson
10
- from spin.containers import DotDict
11
-
12
-
13
- def make_paths(root, paths):
14
- for p in paths:
15
- os.makedirs(pjoin(root, p.lstrip("/")))
16
-
17
-
18
- def test_path_discovery(monkeypatch):
19
- version = sys.version_info
20
- X, Y = version.major, version.minor
21
-
22
- # With multiple site-packages, choose the one that
23
- # matches the current Python version
24
- with tempfile.TemporaryDirectory() as d:
25
- with monkeypatch.context() as m:
26
- install_dir = pjoin(d, "build-install")
27
- m.setattr(meson, "install_dir", install_dir)
28
-
29
- make_paths(
30
- install_dir,
31
- [
32
- f"/usr/lib64/python{X}.{Y}/site-packages",
33
- f"/usr/lib64/python{X}.{Y + 1}/site-packages",
34
- f"/usr/lib64/python{X}.{Y + 2}/site-packages",
35
- ],
36
- )
37
- assert (
38
- normpath(f"/usr/lib64/python{X}.{Y}/site-packages")
39
- in meson._get_site_packages()
40
- )
41
-
42
- # Debian uses dist-packages
43
- with tempfile.TemporaryDirectory() as d:
44
- with monkeypatch.context() as m:
45
- install_dir = pjoin(d, "build-install")
46
- m.setattr(meson, "install_dir", install_dir)
47
-
48
- make_paths(
49
- install_dir,
50
- [
51
- f"/usr/lib64/python{X}.{Y}/dist-packages",
52
- ],
53
- )
54
- assert (
55
- normpath(f"/usr/lib64/python{X}.{Y}/dist-packages")
56
- in meson._get_site_packages()
57
- )
58
-
59
- # If there is no version information in site-packages,
60
- # use whatever site-packages can be found
61
- with tempfile.TemporaryDirectory() as d:
62
- with monkeypatch.context() as m:
63
- install_dir = pjoin(d, "build-install")
64
- m.setattr(meson, "install_dir", install_dir)
65
-
66
- make_paths(install_dir, ["/Python3/site-packages"])
67
- assert normpath("/Python3/site-packages") in meson._get_site_packages()
68
-
69
- # Raise if no site-package directory present
70
- with tempfile.TemporaryDirectory() as d:
71
- with monkeypatch.context() as m:
72
- install_dir = pjoin(d, "build-install")
73
- m.setattr(meson, "install_dir", install_dir)
74
-
75
- with pytest.raises(FileNotFoundError):
76
- meson._get_site_packages()
77
-
78
- # If there are multiple site-package paths, but without version information,
79
- # refuse the temptation to guess
80
- with tempfile.TemporaryDirectory() as d:
81
- install_dir = pjoin(d, "build-install")
82
- make_paths(
83
- install_dir, ["/Python3/x/site-packages", "/Python3/y/site-packages"]
84
- )
85
- with pytest.raises(FileNotFoundError):
86
- meson._get_site_packages()
87
-
88
- # Multiple site-package paths found, but none that matches our Python
89
- with tempfile.TemporaryDirectory() as d:
90
- with monkeypatch.context() as m:
91
- install_dir = pjoin(d, "build-install")
92
- m.setattr(meson, "install_dir", install_dir)
93
-
94
- make_paths(
95
- install_dir,
96
- [
97
- f"/usr/lib64/python{X}.{Y + 1}/site-packages",
98
- f"/usr/lib64/python{X}.{Y + 2}/site-packages",
99
- ],
100
- )
101
- with pytest.raises(FileNotFoundError):
102
- meson._get_site_packages()
103
-
104
-
105
- def test_meson_cli_discovery(monkeypatch):
106
- config0 = DotDict({"tool": {"spin": {"meson": {"cli": "~/envs/py311/bin/meson"}}}})
107
- config1 = DotDict(
108
- {"tool": {"spin": {"meson": {"cli": "~/envs/py311/bin/meson.py"}}}}
109
- )
110
-
111
- with monkeypatch.context() as m:
112
- m.setattr(meson, "get_config", lambda: config0)
113
- assert meson._meson_cli()[0] == os.path.expanduser("~/envs/py311/bin/meson")
114
-
115
- m.setattr(meson, "get_config", lambda: config1)
116
- assert meson._meson_cli()[0] == sys.executable
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