spin 0.13__tar.gz → 0.14__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 (32) hide show
  1. {spin-0.13/spin.egg-info → spin-0.14}/PKG-INFO +28 -5
  2. {spin-0.13 → spin-0.14}/README.md +23 -0
  3. {spin-0.13 → spin-0.14}/pyproject.toml +2 -3
  4. {spin-0.13 → spin-0.14}/spin/__init__.py +1 -1
  5. {spin-0.13 → spin-0.14}/spin/__main__.py +14 -8
  6. {spin-0.13 → spin-0.14}/spin/cmds/meson.py +103 -67
  7. {spin-0.13 → spin-0.14}/spin/cmds/util.py +10 -2
  8. {spin-0.13 → spin-0.14}/spin/tests/test_build_cmds.py +25 -15
  9. {spin-0.13 → spin-0.14}/spin/tests/test_editable.py +3 -3
  10. {spin-0.13 → spin-0.14/spin.egg-info}/PKG-INFO +28 -5
  11. {spin-0.13 → spin-0.14}/spin.egg-info/requires.txt +1 -1
  12. {spin-0.13 → spin-0.14}/LICENSE +0 -0
  13. {spin-0.13 → spin-0.14}/setup.cfg +0 -0
  14. {spin-0.13 → spin-0.14}/spin/cmds/__init__.py +0 -0
  15. {spin-0.13 → spin-0.14}/spin/cmds/build.py +0 -0
  16. {spin-0.13 → spin-0.14}/spin/cmds/meta.py +0 -0
  17. {spin-0.13 → spin-0.14}/spin/cmds/pip.py +0 -0
  18. {spin-0.13 → spin-0.14}/spin/color_format.py +0 -0
  19. {spin-0.13 → spin-0.14}/spin/containers.py +0 -0
  20. {spin-0.13 → spin-0.14}/spin/sectioned_help.py +0 -0
  21. {spin-0.13 → spin-0.14}/spin/tests/__init__.py +0 -0
  22. {spin-0.13 → spin-0.14}/spin/tests/conftest.py +0 -0
  23. {spin-0.13 → spin-0.14}/spin/tests/test_cli.py +0 -0
  24. {spin-0.13 → spin-0.14}/spin/tests/test_extend_command.py +0 -0
  25. {spin-0.13 → spin-0.14}/spin/tests/test_meson.py +0 -0
  26. {spin-0.13 → spin-0.14}/spin/tests/test_test.py +0 -0
  27. {spin-0.13 → spin-0.14}/spin/tests/test_util.py +0 -0
  28. {spin-0.13 → spin-0.14}/spin/tests/testutil.py +0 -0
  29. {spin-0.13 → spin-0.14}/spin.egg-info/SOURCES.txt +0 -0
  30. {spin-0.13 → spin-0.14}/spin.egg-info/dependency_links.txt +0 -0
  31. {spin-0.13 → spin-0.14}/spin.egg-info/entry_points.txt +0 -0
  32. {spin-0.13 → spin-0.14}/spin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: spin
3
- Version: 0.13
3
+ Version: 0.14
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
@@ -41,14 +41,13 @@ Classifier: Intended Audience :: Science/Research
41
41
  Classifier: License :: OSI Approved :: BSD License
42
42
  Classifier: Operating System :: OS Independent
43
43
  Classifier: Programming Language :: Python :: 3
44
- Classifier: Programming Language :: Python :: 3.8
45
44
  Classifier: Programming Language :: Python :: 3.9
46
45
  Classifier: Programming Language :: Python :: 3.10
47
46
  Classifier: Programming Language :: Python :: 3.11
48
47
  Classifier: Programming Language :: Python :: 3.12
49
48
  Classifier: Programming Language :: Python :: 3.13
50
49
  Classifier: Programming Language :: Python :: 3 :: Only
51
- Requires-Python: >=3.8
50
+ Requires-Python: >=3.9
52
51
  Description-Content-Type: text/markdown
53
52
  License-File: LICENSE
54
53
  Requires-Dist: click
@@ -56,9 +55,10 @@ Requires-Dist: tomli; python_version < "3.11"
56
55
  Requires-Dist: colorama; platform_system == "Windows"
57
56
  Requires-Dist: importlib_metadata>=7
58
57
  Provides-Extra: lint
59
- Requires-Dist: pre-commit==4.0.1; extra == "lint"
58
+ Requires-Dist: pre-commit==4.2.0; extra == "lint"
60
59
  Provides-Extra: dev
