Skip to content

Commit b8b57cc

Browse files
authored
Merge pull request #1197 from effigies/type/filehandling
TYPE: Annotate file-handling modules
2 parents cbd7690 + d13768f commit b8b57cc

7 files changed

+183
-115
lines changed

nibabel/dataobj_images.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515

1616
from .arrayproxy import ArrayLike
1717
from .deprecated import deprecate_with_version
18-
from .filebasedimages import FileBasedHeader, FileBasedImage, FileMap, FileSpec
18+
from .filebasedimages import FileBasedHeader, FileBasedImage
19+
from .fileholders import FileMap
1920

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

24+
from .filename_parser import FileSpec
25+
2326
ArrayImgT = ty.TypeVar('ArrayImgT', bound='DataobjImage')
2427

2528

nibabel/filebasedimages.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
from __future__ import annotations
1111

1212
import io
13-
import os
1413
import typing as ty
1514
from copy import deepcopy
1615
from typing import Type
1716
from urllib import request
1817

19-
from .fileholders import FileHolder
20-
from .filename_parser import TypesFilenamesError, splitext_addext, types_filenames
18+
from .fileholders import FileHolder, FileMap
19+
from .filename_parser import TypesFilenamesError, _stringify_path, splitext_addext, types_filenames
2120
from .openers import ImageOpener
2221

23-
FileSpec = ty.Union[str, os.PathLike]
24-
FileMap = ty.Mapping[str, FileHolder]
22+
if ty.TYPE_CHECKING: # pragma: no cover
23+
from .filename_parser import ExtensionSpec, FileSpec
24+
2525
FileSniff = ty.Tuple[bytes, str]
2626

2727
ImgT = ty.TypeVar('ImgT', bound='FileBasedImage')
@@ -160,7 +160,7 @@ class FileBasedImage:
160160
header_class: Type[FileBasedHeader] = FileBasedHeader
161161
_header: FileBasedHeader
162162
_meta_sniff_len: int = 0
163-
files_types: tuple[tuple[str, str | None], ...] = (('image', None),)
163+
files_types: tuple[ExtensionSpec, ...] = (('image', None),)
164164
valid_exts: tuple[str, ...] = ()
165165
_compressed_suffixes: tuple[str, ...] = ()
166166

