Skip to content

Commit

Permalink
Merge pull request #1197 from effigies/type/filehandling
Browse files Browse the repository at this point in the history
TYPE: Annotate file-handling modules
  • Loading branch information
effigies authored Feb 19, 2023
2 parents cbd7690 + d13768f commit b8b57cc
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 115 deletions.
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

0 comments on commit b8b57cc

Please sign in to comment.