spin 0.0.1.dev0__tar.gz → 0.2__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.
@@ -1,6 +1,6 @@
1
1
  BSD 3-Clause License
2
2
 
3
- Copyright (c) 2022, Danila Bredikhin
3
+ Copyright (c) 2021--2022, Scientific Python project
4
4
  All rights reserved.
5
5
 
6
6
  Redistribution and use in source and binary forms, with or without
spin-0.2/PKG-INFO ADDED
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: spin
3
+ Version: 0.2
4
+ Summary: Developer tool for scientific Python libraries
5
+ Maintainer-email: Scientific Python <spin@discuss.scientific-python.org>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2021--2022, Scientific Python project
9
+ All rights reserved.
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are met:
13
+
14
+ 1. Redistributions of source code must retain the above copyright notice, this
15
+ list of conditions and the following disclaimer.
16
+
17
+ 2. Redistributions in binary form must reproduce the above copyright notice,
18
+ this list of conditions and the following disclaimer in the documentation
19
+ and/or other materials provided with the distribution.
20
+
21
+ 3. Neither the name of the copyright holder nor the names of its
22
+ contributors may be used to endorse or promote products derived from
23
+ this software without specific prior written permission.
24
+
25
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+
36
+ Project-URL: homepage, https://github.com/scientific-python/spin
37
+ Classifier: Development Status :: 4 - Beta
38
+ Classifier: Programming Language :: Python
39
+ Requires-Python: >=3.7
40
+ Provides-Extra: lint
41
+ License-File: LICENSE
spin-0.2/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # 💫 Scientific Python INcantations (`spin`)
2
+
3
+ ## A developer tool for scientific Python libraries
4
+
5
+ **NOTE:** If you are looking for `devpy`, this is it! We had to rename
6
+ the package to publish it on PyPi.
7
+
8
+ Developers need to memorize a whole bunch of magic command-line incantations.
9
+ And these incantations change from time to time!
10
+ Typically, their lives are made simpler by a Makefile, but Makefiles can be convoluted, are not written in Python, and are hard to extend.
11
+ The rationale behind `spin` is therefore to provide a simple interface for common development tasks.
12
+ It comes with a few common build commands out the box, but can easily be customized per project.
13
+
14
+ 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.
15
+ When many of the build and installation commands changed, it made sense to abstract away the nuisance of having to re-learn them.
16
+
17
+ ## Installation
18
+
19
+ `spin` is not yet available via PyPi (but soon!).
20
+
21
+ Meanwhile, please install it directly from GitHub:
22
+
23
+ ```
24
+ pip install git+https://github.com/scientific-python/spin
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Settings are in your project's `pyproject.toml`.
30
+ As an example, see the `[tool.spin]` section of [an example `pyproject.toml`](https://github.com/scientific-python/spin/blob/main/example_pkg/pyproject.toml).
31
+
32
+ The `[tool.spin]` section should contain:
33
+
34
+ ```
35
+ package = "pkg_importname" # name of your package
36
+ commands = [
37
+ "spin.cmds.meson.build",
38
+ "spin.cmds.meson.test"
39
+ ]
40
+ ```
41
+
42
+ See [the command selection](#built-in-commands) below.
43
+
44
+ ### Command sections
45
+
46
+ Once you have several commands, it may be useful to organize them into sections.
47
+ In `pyproject.toml`, instead of specifying the commands as a list, use the following structure:
48
+
49
+ ```toml
50
+ [tool.spin.commands]
51
+ "Build" = [
52
+ "spin.cmds.meson.build",
53
+ "spin.cmds.meson.test"
54
+ ]
55
+ "Environments" = [
56
+ "spin.cmds.meson.shell",
57
+ "spin.cmds.meson.ipython",
58
+ "spin.cmds.meson.python"
59
+ ]
60
+ ```
61
+
62
+ These commands will then be rendered as:
63
+
64
+ ```
65
+ Build:
66
+ build 🔧 Build package with Meson/ninja and install
67
+ test 🔧 Run tests
68
+
69
+ Environments:
70
+ shell 💻 Launch shell with PYTHONPATH set
71
+ ipython 💻 Launch IPython shell with PYTHONPATH set
72
+ python 🐍 Launch Python shell with PYTHONPATH set
73
+ ```
74
+
75
+ ## Running
76
+
77
+ ```
78
+ python -m spin
79
+ ```
80
+
81
+ On Unix-like systems, you can also copy the [`dev.py` script](https://github.com/scientific-python/spin/blob/main/example_pkg/dev.py) to the root of your project directory, and launch it as:
82
+
83
+ ```
84
+ ./dev.py
85
+ ```
86
+
87
+ ## Built-in commands
88
+
89
+ ### [Meson](https://meson-python.readthedocs.io)
90
+
91
+ ```
92
+ build 🔧 Build package with Meson/ninja and install to `build-install`
93
+ ipython 💻 Launch IPython shell with PYTHONPATH set
94
+ python 🐍 Launch Python shell with PYTHONPATH set
95
+ shell 💻 Launch shell with PYTHONPATH set
96
+ test 🔧 Run pytest
97
+ ```
98
+
99
+ ### [Build](https://pypa-build.readthedocs.io/en/stable/) (PEP 517 builder)
100
+
101
+ `spin` was started with Meson in mind, but we're working on expanding commands for PEP 517 `build`.
102
+
103
+ ```
104
+ sdist 📦 Build a source distribution in `dist/`.
105
+ ```
106
+
107
+ ## 🧪 Custom commands
108
+
109
+ `spin` can invoke custom commands. These commands define their own arguments, and have access to the `pyproject.toml` file for further configuration.
110
+
111
+ See, e.g., the [example custom command](https://github.com/scientific-python/spin/blob/main/example_pkg/.spin/cmds.py).
112
+
113
+ Add custom commands to the `commands` variable in the `[tool.spin]` section of `pyproject.toml` as follows:
114
+
115
+ ```
116
+ commands = [..., '.spin/cmds.py:example']
117
+ ```
118
+
119
+ Here, the command is stored in `.spin/cmds.py`, and the function
120
+ is named `example`.
121
+
122
+ ### Configuration
123
+
124
+ Custom commands can access the `pyproject.toml` as follows:
125
+
126
+ ```python
127
+ from spin import util
128
+
129
+ @click.command()
130
+ def example():
131
+ """Command that accesses `pyproject.toml` configuration"""
132
+ config = util.get_config()
133
+ print(config["tool.spin"])
134
+ ```
135
+
136
+ ## History
137
+
138
+ 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)).
139
+ Inspired by that implementation, `spin` (this package) is a minimal rewrite by Stéfan van der Walt, that aims to be easily extendable so that it can be used across ecosystem libraries.
140
+ We thank Danila Bredikhin and Luca Marconato who kindly donated the `spin` name on PyPi.
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "spin"
3
+ version = "0.2"
4
+ requires-python = ">=3.7"
5
+ description = "Developer tool for scientific Python libraries"
6
+ license = {file = "LICENSE"}
7
+ maintainers = [
8
+ {name = "Scientific Python", email = "spin@discuss.scientific-python.org"}
9
+ ]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Programming Language :: Python"
13
+ ]
14
+ dependencies = [
15
+ "click",
16
+ "toml",
17
+ "colorama; platform_system == 'Windows'"
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ lint = ["pre-commit >= 3.1"]
22
+
23
+ [project.urls]
24
+ homepage = "https://github.com/scientific-python/spin"
25
+
26
+ [tool.setuptools.packages.find]
27
+ include = ["spin*"]
28
+
29
+ [tool.pytest.ini_options]
30
+ filterwarnings = [
31
+ "ignore:cannot collect 'test' because it is not a function:",
32
+ ]
spin-0.2/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """
2
+ `spin` is a developer tool for scientific Python libraries
3
+
4
+ For usage information see `python -m spin`.
5
+
6
+ Read more at https://github.com/scientific-python/spin
7
+ """
8
+
9
+ from .cmds import util
@@ -0,0 +1,123 @@
1
+ import collections
2
+ import os
3
+ import sys
4
+ import importlib
5
+ from glob import glob
6
+
7
+ import click
8
+ import toml
9
+
10
+ from . import cmds
11
+ from .cmds import util
12
+ from .cmds import *
13
+ from .sectioned_help import SectionedHelpGroup
14
+ from .color_format import ColorHelpFormatter
15
+
16
+
17
+ click.Context.formatter_class = ColorHelpFormatter
18
+
19
+
20
+ class DotDict(collections.UserDict):
21
+ def __getitem__(self, key):
22
+ subitem = self.data
23
+ for subkey in key.split("."):
24
+ try:
25
+ subitem = subitem[subkey]
26
+ except KeyError:
27
+ raise KeyError(f"`{key}` not found in configuration") from None
28
+ return subitem
29
+
30
+
31
+ if __name__ == "__main__":
32
+ if not os.path.exists("pyproject.toml"):
33
+ print("Error: cannot find [pyproject.toml]")
34
+ sys.exit(1)
35
+
36
+ with open("pyproject.toml") as f:
37
+ try:
38
+ toml_config = toml.load(f)
39
+ except:
40
+ print("Cannot parse [pyproject.toml]")
41
+ sys.exit(1)
42
+
43
+ project_config = toml_config.get("project", {})
44
+
45
+ try:
46
+ config = toml_config["tool"]["spin"]
47
+ except KeyError:
48
+ print("No configuration found in [pyproject.toml] for [tool.spin]")
49
+ sys.exit(1)
50
+
51
+ proj_name = project_config.get("name", config["package"])
52
+
53
+ @click.group(help=f"Developer tool for {proj_name}", cls=SectionedHelpGroup)
54
+ @click.pass_context
55
+ def group(ctx):
56
+ ctx.meta["config"] = DotDict(toml_config)
57
+ ctx.meta["commands"] = ctx.command.section_commands
58
+ ctx.show_default = True
59
+
60
+ config_cmds = config["commands"]
61
+ # Commands can be provided as a list, or as a dictionary
62
+ # so that they can be sorted into sections
63
+ if isinstance(config_cmds, list):
64
+ config_cmds = {"Commands": config_cmds}
65
+
66
+ # Backward compatibility workaround
67
+ # Originally, you could specify any of these commands as `spin.cmd`
68
+ # and we'd fetch it from util
69
+ commands = {
70
+ "spin.build": cmds.meson.build,
71
+ "spin.test": cmds.meson.test,
72
+ "spin.ipython": cmds.meson.ipython,
73
+ "spin.python": cmds.meson.python,
74
+ "spin.shell": cmds.meson.shell,
75
+ }
76
+
77
+ for section, cmds in config_cmds.items():
78
+ for cmd in cmds:
79
+ if cmd not in commands:
80
+ # First, see if we can directly import the command
81
+ if not ":" in cmd:
82
+ path, func = cmd.rsplit(".", maxsplit=1)
83
+ try:
84
+ mod = importlib.import_module(path)
85
+ except ImportError:
86
+ print(
87
+ f"!! Could not import module `{path}` to load command `{cmd}`"
88
+ )
89
+ continue
90
+ else:
91
+ try:
92
+ path, func = cmd.split(":")
93
+ spec = importlib.util.spec_from_file_location(
94
+ "custom_mod", path
95
+ )
96
+ mod = importlib.util.module_from_spec(spec)
97
+ spec.loader.exec_module(mod)
98
+ except FileNotFoundError:
99
+ print(
100
+ f"!! Could not find file `{path}` to load custom command `{cmd}`.\n"
101
+ )
102
+ continue
103
+ except Exception as e:
104
+ print(
105
+ f"!! Could not import file `{path}` to load custom command `{cmd}`.\n"
106
+ )
107
+ raise e
108
+
109
+ try:
110
+ cmd_func = getattr(mod, func)
111
+ except AttributeError:
112
+ print(f"!! Could not load command `{func}` from file `{path}`.\n")
113
+ continue
114
+
115
+ commands[cmd] = cmd_func
116
+
117
+ group.add_command(commands[cmd], section=section)
118
+
119
+ try:
120
+ group()
121
+ except Exception as e:
122
+ print(f"{e}; aborting.")
123
+ sys.exit(1)
@@ -0,0 +1,8 @@
1
+ from . import meson
2
+
3
+ # Backward compatibility with older versions
4
+ build = meson.build
5
+ test = meson.test
6
+ ipython = meson.ipython
7
+ python = meson.python
8
+ shell = meson.shell
@@ -0,0 +1,9 @@
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(["python", "-m", "build", ".", "--sdist"])
@@ -0,0 +1,250 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import json
5
+
6
+ import click
7
+
8
+ from .util import run, get_config, get_commands
9
+
10
+
11
+ install_dir = "build-install"
12
+
13
+
14
+ def _set_pythonpath():
15
+ site_packages = _get_site_packages()
16
+ env = os.environ
17
+
18
+ if "PYTHONPATH" in env:
19
+ env["PYTHONPATH"] = f"{site_packages}{os.pathsep}{env['PYTHONPATH']}"
20
+ else:
21
+ env["PYTHONPATH"] = site_packages
22
+
23
+ return env["PYTHONPATH"]
24
+
25
+
26
+ def _get_site_packages():
27
+ candidate_paths = []
28
+ for root, dirs, files in os.walk(install_dir):
29
+ for subdir in dirs:
30
+ if subdir == "site-packages" or subdir == "dist-packages":
31
+ candidate_paths.append(os.path.abspath(os.path.join(root, subdir)))
32
+
33
+ X, Y = sys.version_info.major, sys.version_info.minor
34
+
35
+ site_packages = None
36
+ if any(f"python{X}." in p for p in candidate_paths):
37
+ # We have a system that uses `python3.X/site-packages` or `python3.X/dist-packages`
38
+ site_packages = [p for p in candidate_paths if f"python{X}.{Y}" in p]
39
+ if len(site_packages) == 0:
40
+ raise FileNotFoundError(
41
+ f"No site-packages found in {install_dir} for Python {X}.{Y}"
42
+ )
43
+ else:
44
+ site_packages = site_packages[0]
45
+ else:
46
+ # A naming scheme that does not encode the Python major/minor version is used, so return
47
+ # whatever site-packages path was found
48
+ if len(candidate_paths) > 1:
49
+ raise FileNotFoundError(
50
+ f"Multiple `site-packages` found in `{install_dir}`, but cannot use Python version to disambiguate"
51
+ )
52
+ elif len(candidate_paths) == 1:
53
+ site_packages = candidate_paths[0]
54
+
55
+ if site_packages is None:
56
+ raise FileNotFoundError(
57
+ f"No `site-packages` or `dist-packages` found under `{install_dir}`"
58
+ )
59
+
60
+ return site_packages
61
+
62
+
63
+ def _meson_version():
64
+ try:
65
+ p = run(["meson", "--version"], output=False, echo=False)
66
+ return p.stdout.decode("ascii").strip()
67
+ except:
68
+ pass
69
+
70
+
71
+ def _meson_version_configured():
72
+ try:
73
+ meson_info_fn = os.path.join("build", "meson-info", "meson-info.json")
74
+ meson_info = json.load(open(meson_info_fn))
75
+ return meson_info["meson_version"]["full"]
76
+ except:
77
+ pass
78
+
79
+
80
+ @click.command()
81
+ @click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int)
82
+ @click.option("--clean", is_flag=True, help="Clean build directory before build")
83
+ @click.option(
84
+ "-v", "--verbose", is_flag=True, help="Print all build output, even installation"
85
+ )
86
+ @click.argument("meson_args", nargs=-1)
87
+ def build(meson_args, jobs=None, clean=False, verbose=False):
88
+ """🔧 Build package with Meson/ninja and install
89
+
90
+ MESON_ARGS are passed through e.g.:
91
+
92
+ ./dev.py build -- -Dpkg_config_path=/lib64/pkgconfig
93
+
94
+ The package is installed to build-install
95
+
96
+ By default builds for release, to be able to use a debugger set CFLAGS
97
+ appropriately. For example, for linux use
98
+
99
+ CFLAGS="-O0 -g" ./dev.py build
100
+ """
101
+ build_dir = "build"
102
+ setup_cmd = ["meson", "setup", build_dir, "--prefix=/usr"] + list(meson_args)
103
+
104
+ if clean:
105
+ print(f"Removing `{build_dir}`")
106
+ if os.path.isdir(build_dir):
107
+ shutil.rmtree(build_dir)
108
+ print(f"Removing `{install_dir}`")
109
+ if os.path.isdir(install_dir):
110
+ shutil.rmtree(install_dir)
111
+
112
+ if not (os.path.exists(build_dir) and _meson_version_configured()):
113
+ p = run(setup_cmd, sys_exit=False)
114
+ if p.returncode != 0:
115
+ raise RuntimeError(
116
+ "Meson configuration failed; please try `dev.py build` again with the `--clean` flag."
117
+ )
118
+ else:
119
+ # Build dir has been configured; check if it was configured by
120
+ # current version of Meson
121
+
122
+ if _meson_version() != _meson_version_configured():
123
+ run(setup_cmd + ["--reconfigure"])
124
+
125
+ # Any other conditions that warrant a reconfigure?
126
+
127
+ p = run(["meson", "compile", "-C", build_dir], sys_exit=False)
128
+ p = run(
129
+ [
130
+ "meson",
131
+ "install",
132
+ "--only-changed",
133
+ "-C",
134
+ build_dir,
135
+ "--destdir",
136
+ f"../{install_dir}",
137
+ ],
138
+ output=verbose,
139
+ )
140
+
141
+
142
+ @click.command()
143
+ @click.argument("pytest_args", nargs=-1)
144
+ @click.pass_context
145
+ def test(ctx, pytest_args):
146
+ """🔧 Run tests
147
+
148
+ PYTEST_ARGS are passed through directly to pytest, e.g.:
149
+
150
+ ./dev.py test -- -v
151
+
152
+ By default, runs the full test suite. To skip "slow" tests, run
153
+
154
+ ./dev.py test -- -m "not slow"
155
+ """
156
+ cfg = get_config()
157
+
158
+ command_groups = get_commands()
159
+ commands = [cmd for section in command_groups for cmd in command_groups[section]]
160
+ build_cmd = next((cmd for cmd in commands if cmd.name == "build"), None)
161
+ if build_cmd:
162
+ click.secho(
163
+ f"Invoking `build` prior to running tests:", bold=True, fg="bright_green"
164
+ )
165
+ ctx.invoke(build_cmd)
166
+
167
+ if not pytest_args:
168
+ pytest_args = (cfg.get("tool.spin.package", None),)
169
+ if pytest_args == (None,):
170
+ print(
171
+ "Please specify `package = packagename` under `tool.spin` section of `pyproject.toml`"
172
+ )
173
+ sys.exit(1)
174
+
175
+ site_path = _get_site_packages()
176
+ _set_pythonpath()
177
+
178
+ print(f'$ export PYTHONPATH="{site_path}"')
179
+ run(
180
+ [sys.executable, "-m", "pytest", f"--rootdir={site_path}"] + list(pytest_args),
181
+ cwd=site_path,
182
+ replace=True,
183
+ )
184
+
185
+
186
+ @click.command()
187
+ @click.argument("ipython_args", nargs=-1)
188
+ def ipython(ipython_args):
189
+ """💻 Launch IPython shell with PYTHONPATH set
190
+
191
+ IPYTHON_ARGS are passed through directly to IPython, e.g.:
192
+
193
+ ./dev.py ipython -- -i myscript.py
194
+ """
195
+ p = _set_pythonpath()
196
+ print(f'💻 Launching IPython with PYTHONPATH="{p}"')
197
+ run(["ipython", "--ignore-cwd"] + list(ipython_args), replace=True)
198
+
199
+
200
+ @click.command()
201
+ @click.argument("shell_args", nargs=-1)
202
+ def shell(shell_args=[]):
203
+ """💻 Launch shell with PYTHONPATH set
204
+
205
+ SHELL_ARGS are passed through directly to the shell, e.g.:
206
+
207
+ ./dev.py shell -- -c 'echo $PYTHONPATH'
208
+
209
+ Ensure that your shell init file (e.g., ~/.zshrc) does not override
210
+ the PYTHONPATH.
211
+ """
212
+ p = _set_pythonpath()
213
+ shell = os.environ.get("SHELL", "sh")
214
+ cmd = [shell] + list(shell_args)
215
+ print(f'💻 Launching shell with PYTHONPATH="{p}"')
216
+ print(f"⚠ Change directory to avoid importing source instead of built package")
217
+ print(f"⚠ Ensure that your ~/.shellrc does not unset PYTHONPATH")
218
+ run(cmd, replace=True)
219
+
220
+
221
+ @click.command()
222
+ @click.argument("python_args", nargs=-1)
223
+ def python(python_args):
224
+ """🐍 Launch Python shell with PYTHONPATH set
225
+
226
+ PYTHON_ARGS are passed through directly to Python, e.g.:
227
+
228
+ ./dev.py python -- -c 'import sys; print(sys.path)'
229
+ """
230
+ p = _set_pythonpath()
231
+ v = sys.version_info
232
+ if (v.major < 3) or (v.major == 3 and v.minor < 11):
233
+ print("We're sorry, but this feature only works on Python 3.11 and greater 😢")
234
+ print()
235
+ print(
236
+ "Why? Because we need the '-P' flag so the interpreter doesn't muck with PYTHONPATH"
237
+ )
238
+ print()
239
+ print("However! You can still launch your own interpreter:")
240
+ print()
241
+ print(f" PYTHONPATH='{p}' python")
242
+ print()
243
+ print("And then call:")
244
+ print()
245
+ print("import sys; del(sys.path[0])")
246
+ sys.exit(-1)
247
+
248
+ print(f'🐍 Launching Python with PYTHONPATH="{p}"')
249
+
250
+ run(["/usr/bin/env", "python", "-P"] + list(python_args), replace=True)
@@ -0,0 +1,80 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import shlex
5
+ import sysconfig
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+
11
+ def run(
12
+ cmd, cwd=None, replace=False, sys_exit=True, output=True, echo=True, *args, **kwargs
13
+ ):
14
+ """Run a shell command.
15
+
16
+ Parameters
17
+ ----------
18
+ cwd : str
19
+ Change to this directory before execution.
20
+ replace : bool
21
+ Whether to replace the current process.
22
+ sys_exit : bool
23
+ Whether to exit if the shell command returns with error != 0.
24
+ output : bool
25
+ Whether to display output as the process runs.
26
+ If set to ``False``, can be accessed afterwards as
27
+ ``p.stdout``.
28
+ If `sys_exit` is True and the process fails, output is printed
29
+ regardless.
30
+ echo : bool
31
+ Whether or not to echo commands.
32
+
33
+ Other arguments and keywords are passed directly to `subprocess.run`.
34
+
35
+ """
36
+ if cwd:
37
+ if echo:
38
+ click.secho(f"$ cd {cwd}", bold=True, fg="bright_blue")
39
+ os.chdir(cwd)
40
+
41
+ cmdstr = " ".join(shlex.quote(arg) for arg in cmd)
42
+ if echo:
43
+ click.secho(f"$ {cmdstr}", bold=True, fg="bright_blue")
44
+
45
+ if output is False:
46
+ output_kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT}
47
+ kwargs = {**output_kwargs, **kwargs}
48
+
49
+ if replace:
50
+ os.execvp(cmd[0], cmd)
51
+ print(f"Failed to launch `{cmd}`")
52
+ sys.exit(-1)
53
+ else:
54
+ p = subprocess.run(cmd, *args, **kwargs)
55
+ if p.returncode != 0 and sys_exit:
56
+ # Output was suppressed, but the process failed, so print it anyway
57
+ if output is False:
58
+ print(p.stdout.decode("utf-8"), end="")
59
+ sys.exit(p.returncode)
60
+ return p
61
+
62
+
63
+ def get_config():
64
+ """Return the pyproject.toml as a dictionary."""
65
+ return click.get_current_context().meta["config"]
66
+
67
+
68
+ def get_commands():
69
+ """Return a list of all commands.
70
+
71
+ Returns
72
+ -------
73
+ cmds : dict
74
+ The keys of the dictionary are the section names, the values
75
+ lists of commands.
76
+
77
+ If no sections are provided, all commands are listed under the
78
+ ``commands`` key.
79
+ """
80
+ return click.get_current_context().meta["commands"]
@@ -0,0 +1,51 @@
1
+ import re
2
+ import shutil
3
+
4
+ import click
5
+
6
+
7
+ class RegexpFormatter:
8
+ def __init__(self, rules, default=None):
9
+ if default is None:
10
+ default = {}
11
+ self.rules = {re.compile(expr): format for (expr, format) in rules.items()}
12
+ self.default_style = click.style("", **default, reset=False)
13
+ self.reset_style = click.style("", reset=True)
14
+
15
+ def __call__(self, txt):
16
+ for expr, format in self.rules.items():
17
+ txt = expr.sub(click.style("\\g<0>", **format) + self.default_style, txt)
18
+ txt = self.default_style + txt + self.reset_style
19
+ return txt
20
+
21
+
22
+ class ColorHelpFormatter(click.formatting.HelpFormatter):
23
+ def __init__(self, *args, **kwargs):
24
+ kwargs["width"] = shutil.get_terminal_size()[0]
25
+ super().__init__(*args, **kwargs)
26
+
27
+ def write_heading(self, heading):
28
+ super().write_heading(click.style(heading, dim=True))
29
+
30
+ def write_dl(self, items):
31
+ """Print definition list"""
32
+ key_fmt = RegexpFormatter(
33
+ {
34
+ " [A-Z]+": {"bold": True, "fg": "yellow"},
35
+ r"\-[a-z]{1}(?=[ ,]{1})": {"fg": "green"},
36
+ },
37
+ default={"bold": True, "fg": "cyan"},
38
+ )
39
+ val_fmt = RegexpFormatter({r"\[default: .*?\]": {"dim": True}})
40
+ items = [(key_fmt(key), val_fmt(val)) for (key, val) in items]
41
+ super().write_dl(items)
42
+
43
+ def write_usage(self, prog, args="", prefix=None):
44
+ fmt = RegexpFormatter(
45
+ {r"(?<=\[)[A-Z_]+(?=\])": {"fg": "cyan"}}, default={"bold": True}
46
+ )
47
+ super().write_usage(
48
+ click.style(prog, bold=True, fg="white"),
49
+ click.style(fmt(args), bold=True),
50
+ prefix=click.style("Usage: ", bold=True, fg="yellow"),
51
+ )
@@ -0,0 +1,21 @@
1
+ import click
2
+ import collections
3
+
4
+
5
+ class SectionedHelpGroup(click.Group):
6
+ """Organize commands as sections"""
7
+
8
+ def __init__(self, *args, **kwargs):
9
+ self.section_commands = collections.defaultdict(list)
10
+ super().__init__(*args, **kwargs)
11
+
12
+ def add_command(self, cmd, name=None, section=None):
13
+ self.section_commands[section].append(cmd)
14
+ super().add_command(cmd, name=name)
15
+
16
+ def format_commands(self, ctx, formatter):
17
+ for group, cmds in self.section_commands.items():
18
+ with formatter.section(group):
19
+ formatter.write_dl(
20
+ [(cmd.name, cmd.get_short_help_str() or "") for cmd in cmds]
21
+ )
@@ -0,0 +1,100 @@
1
+ import tempfile
2
+ import sys
3
+ import os
4
+ from os.path import join as pjoin, normpath
5
+
6
+ import pytest
7
+
8
+ from spin.cmds import meson
9
+
10
+
11
+ def make_paths(root, paths):
12
+ for p in paths:
13
+ os.makedirs(pjoin(root, p.lstrip("/")))
14
+
15
+
16
+ def test_path_discovery(monkeypatch):
17
+ version = sys.version_info
18
+ X, Y = version.major, version.minor
19
+
20
+ # With multiple site-packages, choose the one that
21
+ # matches the current Python version
22
+ with tempfile.TemporaryDirectory() as d:
23
+ with monkeypatch.context() as m:
24
+ install_dir = pjoin(d, "build-install")
25
+ m.setattr(meson, "install_dir", install_dir)
26
+
27
+ make_paths(
28
+ install_dir,
29
+ [
30
+ f"/usr/lib64/python{X}.{Y}/site-packages",
31
+ f"/usr/lib64/python{X}.{Y + 1}/site-packages",
32
+ f"/usr/lib64/python{X}.{Y + 2}/site-packages",
33
+ ],
34
+ )
35
+ assert (
36
+ normpath(f"/usr/lib64/python{X}.{Y}/site-packages")
37
+ in meson._get_site_packages()
38
+ )
39
+
40
+ # Debian uses dist-packages
41
+ with tempfile.TemporaryDirectory() as d:
42
+ with monkeypatch.context() as m:
43
+ install_dir = pjoin(d, "build-install")
44
+ m.setattr(meson, "install_dir", install_dir)
45
+
46
+ make_paths(
47
+ install_dir,
48
+ [
49
+ f"/usr/lib64/python{X}.{Y}/dist-packages",
50
+ ],
51
+ )
52
+ assert (
53
+ normpath(f"/usr/lib64/python{X}.{Y}/dist-packages")
54
+ in meson._get_site_packages()
55
+ )
56
+
57
+ # If there is no version information in site-packages,
58
+ # use whatever site-packages can be found
59
+ with tempfile.TemporaryDirectory() as d:
60
+ with monkeypatch.context() as m:
61
+ install_dir = pjoin(d, "build-install")
62
+ m.setattr(meson, "install_dir", install_dir)
63
+
64
+ make_paths(install_dir, ["/Python3/site-packages"])
65
+ assert normpath("/Python3/site-packages") in meson._get_site_packages()
66
+
67
+ # Raise if no site-package directory present
68
+ with tempfile.TemporaryDirectory() as d:
69
+ with monkeypatch.context() as m:
70
+ install_dir = pjoin(d, "build-install")
71
+ m.setattr(meson, "install_dir", install_dir)
72
+
73
+ with pytest.raises(FileNotFoundError):
74
+ meson._get_site_packages()
75
+
76
+ # If there are multiple site-package paths, but without version information,
77
+ # refuse the temptation to guess
78
+ with tempfile.TemporaryDirectory() as d:
79
+ install_dir = pjoin(d, "build-install")
80
+ make_paths(
81
+ install_dir, [f"/Python3/x/site-packages", f"/Python3/y/site-packages"]
82
+ )
83
+ with pytest.raises(FileNotFoundError):
84
+ meson._get_site_packages()
85
+
86
+ # Multiple site-package paths found, but none that matches our Python
87
+ with tempfile.TemporaryDirectory() as d:
88
+ with monkeypatch.context() as m:
89
+ install_dir = pjoin(d, "build-install")
90
+ m.setattr(meson, "install_dir", install_dir)
91
+
92
+ make_paths(
93
+ install_dir,
94
+ [
95
+ f"/usr/lib64/python{X}.{Y + 1}/site-packages",
96
+ f"/usr/lib64/python{X}.{Y + 2}/site-packages",
97
+ ],
98
+ )
99
+ with pytest.raises(FileNotFoundError):
100
+ meson._get_site_packages()
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: spin
3
+ Version: 0.2
4
+ Summary: Developer tool for scientific Python libraries
5
+ Maintainer-email: Scientific Python <spin@discuss.scientific-python.org>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2021--2022, Scientific Python project
9
+ All rights reserved.
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are met:
13
+
14
+ 1. Redistributions of source code must retain the above copyright notice, this
15
+ list of conditions and the following disclaimer.
16
+
17
+ 2. Redistributions in binary form must reproduce the above copyright notice,
18
+ this list of conditions and the following disclaimer in the documentation
19
+ and/or other materials provided with the distribution.
20
+
21
+ 3. Neither the name of the copyright holder nor the names of its
22
+ contributors may be used to endorse or promote products derived from
23
+ this software without specific prior written permission.
24
+
25
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
29
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+
36
+ Project-URL: homepage, https://github.com/scientific-python/spin
37
+ Classifier: Development Status :: 4 - Beta
38
+ Classifier: Programming Language :: Python
39
+ Requires-Python: >=3.7
40
+ Provides-Extra: lint
41
+ License-File: LICENSE
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ spin/__init__.py
5
+ spin/__main__.py
6
+ spin/color_format.py
7
+ spin/sectioned_help.py
8
+ spin.egg-info/PKG-INFO
9
+ spin.egg-info/SOURCES.txt
10
+ spin.egg-info/dependency_links.txt
11
+ spin.egg-info/requires.txt
12
+ spin.egg-info/top_level.txt
13
+ spin/cmds/__init__.py
14
+ spin/cmds/build.py
15
+ spin/cmds/meson.py
16
+ spin/cmds/util.py
17
+ spin/tests/test_meson.py
@@ -0,0 +1,8 @@
1
+ click
2
+ toml
3
+
4
+ [:platform_system == "Windows"]
5
+ colorama
6
+
7
+ [lint]
8
+ pre-commit>=3.1
@@ -0,0 +1 @@
1
+ spin
spin-0.0.1.dev0/PKG-INFO DELETED
@@ -1,83 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: spin
3
- Version: 0.0.1.dev0
4
- Summary: Spatial omics analysis
5
- Author: Danila Bredikhin
6
- Maintainer-email: Danila Bredikhin <danila.bredikhin@embl.de>
7
- Requires-Python: >=3.8
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: anndata
10
- Requires-Dist: bump2version ; extra == "dev"
11
- Requires-Dist: sphinx>=4 ; extra == "doc"
12
- Requires-Dist: furo ; extra == "doc"
13
- Requires-Dist: myst-parser ; extra == "doc"
14
- Requires-Dist: sphinxcontrib-bibtex>=1.0.0 ; extra == "doc"
15
- Requires-Dist: scanpydoc[typehints]>=0.7.4 ; extra == "doc"
16
- Requires-Dist: nbsphinx ; extra == "doc"
17
- Requires-Dist: ipython>=8.3.0 ; extra == "doc"
18
- Requires-Dist: pytest ; extra == "test"
19
- Requires-Dist: pytest-cov ; extra == "test"
20
- Project-URL: Documentation, https://spin.readthedocs.io/
21
- Project-URL: Home-page, https://github.com/gtca/spin
22
- Project-URL: Source, https://github.com/gtca/spin
23
- Provides-Extra: dev
24
- Provides-Extra: doc
25
- Provides-Extra: test
26
-
27
- # spin
28
-
29
- [![Tests][badge-tests]][link-tests]
30
- [![Documentation][badge-docs]][link-docs]
31
-
32
- [badge-tests]: https://img.shields.io/github/workflow/status/gtca/spin/Test/main
33
- [link-tests]: https://github.com/gtca/spin/actions/workflows/test.yml
34
- [badge-docs]: https://img.shields.io/readthedocs/spin
35
-
36
- Spatial omics analysis
37
-
38
- ## Getting started
39
-
40
- Please refer to the [documentation][link-docs]. In particular, the
41
-
42
- - [API documentation][link-api].
43
-
44
- ## Installation
45
-
46
- You need to have Python 3.8 or newer installed on your system. If you don't have
47
- Python installed, we recommend installing `Miniconda <https://docs.conda.io/en/latest/miniconda.html>`\_.
48
-
49
- There are several alternative options to install spin:
50
-
51
- <!--
52
- 1) Install the latest release of `spin` from `PyPI <https://pypi.org/project/spin/>`_:
53
-
54
- ```bash
55
- pip install spin
56
- ```
57
- -->
58
-
59
- 1. Install the latest development version:
60
-
61
- ```bash
62
- pip install git+https://github.com/gtca/spin.git@main
63
- ```
64
-
65
- ## Release notes
66
-
67
- See the [changelog][changelog].
68
-
69
- ## Contact
70
-
71
- For questions and help requests, you can reach out in the [scverse discourse][scverse-discourse].
72
- If you found a bug, please use the [issue tracker][issue-tracker].
73
-
74
- ## Citation
75
-
76
- > t.b.a
77
-
78
- [scverse-discourse]: https://discourse.scverse.org/
79
- [issue-tracker]: https://github.com/gtca/spin/issues
80
- [changelog]: https://spin.readthedocs.io/latest/changelog.html
81
- [link-docs]: https://spin.readthedocs.io
82
- [link-api]: https://spin.readthedocs.io/latest/api.html
83
-
spin-0.0.1.dev0/README.md DELETED
@@ -1,56 +0,0 @@
1
- # spin
2
-
3
- [![Tests][badge-tests]][link-tests]
4
- [![Documentation][badge-docs]][link-docs]
5
-
6
- [badge-tests]: https://img.shields.io/github/workflow/status/gtca/spin/Test/main
7
- [link-tests]: https://github.com/gtca/spin/actions/workflows/test.yml
8
- [badge-docs]: https://img.shields.io/readthedocs/spin
9
-
10
- Spatial omics analysis
11
-
12
- ## Getting started
13
-
14
- Please refer to the [documentation][link-docs]. In particular, the
15
-
16
- - [API documentation][link-api].
17
-
18
- ## Installation
19
-
20
- You need to have Python 3.8 or newer installed on your system. If you don't have
21
- Python installed, we recommend installing `Miniconda <https://docs.conda.io/en/latest/miniconda.html>`\_.
22
-
23
- There are several alternative options to install spin:
24
-
25
- <!--
26
- 1) Install the latest release of `spin` from `PyPI <https://pypi.org/project/spin/>`_:
27
-
28
- ```bash
29
- pip install spin
30
- ```
31
- -->
32
-
33
- 1. Install the latest development version:
34
-
35
- ```bash
36
- pip install git+https://github.com/gtca/spin.git@main
37
- ```
38
-
39
- ## Release notes
40
-
41
- See the [changelog][changelog].
42
-
43
- ## Contact
44
-
45
- For questions and help requests, you can reach out in the [scverse discourse][scverse-discourse].
46
- If you found a bug, please use the [issue tracker][issue-tracker].
47
-
48
- ## Citation
49
-
50
- > t.b.a
51
-
52
- [scverse-discourse]: https://discourse.scverse.org/
53
- [issue-tracker]: https://github.com/gtca/spin/issues
54
- [changelog]: https://spin.readthedocs.io/latest/changelog.html
55
- [link-docs]: https://spin.readthedocs.io
56
- [link-api]: https://spin.readthedocs.io/latest/api.html
@@ -1,92 +0,0 @@
1
- [build-system]
2
- build-backend = "flit_core.buildapi"
3
- requires = [
4
- "flit_core >=3.4,<4",
5
- "importlib_metadata>=0.7; python_version < '3.8'",
6
- ]
7
-
8
-
9
- [project]
10
- name = "spin"
11
- description = "Spatial omics analysis"
12
- authors = [
13
- {name = "Danila Bredikhin"},
14
- ]
15
- maintainers = [
16
- {name = "Danila Bredikhin", email = "danila.bredikhin@embl.de"},
17
- ]
18
- urls.Documentation = "https://spin.readthedocs.io/"
19
- urls.Source = "https://github.com/gtca/spin"
20
- urls.Home-page = "https://github.com/gtca/spin"
21
- version = "0.0.1.dev0"
22
- requires-python = ">=3.8"
23
- license = {file = "LICENSE"}
24
- readme = "README.md"
25
- dependencies = ["anndata"]
26
-
27
- [project.optional-dependencies]
28
- dev = [
29
- # dev version generation
30
- "bump2version",
31
- ]
32
- doc = [
33
- "sphinx>=4",
34
- "furo",
35
- "myst-parser",
36
- "sphinxcontrib-bibtex>=1.0.0",
37
- "scanpydoc[typehints]>=0.7.4",
38
- # For notebooks
39
- "nbsphinx",
40
- "ipython>=8.3.0"
41
- ]
42
- test = [
43
- "pytest",
44
- "pytest-cov",
45
- ]
46
-
47
- [tool.coverage.run]
48
- source = ["spin"]
49
- omit = [
50
- "**/test_*.py",
51
- ]
52
-
53
- [tool.pytest.ini_options]
54
- testpaths = ["tests"]
55
- xfail_strict = true
56
- addopts = [
57
- "-Werror", # if 3rd party libs raise DeprecationWarnings, just use filterwarnings below
58
- "--import-mode=importlib", # allow using test files with same name
59
- ]
60
- filterwarnings = [
61
- # "ignore:.*U.*mode is deprecated:DeprecationWarning",
62
- ]
63
-
64
- [tool.isort]
65
- include_trailing_comma = true
66
- multi_line_output = 3
67
- profile = "black"
68
- skip_glob = ["docs/*"]
69
-
70
- [tool.black]
71
- line-length = 120
72
- target-version = ['py38']
73
- include = '\.pyi?$'
74
- exclude = '''
75
- (
76
- /(
77
- \.eggs
78
- | \.git
79
- | \.hg
80
- | \.mypy_cache
81
- | \.tox
82
- | \.venv
83
- | _build
84
- | buck-out
85
- | build
86
- | dist
87
- )/
88
- )
89
- '''
90
-
91
- [tool.jupytext]
92
- formats = "ipynb,md"
@@ -1,7 +0,0 @@
1
- from importlib.metadata import version
2
-
3
- from . import pl, pp, tl
4
-
5
- __all__ = ["pl", "pp", "tl"]
6
-
7
- __version__ = version("spin")
@@ -1 +0,0 @@
1
- from .basic import basic_plot
@@ -1,7 +0,0 @@
1
- from anndata import AnnData
2
-
3
-
4
- def basic_plot(adata: AnnData) -> int:
5
- """Generate a basic plot for an AnnData object."""
6
- print("Import matplotlib and implement a plotting function here.")
7
- return 0
@@ -1 +0,0 @@
1
- from .basic import basic_preproc
@@ -1,7 +0,0 @@
1
- from anndata import AnnData
2
-
3
-
4
- def basic_preproc(adata: AnnData) -> int:
5
- """Run a basic preprocessing on the AnnData object."""
6
- print("Implement a preprocessing function here.")
7
- return 0
@@ -1 +0,0 @@
1
- from .basic import basic_tool
@@ -1,7 +0,0 @@
1
- from anndata import AnnData
2
-
3
-
4
- def basic_tool(adata: AnnData) -> int:
5
- """Run a tool on the AnnData object."""
6
- print("Implement a tool to run on the AnnData object.")
7
- return 0