61
60
  Requires-Dist: changelist==0.5; extra == "dev"
61
+ Dynamic: license-file
62
62
 
63
63
  # 💫 Scientific Python INcantations (`spin`)
64
64
 
@@ -92,6 +92,7 @@ Most of the Meson commands listed below should work "out of the box" for those.
92
92
  - [Argument overrides](#argument-overrides)
93
93
  - [Advanced: adding arguments to built-in commands](#advanced-adding-arguments-to-built-in-commands)
94
94
  - [Advanced: override Meson CLI](#advanced-override-meson-cli)
95
+ - [Auto-completion](#auto-completion)
95
96
  - [FAQ](#faq)
96
97
  - [For contributors](#for-contributors)
97
98
  - [History](#history)
@@ -307,6 +308,28 @@ Meson CLI can be set in `pyproject.toml`:
307
308
  cli = 'path/to/custom/meson'
308
309
  ```
309
310
 
311
+ ## Auto-completion
312
+
313
+ To enable shell auto-completion, first install `spin`, then follow these instructions
314
+ (from the [click documentation](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion)).
315
+ The same instructions work for ZSH, just replace "bash" with "zsh".
316
+
317
+ 1. Create a completions file:
318
+
319
+ ```
320
+ _SPIN_COMPLETE=bash_source spin > ~/.spin-complete.bash
321
+ ```
322
+
323
+ Ignore the "need valid configuration" error messages.
324
+
325
+ 2. In your `~/.bashrc`, add:
326
+
327
+ ```
328
+ source ~/.spin-complete.bash
329
+ ```
330
+
331
+ Auto-completions should now work in any spin-enabled project directory.
332
+
310
333
  ## FAQ
311
334
 
312
335
  - Running `spin`, the emojis in the command list don't show up.
@@ -30,6 +30,7 @@ Most of the Meson commands listed below should work "out of the box" for those.
30
30
  - [Argument overrides](#argument-overrides)
31
31
  - [Advanced: adding arguments to built-in commands](#advanced-adding-arguments-to-built-in-commands)
32
32
  - [Advanced: override Meson CLI](#advanced-override-meson-cli)
33
+ - [Auto-completion](#auto-completion)
33
34
  - [FAQ](#faq)
34
35
  - [For contributors](#for-contributors)
35
36
  - [History](#history)
@@ -245,6 +246,28 @@ Meson CLI can be set in `pyproject.toml`:
245
246
  cli = 'path/to/custom/meson'
246
247
  ```
247
248
 
249
+ ## Auto-completion
250
+
251
+ To enable shell auto-completion, first install `spin`, then follow these instructions
252
+ (from the [click documentation](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion)).
253
+ The same instructions work for ZSH, just replace "bash" with "zsh".
254
+
255
+ 1. Create a completions file:
256
+
257
+ ```
258
+ _SPIN_COMPLETE=bash_source spin > ~/.spin-complete.bash
259
+ ```
260
+
261
+ Ignore the "need valid configuration" error messages.
262
+
263
+ 2. In your `~/.bashrc`, add:
264
+
265
+ ```
266
+ source ~/.spin-complete.bash
267
+ ```
268
+
269
+ Auto-completions should now work in any spin-enabled project directory.
270
+
248
271
  ## FAQ
249
272
 
250
273
  - Running `spin`, the emojis in the command list don't show up.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spin"
3
- requires-python = ">=3.8"
3
+ requires-python = ">=3.9"
4
4
  description = "Developer tool for scientific Python libraries"
5
5
  readme = "README.md"
6
6
  license = {file = "LICENSE"}
@@ -15,7 +15,6 @@ classifiers = [
15
15
  "License :: OSI Approved :: BSD License",
16
16
  "Operating System :: OS Independent",
17
17
  "Programming Language :: Python :: 3",
18
- "Programming Language :: Python :: 3.8",
19
18
  "Programming Language :: Python :: 3.9",
20
19
  "Programming Language :: Python :: 3.10",
21
20
  "Programming Language :: Python :: 3.11",
@@ -35,7 +34,7 @@ dynamic = ['version']
35
34
  spin = "spin.__main__:main"
36
35
 
37
36
  [project.optional-dependencies]
38
- lint = ["pre-commit == 4.0.1"]
37
+ lint = ["pre-commit == 4.2.0"]
39
38
  dev = ["changelist == 0.5"]
40
39
 
41
40
  [project.urls]
@@ -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.13"
9
+ __version__ = "0.14"
10
10
 
11
11
  from .cmds import util
12
12
 
@@ -1,11 +1,11 @@
1
1
  import collections
2
2
  import importlib
3
3
  import importlib.util
4
+ import inspect
4
5
  import os
5
6
  import pathlib
6
7
  import sys
7
8
  import textwrap
8
- import traceback
9
9
  from typing import Union
10
10
 
11
11
  import click
@@ -199,20 +199,26 @@ def main():
199
199
 
200
200
  try:
201
201
  group()
202
- except Exception:
203
- click.secho("\n" + traceback.format_exc(limit=-3), fg="red", file=sys.stderr)
202
+ except Exception as e:
204
203
  click.secho(
205
204
  textwrap.dedent(
206
205
  f"""\
207
- An internal error has occurred. Please file a bug report at
208
206
 
209
- https://github.com/scientific-python/spin
207
+ {type(e).__name__}: {e}
210
208
 
211
- including the above traceback and the following information:
209
+ This exception was raised from:
212
210
 
213
- spin: {__version__}, package: {proj_name}
211
+ {inspect.trace()[-1].filename}
214
212
 
215
- Aborting."""
213
+ If you suspect this is a bug in `spin`, please file a report at:
214
+
215
+ https://github.com/scientific-python/spin
216
+
217
+ including the above traceback and the following information:
218
+
219
+ spin: {__version__}, package: {proj_name}
220
+
221
+ Aborting."""
216
222
  ),
217
223
  fg="red",
218
224
  bold=True,
@@ -236,6 +236,16 @@ if sys.platform.startswith("win"):
236
236
  else:
237
237
  DEFAULT_PREFIX = "/usr"
238
238
 
239
+
240
+ build_option = click.option(
241
+ "--no-build",
242
+ "build",
243
+ is_flag=True,
244
+ callback=lambda ctx, param, value: not value, # store opposite value in `build` var
245
+ default=False,
246
+ help="Disable building before executing command",
247
+ )
248
+
239
249
  build_dir_option = click.option(
240
250
  "-C",
241
251
  "--build-dir",
@@ -283,13 +293,15 @@ def build(
283
293
  quiet=False,
284
294
  build_dir=None,
285
295
  prefix=None,
296
+ meson_compile_args=(),
297
+ meson_install_args=(),
286
298
  ):
287
299
  """🔧 Build package with Meson/ninja
288
300
 
289
301
  The package is installed to `build-install` (unless a different
290
302
  build directory is specified with `-C`).
291
303
 
292
- MESON_ARGS are passed through e.g.:
304
+ MESON_ARGS are passed through to `meson setup` e.g.:
293
305
 
294
306
  spin build -- -Dpkg_config_path=/lib64/pkgconfig
295
307
 
@@ -328,12 +340,14 @@ def build(
328
340
  )
329
341
  return
330
342
 
331
- meson_args = list(meson_args)
343
+ meson_args_setup = list(meson_args)
332
344
 
333
345
  if gcov:
334
- meson_args = meson_args + ["-Db_coverage=true"]
346
+ meson_args_setup = meson_args_setup + ["-Db_coverage=true"]
335
347
 
336
- setup_cmd = _meson_cli() + ["setup", build_dir, f"--prefix={prefix}"] + meson_args
348
+ setup_cmd = (
349
+ _meson_cli() + ["setup", build_dir, f"--prefix={prefix}"] + meson_args_setup
350
+ )
337
351
 
338
352
  if clean:
339
353
  print(f"Removing `{build_dir}`")
@@ -365,10 +379,15 @@ def build(
365
379
  compile_flags += ["-j", str(jobs)]
366
380
 
367
381
  p = _run(
368
- _meson_cli() + ["compile"] + compile_flags + ["-C", build_dir],
382
+ _meson_cli()
383
+ + ["compile"]
384
+ + compile_flags
385
+ + ["-C", build_dir]
386
+ + list(meson_compile_args),
369
387
  sys_exit=True,
370
388
  output=not quiet,
371
389
  )
390
+
372
391
  p = _run(
373
392
  _meson_cli()
374
393
  + [
@@ -380,7 +399,8 @@ def build(
380
399
  install_dir
381
400
  if os.path.isabs(install_dir)
382
401
  else os.path.relpath(abs_install_dir, abs_build_dir),
383
- ],
402
+ ]
403
+ + list(meson_install_args),
384
404
  output=(not quiet) and verbose,
385
405
  )
386
406
 
@@ -437,6 +457,7 @@ Which tests to run. Can be a module, function, class, or method:
437
457
  default="html",
438
458
  help=f"Format of the gcov report. Can be one of {', '.join(e.value for e in GcovReportFormat)}.",
439
459
  )
460
+ @build_option
440
461
  @build_dir_option
441
462
  @click.pass_context
442
463
  def test(
@@ -449,6 +470,7 @@ def test(
449
470
  coverage=False,
450
471
  gcov=None,
451
472
  gcov_format=None,
473
+ build=None,
452
474
  build_dir=None,
453
475
  ):
454
476
  """🔧 Run tests
@@ -531,15 +553,16 @@ def test(
531
553
  )
532
554
  raise SystemExit(1)
533
555
 
534
- build_cmd = _get_configured_command("build")
535
- if build_cmd:
536
- click.secho(
537
- "Invoking `build` prior to running tests:", bold=True, fg="bright_green"
538
- )
539
- if gcov is not None:
540
- ctx.invoke(build_cmd, build_dir=build_dir, gcov=bool(gcov))
541
- else:
542
- ctx.invoke(build_cmd, build_dir=build_dir)
556
+ if build:
557
+ build_cmd = _get_configured_command("build")
558
+ if build_cmd:
559
+ click.secho(
560
+ "Invoking `build` prior to running tests:", bold=True, fg="bright_green"
561
+ )
562
+ if gcov is not None:
563
+ ctx.invoke(build_cmd, build_dir=build_dir, gcov=bool(gcov))
564
+ else:
565
+ ctx.invoke(build_cmd, build_dir=build_dir)
543
566
 
544
567
  site_path = _set_pythonpath(build_dir)
545
568
 
@@ -637,9 +660,10 @@ def test(
637
660
  "--code", "-c", metavar="CODE", help="Python program passed in as a string"
638
661
  )
639
662
  @click.argument("gdb_args", nargs=-1)
663
+ @build_option
640
664
  @build_dir_option
641
665
  @click.pass_context
642
- def gdb(ctx, *, code, gdb_args, build_dir):
666
+ def gdb(ctx, *, code, gdb_args, build=None, build_dir=None):
643
667
  """👾 Execute code through GDB
644
668
 
645
669
  spin gdb -c 'import numpy as np; print(np.__version__)'
@@ -660,12 +684,13 @@ def gdb(ctx, *, code, gdb_args, build_dir):
660
684
  spin gdb my_tests.py
661
685
  spin gdb -- my_tests.py --mytest-flag
662
686
  """
663
- build_cmd = _get_configured_command("build")
664
- if build_cmd:
665
- click.secho(
666
- "Invoking `build` prior to invoking gdb:", bold=True, fg="bright_green"
667
- )
668
- ctx.invoke(build_cmd, build_dir=build_dir)
687
+ if build:
688
+ build_cmd = _get_configured_command("build")
689
+ if build_cmd:
690
+ click.secho(
691
+ "Invoking `build` prior to invoking gdb:", bold=True, fg="bright_green"
692
+ )
693
+ ctx.invoke(build_cmd, build_dir=build_dir)
669
694
 
670
695
  _set_pythonpath(build_dir)
671
696
  gdb_args = list(gdb_args)
@@ -690,21 +715,25 @@ def gdb(ctx, *, code, gdb_args, build_dir):
690
715
 
691
716
  @click.command()
692
717
  @click.argument("ipython_args", nargs=-1)
718
+ @build_option
693
719
  @build_dir_option
694
720
  @click.pass_context
695
- def ipython(ctx, *, ipython_args, build_dir, pre_import=""):
721
+ def ipython(ctx, *, ipython_args, build=None, build_dir=None, pre_import=""):
696
722
  """💻 Launch IPython shell with PYTHONPATH set
697
723
 
698
724
  IPYTHON_ARGS are passed through directly to IPython, e.g.:
699
725
 
700
726
  spin ipython -- -i myscript.py
701
727
  """
702
- build_cmd = _get_configured_command("build")
703
- if build_cmd:
704
- click.secho(
705
- "Invoking `build` prior to launching ipython:", bold=True, fg="bright_green"
706
- )
707
- ctx.invoke(build_cmd, build_dir=build_dir)
728
+ if build:
729
+ build_cmd = _get_configured_command("build")
730
+ if build_cmd:
731
+ click.secho(
732
+ "Invoking `build` prior to launching ipython:",
733
+ bold=True,
734
+ fg="bright_green",
735
+ )
736
+ ctx.invoke(build_cmd, build_dir=build_dir)
708
737
 
709
738
  p = _set_pythonpath(build_dir)
710
739
  if p:
@@ -716,9 +745,10 @@ def ipython(ctx, *, ipython_args, build_dir, pre_import=""):
716
745
 
717
746
  @click.command()
718
747
  @click.argument("shell_args", nargs=-1)
748
+ @build_option
719
749
  @build_dir_option
720
750
  @click.pass_context
721
- def shell(ctx, shell_args=[], build_dir=None):
751
+ def shell(ctx, shell_args=[], build=None, build_dir=None):
722
752
  """💻 Launch shell with PYTHONPATH set
723
753
 
724
754
  SHELL_ARGS are passed through directly to the shell, e.g.:
@@ -728,12 +758,15 @@ def shell(ctx, shell_args=[], build_dir=None):
728
758
  Ensure that your shell init file (e.g., ~/.zshrc) does not override
729
759
  the PYTHONPATH.
730
760
  """
731
- build_cmd = _get_configured_command("build")
732
- if build_cmd:
733
- click.secho(
734
- "Invoking `build` prior to invoking shell:", bold=True, fg="bright_green"
735
- )
736
- ctx.invoke(build_cmd, build_dir=build_dir)
761
+ if build:
762
+ build_cmd = _get_configured_command("build")
763
+ if build_cmd:
764
+ click.secho(
765
+ "Invoking `build` prior to invoking shell:",
766
+ bold=True,
767
+ fg="bright_green",
768
+ )
769
+ ctx.invoke(build_cmd, build_dir=build_dir)
737
770
 
738
771
  p = _set_pythonpath(build_dir)
739
772
  if p:
@@ -748,21 +781,25 @@ def shell(ctx, shell_args=[], build_dir=None):
748
781
 
749
782
  @click.command()
750
783
  @click.argument("python_args", nargs=-1)
784
+ @build_option
751
785
  @build_dir_option
752
786
  @click.pass_context
753
- def python(ctx, *, python_args, build_dir):
787
+ def python(ctx, *, python_args, build=None, build_dir=None):
754
788
  """🐍 Launch Python shell with PYTHONPATH set
755
789
 
756
790
  PYTHON_ARGS are passed through directly to Python, e.g.:
757
791
 
758
792
  spin python -- -c 'import sys; print(sys.path)'
759
793
  """
760
- build_cmd = _get_configured_command("build")
761
- if build_cmd:
762
- click.secho(
763
- "Invoking `build` prior to invoking Python:", bold=True, fg="bright_green"
764
- )
765
- ctx.invoke(build_cmd, build_dir=build_dir)
794
+ if build:
795
+ build_cmd = _get_configured_command("build")
796
+ if build_cmd:
797
+ click.secho(
798
+ "Invoking `build` prior to invoking Python:",
799
+ bold=True,
800
+ fg="bright_green",
801
+ )
802
+ ctx.invoke(build_cmd, build_dir=build_dir)
766
803
 
767
804
  p = _set_pythonpath(build_dir)
768
805
  if p:
@@ -789,10 +826,11 @@ def python(ctx, *, python_args, build_dir):
789
826
 
790
827
 
791
828
  @click.command(context_settings={"ignore_unknown_options": True})
829
+ @build_option
792
830
  @build_dir_option
793
831
  @click.argument("args", nargs=-1)
794
832
  @click.pass_context
795
- def run(ctx, *, args, build_dir=None):
833
+ def run(ctx, *, args, build=None, build_dir=None):
796
834
  """🏁 Run a shell command with PYTHONPATH set
797
835
 
798
836
  \b
@@ -811,12 +849,13 @@ def run(ctx, *, args, build_dir=None):
811
849
  if not len(args) > 0:
812
850
  raise RuntimeError("No command given")
813
851
 
814
- build_cmd = _get_configured_command("build")
815
- if build_cmd:
816
- # Redirect spin generated output
817
- with contextlib.redirect_stdout(sys.stderr):
818
- # Also ask build to be quiet
819
- ctx.invoke(build_cmd, build_dir=build_dir, quiet=True)
852
+ if build:
853
+ build_cmd = _get_configured_command("build")
854
+ if build_cmd:
855
+ # Redirect spin generated output
856
+ with contextlib.redirect_stdout(sys.stderr):
857
+ # Also ask build to be quiet
858
+ ctx.invoke(build_cmd, build_dir=build_dir, quiet=True)
820
859
 
821
860
  is_posix = sys.platform in ("linux", "darwin")
822
861
  shell = len(args) == 1
@@ -872,12 +911,6 @@ def run(ctx, *, args, build_dir=None):
872
911
  default=False,
873
912
  help="Clean previously built docs before building",
874
913
  )
875
- @click.option(
876
- "--build/--no-build",
877
- "first_build",
878
- default=True,
879
- help="Build project before generating docs",
880
- )
881
914
  @click.option(
882
915
  "--plot/--no-plot",
883
916
  "sphinx_gallery_plot",
@@ -891,6 +924,7 @@ def run(ctx, *, args, build_dir=None):
891
924
  metavar="N_JOBS",
892
925
  help="Number of parallel build jobs",
893
926
  )
927
+ @build_option
894
928
  @build_dir_option
895
929
  @click.pass_context
896
930
  def docs(
@@ -898,10 +932,10 @@ def docs(
898
932
  *,
899
933
  sphinx_target,
900
934
  clean,
901
- first_build,
902
935
  jobs,
903
936
  sphinx_gallery_plot,
904
937
  clean_dirs=None,
938
+ build=None,
905
939
  build_dir=None,
906
940
  ):
907
941
  """📖 Build Sphinx documentation
@@ -931,7 +965,7 @@ def docs(
931
965
 
932
966
  if sphinx_target in ("targets", "help"):
933
967
  clean = False
934
- first_build = False
968
+ build = False
935
969
  sphinx_target = "help"
936
970
 
937
971
  if clean:
@@ -953,7 +987,7 @@ def docs(
953
987
 
954
988
  build_cmd = _get_configured_command("build")
955
989
 
956
- if build_cmd and first_build:
990
+ if build_cmd and build:
957
991
  click.secho(
958
992
  "Invoking `build` prior to building docs:", bold=True, fg="bright_green"
959
993
  )
@@ -979,7 +1013,7 @@ def docs(
979
1013
 
980
1014
  if site_path:
981
1015
  os.environ["PYTHONPATH"] = (
982
- f'{site_path}{os.sep}:{os.environ.get("PYTHONPATH", "")}'
1016
+ f"{site_path}{os.sep}:{os.environ.get('PYTHONPATH', '')}"
983
1017
  )
984
1018
  click.secho(
985
1019
  f"$ export PYTHONPATH={os.environ['PYTHONPATH']}",
@@ -997,9 +1031,10 @@ def docs(
997
1031
  "--code", "-c", metavar="CODE", help="Python program passed in as a string"
998
1032
  )
999
1033
  @click.argument("lldb_args", nargs=-1)
1034
+ @build_option
1000
1035
  @build_dir_option
1001
1036
  @click.pass_context
1002
- def lldb(ctx, *, code, lldb_args, build_dir=None):
1037
+ def lldb(ctx, *, code, lldb_args, build=None, build_dir=None):
1003
1038
  """👾 Execute code through LLDB
1004
1039
 
1005
1040
  spin lldb -c 'import numpy as np; print(np.__version__)'
@@ -1022,12 +1057,13 @@ def lldb(ctx, *, code, lldb_args, build_dir=None):
1022
1057
  spin lldb -- --arch x86_64 -- my_tests.py
1023
1058
  spin lldb -c 'import numpy as np; print(np.__version__)' -- --arch x86_64
1024
1059
  """
1025
- build_cmd = _get_configured_command("build")
1026
- if build_cmd:
1027
- click.secho(
1028
- "Invoking `build` prior to invoking lldb:", bold=True, fg="bright_green"
1029
- )
1030
- ctx.invoke(build_cmd, build_dir=build_dir)
1060
+ if build:
1061
+ build_cmd = _get_configured_command("build")
1062
+ if build_cmd:
1063
+ click.secho(
1064
+ "Invoking `build` prior to invoking lldb:", bold=True, fg="bright_green"
1065
+ )
1066
+ ctx.invoke(build_cmd, build_dir=build_dir)
1031
1067
 
1032
1068
  _set_pythonpath(build_dir)
1033
1069
  lldb_args = list(lldb_args)
@@ -1,10 +1,11 @@
1
1
  from __future__ import (
2
- annotations, # noqa: F401 # TODO: remove once only >3.8 is supported
2
+ annotations, # noqa: F401 # TODO: remove once only >3.14 is supported
3
3
  )
4
4
 
5
5
  import copy
6
6
  import os
7
7
  import shlex
8
+ import shutil
8
9
  import subprocess
9
10
  import sys
10
11
  from collections.abc import Callable
@@ -65,8 +66,15 @@ def run(
65
66
  kwargs = {**output_kwargs, **kwargs}
66
67
 
67
68
  if replace and (sys.platform in ("linux", "darwin")):
69
+ if not shutil.which(cmd[0]):
70
+ click.secho(
71
+ f"`{cmd[0]}` executable not found; exiting.",
72
+ fg="bright_red",
73
+ )
74
+ raise SystemExit(1) from None
75
+
68
76
  os.execvp(cmd[0], cmd)
69
- print(f"Failed to launch `{cmd}`")
77
+ print(f"Failed to launch `{cmd}`; exiting.")
70
78
  sys.exit(-1)
71
79
  else:
72
80
  try:
@@ -14,6 +14,7 @@ from .testutil import (
14
14
  skip_unless_linux,
15
15
  skip_unless_macos,
16
16
  spin,
17
+ stderr,
17
18
  stdout,
18
19
  )
19
20
 
@@ -23,9 +24,9 @@ def test_basic_build(example_pkg):
23
24
  spin("build")
24
25
 
25
26
  assert Path("build").exists(), "`build` folder not created after `spin build`"
26
- assert Path(
27
- "build-install"
28
- ).exists(), "`build-install` folder not created after `spin build`"
27
+ assert Path("build-install").exists(), (
28
+ "`build-install` folder not created after `spin build`"
29
+ )
29
30
 
30
31
 
31
32
  def test_debug_builds(example_pkg):
@@ -64,17 +65,17 @@ def test_coverage_reports(example_pkg, report_type, output_file):
64
65
  spin("test", "--gcov", f"--gcov-format={report_type}")
65
66
 
66
67
  coverage_report = Path("./build/meson-logs", output_file)
67
- assert (
68
- coverage_report.exists()
69
- ), f"coverage report not generated for gcov build ({report_type})"
68
+ assert coverage_report.exists(), (
69
+ f"coverage report not generated for gcov build ({report_type})"
70
+ )
70
71
 
71
72
 
72
73
  def test_expand_pythonpath(example_pkg):
73
74
  """Does an $ENV_VAR get expanded in `spin run`?"""
74
75
  output = spin("run", "echo $PYTHONPATH")
75
- assert any(
76
- p in stdout(output) for p in ("site-packages", "dist-packages")
77
- ), f"Expected value of $PYTHONPATH, got {stdout(output)} instead"
76
+ assert any(p in stdout(output) for p in ("site-packages", "dist-packages")), (
77
+ f"Expected value of $PYTHONPATH, got {stdout(output)} instead"
78
+ )
78
79
 
79
80
 
80
81
  def test_run_stdout(example_pkg):
@@ -85,9 +86,18 @@ def test_run_stdout(example_pkg):
85
86
  "-c",
86
87
  "import sys; del sys.path[0]; import example_pkg; print(example_pkg.__version__)",
87
88
  )
88
- assert (
89
- stdout(p) == "0.0.0dev0"
90
- ), f"`spin run` stdout did not yield version, but {stdout(p)}"
89
+ assert stdout(p) == "0.0.0dev0", (
90
+ f"`spin run` stdout did not yield version, but {stdout(p)}"
91
+ )
92
+
93
+
94
+ def test_run_no_build(example_pkg):
95
+ """Does spin run ... --no-build correctly avoid building."""
96
+ output = stderr(spin("run", "echo $PYTHONPATH"))
97
+ assert "meson compile" in output
98
+
99
+ output = stderr(spin("run", "--no-build", "echo $PYTHONPATH"))
100
+ assert "meson compile" not in output
91
101
 
92
102
 
93
103
  # Detecting whether a file is executable is not that easy on Windows,
@@ -97,9 +107,9 @@ def test_recommend_run_python(example_pkg):
97
107
  """If `spin run file.py` is called, is `spin run python file.py` recommended?"""
98
108
  with tempfile.NamedTemporaryFile(suffix=".py") as f:
99
109
  p = spin("run", f.name, sys_exit=False)
100
- assert "Did you mean to call" in stdout(
101
- p
102
- ), "Failed to recommend `python run python file.py`"
110
+ assert "Did you mean to call" in stdout(p), (
111
+ "Failed to recommend `python run python file.py`"
112
+ )
103
113
 
104
114
 
105
115
  def test_sdist(example_pkg):
@@ -2,9 +2,9 @@ from .testutil import spin, stdout
2
2
 
3
3
 
4
4
  def test_detect_editable(example_pkg, editable_install):
5
- assert "Editable install of same source detected" in stdout(
6
- spin("build")
7
- ), "Failed to detect and warn about editable install"
5
+ assert "Editable install of same source detected" in stdout(spin("build")), (
6
+ "Failed to detect and warn about editable install"
7
+ )
8
8
 
9
9
 
10
10
  def test_editable_tests(example_pkg, editable_install):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: spin
3
- Version: 0.13
3
+ Version: 0.14
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
@@ -41,14 +41,13 @@ Classifier: Intended Audience :: Science/Research
41
41
  Classifier: License :: OSI Approved :: BSD License
42
42
  Classifier: Operating System :: OS Independent
43
43
  Classifier: Programming Language :: Python :: 3
44
- Classifier: Programming Language :: Python :: 3.8
45
44
  Classifier: Programming Language :: Python :: 3.9
46
45
  Classifier: Programming Language :: Python :: 3.10
47
46
  Classifier: Programming Language :: Python :: 3.11
48
47
  Classifier: Programming Language :: Python :: 3.12
49
48
  Classifier: Programming Language :: Python :: 3.13
50
49
  Classifier: Programming Language :: Python :: 3 :: Only
51
- Requires-Python: >=3.8
50
+ Requires-Python: >=3.9
52
51
  Description-Content-Type: text/markdown
53
52
  License-File: LICENSE
54
53
  Requires-Dist: click
@@ -56,9 +55,10 @@ Requires-Dist: tomli; python_version < "3.11"
56
55
  Requires-Dist: colorama; platform_system == "Windows"
57
56
  Requires-Dist: importlib_metadata>=7
58
57
  Provides-Extra: lint
59
- Requires-Dist: pre-commit==4.0.1; extra == "lint"
58
+ Requires-Dist: pre-commit==4.2.0; extra == "lint"
60
59
  Provides-Extra: dev
61
60
  Requires-Dist: changelist==0.5; extra == "dev"
61
+ Dynamic: license-file
62
62
 
63
63
  # 💫 Scientific Python INcantations (`spin`)
64
64
 
@@ -92,6 +92,7 @@ Most of the Meson commands listed below should work "out of the box" for those.
92
92
  - [Argument overrides](#argument-overrides)
93
93
  - [Advanced: adding arguments to built-in commands](#advanced-adding-arguments-to-built-in-commands)
94
94
  - [Advanced: override Meson CLI](#advanced-override-meson-cli)
95
+ - [Auto-completion](#auto-completion)
95
96
  - [FAQ](#faq)
96
97
  - [For contributors](#for-contributors)
97
98
  - [History](#history)
@@ -307,6 +308,28 @@ Meson CLI can be set in `pyproject.toml`:
307
308
  cli = 'path/to/custom/meson'
308
309
  ```
309
310
 
311
+ ## Auto-completion
312
+
313
+ To enable shell auto-completion, first install `spin`, then follow these instructions
314
+ (from the [click documentation](https://click.palletsprojects.com/en/stable/shell-completion/#enabling-completion)).
315
+ The same instructions work for ZSH, just replace "bash" with "zsh".
316
+
317
+ 1. Create a completions file:
318
+
319
+ ```
320
+ _SPIN_COMPLETE=bash_source spin > ~/.spin-complete.bash
321
+ ```
322
+
323
+ Ignore the "need valid configuration" error messages.
324
+
325
+ 2. In your `~/.bashrc`, add:
326
+
327
+ ```
328
+ source ~/.spin-complete.bash
329
+ ```
330
+
331
+ Auto-completions should now work in any spin-enabled project directory.
332
+
310
333
  ## FAQ
311
334
 
312
335
  - Running `spin`, the emojis in the command list don't show up.
@@ -11,4 +11,4 @@ tomli
11
11
  changelist==0.5
12
12
 
13
13
  [lint]
14
- pre-commit==4.0.1
14
+ pre-commit==4.2.0
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes