Skip to content

TYPE: Annotate file-handling modules #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion nibabel/dataobj_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@

from .arrayproxy import ArrayLike
from .deprecated import deprecate_with_version
from .filebasedimages import FileBasedHeader, FileBasedImage, FileMap, FileSpec
from .filebasedimages import FileBasedHeader, FileBasedImage
from .fileholders import FileMap

if ty.TYPE_CHECKING: # pragma: no cover
import numpy.typing as npt

from .filename_parser import FileSpec

ArrayImgT = ty.TypeVar('ArrayImgT', bound='DataobjImage')


Expand Down
14 changes: 7 additions & 7 deletions nibabel/filebasedimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
from __future__ import annotations

import io
import os
import typing as ty
from copy import deepcopy
from typing import Type
from urllib import request

from .fileholders import FileHolder
from .filename_parser import TypesFilenamesError, splitext_addext, types_filenames
from .fileholders import FileHolder, FileMap
from .filename_parser import TypesFilenamesError, _stringify_path, splitext_addext, types_filenames
from .openers import ImageOpener

FileSpec = ty.Union[str, os.PathLike]
FileMap = ty.Mapping[str, FileHolder]
if ty.TYPE_CHECKING: # pragma: no cover
from .filename_parser import ExtensionSpec, FileSpec

FileSniff = ty.Tuple[bytes, str]

ImgT = ty.TypeVar('ImgT', bound='FileBasedImage')
Expand Down Expand Up @@ -160,7 +160,7 @@ class FileBasedImage:
header_class: Type[FileBasedHeader] = FileBasedHeader
_header: FileBasedHeader
_meta_sniff_len: int = 0
files_types: tuple[tuple[str, str | None], ...] = (('image', None),)
files_types: tuple[ExtensionSpec, ...] = (('image', None),)
valid_exts: tuple[str, ...] = ()
_compressed_suffixes: tuple[str, ...] = ()

