Skip to content

Commit 574071f

Browse files
committed
Merge branch 'release/v1.1.4'
2 parents 46807ed + 61e1187 commit 574071f

27 files changed

+2080
-1731
lines changed

.github/workflows/python-package.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ jobs:
1111
python-version: [3.7, 3.8]
1212

1313
steps:
14+
1415
- uses: actions/checkout@v2
1516
with:
1617
path: scanpy-scripts
17-
18+
19+
- uses: psf/black@stable
20+
with:
21+
options: '--check --verbose --include="\.pyi?$" .'
22+
1823
- uses: actions/checkout@v2
1924
with:
2025
repository: theislab/scanpy
@@ -38,11 +43,13 @@ jobs:
3843
popd
3944
4045
sudo apt-get install libhdf5-dev
41-
pip install -U setuptools>=40.1 wheel 'cmake<3.20'
46+
pip install -U setuptools>=40.1 wheel 'cmake<3.20' pytest
4247
pip install $(pwd)/scanpy-scripts
4348
python -m pip install $(pwd)/scanpy --no-deps --ignore-installed -vv
4449
50+
- name: Run unit tests
51+
run: pytest --doctest-modules -v ./scanpy-scripts
52+
4553
- name: Test with bats
4654
run: |
4755
./scanpy-scripts/scanpy-scripts-tests.bats
48-

scanpy_scripts/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
"""
44
import pkg_resources
55

6-
__version__ = pkg_resources.get_distribution('scanpy-scripts').version
6+
__version__ = pkg_resources.get_distribution("scanpy-scripts").version
77

8-
__author__ = ', '.join([
9-
'Ni Huang',
10-
'Pablo Moreno',
11-
'Jonathan Manning',
12-
'Philipp Angerer',
13-
])
8+
__author__ = ", ".join(
9+
[
10+
"Ni Huang",
11+
"Pablo Moreno",
12+
"Jonathan Manning",
13+
"Philipp Angerer",
14+
]
15+
)
1416

1517
from . import lib

scanpy_scripts/cli.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,20 @@
4242

4343
@click.group(cls=NaturalOrderGroup)
4444
@click.option(
45-
'--debug',
45+
"--debug",
4646
is_flag=True,
4747
default=False,
48-
help='Print debug information',
48+
help="Print debug information",
4949
)
5050
@click.option(
51-
'--verbosity',
51+
"--verbosity",
5252
type=click.INT,
5353
default=3,
54-
help='Set scanpy verbosity',
54+
help="Set scanpy verbosity",
5555
)
5656
@click.version_option(
57-
version='0.2.0',
58-
prog_name='scanpy',
57+
version="0.2.0",
58+
prog_name="scanpy",
5959
)
6060
def cli(debug=False, verbosity=3):
6161
"""
@@ -64,11 +64,12 @@ def cli(debug=False, verbosity=3):
6464
log_level = logging.DEBUG if debug else logging.INFO
6565
logging.basicConfig(
6666
level=log_level,
67-
format=('%(asctime)s; %(levelname)s; %(filename)s; '
68-
'%(funcName)s(): %(message)s'),
69-
datefmt='%y-%m-%d %H:%M:%S',
67+
format=(
68+
"%(asctime)s; %(levelname)s; %(filename)s; " "%(funcName)s(): %(message)s"
69+
),
70+
datefmt="%y-%m-%d %H:%M:%S",
7071
)
71-
logging.debug('debugging')
72+
logging.debug("debugging")
7273
sc.settings.verbosity = verbosity
7374
return 0
7475

@@ -112,15 +113,18 @@ def cluster():
112113
def integrate():
113114
"""Integrate cells from different experimental batches."""
114115

116+
115117
integrate.add_command(HARMONY_INTEGRATE_CMD)
116118
integrate.add_command(BBKNN_CMD)
117119
integrate.add_command(MNN_CORRECT_CMD)
118120
integrate.add_command(COMBAT_CMD)
119121

122+
120123
@cli.group(cls=NaturalOrderGroup)
121124
def multiplet():
122125
"""Execute methods for multiplet removal."""
123126

127+
124128
multiplet.add_command(SCRUBLET_MULTIPLET_CMD)
125129
multiplet.add_command(SCRUBLET_MULTIPLET_SIMULATE_CMD)
126130

scanpy_scripts/click_utils.py

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import click
6+
import sys
67

78

