Skip to content

Commit

Permalink
Merge branch 'release/v1.1.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
pinin4fjords committed Mar 1, 2022
2 parents 46807ed + 61e1187 commit 574071f
Show file tree
Hide file tree
Showing 27 changed files with 2,080 additions and 1,731 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ jobs:
python-version: [3.7, 3.8]

steps:

- uses: actions/checkout@v2
with:
path: scanpy-scripts


- uses: psf/black@stable
with:
options: '--check --verbose --include="\.pyi?$" .'

- uses: actions/checkout@v2
with:
repository: theislab/scanpy
Expand All @@ -38,11 +43,13 @@ jobs:
popd
sudo apt-get install libhdf5-dev
pip install -U setuptools>=40.1 wheel 'cmake<3.20'
pip install -U setuptools>=40.1 wheel 'cmake<3.20' pytest
pip install $(pwd)/scanpy-scripts
python -m pip install $(pwd)/scanpy --no-deps --ignore-installed -vv
- name: Run unit tests
run: pytest --doctest-modules -v ./scanpy-scripts

- name: Test with bats
run: |
./scanpy-scripts/scanpy-scripts-tests.bats
16 changes: 9 additions & 7 deletions scanpy_scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"""
import pkg_resources

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

__author__ = ', '.join([
'Ni Huang',
'Pablo Moreno',
'Jonathan Manning',
'Philipp Angerer',
])
__author__ = ", ".join(
[
"Ni Huang",
"Pablo Moreno",
"Jonathan Manning",
"Philipp Angerer",
]
)

from . import lib
24 changes: 14 additions & 10 deletions scanpy_scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@

@click.group(cls=NaturalOrderGroup)
@click.option(
'--debug',
"--debug",
is_flag=True,
default=False,
help='Print debug information',
help="Print debug information",
)
@click.option(
'--verbosity',
"--verbosity",
type=click.INT,
default=3,
help='Set scanpy verbosity',
help="Set scanpy verbosity",
)
@click.version_option(
version='0.2.0',
prog_name='scanpy',
version="0.2.0",
prog_name="scanpy",
)
def cli(debug=False, verbosity=3):
"""
Expand All @@ -64,11 +64,12 @@ def cli(debug=False, verbosity=3):
log_level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(
level=log_level,
format=('%(asctime)s; %(levelname)s; %(filename)s; '
'%(funcName)s(): %(message)s'),
datefmt='%y-%m-%d %H:%M:%S',
format=(
"%(asctime)s; %(levelname)s; %(filename)s; " "%(funcName)s(): %(message)s"
),
datefmt="%y-%m-%d %H:%M:%S",
)
logging.debug('debugging')
logging.debug("debugging")
sc.settings.verbosity = verbosity
return 0

Expand Down Expand Up @@ -112,15 +113,18 @@ def cluster():
def integrate():
"""Integrate cells from different experimental batches."""


integrate.add_command(HARMONY_INTEGRATE_CMD)
integrate.add_command(BBKNN_CMD)
integrate.add_command(MNN_CORRECT_CMD)
integrate.add_command(COMBAT_CMD)


@cli.group(cls=NaturalOrderGroup)
def multiplet():
"""Execute methods for multiplet removal."""


multiplet.add_command(SCRUBLET_MULTIPLET_CMD)
multiplet.add_command(SCRUBLET_MULTIPLET_SIMULATE_CMD)

Expand Down
171 changes: 138 additions & 33 deletions scanpy_scripts/click_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import click
import sys


class NaturalOrderGroup(click.Group):
Expand All @@ -12,6 +13,7 @@ class NaturalOrderGroup(click.Group):
@click.group(cls=NaturalOrderGroup)
"""

def list_commands(self, ctx):
"""List command names as they are in commands dict.
Expand All @@ -25,38 +27,67 @@ class CommaSeparatedText(click.ParamType):
"""
Comma separated text
"""

