diff --git a/.flake8 b/.flake8 deleted file mode 100644 index cb2ed23..0000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -exclude = - .eggs, - build, -extend-ignore = - # math, https://github.com/PyCQA/pycodestyle/issues/513 - W503, -per-file-ignores = - # ignore unused imports - __init__.py: F401 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2f8baef..1f5613e 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.8 + - name: Set up Python 3.10 uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.10' - name: Install pre-commit hooks run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92a17ee..119cb13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,13 +9,15 @@ # # default_language_version: - python: python3.8 + python: python3.10 repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.276 + rev: v0.1.8 hooks: - id: ruff + args: [ --fix ] + - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.2.4 hooks: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a7c161b..7debd19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,8 +17,8 @@ Version 1.21.0 (2023-12-06) ``audeer.mkdir()``, ``audeer.rmdir()``, and ``audeer.touch()``. - Instead of writing ``audeer.mkdir(os.path.join('a', 'b'))``, - you can now write ``audeer.mkdir('a', 'b')`` + Instead of writing ``audeer.mkdir(os.path.join("a", "b"))``, + you can now write ``audeer.mkdir("a", "b")`` Version 1.20.2 (2023-11-28) @@ -90,7 +90,7 @@ Version 1.20.0 (2023-05-02) * Fixed: ``audeer.replace_file_extension()`` now returns the original filename when an empty new file extension is provided - instead of adding ``'.'`` at the end of the filename + instead of adding ``"."`` at the end of the filename * Fixed: ``audeer.extract_archive()`` and ``audeer.extract_archives()`` now return normalized relative paths diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 3f78d53..b9c30fe 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -41,7 +41,7 @@ Coding Convention ----------------- We follow the PEP8_ convention for Python code -and check for correct syntax with ruff_. +and use ruff_ as a linter and code formatter. In addition, we check for common spelling errors with codespell_. Both tools and possible exceptions @@ -61,7 +61,8 @@ You can also install ruff_ and codespell_ and call it directly:: pip install ruff codespell # consider system wide installation - ruff check . + ruff check --fix . # lint all Python files, and fix any fixable errors + ruff format . # format code of all Python files codespell It can be restricted to specific folders:: diff --git a/README.rst b/README.rst index 123e713..0a8564f 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ that lists all WAV files in the ``data`` folder: import audeer - files = audeer.list_file_names('data', filetype='wav') + files = audeer.list_file_names("data", filetype="wav") .. _tqdm: https://tqdm.github.io/ diff --git a/audeer/__init__.py b/audeer/__init__.py index 606cd3f..f56d381 100644 --- a/audeer/__init__.py +++ b/audeer/__init__.py @@ -45,6 +45,7 @@ # Dynamically get the version of the installed module try: import importlib.metadata + __version__ = importlib.metadata.version(__name__) except Exception: # pragma: no cover importlib = None # pragma: no cover diff --git a/audeer/core/config.py b/audeer/core/config.py index 44f895c..bc1548e 100644 --- a/audeer/core/config.py +++ b/audeer/core/config.py @@ -5,6 +5,7 @@ class config: for the progress bar:: import audeer + audeer.config.TQDM_COLUMNS = 50 """ @@ -13,8 +14,8 @@ class config: """Length of progress bar description.""" TQDM_FORMAT = ( - '{percentage:3.0f}%|{bar} [{elapsed}<{remaining}] ' - '{desc:' + str(TQDM_DESCLEN) + '}' + "{percentage:3.0f}%|{bar} [{elapsed}<{remaining}] " + "{desc:" + str(TQDM_DESCLEN) + "}" ) """Format of progress bars.""" diff --git a/audeer/core/conftest.py b/audeer/core/conftest.py index bb941eb..72378da 100644 --- a/audeer/core/conftest.py +++ b/audeer/core/conftest.py @@ -6,8 +6,8 @@ @pytest.fixture(autouse=True) def docdir(doctest_namespace, request): # make sure audeer is imported - doctest_namespace['audeer'] = audeer + doctest_namespace["audeer"] = audeer # set temporal working directory in docstring examples - tmpdir = request.getfixturevalue('tmpdir') + tmpdir = request.getfixturevalue("tmpdir") with tmpdir.as_cwd(): yield diff --git a/audeer/core/io.py b/audeer/core/io.py index 8bf9433..b4a7efa 100644 --- a/audeer/core/io.py +++ b/audeer/core/io.py @@ -20,18 +20,18 @@ # on Windows and MacOS # (which adds /System/Volumes/Data in front in the Github runner) # as it outputs a path in Linux syntax in the example -if platform.system() in ['Darwin', 'Windows']: # pragma: no cover +if platform.system() in ["Darwin", "Windows"]: # pragma: no cover __doctest_skip__ = [ - 'common_directory', - 'list_dir_names', - 'list_file_names', + "common_directory", + "list_dir_names", + "list_file_names", ] def basename_wo_ext( - path: typing.Union[str, bytes], - *, - ext: str = None + path: typing.Union[str, bytes], + *, + ext: str = None, ) -> str: """File basename without file extension. @@ -43,7 +43,7 @@ def basename_wo_ext( basename of directory or file without extension Examples: - >>> path = '/test/file.wav' + >>> path = "/test/file.wav" >>> basename_wo_ext(path) 'file' @@ -51,10 +51,10 @@ def basename_wo_ext( path = safe_path(path) path = os.path.basename(path) if ext is not None: - if not ext.startswith('.'): - ext = '.' + ext # 'mp3' => '.mp3' + if not ext.startswith("."): + ext = "." + ext # 'mp3' => '.mp3' if path.endswith(ext): - path = path[:-len(ext)] + path = path[: -len(ext)] else: path = os.path.splitext(path)[0] return path @@ -76,30 +76,35 @@ def common_directory( Examples: >>> paths = [ - ... '/home/user1/tmp/coverage/test', - ... '/home/user1/tmp/covert/operator', - ... '/home/user1/tmp/coven/members', + ... "/home/user1/tmp/coverage/test", + ... "/home/user1/tmp/covert/operator", + ... "/home/user1/tmp/coven/members", ... ] >>> common_directory(paths) '/home/user1/tmp' """ + def all_names_equal(name): return all(n == name[0] for n in name[1:]) dirs = [safe_path(path) for path in dirs] by_directory_levels = zip(*[p.split(sep) for p in dirs]) - return sep.join(x[0] for x in itertools.takewhile( - all_names_equal, by_directory_levels, - )) + return sep.join( + x[0] + for x in itertools.takewhile( + all_names_equal, + by_directory_levels, + ) + ) def create_archive( - root: str, - files: typing.Optional[typing.Union[str, typing.Sequence[str]]], - archive: str, - *, - verbose: bool = False, + root: str, + files: typing.Optional[typing.Union[str, typing.Sequence[str]]], + archive: str, + *, + verbose: bool = False, ): r"""Create ZIP or TAR.GZ archive. @@ -135,13 +140,13 @@ def create_archive( or a file in ``files`` is not below ``root`` Examples: - >>> _ = touch('a.txt') - >>> _ = touch('b.txt') - >>> create_archive('.', None, 'archive.zip') - >>> extract_archive('archive.zip', '.') + >>> _ = touch("a.txt") + >>> _ = touch("b.txt") + >>> create_archive(".", None, "archive.zip") + >>> extract_archive("archive.zip", ".") ['a.txt', 'b.txt'] - >>> create_archive('.', ['a.txt'], 'archive.tar.gz') - >>> extract_archive('archive.tar.gz', '.') + >>> create_archive(".", ["a.txt"], "archive.tar.gz") + >>> extract_archive("archive.tar.gz", ".") ['a.txt'] """ @@ -164,7 +169,6 @@ def create_archive( ) if files is None: - files = list_file_names( root, basenames=True, @@ -173,12 +177,10 @@ def create_archive( ) else: - files_org = to_list(files) files = [] for file in files_org: - # convert to absolute path if not os.path.isabs(file): path = safe_path(root, file) @@ -204,38 +206,37 @@ def create_archive( ) # convert to relative path - files.append(path[len(root) + 1:]) + files.append(path[len(root) + 1 :]) # Progress bar arguments desc = format_display_message( - f'Create {os.path.basename(archive)}', + f"Create {os.path.basename(archive)}", pbar=True, ) disable = not verbose - if archive.endswith('zip'): - with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as zf: + if archive.endswith("zip"): + with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: for file in progress_bar(files, desc=desc, disable=disable): full_file = safe_path(root, file) zf.write(full_file, arcname=file) - elif archive.endswith('tar.gz'): - with tarfile.open(archive, 'w:gz') as tf: + elif archive.endswith("tar.gz"): + with tarfile.open(archive, "w:gz") as tf: for file in progress_bar(files, desc=desc, disable=disable): full_file = safe_path(root, file) tf.add(full_file, file) else: raise RuntimeError( - f'You can only create a ZIP or TAR.GZ archive, ' - f'not {archive}' + f"You can only create a ZIP or TAR.GZ archive, " f"not {archive}" ) def download_url( - url: str, - destination: str, - *, - force_download: bool = False, - verbose: bool = False, + url: str, + destination: str, + *, + force_download: bool = False, + verbose: bool = False, ) -> str: r"""Download URL to destination. @@ -250,7 +251,7 @@ def download_url( path of locally stored file Examples: - >>> dst = download_url('https://audeering.github.io/audeer/_static/favicon.png', '.') + >>> dst = download_url("https://audeering.github.io/audeer/_static/favicon.png", ".") >>> os.path.basename(dst) 'favicon.png' @@ -262,8 +263,8 @@ def download_url( return destination with progress_bar( - disable=not verbose, - desc=format_display_message(f'Downloading {url}', pbar=True), + disable=not verbose, + desc=format_display_message(f"Downloading {url}", pbar=True), ) as pbar: def bar_update(block_num, block_size, total_size): @@ -277,11 +278,11 @@ def bar_update(block_num, block_size, total_size): def extract_archive( - archive: str, - destination: str, - *, - keep_archive: bool = True, - verbose: bool = False, + archive: str, + destination: str, + *, + keep_archive: bool = True, + verbose: bool = False, ) -> typing.List[str]: r"""Extract ZIP or TAR.GZ file. @@ -305,11 +306,11 @@ def extract_archive( RuntimeError: if ``archive`` is malformed Examples: - >>> _ = touch('a.txt') - >>> create_archive('.', None, 'archive.zip') - >>> extract_archive('archive.zip', '.') + >>> _ = touch("a.txt") + >>> create_archive(".", None, "archive.zip") + >>> extract_archive("archive.zip", ".") ['a.txt'] - >>> extract_archive('archive.zip', 'sub') + >>> extract_archive("archive.zip", "sub") ['a.txt'] """ @@ -345,14 +346,14 @@ def extract_archive( # Progress bar arguments desc = format_display_message( - f'Extract {os.path.basename(archive)}', + f"Extract {os.path.basename(archive)}", pbar=True, ) disable = not verbose try: - if archive.endswith('zip'): - with zipfile.ZipFile(archive, 'r') as zf: + if archive.endswith("zip"): + with zipfile.ZipFile(archive, "r") as zf: members = zf.infolist() for member in progress_bar( members, @@ -361,8 +362,8 @@ def extract_archive( ): zf.extract(member, destination) files = [m.filename for m in members] - elif archive.endswith('tar.gz'): - with tarfile.open(archive, 'r') as tf: + elif archive.endswith("tar.gz"): + with tarfile.open(archive, "r") as tf: members = tf.getmembers() for member in progress_bar( members, @@ -373,11 +374,10 @@ def extract_archive( files = [m.name for m in members] else: raise RuntimeError( - f'You can only extract ZIP and TAR.GZ files, ' - f'not {archive}' + f"You can only extract ZIP and TAR.GZ files, " f"not {archive}" ) except (EOFError, zipfile.BadZipFile, tarfile.ReadError): - raise RuntimeError(f'Broken archive: {archive}') + raise RuntimeError(f"Broken archive: {archive}") except (KeyboardInterrupt, Exception): # pragma: no cover # Clean up broken extraction files if destination_created: @@ -388,19 +388,19 @@ def extract_archive( if not keep_archive: os.remove(archive) - if os.name == 'nt': # pragma: no cover + if os.name == "nt": # pragma: no cover # replace '/' with '\' on Windows - files = [file.replace('/', os.path.sep) for file in files] + files = [file.replace("/", os.path.sep) for file in files] return files def extract_archives( - archives: typing.Sequence[str], - destination: str, - *, - keep_archive: bool = True, - verbose: bool = False, + archives: typing.Sequence[str], + destination: str, + *, + keep_archive: bool = True, + verbose: bool = False, ) -> typing.List[str]: r"""Extract multiple ZIP or TAR.GZ archives at once. @@ -424,11 +424,11 @@ def extract_archives( RuntimeError: if an archive file is malformed Examples: - >>> _ = touch('a.txt') - >>> create_archive('.', ['a.txt'], 'archive.zip') - >>> _ = touch('b.txt') - >>> create_archive('.', ['b.txt'], 'archive.tar.gz') - >>> extract_archives(['archive.zip', 'archive.tar.gz'], '.') + >>> _ = touch("a.txt") + >>> create_archive(".", ["a.txt"], "archive.zip") + >>> _ = touch("b.txt") + >>> create_archive(".", ["b.txt"], "archive.tar.gz") + >>> extract_archives(["archive.zip", "archive.tar.gz"], ".") ['a.txt', 'b.txt'] """ @@ -439,7 +439,7 @@ def extract_archives( member_names = [] for archive in archives: desc = format_display_message( - f'Extract {os.path.basename(archive)}', + f"Extract {os.path.basename(archive)}", pbar=True, ) pbar.set_description_str(desc) @@ -456,7 +456,7 @@ def extract_archives( def file_extension( - path: typing.Union[str, bytes] + path: typing.Union[str, bytes], ) -> str: """File extension. @@ -467,7 +467,7 @@ def file_extension( extension of file without "." Examples: - >>> path = '/test/file.wav' + >>> path = "/test/file.wav" >>> file_extension(path) 'wav' @@ -477,11 +477,11 @@ def file_extension( def list_dir_names( - path: typing.Union[str, bytes], - *, - basenames: bool = False, - recursive: bool = False, - hidden: bool = True, + path: typing.Union[str, bytes], + *, + basenames: bool = False, + recursive: bool = False, + hidden: bool = True, ) -> typing.List[str]: """List of folder names located inside provided path. @@ -499,20 +499,20 @@ def list_dir_names( FileNotFoundError: if path does not exists Examples: - >>> _ = mkdir('path', 'a', '.b', 'c') + >>> _ = mkdir("path", "a", ".b", "c") >>> list_dir_names( - ... 'path', + ... "path", ... basenames=True, ... ) ['a'] >>> list_dir_names( - ... 'path', + ... "path", ... basenames=True, ... recursive=True, ... ) ['a', 'a/.b', 'a/.b/c'] >>> list_dir_names( - ... 'path', + ... "path", ... basenames=True, ... recursive=True, ... hidden=False, @@ -526,7 +526,7 @@ def helper(p: str, paths: typing.List[str]): ps = [os.path.join(p, x) for x in os.listdir(p)] ps = [x for x in ps if os.path.isdir(x)] if not hidden: - ps = [x for x in ps if not os.path.basename(x).startswith('.')] + ps = [x for x in ps if not os.path.basename(x).startswith(".")] paths.extend(ps) if len(ps) > 0 and recursive: for p in ps: @@ -535,18 +535,18 @@ def helper(p: str, paths: typing.List[str]): paths = [] helper(path, paths) if basenames: - paths = [p[len(path) + 1:] for p in paths] + paths = [p[len(path) + 1 :] for p in paths] return sorted(paths) def list_file_names( - path: typing.Union[str, bytes], - *, - filetype: str = '', - basenames: bool = False, - recursive: bool = False, - hidden: bool = False, + path: typing.Union[str, bytes], + *, + filetype: str = "", + basenames: bool = False, + recursive: bool = False, + hidden: bool = False, ) -> typing.List[str]: """List of file names inferred from provided path. @@ -575,13 +575,13 @@ def list_file_names( and ``os.dirname(path)`` does not exist Examples: - >>> dir_path = mkdir('path') - >>> _ = touch(dir_path, 'file.wav') - >>> _ = touch(dir_path, 'File.wav') - >>> _ = touch(dir_path, '.lock') - >>> sub_dir_path = mkdir('path', 'sub') - >>> _ = touch(sub_dir_path, 'file.ogg') - >>> _ = touch(sub_dir_path, '.lock') + >>> dir_path = mkdir("path") + >>> _ = touch(dir_path, "file.wav") + >>> _ = touch(dir_path, "File.wav") + >>> _ = touch(dir_path, ".lock") + >>> sub_dir_path = mkdir("path", "sub") + >>> _ = touch(sub_dir_path, "file.ogg") + >>> _ = touch(sub_dir_path, ".lock") >>> list_file_names( ... dir_path, ... basenames=True, @@ -607,29 +607,29 @@ def list_file_names( ... ) ['.lock', 'File.wav', 'file.wav', 'sub/.lock', 'sub/file.ogg'] >>> list_file_names( - ... os.path.join(dir_path, 'f*'), + ... os.path.join(dir_path, "f*"), ... basenames=True, ... recursive=True, ... hidden=True, ... ) ['file.wav', 'sub/file.ogg'] >>> list_file_names( - ... os.path.join(dir_path, '[fF]*'), + ... os.path.join(dir_path, "[fF]*"), ... basenames=True, ... recursive=True, ... hidden=True, ... ) ['File.wav', 'file.wav', 'sub/file.ogg'] >>> list_file_names( - ... os.path.join(dir_path, '[!f]*'), + ... os.path.join(dir_path, "[!f]*"), ... basenames=True, ... recursive=True, ... hidden=True, ... ) ['.lock', 'File.wav', 'sub/.lock'] >>> list_file_names( - ... os.path.join(dir_path, 'f*'), - ... filetype='ogg', + ... os.path.join(dir_path, "f*"), + ... filetype="ogg", ... basenames=True, ... recursive=True, ... hidden=True, @@ -640,20 +640,17 @@ def list_file_names( path = safe_path(path) if os.path.isdir(path): - pattern = None folder = path elif os.path.exists(path) and not recursive: - - if not hidden and os.path.basename(path).startswith('.'): + if not hidden and os.path.basename(path).startswith("."): return [] if basenames: path = os.path.basename(path) return [path] else: - pattern = os.path.basename(path) folder = os.path.dirname(path) @@ -666,13 +663,15 @@ def helper(p: str, paths: typing.List[str]): files = [x for x in ps if os.path.isfile(x)] if pattern: files = [ - file for file in files - if fnmatch.fnmatch(os.path.basename(file), f'{pattern}') + file + for file in files + if fnmatch.fnmatch(os.path.basename(file), f"{pattern}") ] if filetype: files = [ - file for file in files - if fnmatch.fnmatch(os.path.basename(file), f'*{filetype}') + file + for file in files + if fnmatch.fnmatch(os.path.basename(file), f"*{filetype}") ] paths.extend(files) if len(folders) > 0 and recursive: @@ -683,39 +682,28 @@ def helper(p: str, paths: typing.List[str]): helper(folder, paths) def is_pattern(pattern): - return ( - '*' in pattern or - '?' in pattern or - ('[' in pattern and ']' in pattern) - ) + return "*" in pattern or "?" in pattern or ("[" in pattern and "]" in pattern) # if we have no match, # raise an error unless # 1. path is a folder (i.e. pattern is None) # 2. or we have a valid pattern - if ( - len(paths) == 0 - and pattern is not None - and not is_pattern(pattern) - ): + if len(paths) == 0 and pattern is not None and not is_pattern(pattern): raise NotADirectoryError(path) if not hidden: - paths = [ - p for p in paths - if not os.path.basename(p).startswith('.') - ] + paths = [p for p in paths if not os.path.basename(p).startswith(".")] if basenames: - paths = [p[len(folder) + 1:] for p in paths] + paths = [p[len(folder) + 1 :] for p in paths] return sorted(paths) def mkdir( - path: typing.Union[str, bytes], - *paths: typing.Sequence[typing.Union[str, bytes]], - mode: int = 0o777 + path: typing.Union[str, bytes], + *paths: typing.Sequence[typing.Union[str, bytes]], + mode: int = 0o777, ) -> str: """Create directory. @@ -747,7 +735,7 @@ def mkdir( absolute path to the created directory Examples: - >>> p = mkdir('path1', 'path2', 'path3') + >>> p = mkdir("path1", "path2", "path3") >>> os.path.basename(p) 'path3' @@ -759,8 +747,8 @@ def mkdir( def md5( - path: str, - chunk_size: int = 8192, + path: str, + chunk_size: int = 8192, ) -> str: r"""Calculate MD5 checksum of file or folder. @@ -788,10 +776,10 @@ def md5( FileNotFoundError: if ``path`` does not exist Examples: - >>> path = touch('file.txt') + >>> path = touch("file.txt") >>> md5(path) 'd41d8cd98f00b204e9800998ecf8427e' - >>> md5('.') + >>> md5(".") '3d8e577bddb17db339eae0b3d9bcf180' """ @@ -806,13 +794,11 @@ def md5( ) if not os.path.isdir(path): - - with open(path, 'rb') as fp: + with open(path, "rb") as fp: for chunk in md5_read_chunk(fp, chunk_size): hasher.update(chunk) else: - files = list_file_names( path, recursive=True, @@ -823,8 +809,8 @@ def md5( for file in files: # encode file name that renaming of files # produces different checksum - hasher.update(file.replace(os.path.sep, '/').encode()) - with open(safe_path(path, file), 'rb') as fp: + hasher.update(file.replace(os.path.sep, "/").encode()) + with open(safe_path(path, file), "rb") as fp: for chunk in md5_read_chunk(fp, chunk_size): hasher.update(chunk) @@ -832,8 +818,8 @@ def md5( def md5_read_chunk( - fp: typing.IO, - chunk_size: int = 8192, + fp: typing.IO, + chunk_size: int = 8192, ): while True: data = fp.read(chunk_size) @@ -843,8 +829,8 @@ def md5_read_chunk( def move( - src_path, - dst_path, + src_path, + dst_path, ): """Move a file or folder independent of operating system. @@ -873,9 +859,9 @@ def move( (raised only under Windows) Examples: - >>> path = mkdir('folder') - >>> src_path = touch(path, 'file1') - >>> dst_path = os.path.join(path, 'file2') + >>> path = mkdir("folder") + >>> src_path = touch(path, "file1") + >>> dst_path = os.path.join(path, "file2") >>> move(src_path, dst_path) >>> list_file_names(path, basenames=True) ['file2'] @@ -885,8 +871,8 @@ def move( def move_file( - src_path, - dst_path, + src_path, + dst_path, ): """Move a file independent of operating system. @@ -905,9 +891,9 @@ def move_file( dst_path: destination file path Examples: - >>> path = mkdir('folder') - >>> src_path = touch(path, 'file1') - >>> dst_path = os.path.join(path, 'file2') + >>> path = mkdir("folder") + >>> src_path = touch(path, "file1") + >>> dst_path = os.path.join(path, "file2") >>> move_file(src_path, dst_path) >>> list_file_names(path, basenames=True) ['file2'] @@ -917,10 +903,10 @@ def move_file( def replace_file_extension( - path: typing.Union[str, bytes], - new_extension: str, - *, - ext: str = None, + path: typing.Union[str, bytes], + new_extension: str, + *, + ext: str = None, ) -> str: """Replace file extension. @@ -942,15 +928,15 @@ def replace_file_extension( path to file with a possibly new extension Examples: - >>> replace_file_extension('file.txt', 'rst') + >>> replace_file_extension("file.txt", "rst") 'file.rst' - >>> replace_file_extension('file', 'rst') + >>> replace_file_extension("file", "rst") 'file.rst' - >>> replace_file_extension('file.txt', '') + >>> replace_file_extension("file.txt", "") 'file' - >>> replace_file_extension('file.tar.gz', 'zip', ext='tar.gz') + >>> replace_file_extension("file.tar.gz", "zip", ext="tar.gz") 'file.zip' - >>> replace_file_extension('file.zip', 'rst', ext='txt') + >>> replace_file_extension("file.zip", "rst", ext="txt") 'file.zip' """ @@ -958,12 +944,12 @@ def replace_file_extension( ext = file_extension(path) # '.mp3' => 'mp3' - if ext.startswith('.'): + if ext.startswith("."): ext = ext[1:] - if new_extension.startswith('.'): + if new_extension.startswith("."): new_extension = new_extension[1:] - if ext and not path.endswith(f'.{ext}'): + if ext and not path.endswith(f".{ext}"): return path if not path: @@ -972,18 +958,18 @@ def replace_file_extension( if not ext and not new_extension: pass elif not ext: - path = f'{path}.{new_extension}' + path = f"{path}.{new_extension}" elif not new_extension: - path = path[:-len(ext) - 1] + path = path[: -len(ext) - 1] else: - path = f'{path[:-len(ext)]}{new_extension}' + path = f"{path[:-len(ext)]}{new_extension}" return path def rmdir( - path: typing.Union[str, bytes], - *paths: typing.Sequence[typing.Union[str, bytes]], + path: typing.Union[str, bytes], + *paths: typing.Sequence[typing.Union[str, bytes]], ): """Remove directory. @@ -1001,11 +987,11 @@ def rmdir( NotADirectoryError: if path is not a directory Examples: - >>> _ = mkdir('path1', 'path2', 'path3') - >>> list_dir_names('path1', basenames=True) + >>> _ = mkdir("path1", "path2", "path3") + >>> list_dir_names("path1", basenames=True) ['path2'] - >>> rmdir('path1', 'path2') - >>> list_dir_names('path1') + >>> rmdir("path1", "path2") + >>> list_dir_names("path1") [] """ @@ -1015,8 +1001,8 @@ def rmdir( def touch( - path: typing.Union[str, bytes], - *paths: typing.Sequence[typing.Union[str, bytes]], + path: typing.Union[str, bytes], + *paths: typing.Sequence[typing.Union[str, bytes]], ) -> str: """Create an empty file. @@ -1034,7 +1020,7 @@ def touch( expanded path to file Examples: - >>> path = touch('file.txt') + >>> path = touch("file.txt") >>> os.path.basename(path) 'file.txt' @@ -1043,5 +1029,5 @@ def touch( if os.path.exists(path): os.utime(path, None) else: - open(path, 'a').close() + open(path, "a").close() return path diff --git a/audeer/core/path.py b/audeer/core/path.py index 90f7ea1..04056d9 100644 --- a/audeer/core/path.py +++ b/audeer/core/path.py @@ -7,17 +7,17 @@ # on Windows and MacOS # (which adds /System/Volumes/Data in front in the Github runner) # as it outputs a path in Linux syntax in the example -if platform.system() in ['Darwin', 'Windows']: # pragma: no cover +if platform.system() in ["Darwin", "Windows"]: # pragma: no cover __doctest_skip__ = [ - 'path', - 'safe_path', + "path", + "safe_path", ] def path( - path: typing.Union[str, bytes], - *paths: typing.Sequence[typing.Union[str, bytes]], - follow_symlink: bool = True, + path: typing.Union[str, bytes], + *paths: typing.Sequence[typing.Union[str, bytes]], + follow_symlink: bool = True, ) -> str: """Expand and normalize to absolute path. @@ -45,15 +45,15 @@ def path( (joined and) expanded path Examples: - >>> home = path('~') - >>> folder = path('~/path/.././path') - >>> folder[len(home) + 1:] + >>> home = path("~") + >>> folder = path("~/path/.././path") + >>> folder[len(home) + 1 :] 'path' - >>> file = path('~/path/.././path', './file.txt') - >>> file[len(home) + 1:] + >>> file = path("~/path/.././path", "./file.txt") + >>> file[len(home) + 1 :] 'path/file.txt' - >>> file = audeer.touch('file.txt') - >>> link = path('link.txt') + >>> file = audeer.touch("file.txt") + >>> link = path("link.txt") >>> os.symlink(file, link) >>> file = path(link) >>> os.path.basename(file) @@ -72,8 +72,8 @@ def path( else: path = os.path.abspath(path) # Convert bytes to str, see https://stackoverflow.com/a/606199 - if type(path) == bytes: - path = path.decode('utf-8').strip('\x00') + if isinstance(path, bytes): + path = path.decode("utf-8").strip("\x00") return path @@ -83,9 +83,9 @@ def path( def safe_path( - path: typing.Union[str, bytes], - *paths: typing.Sequence[typing.Union[str, bytes]], - follow_symlink: bool = True, + path: typing.Union[str, bytes], + *paths: typing.Sequence[typing.Union[str, bytes]], + follow_symlink: bool = True, ) -> str: """Expand and normalize to absolute path. @@ -117,15 +117,15 @@ def safe_path( (joined and) expanded path Examples: - >>> home = safe_path('~') - >>> folder = safe_path('~/path/.././path') - >>> folder[len(home) + 1:] + >>> home = safe_path("~") + >>> folder = safe_path("~/path/.././path") + >>> folder[len(home) + 1 :] 'path' - >>> file = safe_path('~/path/.././path', './file.txt') - >>> file[len(home) + 1:] + >>> file = safe_path("~/path/.././path", "./file.txt") + >>> file[len(home) + 1 :] 'path/file.txt' - >>> file = audeer.touch('file.txt') - >>> link = path('link.txt') + >>> file = audeer.touch("file.txt") + >>> link = path("link.txt") >>> os.symlink(file, link) >>> file = path(link) >>> os.path.basename(file) diff --git a/audeer/core/tqdm.py b/audeer/core/tqdm.py index 40cacad..dd3eff0 100644 --- a/audeer/core/tqdm.py +++ b/audeer/core/tqdm.py @@ -5,10 +5,7 @@ from audeer.core.config import config -def format_display_message( - text: str, - pbar: bool = False -) -> str: +def format_display_message(text: str, pbar: bool = False) -> str: """Ensure a fixed length of text printed to screen. The length of the text message is the same as the overall @@ -25,7 +22,7 @@ def format_display_message( Examples: >>> config.TQDM_COLUMNS = 20 - >>> format_display_message('Long text that will be shorten to fit') + >>> format_display_message("Long text that will be shorten to fit") 'Long te...n to fit' """ @@ -41,15 +38,15 @@ def format_display_message( return text.ljust(n) else: m = (n - 3) // 2 - return f'{text[:m]}...{text[len(text) - (n - m - 3):]}' + return f"{text[:m]}...{text[len(text) - (n - m - 3):]}" def progress_bar( - iterable: Sequence = None, - *, - total: int = None, - desc: str = None, - disable: bool = False + iterable: Sequence = None, + *, + total: int = None, + desc: str = None, + disable: bool = False, ) -> tqdm: r"""Progress bar with optional text on the right. @@ -59,7 +56,7 @@ def progress_bar( .. code-block:: python - for file in progress_bar(files, desc='Copying'): + for file in progress_bar(files, desc="Copying"): copy(file) When the text should be updated as well, @@ -70,7 +67,7 @@ def progress_bar( with progress_bar(files) as pbar: for file in pbar: desc = format_display_message( - f'Copying {file}', + f"Copying {file}", pbar=True, ) pbar.set_description_str(desc) @@ -89,7 +86,7 @@ def progress_bar( """ if desc is None: - desc = '' + desc = "" return tqdm( iterable=iterable, ncols=config.TQDM_COLUMNS, diff --git a/audeer/core/utils.py b/audeer/core/utils.py index 008db67..e4cabb4 100644 --- a/audeer/core/utils.py +++ b/audeer/core/utils.py @@ -20,13 +20,13 @@ from audeer.core.version import LooseVersion -__doctest_skip__ = ['git_repo_tags', 'git_repo_version'] +__doctest_skip__ = ["git_repo_tags", "git_repo_version"] def deprecated( - *, - removal_version: str, - alternative: str = None + *, + removal_version: str, + alternative: str = None, ) -> typing.Callable: """Mark code as deprecated. @@ -46,11 +46,12 @@ def deprecated( alternative: alternative code to use Examples: - >>> @deprecated(removal_version='2.0.0') + >>> @deprecated(removal_version="2.0.0") ... def deprecated_function(): ... pass """ + def _deprecated(func): # functools.wraps preserves the name # and docstring of the decorated code: @@ -58,22 +59,24 @@ def _deprecated(func): @functools.wraps(func) def new_func(*args, **kwargs): message = ( - f'{func.__name__} is deprecated and will be removed ' - f'with version {removal_version}.' + f"{func.__name__} is deprecated and will be removed " + f"with version {removal_version}." ) if alternative is not None: - message += f' Use {alternative} instead.' + message += f" Use {alternative} instead." warnings.warn(message, category=UserWarning, stacklevel=2) return func(*args, **kwargs) + return new_func + return _deprecated def deprecated_default_value( - *, - argument: str, - change_in_version: str, - new_default_value: typing.Any, + *, + argument: str, + change_in_version: str, + new_default_value: typing.Any, ) -> typing.Callable: """Mark default value of keyword argument as deprecated. @@ -90,13 +93,14 @@ def deprecated_default_value( Examples: >>> @deprecated_default_value( - ... argument='foo', - ... change_in_version='2.0.0', - ... new_default_value='bar', + ... argument="foo", + ... change_in_version="2.0.0", + ... new_default_value="bar", ... ) - ... def deprecated_function(foo='foo'): + ... def deprecated_function(foo="foo"): ... pass """ + def _deprecated(func): # functools.wraps preserves the name # and docstring of the decorated code: @@ -109,21 +113,23 @@ def new_func(*args, **kwargs): message = ( f"The default of '{argument}' will change from " f"'{default_value}' to '{new_default_value}' " - f'with version {change_in_version}.' + f"with version {change_in_version}." ) warnings.warn(message, category=UserWarning, stacklevel=2) return func(*args, **kwargs) + return new_func + return _deprecated def deprecated_keyword_argument( - *, - deprecated_argument: str, - removal_version: str, - new_argument: str = None, - mapping: typing.Callable = None, - remove_from_kwargs: bool = True, + *, + deprecated_argument: str, + removal_version: str, + new_argument: str = None, + mapping: typing.Callable = None, + remove_from_kwargs: bool = True, ) -> typing.Callable: r"""Mark keyword argument as deprecated. @@ -148,14 +154,15 @@ def deprecated_keyword_argument( Examples: >>> @deprecated_keyword_argument( - ... deprecated_argument='foo', - ... new_argument='bar', - ... removal_version='2.0.0', + ... deprecated_argument="foo", + ... new_argument="bar", + ... removal_version="2.0.0", ... ) ... def function_with_new_argument(*, bar): ... pass """ + def _deprecated(func): # functools.wraps preserves the name # and docstring of the decorated code: @@ -183,13 +190,13 @@ def new_func(*args, **kwargs): stacklevel=2, ) return func(*args, **kwargs) + return new_func + return _deprecated -def flatten_list( - nested_list: typing.List -) -> typing.List: +def flatten_list(nested_list: typing.List) -> typing.List: """Flatten an arbitrarily nested list. Implemented without recursion to avoid stack overflows. @@ -208,6 +215,7 @@ def flatten_list( [1, 2, 3] """ + def _flat_generator(nested_list): while nested_list: sublist = nested_list.pop(0) @@ -215,6 +223,7 @@ def _flat_generator(nested_list): nested_list = sublist + nested_list else: yield sublist + nested_list = copy.deepcopy(nested_list) return list(_flat_generator(nested_list)) @@ -230,21 +239,21 @@ def freeze_requirements(outfile: str): RuntimeError: if running ``pip freeze`` returns an error """ - cmd = f'pip freeze > {outfile}' + cmd = f"pip freeze > {outfile}" with subprocess.Popen( - args=cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - shell=True, + args=cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + shell=True, ) as p: _, err = p.communicate() if bool(p.returncode): - raise RuntimeError(f'Freezing Python packages failed: {err}') + raise RuntimeError(f"Freezing Python packages failed: {err}") def git_repo_tags( - *, - v: bool = None, + *, + v: bool = None, ) -> typing.List: r"""Get a list of available git tags. @@ -267,23 +276,23 @@ def git_repo_tags( """ try: - git = ['git', 'tag'] + git = ["git", "tag"] tags = subprocess.check_output(git) - tags = tags.decode().strip().split('\n') + tags = tags.decode().strip().split("\n") except Exception: # pragma: no cover tags = [] if v is None: return tags if v: - tags = [f'v{t}' if not t.startswith('v') else t for t in tags] + tags = [f"v{t}" if not t.startswith("v") else t for t in tags] else: - tags = [t[1:] if t.startswith('v') else t for t in tags] + tags = [t[1:] if t.startswith("v") else t for t in tags] return tags def git_repo_version( - *, - v: bool = True, + *, + v: bool = True, ) -> str: r"""Get a version number from current git ref. @@ -305,23 +314,23 @@ def git_repo_version( """ try: - git = ['git', 'describe', '--tags', '--always'] + git = ["git", "describe", "--tags", "--always"] version = subprocess.check_output(git) version = version.decode().strip() except Exception: # pragma: no cover - version = '' - if version.startswith('v') and not v: # pragma: no cover (only local) + version = "" + if version.startswith("v") and not v: # pragma: no cover (only local) version = version[1:] - elif not version.startswith('v') and v: # pragma: no cover (only github) - version = f'v{version}' + elif not version.startswith("v") and v: # pragma: no cover (only github) + version = f"v{version}" return version def install_package( - name: str, - *, - version: str = None, - silent: bool = False, + name: str, + *, + version: str = None, + silent: bool = False, ): r"""Install a Python package with pip. @@ -357,16 +366,16 @@ def install_package( # check for operators, e.g. # >=1.0, >1.0, <=1.0, <1.0 if version is not None: - if version.startswith('>='): + if version.startswith(">="): op = operator.ge version = version[2:].strip() - elif version.startswith('>'): + elif version.startswith(">"): op = operator.gt version = version[1:].strip() - elif version.startswith('<='): + elif version.startswith("<="): op = operator.le version = version[2:].strip() - elif version.startswith('<'): + elif version.startswith("<"): op = operator.lt version = version[1:].strip() @@ -397,16 +406,16 @@ def install_package( if op == operator.eq: # since we do not support ==1.0 # we have to add it here - name = f'{name}=={version}' + name = f"{name}=={version}" else: - name = f'{name}{version}' + name = f"{name}{version}" subprocess.check_call( [ sys.executable, - '-m', - 'pip', - 'install', + "-m", + "pip", + "install", name, ], stdout=subprocess.DEVNULL if silent else None, @@ -437,15 +446,15 @@ def is_semantic_version(version: str) -> bool: ``True`` if version is a semantic version Examples: - >>> is_semantic_version('v1') + >>> is_semantic_version("v1") False - >>> is_semantic_version('1.2.3-r3') + >>> is_semantic_version("1.2.3-r3") True - >>> is_semantic_version('v0.7.2-9-g1572b37') + >>> is_semantic_version("v0.7.2-9-g1572b37") True """ - version_parts = version.split('.') + version_parts = version.split(".") if len(version_parts) < 3: return False @@ -459,15 +468,15 @@ def is_integer_convertable(x): x, y = version_parts[:2] # Ignore starting 'v' - if x.startswith('v'): + if x.startswith("v"): x = x[1:] - z = '.'.join(version_parts[2:]) + z = ".".join(version_parts[2:]) # For Z, '-' and '+' are also allowed as separators, # but you are not allowed to have an additional '.' before - z = z.split('-')[0] - z = z.split('+')[0] - if len(z.split('.')) > 1: + z = z.split("-")[0] + z = z.split("+")[0] + if len(z.split(".")) > 1: return False for v in (x, y, z): @@ -489,11 +498,11 @@ def is_uid(uid: str) -> bool: ``True`` if string is a unique identifier Examples: - >>> is_uid('626f68e6-d336-70b9-e753-ed9fad855840') + >>> is_uid("626f68e6-d336-70b9-e753-ed9fad855840") True - >>> is_uid('ad855840') + >>> is_uid("ad855840") True - >>> is_uid('not a unique identifier') + >>> is_uid("not a unique identifier") False """ @@ -505,10 +514,10 @@ def is_uid(uid: str) -> bool: return False if len(uid) == 8: - uid = f'00000000-0000-0000-0000-0000{uid}' + uid = f"00000000-0000-0000-0000-0000{uid}" for pos in [8, 13, 18, 23]: - if not uid[pos] == '-': + if not uid[pos] == "-": return False try: @@ -520,18 +529,18 @@ def is_uid(uid: str) -> bool: def run_tasks( - task_func: typing.Callable, - params: typing.Sequence[ - typing.Tuple[ - typing.Sequence[typing.Any], - typing.Dict[str, typing.Any], - ] - ], - *, - num_workers: int = 1, - multiprocessing: bool = False, - progress_bar: bool = False, - task_description: str = None + task_func: typing.Callable, + params: typing.Sequence[ + typing.Tuple[ + typing.Sequence[typing.Any], + typing.Dict[str, typing.Any], + ] + ], + *, + num_workers: int = 1, + multiprocessing: bool = False, + progress_bar: bool = False, + task_description: str = None, ) -> typing.Sequence[typing.Any]: r"""Run parallel tasks using multprocessing. @@ -555,7 +564,7 @@ def run_tasks( that will be displayed next to progress bar Examples: - >>> power = lambda x, n: x ** n + >>> power = lambda x, n: x**n >>> params = [([2, n], {}) for n in range(10)] >>> run_tasks(power, params, num_workers=3) [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] @@ -565,7 +574,6 @@ def run_tasks( results = [None] * num_tasks if num_workers == 1: # sequential - with tqdm.progress_bar( params, total=len(params), @@ -576,16 +584,15 @@ def run_tasks( results[index] = task_func(*param[0], **param[1]) else: # parallel - if multiprocessing: executor = concurrent.futures.ProcessPoolExecutor else: executor = concurrent.futures.ThreadPoolExecutor with executor(max_workers=num_workers) as pool: with tqdm.progress_bar( - total=len(params), - desc=task_description, - disable=not progress_bar, + total=len(params), + desc=task_description, + disable=not progress_bar, ) as pbar: futures = [] for param in params: @@ -599,14 +606,14 @@ def run_tasks( return results -@deprecated(removal_version='2.0.0', alternative='run_tasks') +@deprecated(removal_version="2.0.0", alternative="run_tasks") def run_worker_threads( - task_fun: typing.Callable, - params: typing.Sequence[typing.Any] = None, - *, - num_workers: int = None, - progress_bar: bool = False, - task_description: str = None + task_fun: typing.Callable, + params: typing.Sequence[typing.Any] = None, + *, + num_workers: int = None, + progress_bar: bool = False, + task_description: str = None, ) -> typing.Sequence[typing.Any]: # pragma: no cover r"""Run parallel tasks using worker threads. @@ -624,7 +631,7 @@ def run_worker_threads( that will be displayed next to progress bar Examples: - >>> power = lambda x, n: x ** n + >>> power = lambda x, n: x**n >>> params = [(2, n) for n in range(10)] >>> run_worker_threads(power, params, num_workers=3) [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] @@ -652,9 +659,9 @@ def run_worker_threads( # num_workers > 1 -> run parallel else: - # Create queue, possibly with a progress bar if progress_bar: + class QueueWithProgbar(queue.Queue): def __init__(self, num_tasks, maxsize=0): super().__init__(maxsize) @@ -666,6 +673,7 @@ def __init__(self, num_tasks, maxsize=0): def task_done(self): super().task_done() self.pbar.update(1) + q = QueueWithProgbar(num_tasks) else: q = queue.Queue() @@ -706,7 +714,7 @@ def _worker(): def sort_versions( - versions: typing.List[str], + versions: typing.List[str], ) -> typing.List: """Sort version numbers. @@ -725,10 +733,10 @@ def sort_versions( Examples: >>> vers = [ - ... '2.0.0', - ... '2.0.1', - ... 'v1.0.0', - ... 'v2.0.0-1-gdf29c4a', + ... "2.0.0", + ... "2.0.1", + ... "v1.0.0", + ... "v2.0.0-1-gdf29c4a", ... ] >>> sort_versions(vers) ['v1.0.0', '2.0.0', 'v2.0.0-1-gdf29c4a', '2.0.1'] @@ -744,7 +752,7 @@ def sort_versions( ) def sort_key(value): - if value.startswith('v'): + if value.startswith("v"): value = value[1:] return LooseVersion(value) @@ -765,7 +773,7 @@ def to_list(x: typing.Any): input as a list Examples: - >>> to_list('abc') + >>> to_list("abc") ['abc'] >>> to_list((1, 2, 3)) [1, 2, 3] @@ -778,9 +786,9 @@ def to_list(x: typing.Any): def uid( - *, - from_string: str = None, - short: bool = False, + *, + from_string: str = None, + short: bool = False, ) -> str: r"""Generate unique identifier. @@ -803,9 +811,9 @@ def uid( unique identifier Examples: - >>> uid(from_string='example_string') + >>> uid(from_string="example_string") '626f68e6-d336-70b9-e753-ed9fad855840' - >>> uid(from_string='example_string', short=True) + >>> uid(from_string="example_string", short=True) 'ad855840' """ @@ -813,9 +821,9 @@ def uid( uid = str(uuid.uuid1()) else: uid = hashlib.md5() - uid.update(from_string.encode('utf-8')) + uid.update(from_string.encode("utf-8")) uid = uid.hexdigest() - uid = f'{uid[0:8]}-{uid[8:12]}-{uid[12:16]}-{uid[16:20]}-{uid[20:]}' + uid = f"{uid[0:8]}-{uid[8:12]}-{uid[12:16]}-{uid[16:20]}-{uid[20:]}" if short: uid = uid[-8:] diff --git a/audeer/core/version.py b/audeer/core/version.py index 5cb2238..5980846 100644 --- a/audeer/core/version.py +++ b/audeer/core/version.py @@ -102,21 +102,22 @@ class StrictVersion(Version): the ``StrictVersion.version_re`` pattern Examples: - >>> v1 = StrictVersion('1.17.2a1') + >>> v1 = StrictVersion("1.17.2a1") >>> v1 StrictVersion ('1.17.2a1') >>> v1.version (1, 17, 2) >>> v1.prerelease ('a', 1) - >>> v2 = StrictVersion('1.17.2') + >>> v2 = StrictVersion("1.17.2") >>> v1 < v2 True """ - version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', - re.VERBOSE | re.ASCII) + version_re = re.compile( + r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", re.VERBOSE | re.ASCII + ) """Version regexp pattern. The regexp pattern is used to split the version @@ -153,8 +154,7 @@ def parse(self, version): if not match: raise ValueError(f"invalid version number '{version}'") - (major, minor, patch, prerelease, prerelease_num) = \ - match.group(1, 2, 4, 5, 6) + (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6) if patch: self.version = tuple(map(int, [major, minor, patch])) @@ -169,9 +169,9 @@ def parse(self, version): def __str__(self): r"""String representation of version.""" if self.version[2] == 0: - vstring = '.'.join(map(str, self.version[0:2])) + vstring = ".".join(map(str, self.version[0:2])) else: - vstring = '.'.join(map(str, self.version)) + vstring = ".".join(map(str, self.version)) if self.prerelease: vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) @@ -198,13 +198,13 @@ def _cmp(self, other): # case 3: self doesn't have prerelease, other does: self is greater # case 4: both have prerelease: must compare them! - if (not self.prerelease and not other.prerelease): + if not self.prerelease and not other.prerelease: return 0 - elif (self.prerelease and not other.prerelease): + elif self.prerelease and not other.prerelease: return -1 - elif (not self.prerelease and other.prerelease): + elif not self.prerelease and other.prerelease: return 1 - elif (self.prerelease and other.prerelease): + elif self.prerelease and other.prerelease: if self.prerelease == other.prerelease: return 0 elif self.prerelease < other.prerelease: @@ -248,6 +248,7 @@ def _cmp(self, other): # "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison # implemented here, this just isn't so. + class LooseVersion(Version): r"""Version numbering for anarchists and software realists. @@ -282,17 +283,18 @@ class LooseVersion(Version): version: version string Examples: - >>> v1 = LooseVersion('1.17.2') + >>> v1 = LooseVersion("1.17.2") >>> v1 LooseVersion ('1.17.2') >>> v1.version [1, 17, 2] - >>> v2 = LooseVersion('1.17.2-3-g70b71bd') + >>> v2 = LooseVersion("1.17.2-3-g70b71bd") >>> v1 < v2 True """ - version_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + version_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) """Version regexp pattern. The regexp pattern is used to split the version @@ -346,10 +348,7 @@ def parse(self, version): # from the parsed tuple -- so I just store the string here for # use by __str__ self.vstring = version - components = [ - x for x in self.version_re.split(version) - if x and x != '.' - ] + components = [x for x in self.version_re.split(version) if x and x != "."] for i, obj in enumerate(components): try: components[i] = int(obj) diff --git a/docs/conf.py b/docs/conf.py index b8400b9..37064d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,63 +2,63 @@ # Project ----------------------------------------------------------------- -project = 'audeer' -author = 'Hagen Wierstorf, Johannes Wagner' +project = "audeer" +author = "Hagen Wierstorf, Johannes Wagner" version = audeer.git_repo_version() -title = '{} Documentation'.format(project) +title = "{} Documentation".format(project) # General ----------------------------------------------------------------- -master_doc = 'index' -source_suffix = '.rst' +master_doc = "index" +source_suffix = ".rst" exclude_patterns = [ - 'build', - 'tests', - 'Thumbs.db', - '.DS_Store', - 'api-src', + "build", + "tests", + "Thumbs.db", + ".DS_Store", + "api-src", ] pygments_style = None extensions = [ - 'sphinx.ext.napoleon', # support for Google-style docstrings - 'sphinx_autodoc_typehints', - 'sphinx_copybutton', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx_apipages', + "sphinx.ext.napoleon", # support for Google-style docstrings + "sphinx_autodoc_typehints", + "sphinx_copybutton", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx_apipages", ] intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), + "python": ("https://docs.python.org/3/", None), } linkcheck_ignore = [ - 'https://gitlab.audeering.com', + "https://gitlab.audeering.com", ] -copybutton_prompt_text = r'>>> |\.\.\. |$ ' +copybutton_prompt_text = r">>> |\.\.\. |$ " copybutton_prompt_is_regexp = True autodoc_default_options = { - 'undoc-members': False, + "undoc-members": False, } apipages_hidden_methods = [ - '__call__', - '__eq__', - '__lt__', - '__le__', - '__gt__', - '__ge__', - '__repr__', - '__str__', + "__call__", + "__eq__", + "__lt__", + "__le__", + "__gt__", + "__ge__", + "__repr__", + "__str__", ] # HTML -------------------------------------------------------------------- -html_theme = 'sphinx_audeering_theme' +html_theme = "sphinx_audeering_theme" html_theme_options = { - 'display_version': True, - 'footer_links': False, - 'logo_only': False, + "display_version": True, + "footer_links": False, + "logo_only": False, } html_context = { - 'display_github': True, + "display_github": True, } html_title = title diff --git a/docs/usage.rst b/docs/usage.rst index b81cd0b..d56e8f8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -31,8 +31,8 @@ to estimate Pi with a Monte Carlo method. progress_bar=True, ) pi = 4.0 * sum(inside_samples) / NUM_SAMPLES - print(f'Pi is roughly {pi}') + print(f"Pi is roughly {pi}") - if __name__ == '__main__': + if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 5ba6d2b..12b086e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,12 @@ addopts = ''' # ----- ruff -------------------------------------------------------------- # [tool.ruff] +cache-dir = '.cache/ruff' + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] select = [ 'D', # pydocstyle 'E', # pycodestyle errors @@ -84,7 +90,6 @@ select = [ 'N', # pep8-naming 'W', # pycodestyle warnings ] - extend-ignore = [ 'D100', # Missing docstring in public module 'D101', # Missing docstring in public class @@ -94,11 +99,7 @@ extend-ignore = [ 'D107', # Missing docstring in `__init__` ] -line-length = 79 - -cache-dir = '.cache/ruff' - -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] '__init__.py' = [ 'F401', # * imported but unused ] @@ -108,7 +109,7 @@ cache-dir = '.cache/ruff' # # Check correct order/syntax of import statements # -[tool.ruff.isort] +[tool.ruff.lint.isort] # All from imports have their own line, e.g. # @@ -145,7 +146,7 @@ section-order = [ 'first-party', 'local-folder', ] -[tool.ruff.isort.sections] +[tool.ruff.lint.isort.sections] 'audeering' = [ 'audb', 'audbackend', @@ -169,7 +170,7 @@ section-order = [ # # Check variable/class names follow PEP8 naming convention # -[tool.ruff.pep8-naming] +[tool.ruff.lint.pep8-naming] ignore-names = [ 'config', # allow lowercase class name 'test_*', # allow uppercase name when testing a class @@ -180,7 +181,7 @@ ignore-names = [ # # Check docstrings follow selected convention # -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = 'google' diff --git a/tests/test_install.py b/tests/test_install.py index 83aad86..eb6615a 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -7,18 +7,18 @@ import audeer -PACKAGE = 'PyYaml' -MODULE = 'yaml' +PACKAGE = "PyYaml" +MODULE = "yaml" def uninstall(): subprocess.run( [ sys.executable, - '-m', - 'pip', - 'uninstall', - '--yes', + "-m", + "pip", + "uninstall", + "--yes", PACKAGE, ], stdout=subprocess.DEVNULL, @@ -40,7 +40,6 @@ def run_around_tests(): def test(): - # verify module is not installed with pytest.raises(ModuleNotFoundError): importlib.import_module(MODULE) @@ -49,31 +48,31 @@ def test(): with pytest.raises(subprocess.CalledProcessError): audeer.install_package( PACKAGE, - version='999.0.0', + version="999.0.0", ) # install package audeer.install_package( PACKAGE, - version='<=5.3', + version="<=5.3", ) # installed version satisfies requested version audeer.install_package( PACKAGE, - version='>=5.3', + version=">=5.3", ) audeer.install_package( PACKAGE, - version='>5.2', + version=">5.2", ) audeer.install_package( PACKAGE, - version='<=6.0', + version="<=6.0", ) audeer.install_package( PACKAGE, - version=' < 5.4 ', # whitespace will be ignored + version=" < 5.4 ", # whitespace will be ignored ) audeer.install_package( PACKAGE, @@ -84,20 +83,20 @@ def test(): with pytest.raises(RuntimeError): audeer.install_package( PACKAGE, - version='>=5.4', + version=">=5.4", ) with pytest.raises(RuntimeError): audeer.install_package( PACKAGE, - version='>5.3', + version=">5.3", ) with pytest.raises(RuntimeError): audeer.install_package( PACKAGE, - version='<=5.2', + version="<=5.2", ) with pytest.raises(RuntimeError): audeer.install_package( PACKAGE, - version='<5.3', + version="<5.3", ) diff --git a/tests/test_io.py b/tests/test_io.py index 903de23..072eb28 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -8,15 +8,15 @@ import audeer -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def tree(tmpdir, request): r"""Create file tree.""" files = request.param paths = [] for path in files: - if os.name == 'nt': - path = path.replace('/', os.path.sep) + if os.name == "nt": + path = path.replace("/", os.path.sep) if path.endswith(os.path.sep): path = audeer.path(tmpdir, path) path = audeer.mkdir(path) @@ -32,253 +32,252 @@ def tree(tmpdir, request): @pytest.mark.parametrize( - 'tree, root, files, archive_create, archive_extract, destination, ' - 'expected', + "tree, root, files, archive_create, archive_extract, destination, " "expected", [ ( # empty [], - '.', + ".", [], - 'archive.zip', - 'archive.zip', - '.', + "archive.zip", + "archive.zip", + ".", [], ), ( [], - '.', + ".", None, - 'archive.zip', - 'archive.zip', - '.', + "archive.zip", + "archive.zip", + ".", [], ), ( # single file - ['file.txt'], - '.', - 'file.txt', - 'archive.zip', - 'archive.zip', - '.', - ['file.txt'], + ["file.txt"], + ".", + "file.txt", + "archive.zip", + "archive.zip", + ".", + ["file.txt"], ), ( # archive folder does not exist - ['file.txt'], - '.', - 'file.txt', - 'sub/archive.zip', - 'sub/archive.zip', - '.', - ['file.txt'], + ["file.txt"], + ".", + "file.txt", + "sub/archive.zip", + "sub/archive.zip", + ".", + ["file.txt"], ), ( # file in sub folder - ['file.txt', 'sub/a/b/file.txt'], - '.', - ['sub/a/b/file.txt', 'file.txt'], - 'archive.zip', - 'archive.zip', - '.', - ['sub/a/b/file.txt', 'file.txt'], + ["file.txt", "sub/a/b/file.txt"], + ".", + ["sub/a/b/file.txt", "file.txt"], + "archive.zip", + "archive.zip", + ".", + ["sub/a/b/file.txt", "file.txt"], ), ( - ['file.txt', 'sub/a/b/file.txt'], - '.', - ['sub/a/b/file.txt'], - 'archive.zip', - 'archive.zip', - '.', - ['sub/a/b/file.txt'], + ["file.txt", "sub/a/b/file.txt"], + ".", + ["sub/a/b/file.txt"], + "archive.zip", + "archive.zip", + ".", + ["sub/a/b/file.txt"], ), ( - ['file.txt', 'sub/a/b/file.txt'], - '.', - ['file.txt'], - 'archive.zip', - 'archive.zip', - '.', - ['file.txt'], + ["file.txt", "sub/a/b/file.txt"], + ".", + ["file.txt"], + "archive.zip", + "archive.zip", + ".", + ["file.txt"], ), ( # include all files - ['file.txt', 'sub/a/b/file.txt', '.hidden'], - '.', + ["file.txt", "sub/a/b/file.txt", ".hidden"], + ".", None, - 'archive.zip', - 'archive.zip', - '.', - ['.hidden', 'file.txt', 'sub/a/b/file.txt'], + "archive.zip", + "archive.zip", + ".", + [".hidden", "file.txt", "sub/a/b/file.txt"], ), ( # exclude empty folder - ['file.txt', 'sub/a/b/file.txt', '.hidden', 'empty/'], - '.', + ["file.txt", "sub/a/b/file.txt", ".hidden", "empty/"], + ".", None, - 'archive.zip', - 'archive.zip', - '.', - ['.hidden', 'file.txt', 'sub/a/b/file.txt'], + "archive.zip", + "archive.zip", + ".", + [".hidden", "file.txt", "sub/a/b/file.txt"], ), ( # tar.gz - ['file.txt', 'sub/a/b/file.txt'], - '.', - ['sub/a/b/file.txt', 'file.txt'], - 'archive.tar.gz', - 'archive.tar.gz', - '.', - ['sub/a/b/file.txt', 'file.txt'], + ["file.txt", "sub/a/b/file.txt"], + ".", + ["sub/a/b/file.txt", "file.txt"], + "archive.tar.gz", + "archive.tar.gz", + ".", + ["sub/a/b/file.txt", "file.txt"], ), ( # root is sub folder - ['sub/file.txt'], - './sub', - ['file.txt'], - 'archive.zip', - 'archive.zip', - '.', - ['file.txt'], + ["sub/file.txt"], + "./sub", + ["file.txt"], + "archive.zip", + "archive.zip", + ".", + ["file.txt"], ), ( - ['sub/file.txt'], - './sub', + ["sub/file.txt"], + "./sub", None, - 'archive.zip', - 'archive.zip', - '.', - ['file.txt'], + "archive.zip", + "archive.zip", + ".", + ["file.txt"], ), ( # destitation is sub folder - ['file.txt'], - '.', - ['file.txt'], - 'archive.zip', - 'archive.zip', - './sub', - ['file.txt'], + ["file.txt"], + ".", + ["file.txt"], + "archive.zip", + "archive.zip", + "./sub", + ["file.txt"], ), ( # relative path with ../ - ['sub/file.txt'], - './sub', - ['../sub/file.txt'], - 'archive.zip', - 'archive.zip', - '.', - ['file.txt'], + ["sub/file.txt"], + "./sub", + ["../sub/file.txt"], + "archive.zip", + "archive.zip", + ".", + ["file.txt"], ), pytest.param( # root does not exit [], - './sub', + "./sub", None, - 'archive.zip', - 'archive.zip', - '.', + "archive.zip", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=FileNotFoundError), ), pytest.param( # root is not a directory - ['file.txt'], - 'file.txt', + ["file.txt"], + "file.txt", None, - 'archive.zip', - 'archive.zip', - '.', + "archive.zip", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=NotADirectoryError), ), pytest.param( # destination is not a directory - ['file.txt'], - '.', + ["file.txt"], + ".", [], - 'archive.zip', - 'archive.zip', - 'file.txt', + "archive.zip", + "archive.zip", + "file.txt", [], marks=pytest.mark.xfail(raises=NotADirectoryError), ), pytest.param( # archive to be extracted does not exit [], - '.', + ".", [], - 'archive.zip', - 'bad.zip', - '.', + "archive.zip", + "bad.zip", + ".", None, marks=pytest.mark.xfail(raises=FileNotFoundError), ), pytest.param( # archive to be extracted is a directory - ['sub/'], - '.', + ["sub/"], + ".", [], - 'archive.zip', - 'sub', - '.', + "archive.zip", + "sub", + ".", None, marks=pytest.mark.xfail(raises=IsADirectoryError), ), pytest.param( # file does not exit [], - '.', - ['file.txt'], - 'archive.zip', - 'archive.zip', - '.', + ".", + ["file.txt"], + "archive.zip", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=FileNotFoundError), ), pytest.param( # file not below root - ['file.txt', 'sub/'], - './sub', - ['../file.txt'], - 'archive.zip', - 'archive.zip', - '.', + ["file.txt", "sub/"], + "./sub", + ["../file.txt"], + "archive.zip", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=RuntimeError), ), pytest.param( # archive type not supported [], - '.', + ".", [], - 'archive.bad', - 'archive.zip', - '.', + "archive.bad", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=RuntimeError), ), pytest.param( - ['archive.bad'], - '.', + ["archive.bad"], + ".", [], - 'archive.zip', - 'archive.bad', - '.', + "archive.zip", + "archive.bad", + ".", None, marks=pytest.mark.xfail(raises=RuntimeError), ), pytest.param( # broken archive - ['archive.zip'], - '.', + ["archive.zip"], + ".", [], - 'archive.tar.gz', - 'archive.zip', - '.', + "archive.tar.gz", + "archive.zip", + ".", None, marks=pytest.mark.xfail(raises=RuntimeError), ), ], - indirect=['tree'], + indirect=["tree"], ) -def test_archives(tmpdir, tree, root, files, archive_create, - archive_extract, destination, expected): - +def test_archives( + tmpdir, tree, root, files, archive_create, archive_extract, destination, expected +): root = audeer.path(tmpdir, root) destination = audeer.path(tmpdir, destination) archive_create = audeer.path(tmpdir, archive_create) archive_extract = audeer.path(tmpdir, archive_extract) - if os.name == 'nt': + if os.name == "nt": if expected is not None: - expected = [file.replace('/', os.path.sep) for file in expected] + expected = [file.replace("/", os.path.sep) for file in expected] if isinstance(files, str): - files = files.replace('/', os.path.sep) + files = files.replace("/", os.path.sep) elif files is not None: - files = [file.replace('/', os.path.sep) for file in files] + files = [file.replace("/", os.path.sep) for file in files] # relative path @@ -341,140 +340,154 @@ def test_archives(tmpdir, tree, root, files, archive_create, assert not os.path.exists(archive_extract) -@pytest.mark.parametrize('path,ext,basename', [ - ('~/.bashrc', None, '.bashrc'), - ('file.tar.gz', None, 'file.tar'), - ('/a/c.d/g', None, 'g'), - (b'/a/c.d/g', None, 'g'), - ('/a/c.d/g.exe', 'exe', 'g'), - ('../.././README.md', '.md', 'README'), - ('folder/file.txt', None, 'file'), - ('folder/file.txt', 'txt', 'file'), - (b'folder/file.txt', 'txt', 'file'), -]) +@pytest.mark.parametrize( + "path,ext,basename", + [ + ("~/.bashrc", None, ".bashrc"), + ("file.tar.gz", None, "file.tar"), + ("/a/c.d/g", None, "g"), + (b"/a/c.d/g", None, "g"), + ("/a/c.d/g.exe", "exe", "g"), + ("../.././README.md", ".md", "README"), + ("folder/file.txt", None, "file"), + ("folder/file.txt", "txt", "file"), + (b"folder/file.txt", "txt", "file"), + ], +) def test_basename_wo_ext(path, ext, basename): b = audeer.basename_wo_ext(path, ext=ext) assert b == basename - assert type(b) is str - - -@pytest.mark.parametrize('dirs,expected', [ - ([], ''), - ( - [ - '/home/user/tmp/coverage/test', - '/home/user/tmp/covert/operator', - '/home/user/tmp/coven/members', - ], '/home/user/tmp', - ), - ( - [ - '/home/user/tmp/coverage/test', - '/home/user/tmp/covert/operator', - '/home/user/tmp', - ], '/home/user/tmp', - ), - ( - [ - '~/tmp/coverage/test', - '~/tmp/covert/operator', - '~/tmp/coven/members', - ], f'{os.path.expanduser("~")}/tmp', - ), - ( - [ - '/etc/tmp/coverage/test', - '/home/user/tmp/covert/operator', - '/home/user/tmp/coven/members', - ], '', - ), - ( - [ - '/home/user/tmp', - '/home/user/tmp', - ], '/home/user/tmp', - ), - ( - [ - '/home1/user/tmp', - '/home2/user/tmp', - ], '', - ), -]) + assert isinstance(b, str) + + +@pytest.mark.parametrize( + "dirs,expected", + [ + ([], ""), + ( + [ + "/home/user/tmp/coverage/test", + "/home/user/tmp/covert/operator", + "/home/user/tmp/coven/members", + ], + "/home/user/tmp", + ), + ( + [ + "/home/user/tmp/coverage/test", + "/home/user/tmp/covert/operator", + "/home/user/tmp", + ], + "/home/user/tmp", + ), + ( + [ + "~/tmp/coverage/test", + "~/tmp/covert/operator", + "~/tmp/coven/members", + ], + f'{os.path.expanduser("~")}/tmp', + ), + ( + [ + "/etc/tmp/coverage/test", + "/home/user/tmp/covert/operator", + "/home/user/tmp/coven/members", + ], + "", + ), + ( + [ + "/home/user/tmp", + "/home/user/tmp", + ], + "/home/user/tmp", + ), + ( + [ + "/home1/user/tmp", + "/home2/user/tmp", + ], + "", + ), + ], +) def test_common_directory(dirs, expected): common = audeer.common_directory(dirs) # Change paths always to Linux syntax _, common = os.path.splitdrive(common) _, expected = os.path.splitdrive(expected) - common = common.replace('\\', '/') - expected = expected.replace('\\', '/') + common = common.replace("\\", "/") + expected = expected.replace("\\", "/") # On MacOS we get a '/System/Volumes/Data' in front - common = common.replace('/System/Volumes/Data', '') + common = common.replace("/System/Volumes/Data", "") assert common == expected def test_download_url(tmpdir): - url = 'https://audeering.github.io/audeer/_static/favicon.png' + url = "https://audeering.github.io/audeer/_static/favicon.png" audeer.download_url(url, tmpdir) audeer.download_url(url, tmpdir) dst = audeer.download_url(url, tmpdir, force_download=True) assert dst == os.path.join(tmpdir, os.path.basename(url)) -@pytest.mark.parametrize('path,extension', [ - ('', ''), - ('~/.bashrc', ''), - ('file.tar.gz', 'gz'), - ('/a/c.d/g', ''), - ('/a/c.d/g.exe', 'exe'), - ('../.././README.md', 'md'), - (b'../.././README.md', 'md'), - ('folder/file.txt', 'txt'), - (b'folder/file.txt', 'txt'), - ('test.WAV', 'WAV'), - ('test.WaV', 'WaV'), -]) +@pytest.mark.parametrize( + "path,extension", + [ + ("", ""), + ("~/.bashrc", ""), + ("file.tar.gz", "gz"), + ("/a/c.d/g", ""), + ("/a/c.d/g.exe", "exe"), + ("../.././README.md", "md"), + (b"../.././README.md", "md"), + ("folder/file.txt", "txt"), + (b"folder/file.txt", "txt"), + ("test.WAV", "WAV"), + ("test.WaV", "WaV"), + ], +) def test_file_extension(path, extension): ext = audeer.file_extension(path) assert ext == extension - assert type(ext) is str + assert isinstance(ext, str) @pytest.mark.parametrize( - 'dir_list,expected,recursive,hidden', + "dir_list,expected,recursive,hidden", [ ([], [], False, False), ([], [], True, False), - (['a', 'b', 'c'], ['a', 'b', 'c'], False, False), - (['a', 'b', 'c'], ['a', 'b', 'c'], True, False), - (['a'], ['a'], False, False), - (['a'], ['a'], True, False), + (["a", "b", "c"], ["a", "b", "c"], False, False), + (["a", "b", "c"], ["a", "b", "c"], True, False), + (["a"], ["a"], False, False), + (["a"], ["a"], True, False), ( - ['a', os.path.join('a', 'b'), os.path.join('a', 'b', 'c')], - ['a', os.path.join('a', 'b'), os.path.join('a', 'b', 'c')], + ["a", os.path.join("a", "b"), os.path.join("a", "b", "c")], + ["a", os.path.join("a", "b"), os.path.join("a", "b", "c")], True, False, ), # hidden - (['a', '.b'], ['a'], True, False), - (['a', '.b'], ['.b', 'a'], True, True), + (["a", ".b"], ["a"], True, False), + (["a", ".b"], [".b", "a"], True, True), ( - ['a', '.b', os.path.join('a', '.b'), os.path.join('a', '.b', 'c')], - ['a'], + ["a", ".b", os.path.join("a", ".b"), os.path.join("a", ".b", "c")], + ["a"], True, False, ), ( - ['a', '.b', os.path.join('a', '.b'), os.path.join('a', '.b', 'c')], - ['.b', 'a', os.path.join('a', '.b'), os.path.join('a', '.b', 'c')], + ["a", ".b", os.path.join("a", ".b"), os.path.join("a", ".b", "c")], + [".b", "a", os.path.join("a", ".b"), os.path.join("a", ".b", "c")], True, True, ), ], ) def test_list_dir_names(tmpdir, dir_list, expected, recursive, hidden): - - dir_tmp = tmpdir.mkdir('folder') + dir_tmp = tmpdir.mkdir("folder") directories = [] for directory in dir_list: directory = os.path.join(str(dir_tmp), directory) @@ -483,7 +496,7 @@ def test_list_dir_names(tmpdir, dir_list, expected, recursive, hidden): for directory in directories: assert os.path.isdir(directory) - path = os.path.join(str(dir_tmp), '.') + path = os.path.join(str(dir_tmp), ".") dirs = audeer.list_dir_names( path, basenames=False, @@ -491,7 +504,7 @@ def test_list_dir_names(tmpdir, dir_list, expected, recursive, hidden): hidden=hidden, ) assert dirs == [audeer.path(dir_tmp, d) for d in expected] - assert type(dirs) is list + assert isinstance(dirs, list) # test basenames dirs = audeer.list_dir_names( @@ -505,50 +518,50 @@ def test_list_dir_names(tmpdir, dir_list, expected, recursive, hidden): def test_list_dir_names_errors(tmpdir): with pytest.raises(NotADirectoryError): - file = audeer.touch(tmpdir, 'file.txt') + file = audeer.touch(tmpdir, "file.txt") audeer.list_dir_names(file) with pytest.raises(FileNotFoundError): - audeer.list_dir_names('not-existent') + audeer.list_dir_names("not-existent") @pytest.mark.parametrize( - 'files,path,filetype,expected,recursive,hidden', + "files,path,filetype,expected,recursive,hidden", [ # empty - ([], '.', '', [], False, False), - ([], '.', '', [], True, False), - ([], '.', 'wav', [], False, False), - ([], '.', 'wav', [], True, False), + ([], ".", "", [], False, False), + ([], ".", "", [], True, False), + ([], ".", "wav", [], False, False), + ([], ".", "wav", [], True, False), # file ( - ['file.txt'], - 'file.txt', - '', - ['file.txt'], + ["file.txt"], + "file.txt", + "", + ["file.txt"], False, False, ), pytest.param( - [os.path.join('sub', 'file.txt')], - 'file.txt', - '', + [os.path.join("sub", "file.txt")], + "file.txt", + "", [], False, False, marks=pytest.mark.xfail(raises=NotADirectoryError), ), ( - [os.path.join('sub', 'file.txt')], - 'file.txt', - '', - [os.path.join('sub', 'file.txt')], + [os.path.join("sub", "file.txt")], + "file.txt", + "", + [os.path.join("sub", "file.txt")], True, False, ), pytest.param( [], - 'file', - '', + "file", + "", None, False, False, @@ -556,64 +569,64 @@ def test_list_dir_names_errors(tmpdir): ), ( [ - 't1.wav', - 't2.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.wav'), + "t1.wav", + "t2.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.wav"), ], - 't1.wav', - '', - [os.path.join('sub', 't1.wav'), 't1.wav'], + "t1.wav", + "", + [os.path.join("sub", "t1.wav"), "t1.wav"], True, False, ), # folder ( - ['t3.ogg', 't2.wav', 't1.wav'], - '.', - '', - ['t1.wav', 't2.wav', 't3.ogg'], + ["t3.ogg", "t2.wav", "t1.wav"], + ".", + "", + ["t1.wav", "t2.wav", "t3.ogg"], False, False, ), ( [ - 't3.ogg', - 't2.wav', - 't1.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't1.ogg'), + "t3.ogg", + "t2.wav", + "t1.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t1.ogg"), ], - '.', - '', - ['t1.wav', 't2.wav', 't3.ogg'], + ".", + "", + ["t1.wav", "t2.wav", "t3.ogg"], False, False, ), ( [ - 't3.ogg', - 't2.wav', - 't1.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 'sub', 't1.ogg'), + "t3.ogg", + "t2.wav", + "t1.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "sub", "t1.ogg"), ], - '.', - '', + ".", + "", [ - os.path.join('sub', 'sub', 't1.ogg'), - os.path.join('sub', 't1.wav'), - 't1.wav', - 't2.wav', - 't3.ogg', + os.path.join("sub", "sub", "t1.ogg"), + os.path.join("sub", "t1.wav"), + "t1.wav", + "t2.wav", + "t3.ogg", ], True, False, ), pytest.param( [], - 'does-not-exist', - '', + "does-not-exist", + "", None, False, False, @@ -621,8 +634,8 @@ def test_list_dir_names_errors(tmpdir): ), pytest.param( [], - os.path.join('does', 'not', 'exist'), - '', + os.path.join("does", "not", "exist"), + "", None, False, False, @@ -631,170 +644,170 @@ def test_list_dir_names_errors(tmpdir): # filetype ( [ - 't3.ogg', - 't2.wav', - 't1.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 'sub', 't1.ogg'), + "t3.ogg", + "t2.wav", + "t1.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "sub", "t1.ogg"), ], - '.', - 'ogg', - ['t3.ogg'], + ".", + "ogg", + ["t3.ogg"], False, False, ), ( [ - 't3.ogg', - 't2.wav', - 't1.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 'sub', 't1.ogg'), + "t3.ogg", + "t2.wav", + "t1.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "sub", "t1.ogg"), ], - '.', - 'ogg', + ".", + "ogg", [ - os.path.join('sub', 'sub', 't1.ogg'), - 't3.ogg', + os.path.join("sub", "sub", "t1.ogg"), + "t3.ogg", ], True, False, ), ( [ - 't1.wav', - os.path.join('sub', 't1.wav'), + "t1.wav", + os.path.join("sub", "t1.wav"), ], - 't1.wav', - '', - ['t1.wav'], + "t1.wav", + "", + ["t1.wav"], False, False, ), # pattern ( [ - 't1.wav', - 't2.ogg', - 's3.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), - os.path.join('sub', 's3.wav'), + "t1.wav", + "t2.ogg", + "s3.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), + os.path.join("sub", "s3.wav"), ], - 't*', - '', + "t*", + "", [ - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), - 't1.wav', - 't2.ogg' + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), + "t1.wav", + "t2.ogg", ], True, False, ), ( [ - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), - os.path.join('sub', 's3.wav'), + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), + os.path.join("sub", "s3.wav"), ], - 't*', - '', + "t*", + "", [ - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), ], True, False, ), ( [ - 't1.wav', - 't2.ogg', - 's3.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), - os.path.join('sub', 's3.wav'), + "t1.wav", + "t2.ogg", + "s3.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), + os.path.join("sub", "s3.wav"), ], - 'x*', - '', + "x*", + "", [], True, False, ), ( [ - 't1.wav', - 't2.wav', - 's1.wav', - 's2.wav', + "t1.wav", + "t2.wav", + "s1.wav", + "s2.wav", ], - 't?.wav', - '', - ['t1.wav', 't2.wav'], + "t?.wav", + "", + ["t1.wav", "t2.wav"], False, False, ), ( [ - 't1.wav', - 't2.wav', - 's1.wav', - 's2.wav', + "t1.wav", + "t2.wav", + "s1.wav", + "s2.wav", ], - '[ts]1.wav', - '', - ['s1.wav', 't1.wav'], + "[ts]1.wav", + "", + ["s1.wav", "t1.wav"], False, False, ), ( [ - 't1.wav', - 't2.wav', - 's1.wav', - 's.wav', + "t1.wav", + "t2.wav", + "s1.wav", + "s.wav", ], - '[ts]?.wav', - '', - ['s1.wav', 't1.wav', 't2.wav'], + "[ts]?.wav", + "", + ["s1.wav", "t1.wav", "t2.wav"], False, False, ), ( [ - 't1.wav', - 't2.wav', - 's1.wav', - 's.wav', - 'x.wav', + "t1.wav", + "t2.wav", + "s1.wav", + "s.wav", + "x.wav", ], - '[ts]*.wav', - '', - ['s.wav', 's1.wav', 't1.wav', 't2.wav'], + "[ts]*.wav", + "", + ["s.wav", "s1.wav", "t1.wav", "t2.wav"], False, False, ), ( [], - 'file*', - '', + "file*", + "", [], False, False, ), ( [], - '?ile', - '', + "?ile", + "", [], False, False, ), pytest.param( [], - os.path.join('does', 'not', 'exist', 'file*'), - '', + os.path.join("does", "not", "exist", "file*"), + "", None, False, False, @@ -802,8 +815,8 @@ def test_list_dir_names_errors(tmpdir): ), pytest.param( [], - os.path.join('not!a[pattern'), - '', + os.path.join("not!a[pattern"), + "", None, False, False, @@ -812,132 +825,128 @@ def test_list_dir_names_errors(tmpdir): # pattern + filetype ( [ - 't1.wav', - 't2.ogg', - 's3.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', 't2.ogg'), - os.path.join('sub', 's3.wav'), - ], - 't*', - 'ogg', - [ - os.path.join('sub', 't2.ogg'), - 't2.ogg' + "t1.wav", + "t2.ogg", + "s3.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", "t2.ogg"), + os.path.join("sub", "s3.wav"), ], + "t*", + "ogg", + [os.path.join("sub", "t2.ogg"), "t2.ogg"], True, False, ), # hidden ( - ['.file.txt'], - '.file.txt', - '', + [".file.txt"], + ".file.txt", + "", [], False, False, ), ( - ['.file.txt'], - '.file.txt', - '', - ['.file.txt'], + [".file.txt"], + ".file.txt", + "", + [".file.txt"], False, True, ), ( - [os.path.join('sub', '.file.txt')], - '.file.txt', - '', + [os.path.join("sub", ".file.txt")], + ".file.txt", + "", [], True, False, ), ( - [os.path.join('sub', '.file.txt')], - '.file.txt', - '', - [os.path.join('sub', '.file.txt')], + [os.path.join("sub", ".file.txt")], + ".file.txt", + "", + [os.path.join("sub", ".file.txt")], True, True, ), ( [ - 't1.wav', - '.t2.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', '.t2.wav'), + "t1.wav", + ".t2.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", ".t2.wav"), ], - '', - '', + "", + "", [ - 't1.wav', + "t1.wav", ], False, False, ), ( [ - 't1.wav', - '.t2.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', '.t2.wav'), + "t1.wav", + ".t2.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", ".t2.wav"), ], - '', - '', + "", + "", [ - os.path.join('sub', 't1.wav'), - 't1.wav', + os.path.join("sub", "t1.wav"), + "t1.wav", ], True, False, ), ( [ - 't1.wav', - '.t2.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', '.t2.wav'), + "t1.wav", + ".t2.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", ".t2.wav"), ], - '', - '', + "", + "", [ - '.t2.wav', - 't1.wav', + ".t2.wav", + "t1.wav", ], False, True, ), ( [ - 't1.wav', - '.t2.wav', - os.path.join('sub', 't1.wav'), - os.path.join('sub', '.t2.wav'), + "t1.wav", + ".t2.wav", + os.path.join("sub", "t1.wav"), + os.path.join("sub", ".t2.wav"), ], - '', - '', + "", + "", [ - '.t2.wav', - os.path.join('sub', '.t2.wav'), - os.path.join('sub', 't1.wav'), - 't1.wav', + ".t2.wav", + os.path.join("sub", ".t2.wav"), + os.path.join("sub", "t1.wav"), + "t1.wav", ], True, True, ), ], ) -def test_list_file_names(tmpdir, files, path, filetype, expected, - recursive, hidden): - dir_tmp = tmpdir.mkdir('folder') - dir_tmp.mkdir('subfolder') +def test_list_file_names(tmpdir, files, path, filetype, expected, recursive, hidden): + dir_tmp = tmpdir.mkdir("folder") + dir_tmp.mkdir("subfolder") path = os.path.join(str(dir_tmp), path) for file in files: # Create the files file_tmp = dir_tmp.join(file) audeer.mkdir(os.path.dirname(file_tmp)) - file_tmp.write('') + file_tmp.write("") f = audeer.list_file_names( path, filetype=filetype, @@ -947,7 +956,7 @@ def test_list_file_names(tmpdir, files, path, filetype, expected, ) # test full path assert f == [audeer.path(dir_tmp, f) for f in expected] - assert type(f) is list + assert isinstance(f, list) # test basenames f = audeer.list_file_names( path, @@ -961,38 +970,37 @@ def test_list_file_names(tmpdir, files, path, filetype, expected, def test_md5_errors(): with pytest.raises(FileNotFoundError): - audeer.md5('does/not/exist') + audeer.md5("does/not/exist") @pytest.mark.parametrize( - 'file, content, expected', + "file, content, expected", [ ( # empty file - 'file.txt', + "file.txt", None, - 'd41d8cd98f00b204e9800998ecf8427e', + "d41d8cd98f00b204e9800998ecf8427e", ), ( # different content - 'file.txt', - 'hello world', - '5eb63bbbe01eeed093cb22bb8f5acdc3', + "file.txt", + "hello world", + "5eb63bbbe01eeed093cb22bb8f5acdc3", ), ( - 'file.txt', - 'Hello World', - 'b10a8db164e0754105b7a99be72e3fe5', + "file.txt", + "Hello World", + "b10a8db164e0754105b7a99be72e3fe5", ), ( # different filename - 'file.TXT', - 'Hello World', - 'b10a8db164e0754105b7a99be72e3fe5', + "file.TXT", + "Hello World", + "b10a8db164e0754105b7a99be72e3fe5", ), ], ) def test_md5_file(tmpdir, file, content, expected): - path = audeer.path(tmpdir, file) - with open(path, 'w') as fp: + with open(path, "w") as fp: if content is not None: fp.write(content) @@ -1000,66 +1008,65 @@ def test_md5_file(tmpdir, file, content, expected): @pytest.mark.parametrize( - 'tree, content, expected', + "tree, content, expected", [ ( # empty folder [], None, - 'd41d8cd98f00b204e9800998ecf8427e', + "d41d8cd98f00b204e9800998ecf8427e", ), ( # folder with different content - ['f'], + ["f"], None, - '8fa14cdd754f91cc6554c9e71929cce7', + "8fa14cdd754f91cc6554c9e71929cce7", ), ( - ['sub/f'], + ["sub/f"], None, - '1af042d5a4ec129583f6093f98f64118', + "1af042d5a4ec129583f6093f98f64118", ), ( - ['f', 'sub/f'], + ["f", "sub/f"], None, - 'b540f38948f445622adc657a757f4b0d', + "b540f38948f445622adc657a757f4b0d", ), ( - ['f', 'sub/g'], + ["f", "sub/g"], None, - '305107efbb15f9334d22ae4fbeec4de6', + "305107efbb15f9334d22ae4fbeec4de6", ), ( - ['f', 'sub/g'], - 'hello world', - '47829eb8ef287d0d72e0fed9b96d258d', + ["f", "sub/g"], + "hello world", + "47829eb8ef287d0d72e0fed9b96d258d", ), ( - ['f', 'sub/g'], - 'Hello World', - '442d96d7c43bb18f247888408e5d6977', + ["f", "sub/g"], + "Hello World", + "442d96d7c43bb18f247888408e5d6977", ), ( # with empty sub folder - ['f', 'sub/g', 'sub/'], + ["f", "sub/g", "sub/"], None, - '305107efbb15f9334d22ae4fbeec4de6', + "305107efbb15f9334d22ae4fbeec4de6", ), ( # with hidden file - ['f', 'sub/g', '.hidden'], + ["f", "sub/g", ".hidden"], None, - '97490b233a7717aec19023e28443a1bf', + "97490b233a7717aec19023e28443a1bf", ), ( # umlaute - ['ä', 'ö', 'ü', 'ß'], + ["ä", "ö", "ü", "ß"], None, - '622165ad36122984c6b2c7ba466aa262', + "622165ad36122984c6b2c7ba466aa262", ), ], - indirect=['tree'], + indirect=["tree"], ) def test_md5_folder(tmpdir, tree, content, expected): - if content is not None: for path in tree: - with open(path, 'w') as fp: + with open(path, "w") as fp: fp.write(content) assert audeer.md5(tmpdir) == expected @@ -1067,7 +1074,7 @@ def test_md5_folder(tmpdir, tree, content, expected): def test_mkdir(tmpdir): # New dir - path = str(tmpdir.mkdir('folder1')) + path = str(tmpdir.mkdir("folder1")) p = audeer.mkdir(path) assert os.path.isdir(p) is True assert p == path @@ -1076,82 +1083,81 @@ def test_mkdir(tmpdir): assert os.path.isdir(p) is True assert p == path # Existing dir with content - dir_tmp = tmpdir.mkdir('folder2') - f = dir_tmp.join('file.txt') - f.write('') + dir_tmp = tmpdir.mkdir("folder2") + f = dir_tmp.join("file.txt") + f.write("") path = str(dir_tmp) p = audeer.mkdir(path) assert os.path.isdir(p) is True assert p == path # Relative path - path = str(tmpdir.mkdir('folder3')) + path = str(tmpdir.mkdir("folder3")) current_path = os.getcwd() os.chdir(path) - p = audeer.mkdir('folder4') + p = audeer.mkdir("folder4") os.chdir(current_path) assert os.path.isdir(p) is True - assert p == os.path.join(path, 'folder4') + assert p == os.path.join(path, "folder4") # Subdirectories os.chdir(path) - p = audeer.mkdir('folder5', 'folder6') + p = audeer.mkdir("folder5", "folder6") os.chdir(current_path) assert os.path.isdir(p) is True - assert p == os.path.join(path, 'folder5', 'folder6') + assert p == os.path.join(path, "folder5", "folder6") # Path in bytes - path = str(tmpdir.mkdir('folder7')) - path = bytes(path, 'utf8') + path = str(tmpdir.mkdir("folder7")) + path = bytes(path, "utf8") p = audeer.mkdir(path) assert os.path.isdir(p) is True - assert p == path.decode('utf8') + assert p == path.decode("utf8") # Empty dir - path = '' + path = "" p = audeer.mkdir(path) assert p == path # Mode, see https://stackoverflow.com/a/705088 # Default mode os.umask(0) - p = audeer.mkdir(tmpdir, 'folder8', 'sub-folder') + p = audeer.mkdir(tmpdir, "folder8", "sub-folder") mode = stat.S_IMODE(os.stat(p).st_mode) - expected_mode = int('777', 8) + expected_mode = int("777", 8) assert mode == expected_mode # Non-default modes # Under Windows, changing permissions does not work, # there we always expect 777 os.umask(0) - p = audeer.mkdir(tmpdir, 'folder9', 'sub-folder', mode=0o775) - expected_mode = '775' - if platform.system() == 'Windows': - expected_mode = '777' + p = audeer.mkdir(tmpdir, "folder9", "sub-folder", mode=0o775) + expected_mode = "775" + if platform.system() == "Windows": + expected_mode = "777" mode = stat.S_IMODE(os.stat(p).st_mode) assert mode == int(expected_mode, 8) - assert mode != int('755', 8) + assert mode != int("755", 8) os.umask(0) - p = audeer.mkdir(tmpdir, 'folder10', 'sub-folder', mode=0o755) - expected_mode = '755' - if platform.system() == 'Windows': - expected_mode = '777' + p = audeer.mkdir(tmpdir, "folder10", "sub-folder", mode=0o755) + expected_mode = "755" + if platform.system() == "Windows": + expected_mode = "777" mode = stat.S_IMODE(os.stat(p).st_mode) assert mode == int(expected_mode, 8) - assert mode != int('775', 8) + assert mode != int("775", 8) @pytest.mark.parametrize( - 'src_path, dst_path', + "src_path, dst_path", [ ( - 'path1', - 'path1', + "path1", + "path1", ), ( - 'path1', - 'path2', + "path1", + "path2", ), - ] + ], ) def test_move(tmpdir, src_path, dst_path): - system = platform.system() - tmp_dir = audeer.mkdir(tmpdir, 'folder') + tmp_dir = audeer.mkdir(tmpdir, "folder") # src: file # dst: new file @@ -1182,7 +1188,7 @@ def test_move(tmpdir, src_path, dst_path): # dst: new folder audeer.rmdir(tmp_dir) audeer.mkdir(tmp_dir, src_path) - audeer.touch(tmp_dir, src_path, 'file.txt') + audeer.touch(tmp_dir, src_path, "file.txt") audeer.move( os.path.join(tmp_dir, src_path), os.path.join(tmp_dir, dst_path), @@ -1190,22 +1196,22 @@ def test_move(tmpdir, src_path, dst_path): if src_path != dst_path: assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt')) + assert os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) # src: non-empty folder # dst: existing non-empty folder audeer.rmdir(tmp_dir) audeer.mkdir(tmp_dir, src_path) - audeer.touch(tmp_dir, src_path, 'file.txt') + audeer.touch(tmp_dir, src_path, "file.txt") if src_path != dst_path: audeer.mkdir(tmp_dir, dst_path) - audeer.touch(tmp_dir, dst_path, 'file.txt') + audeer.touch(tmp_dir, dst_path, "file.txt") if src_path != dst_path: - if system == 'Windows': - error_msg = 'Access is denied' + if system == "Windows": + error_msg = "Access is denied" else: - error_msg = 'Directory not empty' + error_msg = "Directory not empty" with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1214,12 +1220,12 @@ def test_move(tmpdir, src_path, dst_path): # src: non-empty folder # dst: existing empty folder - os.remove(os.path.join(tmp_dir, dst_path, 'file.txt')) - if system == 'Windows': + os.remove(os.path.join(tmp_dir, dst_path, "file.txt")) + if system == "Windows": # Only under Windows # we get an error # if destination is an empty folder - with pytest.raises(OSError, match='Access is denied'): + with pytest.raises(OSError, match="Access is denied"): audeer.move( os.path.join(tmp_dir, src_path), os.path.join(tmp_dir, dst_path), @@ -1233,7 +1239,7 @@ def test_move(tmpdir, src_path, dst_path): if src_path != dst_path: assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt')) + assert os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) # src: non-empty folder @@ -1247,7 +1253,7 @@ def test_move(tmpdir, src_path, dst_path): if src_path != dst_path: assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt')) + assert os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) # src: empty folder @@ -1256,12 +1262,12 @@ def test_move(tmpdir, src_path, dst_path): audeer.mkdir(tmp_dir, src_path) if src_path != dst_path: audeer.mkdir(tmp_dir, dst_path) - audeer.touch(tmp_dir, dst_path, 'file.txt') + audeer.touch(tmp_dir, dst_path, "file.txt") if src_path != dst_path: - if system == 'Windows': - error_msg = 'Access is denied' + if system == "Windows": + error_msg = "Access is denied" else: - error_msg = 'Directory not empty' + error_msg = "Directory not empty" with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1270,12 +1276,12 @@ def test_move(tmpdir, src_path, dst_path): # src: empty folder # dst: existing empty folder - os.remove(os.path.join(tmp_dir, dst_path, 'file.txt')) - if system == 'Windows': + os.remove(os.path.join(tmp_dir, dst_path, "file.txt")) + if system == "Windows": # Only under Windows # we get an error # if destination is an empty folder - with pytest.raises(OSError, match='Access is denied'): + with pytest.raises(OSError, match="Access is denied"): audeer.move( os.path.join(tmp_dir, src_path), os.path.join(tmp_dir, dst_path), @@ -1289,9 +1295,7 @@ def test_move(tmpdir, src_path, dst_path): if src_path != dst_path: assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert not os.path.exists( - os.path.join(tmp_dir, dst_path, 'file.txt') - ) + assert not os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) # src: empty folder @@ -1305,22 +1309,21 @@ def test_move(tmpdir, src_path, dst_path): if src_path != dst_path: assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert not os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt')) + assert not os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) if src_path != dst_path: - # src: file # dst: non-empty folder audeer.rmdir(tmp_dir) audeer.mkdir(tmp_dir) audeer.touch(tmp_dir, src_path) audeer.mkdir(tmp_dir, dst_path) - audeer.touch(tmp_dir, dst_path, 'file.txt') - if system == 'Windows': - error_msg = 'Access is denied' + audeer.touch(tmp_dir, dst_path, "file.txt") + if system == "Windows": + error_msg = "Access is denied" else: - error_msg = 'Is a directory' + error_msg = "Is a directory" with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1329,7 +1332,7 @@ def test_move(tmpdir, src_path, dst_path): # src: file # dst: empty folder - os.remove(os.path.join(tmp_dir, dst_path, 'file.txt')) + os.remove(os.path.join(tmp_dir, dst_path, "file.txt")) with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1341,10 +1344,10 @@ def test_move(tmpdir, src_path, dst_path): audeer.rmdir(tmp_dir) audeer.mkdir(tmp_dir) audeer.mkdir(tmp_dir, src_path) - audeer.touch(tmp_dir, src_path, 'file.txt') + audeer.touch(tmp_dir, src_path, "file.txt") audeer.touch(tmp_dir, dst_path) - if system != 'Windows': - error_msg = 'Not a directory' + if system != "Windows": + error_msg = "Not a directory" with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1357,7 +1360,7 @@ def test_move(tmpdir, src_path, dst_path): ) assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt')) + assert os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) # src: empty folder @@ -1365,8 +1368,8 @@ def test_move(tmpdir, src_path, dst_path): audeer.rmdir(tmp_dir) audeer.mkdir(tmp_dir, src_path) audeer.touch(tmp_dir, dst_path) - if system != 'Windows': - error_msg = 'Not a directory' + if system != "Windows": + error_msg = "Not a directory" with pytest.raises(OSError, match=error_msg): audeer.move( os.path.join(tmp_dir, src_path), @@ -1379,28 +1382,25 @@ def test_move(tmpdir, src_path, dst_path): ) assert not os.path.exists(os.path.join(tmp_dir, src_path)) assert os.path.exists(os.path.join(tmp_dir, dst_path)) - assert not os.path.exists( - os.path.join(tmp_dir, dst_path, 'file.txt') - ) + assert not os.path.exists(os.path.join(tmp_dir, dst_path, "file.txt")) assert os.path.isdir(os.path.join(tmp_dir, dst_path)) @pytest.mark.parametrize( - 'src_file, dst_file', + "src_file, dst_file", [ ( - 'file1', - 'file1', + "file1", + "file1", ), ( - 'file1', - 'file2', + "file1", + "file2", ), - ] + ], ) def test_move_file(tmpdir, src_file, dst_file): - - tmp_path = audeer.mkdir(tmpdir, 'folder') + tmp_path = audeer.mkdir(tmpdir, "folder") src_path = audeer.touch(tmp_path, src_file) dst_path = os.path.join(tmp_path, dst_file) @@ -1413,27 +1413,27 @@ def test_move_file(tmpdir, src_file, dst_file): @pytest.mark.parametrize( - 'path, new_extension, ext, expected_path', + "path, new_extension, ext, expected_path", [ - ('', '', None, ''), - ('', 'txt', None, ''), - ('', '', 'rst', ''), - ('', 'txt', 'rst', ''), - ('file', '', None, 'file'), - ('file', 'txt', None, 'file.txt'), - ('file.txt', 'wav', None, 'file.wav'), - ('test/file.txt', 'wav', None, 'test/file.wav'), - ('a/b/../file.txt', 'wav', None, 'a/b/../file.wav'), - ('file.txt', 'wav', 'txt', 'file.wav'), - ('file.txt', 'wav', '.txt', 'file.wav'), - ('file.txt', '.wav', 'txt', 'file.wav'), - ('file.txt', '.wav', '.txt', 'file.wav'), - ('file.a.b', 'wav', 'a.b', 'file.wav'), - ('file.a.b', 'wav', '.a.b', 'file.wav'), - ('file', 'wav', 'ext', 'file'), - ('file.txt', 'wav', 'ext', 'file.txt'), - ('file.txt', 'wav', 't', 'file.txt'), - ] + ("", "", None, ""), + ("", "txt", None, ""), + ("", "", "rst", ""), + ("", "txt", "rst", ""), + ("file", "", None, "file"), + ("file", "txt", None, "file.txt"), + ("file.txt", "wav", None, "file.wav"), + ("test/file.txt", "wav", None, "test/file.wav"), + ("a/b/../file.txt", "wav", None, "a/b/../file.wav"), + ("file.txt", "wav", "txt", "file.wav"), + ("file.txt", "wav", ".txt", "file.wav"), + ("file.txt", ".wav", "txt", "file.wav"), + ("file.txt", ".wav", ".txt", "file.wav"), + ("file.a.b", "wav", "a.b", "file.wav"), + ("file.a.b", "wav", ".a.b", "file.wav"), + ("file", "wav", "ext", "file"), + ("file.txt", "wav", "ext", "file.txt"), + ("file.txt", "wav", "t", "file.txt"), + ], ) def test_replace_file_extension(path, new_extension, ext, expected_path): new_path = audeer.replace_file_extension(path, new_extension, ext=ext) @@ -1442,40 +1442,40 @@ def test_replace_file_extension(path, new_extension, ext, expected_path): def test_rmdir(tmpdir): # Non existing dir - audeer.rmdir('non-esitent') + audeer.rmdir("non-esitent") # Folder with file content - dir_tmp = tmpdir.mkdir('folder') - f = dir_tmp.join('file.txt') - f.write('') + dir_tmp = tmpdir.mkdir("folder") + f = dir_tmp.join("file.txt") + f.write("") path = str(dir_tmp) p = audeer.mkdir(path) with pytest.raises(NotADirectoryError): - audeer.rmdir(os.path.join(p, 'file.txt')) + audeer.rmdir(os.path.join(p, "file.txt")) audeer.rmdir(p) assert not os.path.exists(p) # Folder with folder content - p = audeer.mkdir(tmpdir, 'folder', 'sub-folder') - audeer.rmdir(tmpdir, 'folder') + p = audeer.mkdir(tmpdir, "folder", "sub-folder") + audeer.rmdir(tmpdir, "folder") assert not os.path.exists(p) assert not os.path.exists(os.path.dirname(p)) # Relative path - path = str(tmpdir.mkdir('folder')) + path = str(tmpdir.mkdir("folder")) current_path = os.getcwd() os.chdir(os.path.dirname(path)) assert os.path.exists(path) - audeer.rmdir('folder') + audeer.rmdir("folder") assert not os.path.exists(path) os.chdir(current_path) def test_touch(tmpdir): - path = audeer.mkdir(tmpdir, 'folder1') - path = os.path.join(path, 'file') + path = audeer.mkdir(tmpdir, "folder1") + path = os.path.join(path, "file") assert not os.path.exists(path) audeer.touch(path) assert os.path.exists(path) stat = os.stat(path) - time.sleep(.1) + time.sleep(0.1) audeer.touch(path) assert os.path.exists(path) new_stat = os.stat(path) diff --git a/tests/test_path.py b/tests/test_path.py index a504be4..b0f0181 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -5,57 +5,60 @@ import audeer -@pytest.mark.parametrize('path', [ - ('~/.someconfigrc'), - ('file.tar.gz'), - ('/a/c.d/g'), - ('/a/c.d/g.exe'), - ('../.././README.md'), - ('folder/file.txt'), - (b'folder/file.txt'), - (''), -]) +@pytest.mark.parametrize( + "path", + [ + ("~/.someconfigrc"), + ("file.tar.gz"), + ("/a/c.d/g"), + ("/a/c.d/g.exe"), + ("../.././README.md"), + ("folder/file.txt"), + (b"folder/file.txt"), + (""), + ], +) def test_path(path): if path: expected_path = os.path.abspath(os.path.expanduser(path)) else: - expected_path = '' - if type(expected_path) == bytes: - expected_path = expected_path.decode('utf8') + expected_path = "" + if isinstance(expected_path, bytes): + expected_path = expected_path.decode("utf8") path = audeer.path(path) assert path == expected_path - assert type(path) is str + assert isinstance(path, str) path = audeer.safe_path(path) assert path == expected_path - assert type(path) is str + assert isinstance(path, str) @pytest.mark.parametrize( - 'path, paths', + "path, paths", [ - ('/a/b', ['file.tar.gz']), - ('/a', ['b', 'file.tar.gz']), - ('/a/c.d/g', ['../f']), - ('', ''), - ] + ("/a/b", ["file.tar.gz"]), + ("/a", ["b", "file.tar.gz"]), + ("/a/c.d/g", ["../f"]), + ("", ""), + ], ) def test_path_join(path, paths): expected_path = os.path.join(path, *paths) if expected_path: expected_path = os.path.abspath(os.path.expanduser(expected_path)) else: - expected_path = '' + expected_path = "" path = audeer.path(path, *paths) assert path == expected_path - assert type(path) is str + assert isinstance(path, str) def test_path_symlinks(tmpdir): - filename = 'file.txt' - linkname = 'link.txt' - dir_tmp = tmpdir.mkdir('folder') + filename = "file.txt" + linkname = "link.txt" + dir_tmp = tmpdir.mkdir("folder") f = dir_tmp.join(filename) - f.write('') + f.write("") folder = audeer.mkdir(str(dir_tmp)) file = os.path.join(folder, filename) link = os.path.join(folder, linkname) @@ -69,4 +72,4 @@ def test_path_symlinks(tmpdir): _, path = os.path.splitdrive(path) _, expected_path = os.path.splitdrive(expected_path) assert path == expected_path - assert type(path) is str + assert isinstance(path, str) diff --git a/tests/test_tqdm.py b/tests/test_tqdm.py index 6540de4..9d992a0 100644 --- a/tests/test_tqdm.py +++ b/tests/test_tqdm.py @@ -6,11 +6,11 @@ @pytest.mark.parametrize( - 'text', + "text", [ - ('Hello world'), - (4 * 'abcdefghijklmnopqrstuvwxyz'), - (18 * 'a'), # 18 == audeer.config.TQDM_COLUMNS as returned by doctest + ("Hello world"), + (4 * "abcdefghijklmnopqrstuvwxyz"), + (18 * "a"), # 18 == audeer.config.TQDM_COLUMNS as returned by doctest ], ) def test_format_display_message(text): @@ -18,22 +18,22 @@ def test_format_display_message(text): assert len(t) == audeer.config.TQDM_COLUMNS - 2 if len(text) > audeer.config.TQDM_COLUMNS: m = (audeer.config.TQDM_COLUMNS - 3) // 2 - assert t[m - 1:m + 2] == '...' + assert t[m - 1 : m + 2] == "..." assert t.startswith(text[:2]) t = audeer.format_display_message(text, pbar=True) assert len(t) == audeer.config.TQDM_DESCLEN - 2 if len(text) > audeer.config.TQDM_DESCLEN: m = (audeer.config.TQDM_DESCLEN - 3) // 2 - assert t[m - 1:m + 2] == '...' + assert t[m - 1 : m + 2] == "..." assert t.startswith(text[:2]) def test_progress_bar(): assert audeer.config.TQDM_DESCLEN == 60 assert audeer.config.TQDM_FORMAT == ( - '{percentage:3.0f}%|{bar} [{elapsed}<{remaining}] ' - '{desc:' + str(audeer.config.TQDM_DESCLEN) + '}' + "{percentage:3.0f}%|{bar} [{elapsed}<{remaining}] " + "{desc:" + str(audeer.config.TQDM_DESCLEN) + "}" ) - pbar = audeer.progress_bar([.1]) + pbar = audeer.progress_bar([0.1]) for step in pbar: time.sleep(step) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5d9369c..c1f3b8e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,15 +8,14 @@ def test_deprecated(): - - @audeer.deprecated(removal_version='1.0.0', alternative='audeer.mkdir') + @audeer.deprecated(removal_version="1.0.0", alternative="audeer.mkdir") def deprecated_function(): pass expected_message = ( - 'deprecated_function is deprecated ' - 'and will be removed with version 1.0.0.' - ' Use audeer.mkdir instead.' + "deprecated_function is deprecated " + "and will be removed with version 1.0.0." + " Use audeer.mkdir instead." ) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. @@ -26,13 +25,12 @@ def deprecated_function(): assert issubclass(w[-1].category, UserWarning) assert expected_message == str(w[-1].message) - @audeer.deprecated(removal_version='2.0.0') + @audeer.deprecated(removal_version="2.0.0") def deprecated_function(): pass expected_message = ( - 'deprecated_function is deprecated ' - 'and will be removed with version 2.0.0.' + "deprecated_function is deprecated " "and will be removed with version 2.0.0." ) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. @@ -42,13 +40,12 @@ def deprecated_function(): assert issubclass(w[-1].category, UserWarning) assert expected_message == str(w[-1].message) - @audeer.deprecated(removal_version='2.0.0') - class DeprecatedClass(): + @audeer.deprecated(removal_version="2.0.0") + class DeprecatedClass: pass expected_message = ( - 'DeprecatedClass is deprecated ' - 'and will be removed with version 2.0.0.' + "DeprecatedClass is deprecated " "and will be removed with version 2.0.0." ) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. @@ -60,25 +57,22 @@ class DeprecatedClass(): def test_deprecated_default_value(): - @audeer.deprecated_default_value( - argument='foo', - new_default_value='bar', - change_in_version='1.0.0', + argument="foo", + new_default_value="bar", + change_in_version="1.0.0", ) - def function_with_deprecated_default_value(*, foo='foo'): + def function_with_deprecated_default_value(*, foo="foo"): return foo expected_message = ( - "The default of 'foo' will change from " - "'foo' to 'bar' " - "with version 1.0.0." + "The default of 'foo' will change from " "'foo' to 'bar' " "with version 1.0.0." ) - default_value = 'foo' + default_value = "foo" with pytest.warns(None): # no warning if we set value - function_with_deprecated_default_value(foo='foo') - function_with_deprecated_default_value(foo='bar') + function_with_deprecated_default_value(foo="foo") + function_with_deprecated_default_value(foo="bar") with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") @@ -89,11 +83,10 @@ def function_with_deprecated_default_value(*, foo='foo'): def test_deprecated_keyword_argument(): - @audeer.deprecated_keyword_argument( - deprecated_argument='foo', - new_argument='bar', - removal_version='1.0.0', + deprecated_argument="foo", + new_argument="bar", + removal_version="1.0.0", mapping=lambda x: 2 * x, ) def function_with_deprecated_keyword_argument(*, bar): @@ -116,15 +109,14 @@ def function_with_deprecated_keyword_argument(*, bar): assert r == 2 * value @audeer.deprecated_keyword_argument( - deprecated_argument='foo', - removal_version='1.0.0', + deprecated_argument="foo", + removal_version="1.0.0", ) def function_with_deprecated_keyword_argument(**kwargs): return 1 expected_message = ( - "'foo' argument is deprecated " - "and will be removed with version 1.0.0." + "'foo' argument is deprecated " "and will be removed with version 1.0.0." ) assert function_with_deprecated_keyword_argument() == 1 with warnings.catch_warnings(record=True) as w: @@ -137,19 +129,18 @@ def function_with_deprecated_keyword_argument(**kwargs): assert r == 1 @audeer.deprecated_keyword_argument( - deprecated_argument='foo', - removal_version='1.0.0', + deprecated_argument="foo", + removal_version="1.0.0", remove_from_kwargs=False, ) def function_with_deprecated_keyword_argument(**kwargs): - if 'foo' in kwargs: - return kwargs['foo'] + if "foo" in kwargs: + return kwargs["foo"] else: return 1 expected_message = ( - "'foo' argument is deprecated " - "and will be removed with version 1.0.0." + "'foo' argument is deprecated " "and will be removed with version 1.0.0." ) assert function_with_deprecated_keyword_argument() == 1 with warnings.catch_warnings(record=True) as w: @@ -162,14 +153,14 @@ def function_with_deprecated_keyword_argument(**kwargs): assert r == 2 @audeer.deprecated_keyword_argument( - deprecated_argument='foo', - new_argument='bar', - removal_version='1.0.0', + deprecated_argument="foo", + new_argument="bar", + removal_version="1.0.0", remove_from_kwargs=False, ) def function_with_deprecated_keyword_argument(**kwargs): - if 'foo' in kwargs: - return kwargs['foo'] + if "foo" in kwargs: + return kwargs["foo"] else: return 1 @@ -189,12 +180,11 @@ def function_with_deprecated_keyword_argument(**kwargs): assert r == 2 @audeer.deprecated_keyword_argument( - deprecated_argument='foo', - new_argument='bar', - removal_version='1.0.0', + deprecated_argument="foo", + new_argument="bar", + removal_version="1.0.0", ) class ClassWithDeprecatedKeywordArgument(object): - def __init__(self, *, bar): self.bar = bar @@ -215,179 +205,179 @@ def __init__(self, *, bar): assert r.bar == value -@pytest.mark.parametrize('nested_list,expected_list', [ - ([1, 2, 3, [4], [], [[[[[[[[[5]]]]]]]]]], [1, 2, 3, 4, 5]), - ([[1, 2], 3], [1, 2, 3]), - ([1, 2, 3], [1, 2, 3]), -]) +@pytest.mark.parametrize( + "nested_list,expected_list", + [ + ([1, 2, 3, [4], [], [[[[[[[[[5]]]]]]]]]], [1, 2, 3, 4, 5]), + ([[1, 2], 3], [1, 2, 3]), + ([1, 2, 3], [1, 2, 3]), + ], +) def test_flatten_list(nested_list, expected_list): flattened_list = audeer.flatten_list(nested_list) assert flattened_list == expected_list def test_freeze_requirements(tmpdir): - path = str(tmpdir.mkdir('tmp')) - outfile = os.path.join(path, 'requirements.txt.lock') + path = str(tmpdir.mkdir("tmp")) + outfile = os.path.join(path, "requirements.txt.lock") audeer.freeze_requirements(outfile) with open(outfile) as f: requirements = f.readlines() # Remove whitespace and \n requirements = [r.strip() for r in requirements] - assert any(['pytest' in r for r in requirements]) - assert any(['audeer' in r for r in requirements]) - with pytest.raises(RuntimeError, match=r'Freezing Python packages failed'): - outfile = os.path.join(path, 'tmp2/requirements.txt.lock') + assert any(["pytest" in r for r in requirements]) + assert any(["audeer" in r for r in requirements]) + with pytest.raises(RuntimeError, match=r"Freezing Python packages failed"): + outfile = os.path.join(path, "tmp2/requirements.txt.lock") audeer.freeze_requirements(outfile) def test_git_repo_tags(): - git = ['git', 'tag'] + git = ["git", "tag"] expected_tags = subprocess.check_output(git) - expected_tags = expected_tags.decode().strip().split('\n') + expected_tags = expected_tags.decode().strip().split("\n") tags = audeer.git_repo_tags() assert tags == expected_tags tags = audeer.git_repo_tags(v=True) - expected_tags = [ - f'v{t}' if not t.startswith('v') else t for t in expected_tags - ] + expected_tags = [f"v{t}" if not t.startswith("v") else t for t in expected_tags] assert tags == expected_tags tags = audeer.git_repo_tags(v=False) - expected_tags = [ - t[1:] if t.startswith('v') else t for t in expected_tags - ] + expected_tags = [t[1:] if t.startswith("v") else t for t in expected_tags] assert tags == expected_tags def test_git_repo_version(): - git = ['git', 'describe', '--tags', '--always'] + git = ["git", "describe", "--tags", "--always"] expected_version = subprocess.check_output(git) expected_version = expected_version.decode().strip() version = audeer.git_repo_version(v=True) - if not expected_version.startswith('v'): - expected_version = f'v{expected_version}' + if not expected_version.startswith("v"): + expected_version = f"v{expected_version}" assert version == expected_version version = audeer.git_repo_version(v=False) assert version == expected_version[1:] @pytest.mark.parametrize( - 'version, is_semantic', + "version, is_semantic", [ ( - '1.0.0', + "1.0.0", True, ), ( - 'v1.0.0', + "v1.0.0", True, ), ( - '4.0.0-20200206.095534-3', + "4.0.0-20200206.095534-3", True, ), ( - 'v1.0.1-1-gdf29c4a', + "v1.0.1-1-gdf29c4a", True, ), ( - '1', + "1", False, ), ( - 'v1', + "v1", False, ), ( - 'v1.3-r3', + "v1.3-r3", False, ), ( - 'v1.3.3-r3', + "v1.3.3-r3", True, ), ( - 'a.b.c', + "a.b.c", False, ), ( - 'va.b.c', + "va.b.c", False, ), ( - '1.2.a', + "1.2.a", False, ), ( - 'v1.3.3.3-r3', + "v1.3.3.3-r3", False, ), ( - '1.0.0+20130313144700', + "1.0.0+20130313144700", True, ), ( - '1.0.0-alpha+001', + "1.0.0-alpha+001", True, ), - ] + ], ) def test_is_semantic_version(version, is_semantic): assert audeer.is_semantic_version(version) == is_semantic @pytest.mark.parametrize( - 'uid, expected', + "uid, expected", [ (audeer.uid(), True), (audeer.uid(short=True), True), - (audeer.uid(from_string='from string'), True), - (audeer.uid(from_string='from string', short=True), True), - (audeer.uid(from_string='from string', short=True).upper(), True), + (audeer.uid(from_string="from string"), True), + (audeer.uid(from_string="from string", short=True), True), + (audeer.uid(from_string="from string", short=True).upper(), True), (None, False), (1234, False), - ('', False), - ('some random string', False), + ("", False), + ("some random string", False), (audeer.uid()[:-1], False), (audeer.uid(short=True)[:-1], False), - ('00000000-0000-0000-0000-000000000000', True), - ('000000000-0000-0000-0000-00000000000', False), - ('?0000000-0000-0000-0000-000000000000', False), - ] + ("00000000-0000-0000-0000-000000000000", True), + ("000000000-0000-0000-0000-00000000000", False), + ("?0000000-0000-0000-0000-000000000000", False), + ], ) def test_is_uid(uid, expected): assert audeer.is_uid(uid) == expected def power(a: int = 0, *, b: int = 1): - return a ** b + return a**b @pytest.mark.parametrize( - 'multiprocessing', + "multiprocessing", [ - True, False, - ] + True, + False, + ], ) @pytest.mark.parametrize( - 'num_workers', + "num_workers", [ - 1, 3, None, - ] + 1, + 3, + None, + ], ) @pytest.mark.parametrize( - 'task_fun, params', + "task_fun, params", [ (power, [([], {})]), (power, [([1], {})]), - (power, [([1], {'b': 2})]), - (power, [([], {'a': 1, 'b': 2})]), - (power, [([x], {'b': x}) for x in range(5)]), - ] + (power, [([1], {"b": 2})]), + (power, [([], {"a": 1, "b": 2})]), + (power, [([x], {"b": x}) for x in range(5)]), + ], ) def test_run_tasks(multiprocessing, num_workers, task_fun, params): - expected = [ - task_fun(*param[0], **param[1]) for param in params - ] + expected = [task_fun(*param[0], **param[1]) for param in params] results = audeer.run_tasks( task_fun, params, @@ -397,16 +387,19 @@ def test_run_tasks(multiprocessing, num_workers, task_fun, params): assert expected == results -@pytest.mark.parametrize('func,params,expected_output', [ - ( - lambda x, n: x ** n, - [(2, n) for n in range(7)], - [1, 2, 4, 8, 16, 32, 64], - ), - (3, None, [None]), - (lambda x: x, ['hello'], ['hello']), - (lambda x: x, ['hello', 1, print], ['hello', 1, print]), -]) +@pytest.mark.parametrize( + "func,params,expected_output", + [ + ( + lambda x, n: x**n, + [(2, n) for n in range(7)], + [1, 2, 4, 8, 16, 32, 64], + ), + (3, None, [None]), + (lambda x: x, ["hello"], ["hello"]), + (lambda x: x, ["hello", 1, print], ["hello", 1, print]), + ], +) def test_run_worker_threads(func, params, expected_output): num_workers = [None, 1, 2, 4, 5, 100] for n in num_workers: @@ -416,58 +409,58 @@ def test_run_worker_threads(func, params, expected_output): @pytest.mark.parametrize( - 'versions, expected_versions', + "versions, expected_versions", [ ( - ['1.0.0', '3.0.0', '1.10.1', '1.2.0', '1.0.1'], - ['1.0.0', '1.0.1', '1.2.0', '1.10.1', '3.0.0'], + ["1.0.0", "3.0.0", "1.10.1", "1.2.0", "1.0.1"], + ["1.0.0", "1.0.1", "1.2.0", "1.10.1", "3.0.0"], ), ( [ - '1.0.0-SNAPSHOT', - '4.0.0-20200206.095424-2', - '1.0.0', - '2.0.0-20200131.102442-1', - '3.0.0', - '3.1.0', - '4.0.0-20200206.095316-1', - '3.2.0', - '2.0.0-20200131.102728-2', - '3.3.0', - '3.4.0', - '4.0.0-20200206.095534-3', - '4.0.0', + "1.0.0-SNAPSHOT", + "4.0.0-20200206.095424-2", + "1.0.0", + "2.0.0-20200131.102442-1", + "3.0.0", + "3.1.0", + "4.0.0-20200206.095316-1", + "3.2.0", + "2.0.0-20200131.102728-2", + "3.3.0", + "3.4.0", + "4.0.0-20200206.095534-3", + "4.0.0", ], [ - '1.0.0', - '1.0.0-SNAPSHOT', - '2.0.0-20200131.102442-1', - '2.0.0-20200131.102728-2', - '3.0.0', - '3.1.0', - '3.2.0', - '3.3.0', - '3.4.0', - '4.0.0', - '4.0.0-20200206.095316-1', - '4.0.0-20200206.095424-2', - '4.0.0-20200206.095534-3', + "1.0.0", + "1.0.0-SNAPSHOT", + "2.0.0-20200131.102442-1", + "2.0.0-20200131.102728-2", + "3.0.0", + "3.1.0", + "3.2.0", + "3.3.0", + "3.4.0", + "4.0.0", + "4.0.0-20200206.095316-1", + "4.0.0-20200206.095424-2", + "4.0.0-20200206.095534-3", ], ), ( - ['v1.0.0', 'v1.0.1', 'v1.0.1-1-gdf29c4a'], - ['v1.0.0', 'v1.0.1', 'v1.0.1-1-gdf29c4a'], + ["v1.0.0", "v1.0.1", "v1.0.1-1-gdf29c4a"], + ["v1.0.0", "v1.0.1", "v1.0.1-1-gdf29c4a"], ), # From https://github.com/postmarketOS/pmbootstrap/issues/342 ( - ['22.7.3-r1.3', '22.7.3-r1'], - ['22.7.3-r1', '22.7.3-r1.3'], + ["22.7.3-r1.3", "22.7.3-r1"], + ["22.7.3-r1", "22.7.3-r1.3"], ), ( - ['1.0.0', '1.1.1+1', '1.2.1', '1.2.0'], - ['1.0.0', '1.1.1+1', '1.2.0', '1.2.1'], + ["1.0.0", "1.1.1+1", "1.2.1", "1.2.0"], + ["1.0.0", "1.1.1+1", "1.2.0", "1.2.1"], ), - ] + ], ) def test_sort_versions(versions, expected_versions): sorted_versions = audeer.sort_versions(versions) @@ -475,10 +468,10 @@ def test_sort_versions(versions, expected_versions): @pytest.mark.parametrize( - 'versions, error_message', + "versions, error_message", [ ( - ['1'], + ["1"], ( "All version numbers have to be semantic versions, " "following 'X.Y.Z', " @@ -486,7 +479,7 @@ def test_sort_versions(versions, expected_versions): "But your version is: '1'." ), ), - ] + ], ) def test_sort_versions_errors(versions, error_message): with pytest.raises(ValueError, match=error_message): @@ -494,16 +487,16 @@ def test_sort_versions_errors(versions, error_message): @pytest.mark.parametrize( - 'input,expected_output', + "input,expected_output", [ (1, [1]), - ('abc', ['abc']), - (['abc', 1], ['abc', 1]), + ("abc", ["abc"]), + (["abc", 1], ["abc", 1]), ((1, 2, 3), [1, 2, 3]), (len, [len]), - ({1: 'a', 2: 'b'}, [1, 2]), + ({1: "a", 2: "b"}, [1, 2]), ([], []), - ('', ['']), + ("", [""]), (None, [None]), ], ) @@ -512,18 +505,18 @@ def test_to_list(input, expected_output): @pytest.mark.parametrize( - 'short', + "short", [ False, True, - ] + ], ) @pytest.mark.parametrize( - 'from_string', + "from_string", [ None, - 'example_string', - ] + "example_string", + ], ) def test_uid(from_string, short): uid = audeer.uid(from_string=from_string, short=short) @@ -532,7 +525,7 @@ def test_uid(from_string, short): else: assert len(uid) == 36 for pos in [8, 13, 18, 23]: - assert uid[pos] == '-' + assert uid[pos] == "-" uid2 = audeer.uid(from_string=from_string, short=short) if from_string is not None: assert uid == uid2 diff --git a/tests/test_version.py b/tests/test_version.py index a54b57f..2af44ee 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -6,20 +6,19 @@ @pytest.mark.parametrize( - 'v1, v2, wanted', + "v1, v2, wanted", [ - ('1.5.1', '1.5.2b2', -1), - ('161', '3.10a', 1), - ('8.02', '8.02', 0), - ('3.4j', '1996.07.12', -1), - ('3.2.pl0', '3.1.1.6', 1), - ('2g6', '11g', -1), - ('0.960923', '2.2beta29', -1), - ('1.13++', '5.5.kw', -1), - ] + ("1.5.1", "1.5.2b2", -1), + ("161", "3.10a", 1), + ("8.02", "8.02", 0), + ("3.4j", "1996.07.12", -1), + ("3.2.pl0", "3.1.1.6", 1), + ("2g6", "11g", -1), + ("0.960923", "2.2beta29", -1), + ("1.13++", "5.5.kw", -1), + ], ) def test_loose_version(v1, v2, wanted): - # This test is replicating the original tests from # https://github.com/python/cpython/blob/20a1c8ee4bcb1c421b7cca1f3f5d6ad7ce30a9c9/Lib/distutils/tests/test_version.py @@ -38,24 +37,24 @@ def test_loose_version(v1, v2, wanted): @pytest.mark.parametrize( - 'v1, v2, operator, expected', + "v1, v2, operator, expected", [ - ('1.5.1', '1.5.2b2', operator.eq, False), - ('1.5.1', '1.5.2b2', operator.lt, True), - ('1.5.1', '1.5.2b2', operator.le, True), - ('1.5.1', '1.5.2b2', operator.gt, False), - ('1.5.1', '1.5.2b2', operator.ge, False), - ('1.5.1', '1.5.1', operator.eq, True), - ('1.5.1', '1.5.1', operator.le, True), - ('1.5.1', '1.5.1', operator.ge, True), - ('161', '3.10a', operator.gt, True), - ('8.02', '8.02', operator.eq, True), - ('3.4j', '1996.07.12', operator.lt, True), - ('3.2.pl0', '3.1.1.6', operator.gt, True), - ('2g6', '11g', operator.lt, True), - ('0.960923', '2.2beta29', operator.lt, True), - ('1.13++', '5.5.kw', operator.lt, True), - ] + ("1.5.1", "1.5.2b2", operator.eq, False), + ("1.5.1", "1.5.2b2", operator.lt, True), + ("1.5.1", "1.5.2b2", operator.le, True), + ("1.5.1", "1.5.2b2", operator.gt, False), + ("1.5.1", "1.5.2b2", operator.ge, False), + ("1.5.1", "1.5.1", operator.eq, True), + ("1.5.1", "1.5.1", operator.le, True), + ("1.5.1", "1.5.1", operator.ge, True), + ("161", "3.10a", operator.gt, True), + ("8.02", "8.02", operator.eq, True), + ("3.4j", "1996.07.12", operator.lt, True), + ("3.2.pl0", "3.1.1.6", operator.gt, True), + ("2g6", "11g", operator.lt, True), + ("0.960923", "2.2beta29", operator.lt, True), + ("1.13++", "5.5.kw", operator.lt, True), + ], ) def test_loose_version_operator(v1, v2, operator, expected): v1 = audeer.LooseVersion(v1) @@ -65,52 +64,51 @@ def test_loose_version_operator(v1, v2, operator, expected): @pytest.mark.parametrize( - 'v1, v2, wanted', + "v1, v2, wanted", [ - ('1.5.1', '1.5.2b2', -1), + ("1.5.1", "1.5.2b2", -1), pytest.param( - '161', - '3.10a', + "161", + "3.10a", None, marks=pytest.mark.xfail(raises=ValueError), ), - ('8.02', '8.02', 0), + ("8.02", "8.02", 0), pytest.param( - '3.4j', - '1996.07.12', + "3.4j", + "1996.07.12", None, marks=pytest.mark.xfail(raises=ValueError), ), pytest.param( - '3.2.pl0', - '3.1.1.6', + "3.2.pl0", + "3.1.1.6", None, marks=pytest.mark.xfail(raises=ValueError), ), pytest.param( - '2g6', - '11g', + "2g6", + "11g", None, marks=pytest.mark.xfail(raises=ValueError), ), - ('0.9', '2.2', -1), - ('1.2.1', '1.2', 1), - ('1.1', '1.2.2', -1), - ('1.2', '1.1', 1), - ('1.2.1', '1.2.2', -1), - ('1.2.2', '1.2', 1), - ('1.2', '1.2.2', -1), - ('0.4.0', '0.4', 0), + ("0.9", "2.2", -1), + ("1.2.1", "1.2", 1), + ("1.1", "1.2.2", -1), + ("1.2", "1.1", 1), + ("1.2.1", "1.2.2", -1), + ("1.2.2", "1.2", 1), + ("1.2", "1.2.2", -1), + ("0.4.0", "0.4", 0), pytest.param( - '1.13++', - '5.5.kw', + "1.13++", + "5.5.kw", None, marks=pytest.mark.xfail(raises=ValueError), ), - ] + ], ) def test_strict_version(v1, v2, wanted): - # This test is replicating the original tests from # https://github.com/python/cpython/blob/20a1c8ee4bcb1c421b7cca1f3f5d6ad7ce30a9c9/Lib/distutils/tests/test_version.py @@ -129,31 +127,31 @@ def test_strict_version(v1, v2, wanted): @pytest.mark.parametrize( - 'v1, v2, operator, expected', + "v1, v2, operator, expected", [ - ('1.5.1', '1.5.2b2', operator.eq, False), - ('1.5.1', '1.5.2b2', operator.lt, True), - ('1.5.1', '1.5.2b2', operator.le, True), - ('1.5.1', '1.5.2b2', operator.gt, False), - ('1.5.1', '1.5.2b2', operator.ge, False), - ('1.5.1', '1.5.2b2', operator.ge, False), - ('1.5.1a1', '1.5.1', operator.lt, True), - ('1.5.1a1', '1.5.1a2', operator.lt, True), - ('1.5.1a2', '1.5.1a1', operator.gt, True), - ('1.5.1a1', '1.5.1a1', operator.eq, True), - ('1.5.1', '1.5.1b2', operator.gt, True), - ('1.5.1', '1.5.1', operator.le, True), - ('1.5.1', '1.5.1', operator.ge, True), - ('8.02', '8.02', operator.eq, True), - ('0.9', '2.2', operator.lt, True), - ('1.2.1', '1.2', operator.gt, True), - ('1.1', '1.2.2', operator.lt, True), - ('1.2', '1.1', operator.gt, True), - ('1.2.1', '1.2.2', operator.lt, True), - ('1.2.2', '1.2', operator.gt, True), - ('1.2', '1.2.2', operator.lt, True), - ('0.4.0', '0.4', operator.eq, True), - ] + ("1.5.1", "1.5.2b2", operator.eq, False), + ("1.5.1", "1.5.2b2", operator.lt, True), + ("1.5.1", "1.5.2b2", operator.le, True), + ("1.5.1", "1.5.2b2", operator.gt, False), + ("1.5.1", "1.5.2b2", operator.ge, False), + ("1.5.1", "1.5.2b2", operator.ge, False), + ("1.5.1a1", "1.5.1", operator.lt, True), + ("1.5.1a1", "1.5.1a2", operator.lt, True), + ("1.5.1a2", "1.5.1a1", operator.gt, True), + ("1.5.1a1", "1.5.1a1", operator.eq, True), + ("1.5.1", "1.5.1b2", operator.gt, True), + ("1.5.1", "1.5.1", operator.le, True), + ("1.5.1", "1.5.1", operator.ge, True), + ("8.02", "8.02", operator.eq, True), + ("0.9", "2.2", operator.lt, True), + ("1.2.1", "1.2", operator.gt, True), + ("1.1", "1.2.2", operator.lt, True), + ("1.2", "1.1", operator.gt, True), + ("1.2.1", "1.2.2", operator.lt, True), + ("1.2.2", "1.2", operator.gt, True), + ("1.2", "1.2.2", operator.lt, True), + ("0.4.0", "0.4", operator.eq, True), + ], ) def test_strict_version_operator(v1, v2, operator, expected): v1 = audeer.StrictVersion(v1)