89
class NaturalOrderGroup(click.Group):
@@ -12,6 +13,7 @@ class NaturalOrderGroup(click.Group):
1213
1314
@click.group(cls=NaturalOrderGroup)
1415
"""
16+
1517
def list_commands(self, ctx):
1618
"""List command names as they are in commands dict.
1719
@@ -25,38 +27,67 @@ class CommaSeparatedText(click.ParamType):
2527
"""
2628
Comma separated text
2729
"""
30+
2831
def __init__(self, dtype=click.STRING, simplify=False, length=None):
2932
self.dtype = dtype
3033
self.dtype_name = _get_type_name(dtype)
3134
self.simplify = simplify
3235
self.length = length
3336
if length and length <= 3:
34-
self.name = ','.join([f'{self.dtype_name}'] * length)
37+
self.name = ",".join([f"{self.dtype_name}"] * length)
3538
else:
36-
self.name = '{}[,{}...]'.format(self.dtype_name, self.dtype_name)
39+
self.name = "{}[,{}...]".format(self.dtype_name, self.dtype_name)
3740

3841
def convert(self, value, param, ctx):
42+
"""
43+
>>> @click.command()
44+
... @click.option('--test-param')
45+
... def test_cmd():
46+
... pass
47+
...
48+
>>> ctx = click.Context(test_cmd)
49+
>>> param = test_cmd.params[0]
50+
>>> test_cst1 = CommaSeparatedText()
51+
>>> test_cst2 = CommaSeparatedText(click.INT, length=2)
52+
>>> test_cst3 = CommaSeparatedText(click.FLOAT, simplify=True)
53+
>>>
54+
>>> test_cst1.convert(None, param, ctx)
55+
>>> test_cst2.convert('7,2', param, ctx)
56+
[7, 2]
57+
>>> test_cst2.convert('7.2', param, ctx)
58+
Traceback (most recent call last):
59+
...
60+
click.exceptions.BadParameter: 7.2 is not a valid integer
61+
>>> test_cst2.convert('7', param, ctx)
62+
Traceback (most recent call last):
63+
...
64+
click.exceptions.BadParameter: 7 is not a valid comma separated list of length 2
65+
>>> test_cst3.convert('7.2', param, ctx)
66+
7.2
67+
"""
3968
try:
4069
if value is None:
4170
converted = None
4271
else:
43-
converted = list(map(self.dtype, str(value).split(',')))
72+
converted = list(map(self.dtype, str(value).split(",")))
4473
if self.simplify and len(converted) == 1:
4574
converted = converted[0]
4675
except ValueError:
4776
self.fail(
48-
'{} is not a valid comma separated list of {}'.format(
49-
value, self.dtype_name),
77+
"{} is not a valid comma separated list of {}".format(
78+
value, self.dtype_name
79+
),
5080
param,
51-
ctx
81+
ctx,
5282
)
5383
if self.length:
5484
if len(converted) != self.length:
5585
self.fail(
56-
'{} is not a valid comma separated list of length {}'.format(
57-
value, self.length),
86+
"{} is not a valid comma separated list of length {}".format(
87+
value, self.length
88+
),
5889
param,
59-
ctx
90+
ctx,
6091
)
6192
return converted
6293

@@ -65,26 +96,50 @@ class Dictionary(click.ParamType):
6596
"""
6697
Text to be parsed as a python dict definition
6798
"""
99+
68100
def __init__(self, keys=None):
69-
self.name = 'TEXT:VAL[,TEXT:VAL...]'
101+
self.name = "TEXT:VAL[,TEXT:VAL...]"
70102
self.keys = keys
71103

72104
def convert(self, value, param, ctx):
105+
"""
106+
>>> @click.command()
107+
... @click.option('--my-param', type=Dictionary(keys=('abc', 'def', 'ghi', 'jkl', 'mno')))
108+
... def test_cmd():
109+
... pass
110+
...
111+
>>> ctx = click.Context(test_cmd)
112+
>>> param = test_cmd.params[0]
113+
>>> dict_param = param.type
114+
>>> dict_str1 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mno:some_string'
115+
>>> dict_str2 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mnp:some_string'
116+
>>> dict_str3 = ''
117+
>>> dict_param.convert(dict_str1, param, ctx)
118+
{'abc': 0.1, 'def': True, 'ghi': False, 'jkl': None, 'mno': 'some_string'}
119+
>>> dict_param.convert(dict_str2, param, ctx)
120+
Traceback (most recent call last):
121+
...
122+
click.exceptions.BadParameter: mnp is not a valid key (('abc', 'def', 'ghi', 'jkl', 'mno'))
123+
>>> dict_param.convert(dict_str3, param, ctx)
124+
Traceback (most recent call last):
125+
...
126+
click.exceptions.BadParameter: is not a valid python dict definition
127+
"""
73128
try:
74129
converted = dict()
75-
for token in value.split(','):
76-
if ':' not in token:
130+
for token in value.split(","):
131+
if ":" not in token:
77132
raise ValueError
78-
key, _, value = token.partition(':')
133+
key, _, value = token.partition(":")
79134
if not key:
80135
raise ValueError
81136
if isinstance(self.keys, (list, tuple)) and key not in self.keys:
82-
self.fail(f'{key} is not a valid key ({self.keys})')
83-
if value == 'None':
137+
self.fail(f"{key} is not a valid key ({self.keys})")
138+
if value == "None":
84139
value = None
85-
elif value.lower() == 'true':
140+
elif value.lower() == "true":
86141
value = True
87-
elif value.lower() == 'false':
142+
elif value.lower() == "false":
88143
value = False
89144
else:
90145
try:
@@ -94,39 +149,76 @@ def convert(self, value, param, ctx):
94149
converted[key] = value
95150
return converted
96151
except ValueError:
97-
self.fail(
98-
f'{value} is not a valid python dict definition',
99-
param,
100-
ctx
101-
)
152+
self.fail(f"{value} is not a valid python dict definition", param, ctx)
102153

