diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99844d0..46d3b2d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,14 +1,15 @@ name: Test -on: [push, pull_request] +on: + push: + pull_request: + schedule: + - cron: '15 23 * * 6' # M H d m w (Sat at 23:15) jobs: check: - if: github.event_name != 'pull_request' || github.head_ref != 'devel' + if: github.event_name != 'pull_request' || github.repository_owner != 'AMYPAD' runs-on: ubuntu-latest - name: Check steps: - uses: actions/checkout@v2 - with: - fetch-depth: 0 - uses: actions/setup-python@v2 - name: set PYSHA run: echo "PYSHA=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV @@ -19,7 +20,7 @@ jobs: - name: dependencies run: pip install -U pre-commit - uses: reviewdog/action-setup@v1 - - if: github.event_name != 'schedule' + - if: github.event_name == 'push' || github.event_name == 'pull_request' name: comment run: | if [[ $EVENT == pull_request ]]; then @@ -34,12 +35,12 @@ jobs: EVENT: ${{ github.event_name }} - run: pre-commit run -a --show-diff-on-failure test: - if: github.event_name != 'pull_request' || github.head_ref != 'devel' + if: github.event_name != 'pull_request' || github.repository_owner != 'AMYPAD' + name: py${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: python: [2.7, 3.6, 3.9] - name: Test py${{ matrix.python }} steps: - uses: actions/checkout@v2 with: @@ -52,22 +53,25 @@ jobs: run: pytest --durations-min=1 - if: startsWith(matrix.python, '2') run: pytest - - run: codecov + - uses: codecov/codecov-action@v1 cuda_matlab: - if: github.event_name != 'pull_request' || github.head_ref != 'devel' + if: github.event_name != 'pull_request' || github.repository_owner != 'AMYPAD' + name: CUDA MATLAB py${{ matrix.python }} runs-on: [self-hosted, python, cuda, matlab] - name: Test cuda & matlab + strategy: + matrix: + python: [3.7, 3.9] steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Run setup-python - run: setup-python -p3.7 + run: setup-python -p${{ matrix.python }} - run: pip install -U .[dev,nii,cuda,web,mbeautify] - run: pytest --durations-min=1 - - run: codecov + - uses: codecov/codecov-action@v1 - name: Post Run setup-python - run: setup-python -p3.7 -Dr + run: setup-python -p${{ matrix.python }} -Dr if: ${{ always() }} deploy: needs: [check, test, cuda_matlab] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a65a323..afba6ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,10 +35,11 @@ repos: - flake8-comprehensions - flake8-debugger - flake8-string-format -- repo: https://github.com/psf/black - rev: 21.8b0 +- repo: https://github.com/google/yapf + rev: v0.31.0 hooks: - - id: black + - id: yapf + args: [-i] - repo: https://github.com/PyCQA/isort rev: 5.9.3 hooks: diff --git a/miutil/__init__.py b/miutil/__init__.py index 8c07425..bb590f0 100644 --- a/miutil/__init__.py +++ b/miutil/__init__.py @@ -1,4 +1,4 @@ -from .fdio import * # NOQA +from .fdio import * # yapf: disable # NOQA # version detector. Precedence: installed dist, git, 'UNKNOWN' try: diff --git a/miutil/cuinfo.py b/miutil/cuinfo.py index e8dc80f..2662f63 100755 --- a/miutil/cuinfo.py +++ b/miutil/cuinfo.py @@ -18,13 +18,13 @@ def nvmlDeviceGetCudaComputeCapability(handle): major = pynvml.c_int() minor = pynvml.c_int() - try: # pynvml>=11 + try: # pynvml>=11 get_fn = pynvml.nvml._nvmlGetFunctionPointer except AttributeError: get_fn = pynvml.get_func_pointer fn = get_fn("nvmlDeviceGetCudaComputeCapability") ret = fn(handle, pynvml.byref(major), pynvml.byref(minor)) - try: # pynvml>=11 + try: # pynvml>=11 check_ret = pynvml.nvml._nvmlCheckReturn except AttributeError: check_ret = pynvml.check_return @@ -66,8 +66,7 @@ def name(dev_id=-1): def nvcc_flags(dev_id=-1): return "-gencode=arch=compute_{0:d}{1:d},code=compute_{0:d}{1:d}".format( - *compute_capability(dev_id) - ) + *compute_capability(dev_id)) def main(*args, **kwargs): @@ -86,12 +85,9 @@ def main(*args, **kwargs): noargs = False if noargs: for dev_id in devices: - print( - "Device {:2d}:{}:compute capability:{:d}.{:d}".format( - dev_id, name(dev_id), *compute_capability(dev_id) - ) - ) + print("Device {:2d}:{}:compute capability:{:d}.{:d}".format( + dev_id, name(dev_id), *compute_capability(dev_id))) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover main() diff --git a/miutil/fdio.py b/miutil/fdio.py index 354268b..c551a52 100644 --- a/miutil/fdio.py +++ b/miutil/fdio.py @@ -57,14 +57,14 @@ def extractall(fzip, dest, desc="Extracting"): """zipfile.Zipfile(fzip).extractall(dest) with progress""" dest = Path(dest).expanduser() with ZipFile(fzip) as zipf, tqdm( - desc=desc, - unit="B", - unit_scale=True, - unit_divisor=1024, - total=sum(getattr(i, "file_size", 0) for i in zipf.infolist()), + desc=desc, + unit="B", + unit_scale=True, + unit_divisor=1024, + total=sum(getattr(i, "file_size", 0) for i in zipf.infolist()), ) as pbar: for i in zipf.infolist(): - if not getattr(i, "file_size", 0): # directory + if not getattr(i, "file_size", 0): # directory zipf.extract(i, fspath(dest)) else: (dest / i.filename).parent.mkdir(parents=True, exist_ok=True) @@ -78,7 +78,6 @@ def extractall(fzip, dest, desc="Extracting"): def nsort(fnames): """Sort a file list, automatically detecting embedded numbers""" - def path2parts(fname): parts = re.split(r"([0-9][0-9.]*e[-+][0-9]+|[0-9]+\.[0-9]+|[0-9]+)", fname) parts[1::2] = map(float, parts[1::2]) diff --git a/miutil/imio/nii.py b/miutil/imio/nii.py index 823fc84..c862968 100644 --- a/miutil/imio/nii.py +++ b/miutil/imio/nii.py @@ -4,16 +4,20 @@ import numbers import os.path import re +import sys import nibabel as nib import numpy as np -from six import string_types from ..fdio import create_dir, fspath, hasext from . import RE_NII_GZ RE_GZ = re.compile(r"^(.+)(\.gz)$", flags=re.I) log = logging.getLogger(__name__) +if sys.version_info[0] < 3: + string_types = basestring, # NOQA: F821 +else: + string_types = str, def file_parts(fname, regex=RE_NII_GZ): @@ -86,22 +90,20 @@ def getnii(fim, nan_replace=None, output="image"): flip = tuple(np.int8(ornt[:, 1])) # > voxel size - voxsize = nim.header.get("pixdim")[1 : nim.header.get("dim")[0] + 1] + voxsize = nim.header.get("pixdim")[1:nim.header.get("dim")[0] + 1] # > rearrange voxel size according to the orientation voxsize = voxsize[np.array(trnsp)] # > dimensions - dims = dim[1 : nim.header.get("dim")[0] + 1] + dims = dim[1:nim.header.get("dim")[0] + 1] dims = dims[np.array(trnsp)] # > flip y-axis and z-axis and then transpose. # Depends if dynamic (4 dimensions) or static (3 dimensions) if dimno == 4: - imr = np.transpose( - imr[:: -flip[0], :: -flip[1], :: -flip[2], :], (3,) + trnsp - ) + imr = np.transpose(imr[::-flip[0], ::-flip[1], ::-flip[2], :], (3,) + trnsp) elif dimno == 3: - imr = np.transpose(imr[:: -flip[0], :: -flip[1], :: -flip[2]], trnsp) + imr = np.transpose(imr[::-flip[0], ::-flip[1], ::-flip[2]], trnsp) if output == "affine" or output == "all": # A = nim.get_sform() @@ -111,17 +113,8 @@ def getnii(fim, nan_replace=None, output="image"): if output == "all": out = { - "im": imr, - "affine": A, - "fim": fim, - "dtype": nim.get_data_dtype(), - "shape": imr.shape, - "hdr": nim.header, - "voxsize": voxsize, - "dims": dims, - "transpose": trnsp, - "flip": flip, - } + "im": imr, "affine": A, "fim": fim, "dtype": nim.get_data_dtype(), "shape": imr.shape, + "hdr": nim.header, "voxsize": voxsize, "dims": dims, "transpose": trnsp, "flip": flip} elif output == "image": out = imr elif output == "affine": @@ -161,11 +154,7 @@ def array2nii(im, A, fnii, descrip="", trnsp=None, flip=None, storage_as=None): # >>as obtained from getnii(..., output='all') # > permute the axis order in the image array - if ( - isinstance(storage_as, dict) - and "transpose" in storage_as - and "flip" in storage_as - ): + if (isinstance(storage_as, dict) and "transpose" in storage_as and "flip" in storage_as): trnsp = ( storage_as["transpose"].index(0), @@ -186,12 +175,12 @@ def array2nii(im, A, fnii, descrip="", trnsp=None, flip=None, storage_as=None): # > perform flip of x,y,z axes after transposition into proper NIfTI order if len(flip) == 3: - im = im[:: -flip[0], :: -flip[1], :: -flip[2], ...] + im = im[::-flip[0], ::-flip[1], ::-flip[2], ...] res = nib.Nifti1Image(im, A) hdr = res.header hdr.set_sform(None, code="scanner") - hdr["cal_max"] = np.max(im) # np.percentile(im, 90) # + hdr["cal_max"] = np.max(im) # np.percentile(im, 90) # hdr["cal_min"] = np.min(im) hdr["descrip"] = descrip nib.save(res, fspath(fnii)) @@ -266,12 +255,8 @@ def niisort(fims, memlim=True): raise ValueError("Input image(s) must be 3D.") out = { - "shape": _nii.shape[::-1], - "files": _fims, - "sortlist": sortlist, - "dtype": _nii.get_data_dtype(), - "N": Nim, - } + "shape": _nii.shape[::-1], "files": _fims, "sortlist": sortlist, + "dtype": _nii.get_data_dtype(), "N": Nim} if memlim and Nfrm > 50: imdic = getnii(_fims[0], output="all") @@ -328,12 +313,11 @@ def nii_modify(nii_fd, fimout="", outpath="", fcomment="", voxel_range=None): log.debug("output floating and affine file names:%s", fout) fout = os.path.join(opth, fout) - if len(voxel_range) == 1: # set max value + if len(voxel_range) == 1: # set max value im = voxel_range[0] * dctnii["im"] / np.max(dctnii["im"]) - elif len(voxel_range) == 2: # set range - im = (dctnii["im"] - np.min(dctnii["im"])) * ( - np.ptp(voxel_range) / np.ptp(dctnii["im"]) - ) + voxel_range[0] + elif len(voxel_range) == 2: # set range + im = (dctnii["im"] - np.min(dctnii["im"])) * (np.ptp(voxel_range) / + np.ptp(dctnii["im"])) + voxel_range[0] else: return None diff --git a/miutil/mlab/__init__.py b/miutil/mlab/__init__.py index b697c53..fe6564d 100644 --- a/miutil/mlab/__init__.py +++ b/miutil/mlab/__init__.py @@ -17,7 +17,6 @@ except NameError: FileNotFoundError = OSError - from ..fdio import Path, extractall, fspath, tmpdir __all__ = ["get_engine"] @@ -27,27 +26,20 @@ MATLAB_RUN += ["-wait", "-log"] log = logging.getLogger(__name__) _MCR_URL = { - 99: ( - "https://ssd.mathworks.com/supportfiles/downloads/R2020b/Release/4" - "/deployment_files/installer/complete/" - ), - 713: "https://www.fil.ion.ucl.ac.uk/spm/download/restricted/utopia/MCR/", -} + 99: ("https://ssd.mathworks.com/supportfiles/downloads/R2020b/Release/4" + "/deployment_files/installer/complete/"), + 713: "https://www.fil.ion.ucl.ac.uk/spm/download/restricted/utopia/MCR/"} MCR_ARCH = {"Windows": "win64", "Linux": "glnxa64", "Darwin": "maci64"}[system()] MCR_URL = { "Windows": { 99: _MCR_URL[99] + "win64/MATLAB_Runtime_R2020b_Update_4_win64.zip", - 713: _MCR_URL[713] + "win64/MCRInstaller.exe", - }, + 713: _MCR_URL[713] + "win64/MCRInstaller.exe"}, "Linux": { 99: _MCR_URL[99] + "glnxa64/MATLAB_Runtime_R2020b_Update_4_glnxa64.zip", - 713: _MCR_URL[713] + "glnxa64/MCRInstaller.bin", - }, + 713: _MCR_URL[713] + "glnxa64/MCRInstaller.bin"}, "Darwin": { 99: _MCR_URL[99] + "maci64/MATLAB_Runtime_R2020b_Update_4_maci64.dmg.zip", - 713: _MCR_URL[713] + "maci64/MCRInstaller.dmg", - }, -}[system()] + 713: _MCR_URL[713] + "maci64/MCRInstaller.dmg"}}[system()] # yapf: disable class VersionError(ValueError): @@ -72,19 +64,15 @@ def get_engine(name=None): except ImportError: try: log.warning( - dedent( - """\ + dedent("""\ Python could not find the MATLAB engine. - Attempting to install automatically.""" - ) - ) + Attempting to install automatically.""")) log.debug(_install_engine()) log.info("installed MATLAB engine for Python") from matlab import engine except CalledProcessError: raise ImportError( - dedent( - """\ + dedent("""\ Please install MATLAB and its Python module. See https://www.mathworks.com/help/matlab/matlab_external/\ install-the-matlab-engine-for-python.html @@ -102,11 +90,7 @@ def get_engine(name=None): to the above command. Alternatively, use `get_runtime()` instead of `get_engine()`. - """ - ).format( - matlabroot=matlabroot(default="matlabroot"), exe=sys.executable - ) - ) + """).format(matlabroot=matlabroot(default="matlabroot"), exe=sys.executable)) started = engine.find_matlab() notify = False if not started or (name and name not in started): @@ -121,9 +105,8 @@ def get_engine(name=None): def _matlab_run(command, jvm=False, auto_exit=True): if auto_exit and not command.endswith("exit"): command = command + ", exit" - return check_output_u8( - MATLAB_RUN + ([] if jvm else ["-nojvm"]) + ["-r", command], stderr=STDOUT - ) + return check_output_u8(MATLAB_RUN + ([] if jvm else ["-nojvm"]) + ["-r", command], + stderr=STDOUT) def matlabroot(default=None): @@ -147,21 +130,15 @@ def matlabroot(default=None): def _install_engine(): src = path.join(matlabroot(), "extern", "engines", "python") - with open(path.join(src, "setup.py")) as fd: # check version support + with open(path.join(src, "setup.py")) as fd: # check version support supported = literal_eval( - re.search(r"supported_version.*?=\s*(.*?)$", fd.read(), flags=re.M).group(1) - ) + re.search(r"supported_version.*?=\s*(.*?)$", fd.read(), flags=re.M).group(1)) if ".".join(map(str, sys.version_info[:2])) not in map(str, supported): raise VersionError( - dedent( - """\ + dedent("""\ Python version is {info[0]}.{info[1]}, but the installed MATLAB only supports Python versions: [{supported}] - """.format( - info=sys.version_info[:2], supported=", ".join(supported) - ) - ) - ) + """.format(info=sys.version_info[:2], supported=", ".join(supported)))) with tmpdir() as td: cmd = [sys.executable, "setup.py", "build", "--build-base", td, "install"] try: @@ -175,7 +152,7 @@ def _install_engine(): def get_runtime(cache="~/.mcr", version=99): cache = Path(cache).expanduser() mcr_root = cache - i = mcr_root / ("v%d" % version) + i = mcr_root / ("v%d"%version) if i.is_dir(): mcr_root = i else: @@ -188,41 +165,24 @@ def get_runtime(cache="~/.mcr", version=99): extractall(fd, td) log.info("Installing ... (may take a few min)") if version == 99: - check_output_u8( - [ - fspath( - Path(td) / ("setup" if system() == "Windows" else "install") - ), - "-mode", - "silent", - "-agreeToLicense", - "yes", - "-destinationFolder", - fspath(mcr_root), - ] - ) + check_output_u8([ + fspath(Path(td) / ("setup" if system() == "Windows" else "install")), "-mode", + "silent", "-agreeToLicense", "yes", "-destinationFolder", + fspath(mcr_root)]) elif version == 713: install = cache / MCR_URL[version].rsplit("/", 1)[-1] if system() == "Linux": install.chmod(0o755) - check_output_u8( - [ - fspath(install), - "-P", - 'bean421.installLocation="%s"' % fspath(cache), - "-silent", - ] - ) + check_output_u8([ + fspath(install), "-P", + 'bean421.installLocation="%s"' % fspath(cache), "-silent"]) else: raise NotImplementedError( - dedent( - """\ + dedent("""\ Don't yet know how to handle {} for {!r}. - """ - ).format(fspath(install), system()) - ) + """).format(fspath(install), system())) else: raise IndexError(version) mcr_root /= "v%d" % version @@ -235,11 +195,8 @@ def get_runtime(cache="~/.mcr", version=99): log.warning("Cannot find MCR bin") # libs - env_var = { - "Linux": "LD_LIBRARY_PATH", - "Windows": "PATH", - "Darwin": "DYLD_LIBRARY_PATH", - }[system()] + env_var = {"Linux": "LD_LIBRARY_PATH", "Windows": "PATH", + "Darwin": "DYLD_LIBRARY_PATH"}[system()] if (mcr_root / "runtime" / MCR_ARCH).is_dir(): env_prefix(env_var, mcr_root / "runtime" / MCR_ARCH) else: diff --git a/miutil/mlab/beautify.py b/miutil/mlab/beautify.py index e8d492a..846328a 100755 --- a/miutil/mlab/beautify.py +++ b/miutil/mlab/beautify.py @@ -17,7 +17,7 @@ from . import get_engine, lru_cache log = logging.getLogger(__name__) -MBEAUTIFIER_REV = "1a57849e44662f56271dc0eefa746855698a719a" +MBEAUTIFIER_REV = "6005eeb8b17be8a40be32cea73005cf0d36de4e9" @lru_cache() @@ -52,5 +52,5 @@ def main(*args, **kwargs): log.error("file:%s:\n%s", fn, exc) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover main() diff --git a/miutil/plot.py b/miutil/plot.py index 5eb3171..60bd149 100644 --- a/miutil/plot.py +++ b/miutil/plot.py @@ -7,7 +7,7 @@ from .imio import imread -show = plt.show # convenience: for use after `imscroll` +show = plt.show # convenience: for use after `imscroll` def apply_cmap(**kwargs): @@ -70,18 +70,13 @@ def __init__(self, vol, view="t", fig=None, titles=None, order=0, **kwargs): vol = [vol] elif ndim not in [4, 5]: raise IndexError( - dedent( - """\ + dedent("""\ Expected vol.ndim in 3: single volume 4: multiple volumes 5: multiple RGB volumes but got {} - """.format( - ndim - ) - ) - ) + """.format(ndim))) view = view.lower() if view in ["c", "coronal", "y"]: @@ -112,7 +107,7 @@ def __init__(self, vol, view="t", fig=None, titles=None, order=0, **kwargs): self.fig.canvas.mpl_connect("key_press_event", self._on_key) self.fig.canvas.mpl_connect("key_release_event", self._off_key) self.fig.canvas.mpl_connect("button_press_event", self._on_click) - imscroll._instances.append(self) # prevents gc + imscroll._instances.append(self) # prevents gc @classmethod def clear(cls, self): @@ -162,13 +157,9 @@ def _on_click(self, event): np.vstack((x, y, np.ones_like(x) * i)), order=self.order, mode="nearest", - ) - for i in range(event.inaxes.images[0].get_array().shape[-1]) - ] + ) for i in range(event.inaxes.images[0].get_array().shape[-1])] else: - z = ndi.map_coordinates( - arr, np.vstack((x, y)), order=self.order, mode="nearest" - ) + z = ndi.map_coordinates(arr, np.vstack((x, y)), order=self.order, mode="nearest") self.picked = [] self.key["control"] = False diff --git a/miutil/web.py b/miutil/web.py index 02d1519..0350119 100644 --- a/miutil/web.py +++ b/miutil/web.py @@ -4,11 +4,11 @@ try: from urllib.request import urlopen -except ImportError: # py27 +except ImportError: # py27 from urllib import urlopen try: from urllib.parse import urlparse -except ImportError: # py27 +except ImportError: # py27 from urlparse import urlparse import requests @@ -54,12 +54,12 @@ def get_file(fname, origin, cache_dir=None, chunk_size=None): try: d = requests.get(origin, stream=True) with tqdm( - total=float(d.headers.get("Content-length") or 0), - desc=fname, - unit="B", - unit_scale=True, - unit_divisor=1024, - leave=False, + total=float(d.headers.get("Content-length") or 0), + desc=fname, + unit="B", + unit_scale=True, + unit_divisor=1024, + leave=False, ) as fprog: with open(fpath, "wb") as fo: for chunk in d.iter_content(chunk_size=chunk_size): diff --git a/pyproject.toml b/pyproject.toml index e71191f..4d84990 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,3 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "miutil/_dist_ver.py" write_to_template = "__version__ = '{version}'\n" - -[tool.black] -target_version = ["py27", "py36", "py38"] diff --git a/setup.cfg b/setup.cfg index 2e6d940..3a0da3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,6 @@ dev= pytest-cov pytest-timeout pytest-xdist - codecov nii=nibabel; numpy plot=matplotlib; numpy; scipy cuda=argopt; pynvml @@ -69,12 +68,24 @@ exclude=tests universal=1 [flake8] -max_line_length=88 -extend-ignore=E203,P1 +max_line_length=99 +extend-ignore=E228,E261,P1 exclude=.git,__pycache__,build,dist,.eggs +[yapf] +spaces_before_comment=15, 20 +arithmetic_precedence_indication=true +allow_split_before_dict_value=false +coalesce_brackets=True +column_limit=99 +each_dict_entry_on_separate_line=False +space_between_ending_comma_and_closing_bracket=False +split_before_named_assigns=False +split_before_closing_bracket=False + [isort] profile=black +line_length=99 known_first_party=miutil,tests [tool:pytest] diff --git a/tests/test_fdio.py b/tests/test_fdio.py index 4d929b8..3108e1b 100644 --- a/tests/test_fdio.py +++ b/tests/test_fdio.py @@ -25,20 +25,11 @@ def test_create_dir(tmp_path, caplog): def test_hasext(): - for fname, ext in [ - (".baz", ".baz"), - ("foo.bar", ".bar"), - ("foo.bar", "bar"), - ("foo.bar.baz", "bar.baz"), - ("foo/bar.baz", "baz"), - ("foo.bar.baz", "baz"), - ]: + for fname, ext in [(".baz", ".baz"), ("foo.bar", ".bar"), ("foo.bar", "bar"), + ("foo.bar.baz", "bar.baz"), ("foo/bar.baz", "baz"), ("foo.bar.baz", "baz")]: assert fdio.hasext(fname, ext) - for fname, ext in [ - ("foo.bar", "baz"), - ("foo", "foo"), - ]: + for fname, ext in [("foo.bar", "baz"), ("foo", "foo")]: assert not fdio.hasext(fname, ext) @@ -58,10 +49,7 @@ def test_extractall(tmp_path): fdio.extractall(fd, tmpdir) assert (tmpdir / "miutil-0.6.0" / "README.rst").is_file() - assert ( - "Medical imaging utilities." - in (tmpdir / "miutil-0.6.0" / "README.rst").read_text() - ) + assert ("Medical imaging utilities." in (tmpdir / "miutil-0.6.0" / "README.rst").read_text()) def test_nsort():