nsharper 0.1.0__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.
- nsharper-0.1.0/PKG-INFO +151 -0
- nsharper-0.1.0/README.md +129 -0
- nsharper-0.1.0/nsharper/__init__.py +67 -0
- nsharper-0.1.0/nsharper/backends.py +113 -0
- nsharper-0.1.0/nsharper/coco.py +309 -0
- nsharper-0.1.0/nsharper/dataset.py +257 -0
- nsharper-0.1.0/nsharper/errors.py +34 -0
- nsharper-0.1.0/nsharper/formatting.py +33 -0
- nsharper-0.1.0/nsharper/head.py +247 -0
- nsharper-0.1.0/nsharper/layers.py +222 -0
- nsharper-0.1.0/nsharper/metrics.py +161 -0
- nsharper-0.1.0/nsharper/model.py +500 -0
- nsharper-0.1.0/nsharper/results.py +196 -0
- nsharper-0.1.0/nsharper/serialization.py +94 -0
- nsharper-0.1.0/nsharper/summary.py +212 -0
- nsharper-0.1.0/nsharper.egg-info/PKG-INFO +151 -0
- nsharper-0.1.0/nsharper.egg-info/SOURCES.txt +31 -0
- nsharper-0.1.0/nsharper.egg-info/dependency_links.txt +1 -0
- nsharper-0.1.0/nsharper.egg-info/requires.txt +8 -0
- nsharper-0.1.0/nsharper.egg-info/top_level.txt +1 -0
- nsharper-0.1.0/pyproject.toml +38 -0
- nsharper-0.1.0/setup.cfg +4 -0
- nsharper-0.1.0/tests/test_api.py +56 -0
- nsharper-0.1.0/tests/test_backends.py +34 -0
- nsharper-0.1.0/tests/test_coco.py +125 -0
- nsharper-0.1.0/tests/test_dataset.py +61 -0
- nsharper-0.1.0/tests/test_inference.py +67 -0
- nsharper-0.1.0/tests/test_layers.py +47 -0
- nsharper-0.1.0/tests/test_metrics.py +89 -0
- nsharper-0.1.0/tests/test_model_validation.py +48 -0
- nsharper-0.1.0/tests/test_storage.py +73 -0
- nsharper-0.1.0/tests/test_summary.py +57 -0
- nsharper-0.1.0/tests/test_training.py +68 -0
nsharper-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nsharper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight computer-vision framework for building, training and benchmarking compact object detectors
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://github.com/nsharper/nsharper/blob/main/docs.md
|
|
7
|
+
Keywords: computer-vision,object-detection,deep-learning,benchmark
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: torch>=2.1
|
|
16
|
+
Requires-Dist: torchvision>=0.16
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
Requires-Dist: pillow>=9.0
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# nsharper
|
|
24
|
+
|
|
25
|
+
A lightweight Python framework for building and evaluating computer-vision
|
|
26
|
+
models with a deliberately simple API.
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Dataset → Model → Summary → Train → Test → Benchmark → Save
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> Keep the model definition simple. Keep the workflow obvious.
|
|
33
|
+
|
|
34
|
+
Full documentation lives in [`docs.md`](docs.md).
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nsharper
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## A complete project
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import nsharper as ns
|
|
46
|
+
|
|
47
|
+
train = ns.Dataset("dataset/train")
|
|
48
|
+
val = ns.Dataset("dataset/val")
|
|
49
|
+
|
|
50
|
+
model = ns.Model([
|
|
51
|
+
ns.Conv(224, 224, 3, 16, 3, 2),
|
|
52
|
+
ns.Conv(112, 112, 16, 32, 3, 2),
|
|
53
|
+
ns.Conv(56, 56, 32, 64, 3, 2),
|
|
54
|
+
ns.Detect(28, 28, 64, 10)
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
model.summary()
|
|
58
|
+
|
|
59
|
+
model.fit(train, epochs=50, batch_size=32, lr=0.001, val=val)
|
|
60
|
+
|
|
61
|
+
results = model.test(val)
|
|
62
|
+
results.show()
|
|
63
|
+
|
|
64
|
+
model.save("supercharger.nsh")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What each command answers
|
|
68
|
+
|
|
69
|
+
| Command | Question it answers |
|
|
70
|
+
| ------------- | ---------------------------------------------- |
|
|
71
|
+
| `summary()` | What is my model, and what does it cost? |
|
|
72
|
+
| `test()` | How well does it do on *my* dataset? |
|
|
73
|
+
| `benchmark()` | How well does it do on COCO? |
|
|
74
|
+
|
|
75
|
+
`summary()` reports parameters, FLOPs, model size, memory and measured
|
|
76
|
+
latency/FPS on every backend the machine offers, plus a Mermaid diagram of
|
|
77
|
+
the graph. It needs no trained weights, so architectures can be compared
|
|
78
|
+
before spending time on training.
|
|
79
|
+
|
|
80
|
+
## Dataset layout
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
dataset/
|
|
84
|
+
├── images/001.jpg
|
|
85
|
+
└── labels/001.yaml
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
cat:
|
|
90
|
+
- [0.12, 0.20, 0.43, 0.61]
|
|
91
|
+
dog:
|
|
92
|
+
- [0.55, 0.31, 0.88, 0.72]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Boxes are normalized `[x1, y1, x2, y2]`, all coordinates between `0.0` and `1.0`.
|
|
96
|
+
|
|
97
|
+
## Devices
|
|
98
|
+
|
|
99
|
+
`"auto"` (default) walks CUDA → MPS → CPU and takes the first available
|
|
100
|
+
backend. `"cpu"`, `"cuda"`, `"mps"` and `"gpu"` select one explicitly.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
model.fit(train, epochs=50, device="mps")
|
|
104
|
+
ns.device() # 'mps'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Architecture errors surface immediately
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
ns.Model([
|
|
111
|
+
ns.Conv(224, 224, 3, 16, 3, 2),
|
|
112
|
+
ns.Conv(112, 112, 64, 32, 3, 2),
|
|
113
|
+
])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
error: channel mismatch
|
|
118
|
+
|
|
119
|
+
layer: Conv
|
|
120
|
+
expected: 64
|
|
121
|
+
received: 16
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## COCO benchmark
|
|
125
|
+
|
|
126
|
+
COCO is not bundled. Point `benchmark()` at a local copy — by argument, via
|
|
127
|
+
`NSHARPER_COCO`, or by placing it in `~/.nsharper/coco`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
ns.benchmark("supercharger.nsh")
|
|
131
|
+
ns.benchmark(model, data="~/datasets/coco")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Both the nsharper layout (`images/` + `labels/`) and the official COCO
|
|
135
|
+
layout (`val2017/` + `annotations/instances_val2017.json`) are read.
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
python examples/make_shapes.py dataset # generate a synthetic dataset
|
|
141
|
+
python examples/train.py # train, test and save a detector
|
|
142
|
+
python examples/predict.py dataset/val/images/0000.jpg
|
|
143
|
+
python examples/benchmark.py # benchmark saved models
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
pytest
|
|
151
|
+
```
|
nsharper-0.1.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# nsharper
|
|
2
|
+
|
|
3
|
+
A lightweight Python framework for building and evaluating computer-vision
|
|
4
|
+
models with a deliberately simple API.
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
Dataset → Model → Summary → Train → Test → Benchmark → Save
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> Keep the model definition simple. Keep the workflow obvious.
|
|
11
|
+
|
|
12
|
+
Full documentation lives in [`docs.md`](docs.md).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install nsharper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## A complete project
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import nsharper as ns
|
|
24
|
+
|
|
25
|
+
train = ns.Dataset("dataset/train")
|
|
26
|
+
val = ns.Dataset("dataset/val")
|
|
27
|
+
|
|
28
|
+
model = ns.Model([
|
|
29
|
+
ns.Conv(224, 224, 3, 16, 3, 2),
|
|
30
|
+
ns.Conv(112, 112, 16, 32, 3, 2),
|
|
31
|
+
ns.Conv(56, 56, 32, 64, 3, 2),
|
|
32
|
+
ns.Detect(28, 28, 64, 10)
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
model.summary()
|
|
36
|
+
|
|
37
|
+
model.fit(train, epochs=50, batch_size=32, lr=0.001, val=val)
|
|
38
|
+
|
|
39
|
+
results = model.test(val)
|
|
40
|
+
results.show()
|
|
41
|
+
|
|
42
|
+
model.save("supercharger.nsh")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What each command answers
|
|
46
|
+
|
|
47
|
+
| Command | Question it answers |
|
|
48
|
+
| ------------- | ---------------------------------------------- |
|
|
49
|
+
| `summary()` | What is my model, and what does it cost? |
|
|
50
|
+
| `test()` | How well does it do on *my* dataset? |
|
|
51
|
+
| `benchmark()` | How well does it do on COCO? |
|
|
52
|
+
|
|
53
|
+
`summary()` reports parameters, FLOPs, model size, memory and measured
|
|
54
|
+
latency/FPS on every backend the machine offers, plus a Mermaid diagram of
|
|
55
|
+
the graph. It needs no trained weights, so architectures can be compared
|
|
56
|
+
before spending time on training.
|
|
57
|
+
|
|
58
|
+
## Dataset layout
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
dataset/
|
|
62
|
+
├── images/001.jpg
|
|
63
|
+
└── labels/001.yaml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
cat:
|
|
68
|
+
- [0.12, 0.20, 0.43, 0.61]
|
|
69
|
+
dog:
|
|
70
|
+
- [0.55, 0.31, 0.88, 0.72]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Boxes are normalized `[x1, y1, x2, y2]`, all coordinates between `0.0` and `1.0`.
|
|
74
|
+
|
|
75
|
+
## Devices
|
|
76
|
+
|
|
77
|
+
`"auto"` (default) walks CUDA → MPS → CPU and takes the first available
|
|
78
|
+
backend. `"cpu"`, `"cuda"`, `"mps"` and `"gpu"` select one explicitly.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
model.fit(train, epochs=50, device="mps")
|
|
82
|
+
ns.device() # 'mps'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Architecture errors surface immediately
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
ns.Model([
|
|
89
|
+
ns.Conv(224, 224, 3, 16, 3, 2),
|
|
90
|
+
ns.Conv(112, 112, 64, 32, 3, 2),
|
|
91
|
+
])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
error: channel mismatch
|
|
96
|
+
|
|
97
|
+
layer: Conv
|
|
98
|
+
expected: 64
|
|
99
|
+
received: 16
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## COCO benchmark
|
|
103
|
+
|
|
104
|
+
COCO is not bundled. Point `benchmark()` at a local copy — by argument, via
|
|
105
|
+
`NSHARPER_COCO`, or by placing it in `~/.nsharper/coco`:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
ns.benchmark("supercharger.nsh")
|
|
109
|
+
ns.benchmark(model, data="~/datasets/coco")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Both the nsharper layout (`images/` + `labels/`) and the official COCO
|
|
113
|
+
layout (`val2017/` + `annotations/instances_val2017.json`) are read.
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python examples/make_shapes.py dataset # generate a synthetic dataset
|
|
119
|
+
python examples/train.py # train, test and save a detector
|
|
120
|
+
python examples/predict.py dataset/val/images/0000.jpg
|
|
121
|
+
python examples/benchmark.py # benchmark saved models
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""nsharper — a lightweight computer-vision framework.
|
|
2
|
+
|
|
3
|
+
Dataset → Model → Summary → Train → Test → Benchmark → Save
|
|
4
|
+
|
|
5
|
+
Keep the model definition simple. Keep the workflow obvious.
|
|
6
|
+
|
|
7
|
+
>>> import nsharper as ns
|
|
8
|
+
>>>
|
|
9
|
+
>>> train = ns.Dataset("dataset/train")
|
|
10
|
+
>>> model = ns.Model([
|
|
11
|
+
... ns.Conv(224, 224, 3, 16, 3, 2),
|
|
12
|
+
... ns.Conv(112, 112, 16, 32, 3, 2),
|
|
13
|
+
... ns.Conv(56, 56, 32, 64, 3, 2),
|
|
14
|
+
... ns.Detect(28, 28, 64, 10),
|
|
15
|
+
... ])
|
|
16
|
+
>>> model.summary()
|
|
17
|
+
>>> model.fit(train, epochs=50)
|
|
18
|
+
>>> model.save("supercharger.nsh")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .coco import benchmark
|
|
24
|
+
from .dataset import Box, Dataset, Sample
|
|
25
|
+
from .backends import device
|
|
26
|
+
from .errors import (
|
|
27
|
+
ArchitectureError,
|
|
28
|
+
BenchmarkError,
|
|
29
|
+
DatasetError,
|
|
30
|
+
DeviceError,
|
|
31
|
+
ModelError,
|
|
32
|
+
NsharperError,
|
|
33
|
+
)
|
|
34
|
+
from .layers import Conv, Detect, Layer
|
|
35
|
+
from .model import Model
|
|
36
|
+
from .results import Detection, Prediction, TestResults
|
|
37
|
+
from .serialization import load
|
|
38
|
+
from .summary import Summary
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# core API
|
|
44
|
+
"Dataset",
|
|
45
|
+
"Model",
|
|
46
|
+
"Conv",
|
|
47
|
+
"Detect",
|
|
48
|
+
"load",
|
|
49
|
+
"benchmark",
|
|
50
|
+
"device",
|
|
51
|
+
# supporting types
|
|
52
|
+
"Box",
|
|
53
|
+
"Sample",
|
|
54
|
+
"Layer",
|
|
55
|
+
"Summary",
|
|
56
|
+
"TestResults",
|
|
57
|
+
"Prediction",
|
|
58
|
+
"Detection",
|
|
59
|
+
# errors
|
|
60
|
+
"NsharperError",
|
|
61
|
+
"ArchitectureError",
|
|
62
|
+
"DatasetError",
|
|
63
|
+
"DeviceError",
|
|
64
|
+
"ModelError",
|
|
65
|
+
"BenchmarkError",
|
|
66
|
+
"__version__",
|
|
67
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Compute-backend selection.
|
|
2
|
+
|
|
3
|
+
The public entry point is ``ns.device()``; this module keeps a separate name
|
|
4
|
+
so importing it can never shadow that function.
|
|
5
|
+
|
|
6
|
+
nsharper accepts the identifiers documented in the "Devices" section:
|
|
7
|
+
``auto``, ``cpu``, ``cuda``, ``mps`` and ``gpu``. ``auto`` walks the
|
|
8
|
+
priority chain CUDA -> MPS -> GPU -> CPU and picks the first backend the
|
|
9
|
+
installed runtime actually offers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
from .errors import DeviceError
|
|
17
|
+
|
|
18
|
+
__all__ = ["device", "resolve", "available", "is_available"]
|
|
19
|
+
|
|
20
|
+
#: Priority chain used by ``device="auto"``.
|
|
21
|
+
PRIORITY = ("cuda", "mps", "cpu")
|
|
22
|
+
|
|
23
|
+
ALIASES = {
|
|
24
|
+
None: "auto",
|
|
25
|
+
"auto": "auto",
|
|
26
|
+
"cpu": "cpu",
|
|
27
|
+
"cuda": "cuda",
|
|
28
|
+
"gpu": "gpu",
|
|
29
|
+
"mps": "mps",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_available(name: str) -> bool:
|
|
34
|
+
"""Return whether a concrete backend can be used right now."""
|
|
35
|
+
if name == "cpu":
|
|
36
|
+
return True
|
|
37
|
+
if name == "cuda":
|
|
38
|
+
return torch.cuda.is_available()
|
|
39
|
+
if name == "mps":
|
|
40
|
+
return torch.backends.mps.is_available()
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def available() -> list[str]:
|
|
45
|
+
"""Every usable backend, best first."""
|
|
46
|
+
return [name for name in PRIORITY if is_available(name)]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve(spec: str | torch.device | None = "auto") -> torch.device:
|
|
50
|
+
"""Turn a device identifier into a concrete :class:`torch.device`."""
|
|
51
|
+
if isinstance(spec, torch.device):
|
|
52
|
+
return spec
|
|
53
|
+
|
|
54
|
+
if isinstance(spec, str):
|
|
55
|
+
key = spec.strip().lower()
|
|
56
|
+
# Allow indexed devices such as "cuda:1" to pass through untouched.
|
|
57
|
+
if ":" in key:
|
|
58
|
+
base = key.split(":", 1)[0]
|
|
59
|
+
if base not in ALIASES or base in ("auto", "gpu"):
|
|
60
|
+
raise DeviceError(_unknown(spec))
|
|
61
|
+
if not is_available(base):
|
|
62
|
+
raise DeviceError(_unavailable(base))
|
|
63
|
+
return torch.device(key)
|
|
64
|
+
elif spec is None:
|
|
65
|
+
key = "auto"
|
|
66
|
+
else:
|
|
67
|
+
raise DeviceError(_unknown(spec))
|
|
68
|
+
|
|
69
|
+
if key not in ALIASES:
|
|
70
|
+
raise DeviceError(_unknown(spec))
|
|
71
|
+
|
|
72
|
+
if key == "auto":
|
|
73
|
+
return torch.device(available()[0])
|
|
74
|
+
|
|
75
|
+
if key == "gpu":
|
|
76
|
+
for name in ("cuda", "mps"):
|
|
77
|
+
if is_available(name):
|
|
78
|
+
return torch.device(name)
|
|
79
|
+
raise DeviceError(
|
|
80
|
+
"error: no gpu available\n\n"
|
|
81
|
+
"requested: gpu\n"
|
|
82
|
+
f"available: {', '.join(available())}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not is_available(key):
|
|
86
|
+
raise DeviceError(_unavailable(key))
|
|
87
|
+
return torch.device(key)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _unknown(spec: object) -> str:
|
|
91
|
+
return (
|
|
92
|
+
"error: unknown device\n\n"
|
|
93
|
+
f"received: {spec!r}\n"
|
|
94
|
+
"expected: auto, cpu, cuda, mps or gpu"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _unavailable(name: str) -> str:
|
|
99
|
+
return (
|
|
100
|
+
"error: device unavailable\n\n"
|
|
101
|
+
f"requested: {name}\n"
|
|
102
|
+
f"available: {', '.join(available())}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def device(spec: str | None = "auto") -> str:
|
|
107
|
+
"""Report the execution backend nsharper would use.
|
|
108
|
+
|
|
109
|
+
>>> import nsharper as ns
|
|
110
|
+
>>> ns.device()
|
|
111
|
+
'mps'
|
|
112
|
+
"""
|
|
113
|
+
return str(resolve(spec))
|