@@ -411,7 +411,7 @@ def _sniff_meta_for(
411411
t_fnames = types_filenames(
412412
filename, klass.files_types, trailing_suffixes=klass._compressed_suffixes
413413
)
414-
meta_fname = t_fnames.get('header', filename)
414+
meta_fname = t_fnames.get('header', _stringify_path(filename))
415415

416416
# Do not re-sniff if it would be from the same file
417417
if sniff is not None and sniff[1] == meta_fname:

nibabel/fileholders.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
"""Fileholder class"""
10+
from __future__ import annotations
11+
12+
import io
13+
import typing as ty
1014
from copy import copy
1115

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

22-
def __init__(self, filename=None, fileobj=None, pos=0):
26+
def __init__(
27+
self,
28+
filename: str | None = None,
29+
fileobj: io.IOBase | None = None,
30+
pos: int = 0,
31+
):
2332
"""Initialize FileHolder instance
2433
2534
Parameters
@@ -37,7 +46,7 @@ def __init__(self, filename=None, fileobj=None, pos=0):
3746
self.fileobj = fileobj
3847
self.pos = pos
3948

40-
def get_prepare_fileobj(self, *args, **kwargs):
49+
def get_prepare_fileobj(self, *args, **kwargs) -> ImageOpener:
4150
"""Return fileobj if present, or return fileobj from filename
4251
4352
Set position to that given in self.pos
@@ -69,7 +78,7 @@ def get_prepare_fileobj(self, *args, **kwargs):
6978
raise FileHolderError('No filename or fileobj present')
7079
return obj
7180

72-
def same_file_as(self, other):
81+
def same_file_as(self, other: FileHolder) -> bool:
7382
"""Test if `self` refers to same files / fileobj as `other`
7483
7584
Parameters
@@ -86,12 +95,15 @@ def same_file_as(self, other):
8695
return (self.filename == other.filename) and (self.fileobj == other.fileobj)
8796

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

93102

94-
def copy_file_map(file_map):
103+
FileMap = ty.Mapping[str, FileHolder]
104+
105+
106+
def copy_file_map(file_map: FileMap) -> FileMap:
95107
r"""Copy mapping of fileholders given by `file_map`
96108
97109
Parameters
@@ -105,7 +117,4 @@ def copy_file_map(file_map):
105117
Copy of `file_map`, using shallow copy of ``FileHolder``\s
106118
107119
"""
108-
fm_copy = {}
109-
for key, fh in file_map.items():
110-
fm_copy[key] = copy(fh)
111-
return fm_copy
120+
return {key: copy(fh) for key, fh in file_map.items()}

nibabel/filename_parser.py

+36-30
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@
77
#
88
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
99
"""Create filename pairs, triplets etc, with expected extensions"""
10+
from __future__ import annotations
11+
1012
import os
11-
import pathlib
13+
import typing as ty
14+
15+
if ty.TYPE_CHECKING: # pragma: no cover
16+
FileSpec = str | os.PathLike[str]
17+
ExtensionSpec = tuple[str, str | None]
1218

1319

1420
class TypesFilenamesError(Exception):
1521
pass
1622

1723

18-
def _stringify_path(filepath_or_buffer):
24+
def _stringify_path(filepath_or_buffer: FileSpec) -> str:
1925
"""Attempt to convert a path-like object to a string.
2026
2127
Parameters
@@ -28,30 +34,21 @@ def _stringify_path(filepath_or_buffer):
2834
2935
Notes
3036
-----
31-
Objects supporting the fspath protocol (python 3.6+) are coerced
32-
according to its __fspath__ method.
33-
For backwards compatibility with older pythons, pathlib.Path objects
34-
are specially coerced.
35-
Any other object is passed through unchanged, which includes bytes,
36-
strings, buffers, or anything else that's not even path-like.
37-
38-
Copied from:
39-
https://github.com/pandas-dev/pandas/blob/325dd686de1589c17731cf93b649ed5ccb5a99b4/pandas/io/common.py#L131-L160
37+
Adapted from:
38+
https://github.com/pandas-dev/pandas/blob/325dd68/pandas/io/common.py#L131-L160
4039
"""
41-
if hasattr(filepath_or_buffer, '__fspath__'):
40+
if isinstance(filepath_or_buffer, os.PathLike):
4241
return filepath_or_buffer.__fspath__()
43-
elif isinstance(filepath_or_buffer, pathlib.Path):
44-
return str(filepath_or_buffer)
4542
return filepath_or_buffer
4643

4744

4845
def types_filenames(
49-
template_fname,
50-
types_exts,
51-
trailing_suffixes=('.gz', '.bz2'),
52-
enforce_extensions=True,
53-
match_case=False,
54-
):
46+
template_fname: FileSpec,
47+
types_exts: ty.Sequence[ExtensionSpec],
48+
trailing_suffixes: ty.Sequence[str] = ('.gz', '.bz2'),
49+
enforce_extensions: bool = True,
50+
match_case: bool = False,
51+
) -> dict[str, str]:
5552
"""Return filenames with standard extensions from template name
5653
5754
The typical case is returning image and header filenames for an
@@ -152,12 +149,12 @@ def types_filenames(
152149
# we've found .IMG as the extension, we want .HDR as the matching
153150
# one. Let's only do this when the extension is all upper or all
154151
# lower case.
155-
proc_ext = lambda s: s
152+
proc_ext: ty.Callable[[str], str] = lambda s: s
156153
if found_ext:
157154
if found_ext == found_ext.upper():
158-
proc_ext = lambda s: s.upper()
155+
proc_ext = str.upper
159156
elif found_ext == found_ext.lower():
160-
proc_ext = lambda s: s.lower()
157+
proc_ext = str.lower
161158
for name, ext in types_exts:
162159
if name == direct_set_name:
163160
tfns[name] = template_fname
@@ -171,7 +168,12 @@ def types_filenames(
171168
return tfns
172169

173170

174-
def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
171+
def parse_filename(
172+
filename: FileSpec,
173+
types_exts: ty.Sequence[ExtensionSpec],
174+
trailing_suffixes: ty.Sequence[str],
175+
match_case: bool = False,
176+
) -> tuple[str, str, str | None, str | None]:
175177
"""Split filename into fileroot, extension, trailing suffix; guess type.
176178
177179
Parameters
@@ -230,9 +232,9 @@ def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
230232
break
231233
guessed_name = None
232234
found_ext = None
233-
for name, ext in types_exts:
234-
if ext and endswith(filename, ext):
235-
extpos = -len(ext)
235+
for name, type_ext in types_exts:
236+
if type_ext and endswith(filename, type_ext):
237+
extpos = -len(type_ext)
236238
found_ext = filename[extpos:]
237239
filename = filename[:extpos]
238240
guessed_name = name
@@ -242,15 +244,19 @@ def parse_filename(filename, types_exts, trailing_suffixes, match_case=False):
242244
return (filename, found_ext, ignored, guessed_name)
243245

244246

245-
def _endswith(whole, end):
247+
def _endswith(whole: str, end: str) -> bool:
246248
return whole.endswith(end)
247249

248250

249-
def _iendswith(whole, end):
251+
def _iendswith(whole: str, end: str) -> bool:
250252
return whole.lower().endswith(end.lower())
251253

252254

253-
def splitext_addext(filename, addexts=('.gz', '.bz2', '.zst'), match_case=False):
255+
def splitext_addext(
256+
filename: FileSpec,
257+
addexts: ty.Sequence[str] = ('.gz', '.bz2', '.zst'),
258+
match_case: bool = False,
259+
) -> tuple[str, str, str]:
254260
"""Split ``/pth/fname.ext.gz`` into ``/pth/fname, .ext, .gz``
255261
256262
where ``.gz`` may be any of passed `addext` trailing suffixes.

0 commit comments

Comments
 (0)