Expand Down Expand Up @@ -411,7 +411,7 @@ def _sniff_meta_for(
t_fnames = types_filenames(
filename, klass.files_types, trailing_suffixes=klass._compressed_suffixes
)
meta_fname = t_fnames.get('header', filename)
meta_fname = t_fnames.get('header', _stringify_path(filename))

# Do not re-sniff if it would be from the same file
if sniff is not None and sniff[1] == meta_fname:
Expand Down
27 changes: 18 additions & 9 deletions nibabel/fileholders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Fileholder class"""
from __future__ import annotations

import io
import typing as ty
from copy import copy

from .openers import ImageOpener
Expand All @@ -19,7 +23,12 @@ class FileHolderError(Exception):
class FileHolder:
"""class to contain filename, fileobj and file position"""

def __init__(self, filename=None, fileobj=None, pos=0):
def __init__(
self,
filename: str | None = None,
fileobj: io.IOBase | None = None,
pos: int = 0,
):
"""Initialize FileHolder instance

Parameters
Expand All @@ -37,7 +46,7 @@ def __init__(self, filename=None, fileobj=None, pos=0):
self.fileobj = fileobj
self.pos = pos

def get_prepare_fileobj(self, *args, **kwargs):
def get_prepare_fileobj(self, *args, **kwargs) -> ImageOpener:
"""Return fileobj if present, or return fileobj from filename

Set position to that given in self.pos
Expand Down Expand Up @@ -69,7 +78,7 @@ def get_prepare_fileobj(self, *args, **kwargs):
raise FileHolderError('No filename or fileobj present')
return obj

def same_file_as(self, other):
def same_file_as(self, other: FileHolder) -> bool:
"""Test if `self` refers to same files / fileobj as `other`

Parameters
Expand All @@ -86,12 +95,15 @@ def same_file_as(self, other):
return (self.filename == other.filename) and (self.fileobj == other.fileobj)

@property
def file_like(self):
def file_like(self) -> str | io.IOBase | None:
"""Return ``self.fileobj`` if not None, otherwise ``self.filename``"""
return self.fileobj if self.fileobj is not None else self.filename


def copy_file_map(file_map):
FileMap = ty.Mapping[str, FileHolder]


def copy_file_map(file_map: FileMap) -> FileMap:
r"""Copy mapping of fileholders given by `file_map`

Parameters
Expand All @@ -105,7 +117,4 @@ def copy_file_map(file_map):
Copy of `file_map`, using shallow copy of ``FileHolder``\s

"""
fm_copy = {}
for key, fh in file_map.items():
fm_copy[key] = copy(fh)
return fm_copy
return {key: copy(fh) for key, fh in file_map.items()}
66 changes: 36 additions & 30 deletions nibabel/filename_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Create filename pairs, triplets etc, with expected extensions"""
from __future__ import annotations

import os
import pathlib
import typing as ty

if ty.TYPE_CHECKING: # pragma: no cover
FileSpec = str | os.PathLike[str]
ExtensionSpec = tuple[str, str | None]


class TypesFilenamesError(Exception):
pass


def _stringify_path(filepath_or_buffer):
def _stringify_path(filepath_or_buffer: FileSpec) -> str:
"""Attempt to convert a path-like object to a string.

Parameters
Expand All @@ -28,30 +34,21 @@ def _stringify_path(filepath_or_buffer):

Notes
-----
Objects supporting the fspath protocol (python 3.6+) are coerced
according to its __fspath__ method.
For backwards compatibility with older pythons, pathlib.Path objects
are specially coerced.
Any other object is passed through unchanged, which includes bytes,
strings, buffers, or anything else that's not even path-like.

Copied from:
https://github.com/pandas-dev/pandas/blob/325dd686de1589c17731cf93b649ed5ccb5a99b4/pandas/io/common.py#L131-L160
Adapted from:
https://github.com/pandas-dev/pandas/blob/325dd68/pandas/io/common.py#L131-L160
"""
if hasattr(filepath_or_buffer, '__fspath__'):
if isinstance(filepath_or_buffer, os.PathLike):
return filepath_or_buffer.__fspath__()
elif isinstance(filepath_or_buffer, pathlib.Path):
return str(filepath_or_buffer)
return filepath_or_buffer


def types_filenames(
template_fname,
types_exts,
trailing_suffixes=('.gz', '.bz2'),
enforce_extensions=True,
match_case=False,
):
template_fname: FileSpec,
types_exts: ty.Sequence[ExtensionSpec],
trailing_suffixes: ty.Sequence[str] = ('.gz', '.bz2'),
enforce_extensions: bool = True,
match_case: bool = False,
) -> dict[str, str]:
"""Return filenames with standard extensions from template name

The typical case is returning image and header filenames for an
Expand Down Expand Up @@ -152,12 +149,12 @@ def types_filenames(
# we've found .IMG as the extension, we want .HDR as the matching
# one. Let's only do this when the extension is all upper or all
# lower case.
proc_ext = lambda s: s
proc_ext: ty.Callable[[str], str] = lambda s: s
if found_ext:
if found_ext == found_ext.upper():
proc_ext = lambda s: s.upper()
proc_ext = str.upper
elif found_ext == found_ext.lower():
proc_ext = lambda s: s.lower()
proc_ext = str.lower
for name, ext in types_exts:
if name == direct_set_name:
tfns[name] = template_fname
Expand All @@ -171,7 +168,12 @@ def types_filenames(
return tfns


def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
def parse_filename(
filename: FileSpec,
types_exts: ty.Sequence[ExtensionSpec],
trailing_suffixes: ty.Sequence[str],
match_case: bool = False,
) -> tuple[str, str, str | None, str | None]:
"""Split filename into fileroot, extension, trailing suffix; guess type.

Parameters
Expand Down Expand Up @@ -230,9 +232,9 @@ def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
break
guessed_name = None
found_ext = None
for name, ext in types_exts:
if ext and endswith(filename, ext):
extpos = -len(ext)
for name, type_ext in types_exts:
if type_ext and endswith(filename, type_ext):
extpos = -len(type_ext)
found_ext = filename[extpos:]
filename = filename[:extpos]
guessed_name = name
Expand All @@ -242,15 +244,19 @@ def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
return (filename, found_ext, ignored, guessed_name)


def _endswith(whole, end):
def _endswith(whole: str, end: str) -> bool:
return whole.endswith(end)


def _iendswith(whole, end):
def _iendswith(whole: str, end: str) -> bool:
return whole.lower().endswith(end.lower())


def splitext_addext(filename, addexts=('.gz', '.bz2', '.zst'), match_case=False):
def splitext_addext(
filename: FileSpec,
addexts: ty.Sequence[str] = ('.gz', '.bz2', '.zst'),
match_case: bool = False,
) -> tuple[str, str, str]:
"""Split ``/pth/fname.ext.gz`` into ``/pth/fname, .ext, .gz``

where ``.gz`` may be any of passed `addext` trailing suffixes.
Expand Down
Loading