def __init__(self, dtype=click.STRING, simplify=False, length=None):
self.dtype = dtype
self.dtype_name = _get_type_name(dtype)
self.simplify = simplify
self.length = length
if length and length <= 3:
self.name = ','.join([f'{self.dtype_name}'] * length)
self.name = ",".join([f"{self.dtype_name}"] * length)
else:
self.name = '{}[,{}...]'.format(self.dtype_name, self.dtype_name)
self.name = "{}[,{}...]".format(self.dtype_name, self.dtype_name)

def convert(self, value, param, ctx):
"""
>>> @click.command()
... @click.option('--test-param')
... def test_cmd():
... pass
...
>>> ctx = click.Context(test_cmd)
>>> param = test_cmd.params[0]
>>> test_cst1 = CommaSeparatedText()
>>> test_cst2 = CommaSeparatedText(click.INT, length=2)
>>> test_cst3 = CommaSeparatedText(click.FLOAT, simplify=True)
>>>
>>> test_cst1.convert(None, param, ctx)
>>> test_cst2.convert('7,2', param, ctx)
[7, 2]
>>> test_cst2.convert('7.2', param, ctx)
Traceback (most recent call last):
...
click.exceptions.BadParameter: 7.2 is not a valid integer
>>> test_cst2.convert('7', param, ctx)
Traceback (most recent call last):
...
click.exceptions.BadParameter: 7 is not a valid comma separated list of length 2
>>> test_cst3.convert('7.2', param, ctx)
7.2
"""
try:
if value is None:
converted = None
else:
converted = list(map(self.dtype, str(value).split(',')))
converted = list(map(self.dtype, str(value).split(",")))
if self.simplify and len(converted) == 1:
converted = converted[0]
except ValueError:
self.fail(
'{} is not a valid comma separated list of {}'.format(
value, self.dtype_name),
"{} is not a valid comma separated list of {}".format(
value, self.dtype_name
),
param,
ctx
ctx,
)
if self.length:
if len(converted) != self.length:
self.fail(
'{} is not a valid comma separated list of length {}'.format(
value, self.length),
"{} is not a valid comma separated list of length {}".format(
value, self.length
),
param,
ctx
ctx,
)
return converted

Expand All @@ -65,26 +96,50 @@ class Dictionary(click.ParamType):
"""
Text to be parsed as a python dict definition
"""

def __init__(self, keys=None):
self.name = 'TEXT:VAL[,TEXT:VAL...]'
self.name = "TEXT:VAL[,TEXT:VAL...]"
self.keys = keys

def convert(self, value, param, ctx):
"""
>>> @click.command()
... @click.option('--my-param', type=Dictionary(keys=('abc', 'def', 'ghi', 'jkl', 'mno')))
... def test_cmd():
... pass
...
>>> ctx = click.Context(test_cmd)
>>> param = test_cmd.params[0]
>>> dict_param = param.type
>>> dict_str1 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mno:some_string'
>>> dict_str2 = 'abc:0.1,def:TRUE,ghi:False,jkl:None,mnp:some_string'
>>> dict_str3 = ''
>>> dict_param.convert(dict_str1, param, ctx)
{'abc': 0.1, 'def': True, 'ghi': False, 'jkl': None, 'mno': 'some_string'}
>>> dict_param.convert(dict_str2, param, ctx)
Traceback (most recent call last):
...
click.exceptions.BadParameter: mnp is not a valid key (('abc', 'def', 'ghi', 'jkl', 'mno'))
>>> dict_param.convert(dict_str3, param, ctx)
Traceback (most recent call last):
...
click.exceptions.BadParameter: is not a valid python dict definition
"""
try:
converted = dict()
for token in value.split(','):
if ':' not in token:
for token in value.split(","):
if ":" not in token:
raise ValueError
key, _, value = token.partition(':')
key, _, value = token.partition(":")
if not key:
raise ValueError
if isinstance(self.keys, (list, tuple)) and key not in self.keys:
self.fail(f'{key} is not a valid key ({self.keys})')
if value == 'None':
self.fail(f"{key} is not a valid key ({self.keys})")
if value == "None":
value = None
elif value.lower() == 'true':
elif value.lower() == "true":
value = True
elif value.lower() == 'false':
elif value.lower() == "false":
value = False
else:
try:
Expand All @@ -94,39 +149,76 @@ def convert(self, value, param, ctx):
converted[key] = value
return converted
except ValueError:
self.fail(
f'{value} is not a valid python dict definition',
param,
ctx
)
self.fail(f"{value} is not a valid python dict definition", param, ctx)