103154

104155
def _get_type_name(obj):
105-
name = 'text'
156+
name = "text"
106157
try:
107-
name = getattr(obj, 'name')
158+
name = getattr(obj, "name")
108159
except AttributeError:
109-
name = getattr(obj, '__name__')
160+
name = getattr(obj, "__name__")
110161
return name
111162

112163

113164
def valid_limit(ctx, param, value):
165+
"""
166+
Callback function that checks order of numeric inputs
167+
168+
>>> @click.command()
169+
... @click.option('--test-param', help='Sample help')
170+
... def test_cmd():
171+
... pass
172+
...
173+
>>> ctx = click.Context(test_cmd)
174+
>>> param = test_cmd.params[0]
175+
>>> valid_limit(ctx, param, value=[0.0125, 3])
176+
[0.0125, 3]
177+
>>> valid_limit(ctx, param, value=[0.0125, -0.0125])
178+
Traceback (most recent call last):
179+
...
180+
click.exceptions.BadParameter: lower limit must not exceed upper limit
181+
>>> valid_limit(ctx, param, value=[0.0125, 0.0125])
182+
[0.0125, 0.0125]
183+
"""
114184
if value[0] > value[1]:
115-
param.type.fail(
116-
'lower limit must not exceed upper limit', param, ctx)
185+
param.type.fail("lower limit must not exceed upper limit", param, ctx)
117186
return value
118187

119188

120189
def valid_parameter_limits(ctx, param, value):
190+
"""
191+
Callback function that checks order of multiple numeric inputs
192+
193+
>>> @click.command()
194+
... @click.option('--test-param', type=(click.STRING, click.FLOAT, click.FLOAT), multiple=True)
195+
... def test_cmd():
196+
... pass
197+
...
198+
>>> ctx = click.Context(test_cmd)
199+
>>> param = test_cmd.params[0]
200+
>>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0]])
201+
[['a', 0.0, 2.0]]
202+
>>> valid_parameter_limits(ctx, param, [['b', 0.0, 0.0]])
203+
[['b', 0.0, 0.0]]
204+
>>> valid_parameter_limits(ctx, param, [['c', 0.0, -1.0]])
205+
Traceback (most recent call last):
206+
...
207+
click.exceptions.BadParameter: lower limit must not exceed upper limit
208+
>>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0], ['c', 0.0, -1.0]])
209+
Traceback (most recent call last):
210+
...
211+
click.exceptions.BadParameter: lower limit must not exceed upper limit
212+
"""
121213
for val in value:
122214
if val[1] > val[2]:
123-
param.type.fail(
124-
'lower limit must not exceed upper limit', param, ctx)
215+
param.type.fail("lower limit must not exceed upper limit", param, ctx)
125216
return value
126217

127218

128219
def mutually_exclusive_with(param_name):
129-
internal_name = param_name.strip('-').replace('-', '_').lower()
220+
internal_name = param_name.strip("-").replace("-", "_").lower()
221+
130222
def valid_mutually_exclusive(ctx, param, value):
131223
try:
132224
other_value = ctx.params[internal_name]
@@ -135,22 +227,35 @@ def valid_mutually_exclusive(ctx, param, value):
135227
if (value is None) == (other_value is None):
136228
param.type.fail(
137229
'mutually exclusive with "{}", one and only one must be '
138-
'specified.'.format(param_name),
230+
"specified.".format(param_name),
139231
param,
140232
ctx,
141233
)
142234
return value
235+
143236
return valid_mutually_exclusive
144237

145238

146239
def required_by(param_name):
147-
internal_name = param_name.strip('-').replace('-', '_').lower()
240+
internal_name = param_name.strip("-").replace("-", "_").lower()
241+
148242
def required(ctx, param, value):
149243
try:
150244
other_value = ctx.params[internal_name]
151245
except KeyError:
152246
return value
153247
if other_value and not value:
154-
param.type.fail('required by "{}".'.format(param_name), param, ctx,)
248+
param.type.fail(
249+
'required by "{}".'.format(param_name),
250+
param,
251+
ctx,
252+
)
155253
return value
254+
156255
return required
256+
257+
258+
if __name__ == "__main__":
259+
import doctest
260+
261+
sys.exit(doctest.testmod(verbose=True)[0])

0 commit comments

Comments
 (0)