def _get_type_name(obj):
name = 'text'
name = "text"
try:
name = getattr(obj, 'name')
name = getattr(obj, "name")
except AttributeError:
name = getattr(obj, '__name__')
name = getattr(obj, "__name__")
return name


def valid_limit(ctx, param, value):
"""
Callback function that checks order of numeric inputs
>>> @click.command()
... @click.option('--test-param', help='Sample help')
... def test_cmd():
... pass
...
>>> ctx = click.Context(test_cmd)
>>> param = test_cmd.params[0]
>>> valid_limit(ctx, param, value=[0.0125, 3])
[0.0125, 3]
>>> valid_limit(ctx, param, value=[0.0125, -0.0125])
Traceback (most recent call last):
...
click.exceptions.BadParameter: lower limit must not exceed upper limit
>>> valid_limit(ctx, param, value=[0.0125, 0.0125])
[0.0125, 0.0125]
"""
if value[0] > value[1]:
param.type.fail(
'lower limit must not exceed upper limit', param, ctx)
param.type.fail("lower limit must not exceed upper limit", param, ctx)
return value


def valid_parameter_limits(ctx, param, value):
"""
Callback function that checks order of multiple numeric inputs
>>> @click.command()
... @click.option('--test-param', type=(click.STRING, click.FLOAT, click.FLOAT), multiple=True)
... def test_cmd():
... pass
...
>>> ctx = click.Context(test_cmd)
>>> param = test_cmd.params[0]
>>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0]])
[['a', 0.0, 2.0]]
>>> valid_parameter_limits(ctx, param, [['b', 0.0, 0.0]])
[['b', 0.0, 0.0]]
>>> valid_parameter_limits(ctx, param, [['c', 0.0, -1.0]])
Traceback (most recent call last):
...
click.exceptions.BadParameter: lower limit must not exceed upper limit
>>> valid_parameter_limits(ctx, param, [['a', 0.0, 2.0], ['c', 0.0, -1.0]])
Traceback (most recent call last):
...
click.exceptions.BadParameter: lower limit must not exceed upper limit
"""
for val in value:
if val[1] > val[2]:
param.type.fail(
'lower limit must not exceed upper limit', param, ctx)
param.type.fail("lower limit must not exceed upper limit", param, ctx)
return value


def mutually_exclusive_with(param_name):
internal_name = param_name.strip('-').replace('-', '_').lower()
internal_name = param_name.strip("-").replace("-", "_").lower()

def valid_mutually_exclusive(ctx, param, value):
try:
other_value = ctx.params[internal_name]
Expand All @@ -135,22 +227,35 @@ def valid_mutually_exclusive(ctx, param, value):
if (value is None) == (other_value is None):
param.type.fail(
'mutually exclusive with "{}", one and only one must be '
'specified.'.format(param_name),
"specified.".format(param_name),
param,
ctx,
)
return value

return valid_mutually_exclusive


def required_by(param_name):
internal_name = param_name.strip('-').replace('-', '_').lower()
internal_name = param_name.strip("-").replace("-", "_").lower()

def required(ctx, param, value):
try:
other_value = ctx.params[internal_name]
except KeyError:
return value
if other_value and not value:
param.type.fail('required by "{}".'.format(param_name), param, ctx,)
param.type.fail(
'required by "{}".'.format(param_name),
param,
ctx,
)
return value

return required


if __name__ == "__main__":
import doctest

sys.exit(doctest.testmod(verbose=True)[0])
Loading

0 comments on commit 574071f

Please sign in to comment.