diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30579d2..b09022a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,7 @@ jobs: uses: jpetrucciani/mypy-check@master with: path: './src/fileseq' + mypy_flags: '--no-implicit-reexport --strict' deploy: if: github.event_name == 'release' && github.event.action == 'published' diff --git a/src/fileseq/constants.py b/src/fileseq/constants.py index 26546bf..07e6934 100644 --- a/src/fileseq/constants.py +++ b/src/fileseq/constants.py @@ -3,6 +3,7 @@ """ import re +import typing # The max frame count of a FrameSet before a MaxSizeException # exception is raised @@ -13,16 +14,16 @@ class _PadStyle(object): def __init__(self, name: str): self.__name = name - def __hash__(self): + def __hash__(self) -> int: return hash(str(self)) - def __repr__(self): + def __repr__(self) -> str: return ''.format(self.__name) - def __str__(self): + def __str__(self) -> str: return self.__name - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: if not isinstance(other, _PadStyle): return False return str(self) == str(other) diff --git a/src/fileseq/filesequence.py b/src/fileseq/filesequence.py index 4eaeec7..6c2e3eb 100644 --- a/src/fileseq/filesequence.py +++ b/src/fileseq/filesequence.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import collections.abc import dataclasses import decimal import fnmatch @@ -161,7 +162,7 @@ def copy(self) -> FileSequence: fs._frameSet = self._frameSet.copy() return fs - def format(self, template="{basename}{range}{padding}{extension}") -> str: + def format(self, template: str = "{basename}{range}{padding}{extension}") -> str: """Return the file sequence as a formatted string according to the given template. @@ -195,7 +196,7 @@ def format(self, template="{basename}{range}{padding}{extension}") -> str: except UnicodeEncodeError: return self._format(str(template)) - def _format(self, template): + def _format(self, template: str) -> str: # Potentially expensive if inverted range is large # and user never asked for it in template inverted = (self.invertedFrameRange() or "") if "{inverted}" in template else "" @@ -232,7 +233,7 @@ def dirname(self) -> str: """ return self._dir - def setDirname(self, dirname: str): + def setDirname(self, dirname: str) -> None: """ Set a new directory name for the sequence. @@ -257,7 +258,7 @@ def basename(self) -> str: """ return self._base - def setBasename(self, base: str): + def setBasename(self, base: str) -> None: """ Set a new basename for the sequence. @@ -276,7 +277,7 @@ def padStyle(self) -> constants._PadStyle: """ return self._pad_style - def setPadStyle(self, pad_style: constants._PadStyle, set_zfill: bool = False): + def setPadStyle(self, pad_style: constants._PadStyle, set_zfill: bool = False) -> None: """ Set new padding style for the sequence. See fileseq.constants.PAD_STYLE_HASH1 and fileseq.constants.PAD_STYLE_HASH4 @@ -321,7 +322,7 @@ def padding(self) -> str: """ return self._pad - def setPadding(self, padding: str): + def setPadding(self, padding: str) -> None: """ Set new padding characters for the sequence. i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting. @@ -354,7 +355,7 @@ def framePadding(self) -> str: """ return self._frame_pad - def setFramePadding(self, padding: str): + def setFramePadding(self, padding: str) -> None: """ Set new padding characters for the frames of the sequence. i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting. @@ -388,7 +389,7 @@ def subframePadding(self) -> str: """ return self._subframe_pad - def setSubframePadding(self, padding: str): + def setSubframePadding(self, padding: str) -> None: """ Set new padding characters for the subframes in the sequence. i.e. "#" or "@@@", or an empty string to disable range @@ -415,7 +416,7 @@ def setSubframePadding(self, padding: str): self._pad = pad self._decimal_places = decimal_places - def frameSet(self) -> FrameSet | None: + def frameSet(self) -> FrameSet|None: """ Return the :class:`.FrameSet` of the sequence if specified, otherwise None. @@ -425,7 +426,7 @@ def frameSet(self) -> FrameSet | None: """ return self._frameSet - def setFrameSet(self, frameSet: FrameSet | None): + def setFrameSet(self, frameSet: FrameSet|None) -> None: """ Set a new :class:`.FrameSet` for the sequence. @@ -452,7 +453,7 @@ def extension(self) -> str: """ return self._ext - def setExtension(self, ext: str): + def setExtension(self, ext: str) -> None: """ Set a new file extension for the sequence. @@ -466,7 +467,7 @@ def setExtension(self, ext: str): ext = "." + ext self._ext = utils.asString(ext) - def setExtention(self, ext: str): + def setExtention(self, ext: str) -> None: """ Deprecated: use :meth:`setExtension`. @@ -490,7 +491,7 @@ def frameRange(self) -> str: return '' return self._frameSet.frameRange(self._zfill, self._decimal_places) - def setFrameRange(self, frange): + def setFrameRange(self, frange: typing.Any) -> None: """ Set a new frame range for the sequence. @@ -610,7 +611,7 @@ def index(self, idx: int) -> str: Returns: str: """ - return self.__getitem__(idx) + return self.__getitem__(idx) # type: ignore def batches(self, batch_size: int, paths: bool = False) -> typing.Iterable[str | FileSequence]: """ @@ -638,7 +639,7 @@ def batches(self, batch_size: int, paths: bool = False) -> typing.Iterable[str | frame_gen = utils.batchFrames(0, len(self) - 1, batch_size) return (self[f.start:f.stop + 1] for f in frame_gen) - def __setstate__(self, state): + def __setstate__(self, state: typing.Any) -> None: """ Allows for de-serialization from a pickled :class:`FileSequence`. @@ -652,7 +653,7 @@ def __setstate__(self, state): self.__dict__.setdefault('_subframe_pad', '') self.__dict__.setdefault('_decimal_places', 0) - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, typing.Any]: """ Convert sequence object into a state dict that is suitable for further serialization, such as to JSON @@ -668,7 +669,7 @@ def to_dict(self) -> dict: return state @classmethod - def from_dict(cls, state: dict) -> FileSequence: + def from_dict(cls, state: dict[str, typing.Any]) -> FileSequence: """ Constructor to create a new sequence object from a state that was previously returned by :meth:`FileSequence.to_dict` @@ -691,13 +692,13 @@ def from_dict(cls, state: dict) -> FileSequence: fs.__setstate__(state) return fs - def __iter__(self): + def __iter__(self) -> collections.abc.Generator[str, None, None]: """ Allow iteration over the path or paths this :class:`FileSequence` represents. Yields: - :class:`FileSequence`: + str: path """ # If there is no frame range, or there is no padding # characters, then we only want to represent a single path @@ -708,7 +709,7 @@ def __iter__(self): for f in self._frameSet: yield self.frame(f) - def __getitem__(self, idx): + def __getitem__(self, idx: typing.Any) -> str|FileSequence: """ Allows indexing and slicing into the underlying :class:`.FrameSet` @@ -743,7 +744,7 @@ def __getitem__(self, idx): fs.setFrameSet(fset) return fs - def __len__(self): + def __len__(self) -> int: """ The length (number of files) represented by this :class:`FileSequence`. @@ -754,7 +755,7 @@ def __len__(self): return 1 return len(self._frameSet) - def __str__(self): + def __str__(self) -> str: """ String representation of this :class:`FileSequence`. @@ -772,28 +773,28 @@ def __str__(self): cmpts.frameSet = utils.asString(cmpts.frameSet or "") return "".join(dataclasses.astuple(cmpts)) - def __repr__(self): + def __repr__(self) -> str: try: return "<%s: %r>" % (self.__class__.__name__, self.__str__()) except TypeError: return super(self.__class__, self).__repr__() - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> bool: if not isinstance(other, FileSequence): return str(self) == str(other) a = self.__components() b = other.__components() - a.pad = self.getPaddingNum(a.pad) - b.pad = other.getPaddingNum(b.pad) + a.pad = self.getPaddingNum(str(a.pad)) + b.pad = other.getPaddingNum(str(b.pad)) return a == b - def __ne__(self, other): + def __ne__(self, other: typing.Any) -> bool: return not self.__eq__(other) - def __hash__(self): + def __hash__(self) -> int: # TODO: Technically we should be returning None, # as this class is mutable and cannot reliably be hashed. # Python2 allows it without this definition. @@ -801,7 +802,7 @@ def __hash__(self): # For now, preserving the hashing behaviour in py3. return id(self) - def __components(self): + def __components(self) -> _Components: return self._Components( self._dir, self._base, @@ -863,7 +864,7 @@ def yield_sequences_in_list( path: str for path in filter(None, map(utils.asString, paths)): - frame = path[head:tail] # type: ignore + frame = path[head:tail] try: int(frame) except ValueError: @@ -891,26 +892,27 @@ def yield_sequences_in_list( if frame: seqs[key].add(frame) - def start_new_seq(): + def start_new_seq() -> FileSequence: seq = cls.__new__(cls) seq._dir = dirname or '' seq._base = basename or '' seq._ext = ext or '' return seq - def finish_new_seq(seq): + def finish_new_seq(seq: FileSequence) -> None: if seq._subframe_pad: seq._pad = '.'.join([seq._frame_pad, seq._subframe_pad]) else: seq._pad = seq._frame_pad - seq.__init__(utils.asString(seq), pad_style=pad_style, allow_subframes=allow_subframes) + seq.__init__(utils.asString(seq), pad_style=pad_style, # type: ignore[misc] + allow_subframes=allow_subframes) - def get_frame_width(frame_str): + def get_frame_width(frame_str: str) -> int: frame_num, _, _ = frame_str.partition(".") return len(frame_num) - def get_frame_minwidth(frame_str): + def get_frame_minwidth(frame_str: str) -> int: # find the smallest padding width for a frame string frame_num, _, _ = frame_str.partition(".") size = len(frame_num) @@ -920,7 +922,7 @@ def get_frame_minwidth(frame_str): return 1 return size - def frames_to_seq(frames, pad_length, decimal_places): + def frames_to_seq(frames: typing.Iterable[str], pad_length: int, decimal_places: int) -> FileSequence: seq = start_new_seq() seq._frameSet = FrameSet(sorted(decimal.Decimal(f) for f in frames)) seq._frame_pad = cls.getPaddingChars(pad_length, pad_style=pad_style) @@ -950,11 +952,11 @@ def frames_to_seq(frames, pad_length, decimal_places): sorted_frames = sorted(((get_frame_width(f), f) for f in frames), key=operator.itemgetter(0)) current_frames: list[str] = [] - current_width = None + current_width = -1 for width, frame in sorted_frames: # initialize on first item - if current_width is None: + if current_width < 0: current_width = width if width != current_width and get_frame_minwidth(frame) > current_width: @@ -1231,8 +1233,11 @@ def findSequenceOnDisk( patt = r'.*[/\\]' patt += re.escape(basename) + '(.*)' + re.escape(ext) + r'\Z' - def get_frame(f): - return re.match(patt, f, re.I).group(1) + def get_frame(f: str) -> str: + m = re.match(patt, f, re.I) + if not m: + raise ValueError(f'no frame match: str={f}, pattern={patt}') + return m.group(1) if strictPadding: globbed = pad_filter_ctx( @@ -1310,14 +1315,19 @@ def _globCharsToRegex(filename: str) -> str: return filename class _FilterByPaddingNum(object): - def __init__(self): + def __init__(self) -> None: # Tracks whether a padded frame has been yielded: # padded: file.0001.ext # not padded: file.1001.ext self.has_padded_frames = False self.has_padded_subframes = False - def __call__(self, iterable, zfill, decimal_places=0, get_frame=None): + def __call__(self, + iterable: typing.Iterable[str], + zfill: int|None, + decimal_places: typing.Optional[int] = 0, + get_frame: typing.Optional[typing.Callable[[str], str]] = None + ) -> collections.abc.Generator[str, None, None]: """ Yield only path elements from iterable which have a frame padding that matches the given target padding numbers. If zfill is None only the @@ -1345,10 +1355,12 @@ def __call__(self, iterable, zfill, decimal_places=0, get_frame=None): has_padded_frame = False has_padded_subframe = False - def check_padded(frame): - return frame and (frame[0] == '0' or frame[:2] == '-0') + def check_padded(frame: str) -> bool: + if frame and (frame[0] == '0' or frame[:2] == '-0'): + return True + return False - def set_has_padded(): + def set_has_padded() -> None: if has_padded_frame: self.has_padded_frames = True if has_padded_subframe: @@ -1410,7 +1422,7 @@ def set_has_padded(): continue @classmethod - def _filterByPaddingNum(cls, *args, **kwargs): + def _filterByPaddingNum(cls, *args, **kwargs) -> typing.Generator[str]: # type: ignore ctx = cls._FilterByPaddingNum() return ctx(*args, **kwargs) diff --git a/src/fileseq/frameset.py b/src/fileseq/frameset.py index 69908d1..6d6ad76 100644 --- a/src/fileseq/frameset.py +++ b/src/fileseq/frameset.py @@ -5,8 +5,10 @@ import decimal import numbers +import re import typing from collections.abc import Set, Sized, Iterable +from typing import Union from . import constants # constants.MAX_FRAME_SIZE updated during tests from .constants import PAD_MAP, FRANGE_RE, PAD_RE @@ -15,7 +17,7 @@ normalizeFrame, normalizeFrames, batchIterable) -class FrameSet(Set): +class FrameSet(Set): # type:ignore[type-arg] """ A ``FrameSet`` is an immutable representation of the ordered, unique set of frames in a given frame range. @@ -86,7 +88,7 @@ class FrameSet(Set): _items: frozenset[int] _order: tuple[int, ...] - def __new__(cls, *args, **kwargs): + def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> FrameSet: """ Initialize the :class:`FrameSet` object. @@ -102,11 +104,11 @@ def __new__(cls, *args, **kwargs): self = super(cls, FrameSet).__new__(cls) return self - def __init__(self, frange): + def __init__(self, frange: typing.Any) -> None: """Initialize the :class:`FrameSet` object. """ - def catch_parse_err(fn, *a, **kw): + def catch_parse_err(fn, *a, **kw): # type: ignore try: return fn(*a, **kw) except (TypeError, ValueError) as e: @@ -122,27 +124,27 @@ def catch_parse_err(fn, *a, **kw): # if it's inherently disordered, sort and build elif isinstance(frange, Set): self._maxSizeCheck(frange) - self._items = frozenset(catch_parse_err(normalizeFrames, frange)) + self._items = frozenset(catch_parse_err(normalizeFrames, frange)) # type: ignore self._order = tuple(sorted(self._items)) - self._frange = catch_parse_err( + self._frange = catch_parse_err( # type: ignore self.framesToFrameRange, self._order, sort=False, compress=False) return # if it's ordered, find unique and build elif isinstance(frange, Sized) and isinstance(frange, Iterable): self._maxSizeCheck(frange) - items = set() - order = unique(items, catch_parse_err(normalizeFrames, frange)) + items: typing.Set[int] = set() + order = unique(items, catch_parse_err(normalizeFrames, frange)) # type: ignore self._order = tuple(order) self._items = frozenset(items) - self._frange = catch_parse_err( + self._frange = catch_parse_err( # type: ignore self.framesToFrameRange, self._order, sort=False, compress=False) return # if it's an individual number build directly elif isinstance(frange, (int, float, decimal.Decimal)): frame = normalizeFrame(frange) - self._order = (frame,) - self._items = frozenset([frame]) - self._frange = catch_parse_err( + self._order = (frame,) # type: ignore + self._items = frozenset([frame]) # type: ignore + self._frange = catch_parse_err( # type: ignore self.framesToFrameRange, self._order, sort=False, compress=False) # in all other cases, cast to a string else: @@ -167,12 +169,12 @@ def catch_parse_err(fn, *a, **kw): # build the mutable stores, then cast to immutable for storage items = set() - order = [] + order_f: typing.List[int] = [] maxSize = constants.MAX_FRAME_SIZE - frange_parts = [] - frange_types = [] + frange_parts: typing.List[typing.Any] = [] + frange_types: typing.List[typing.Any] = [] for part in self._frange.split(","): # this is to deal with leading / trailing commas if not part: @@ -186,15 +188,15 @@ def catch_parse_err(fn, *a, **kw): # _parse_frange_part will always return decimal.Decimal for subframes FrameType = int if decimal.Decimal in frange_types: - FrameType = decimal.Decimal + FrameType = decimal.Decimal # type: ignore for start, end, modifier, chunk in frange_parts: # handle batched frames (1-100x5) if modifier == 'x': frames = xfrange(start, end, chunk, maxSize=maxSize) - frames = [FrameType(f) for f in frames if f not in items] - self._maxSizeCheck(len(frames) + len(items)) - order.extend(frames) + frames = [FrameType(f) for f in frames if f not in items] # type: ignore + self._maxSizeCheck(len(frames) + len(items)) # type: ignore + order_f.extend(frames) items.update(frames) # handle staggered frames (1-100:5) elif modifier == ':': @@ -202,9 +204,9 @@ def catch_parse_err(fn, *a, **kw): raise ValueError("Unable to stagger subframes") for stagger in range(chunk, 0, -1): frames = xfrange(start, end, stagger, maxSize=maxSize) - frames = [f for f in frames if f not in items] - self._maxSizeCheck(len(frames) + len(items)) - order.extend(frames) + frames = [f for f in frames if f not in items] # type: ignore + self._maxSizeCheck(len(frames) + len(items)) # type: ignore + order_f.extend(frames) items.update(frames) # handle filled frames (1-100y5) elif modifier == 'y': @@ -213,22 +215,22 @@ def catch_parse_err(fn, *a, **kw): not_good = frozenset(xfrange(start, end, chunk, maxSize=maxSize)) frames = xfrange(start, end, 1, maxSize=maxSize) frames = (f for f in frames if f not in not_good) - frames = [f for f in frames if f not in items] - self._maxSizeCheck(len(frames) + len(items)) - order.extend(frames) + frames = [f for f in frames if f not in items] # type: ignore + self._maxSizeCheck(len(frames) + len(items)) # type: ignore + order_f.extend(frames) items.update(frames) # handle full ranges and single frames else: frames = xfrange(start, end, 1 if start < end else -1, maxSize=maxSize) - frames = [FrameType(f) for f in frames if f not in items] - self._maxSizeCheck(len(frames) + len(items)) - order.extend(frames) + frames = [FrameType(f) for f in frames if f not in items] # type: ignore + self._maxSizeCheck(len(frames) + len(items)) # type: ignore + order_f.extend(frames) items.update(frames) # lock the results into immutable internals # this allows for hashing and fast equality checking self._items = frozenset(items) - self._order = tuple(order) + self._order = tuple(order_f) @property def is_null(self) -> bool: @@ -249,7 +251,7 @@ def frange(self) -> str: Returns: str: """ - return self._frange + return self._frange or '' @property def items(self) -> frozenset[int]: @@ -305,17 +307,17 @@ def from_range(cls, start: int, end: int, step: int = 1) -> FrameSet: elif step == 0: raise ValueError("step argument must not be zero") elif step == 1: - start, end = normalizeFrames([start, end]) + start, end = normalizeFrames([start, end]) # type:ignore[assignment] range_str = "{0}-{1}".format(start, end) else: - start, end = normalizeFrames([start, end]) - step = normalizeFrame(step) + start, end = normalizeFrames([start, end]) # type:ignore[assignment] + step = normalizeFrame(step) # type: ignore range_str = "{0}-{1}x{2}".format(start, end, step) return FrameSet(range_str) @classmethod - def _cast_to_frameset(cls, other): + def _cast_to_frameset(cls, other: typing.Any) -> FrameSet: """ Private method to simplify comparison operations. @@ -515,7 +517,7 @@ def normalize(self) -> FrameSet: return FrameSet(FrameSet.framesToFrameRange( self.items, sort=True, compress=False)) - def batches(self, batch_size: int, frames: bool = False) -> typing.Iterator: + def batches(self, batch_size: int, frames: bool = False) -> typing.Iterator[typing.Any]: """ Returns a generator that yields sub-batches of frames, up to ``batch_size``. If ``frames=False``, each batch is a new ``FrameSet`` subrange. @@ -531,24 +533,24 @@ def batches(self, batch_size: int, frames: bool = False) -> typing.Iterator: batch_it = batchIterable(self, batch_size) if frames: # They just want batches of the frame values - return batch_it + return batch_it # type: ignore # return batches of FrameSet instance return (self.from_iterable(b) for b in batch_it) - def __getstate__(self): + def __getstate__(self) -> tuple[str]: """ Allows for serialization to a pickled :class:`FrameSet`. Returns: - tuple: (frame range string, ) + tuple: (frame range string, """ # we have to special-case the empty FrameSet, because of a quirk in # Python where __setstate__ will not be called if the return value of # bool(__getstate__) == False. A tuple with ('',) will return True. return (self.frange,) - def __setstate__(self, state): + def __setstate__(self, state: typing.Any) -> None: """ Allows for de-serialization from a pickled :class:`FrameSet`. @@ -562,11 +564,11 @@ def __setstate__(self, state): if isinstance(state, tuple): # this is to allow unpickling of "3rd generation" FrameSets, # which are immutable and may be empty. - self.__init__(state[0]) + self.__init__(state[0]) # type: ignore[misc] elif isinstance(state, str): # this is to allow unpickling of "2nd generation" FrameSets, # which were mutable and could not be empty. - self.__init__(state) + self.__init__(state) # type: ignore[misc] elif isinstance(state, dict): # this is to allow unpickling of "1st generation" FrameSets, # when the full __dict__ was stored @@ -581,7 +583,7 @@ def __setstate__(self, state): msg = "Unrecognized state data from which to deserialize FrameSet" raise ValueError(msg) - def __getitem__(self, index): + def __getitem__(self, index: int) -> int: """ Allows indexing into the ordered frames of this :class:`FrameSet`. @@ -596,7 +598,7 @@ def __getitem__(self, index): """ return self.order[index] - def __len__(self): + def __len__(self) -> int: """ Returns the length of the ordered frames of this :class:`FrameSet`. @@ -605,7 +607,7 @@ def __len__(self): """ return len(self.order) - def __str__(self): + def __str__(self) -> str: """ Returns the frame range string of this :class:`FrameSet`. @@ -614,7 +616,7 @@ def __str__(self): """ return self.frange - def __repr__(self): + def __repr__(self) -> str: """ Returns a long-form representation of this :class:`FrameSet`. @@ -623,7 +625,7 @@ def __repr__(self): """ return '{0}("{1}")'.format(self.__class__.__name__, self.frange) - def __iter__(self): + def __iter__(self): # type: ignore """ Allows for iteration over the ordered frames of this :class:`FrameSet`. @@ -632,7 +634,7 @@ def __iter__(self): """ return (i for i in self.order) - def __reversed__(self): + def __reversed__(self): # type: ignore """ Allows for reversed iteration over the ordered frames of this :class:`FrameSet`. @@ -642,7 +644,7 @@ def __reversed__(self): """ return (i for i in reversed(self.order)) - def __contains__(self, item): + def __contains__(self, item: typing.Any) -> bool: """ Check if item is a member of this :class:`FrameSet`. @@ -654,7 +656,7 @@ def __contains__(self, item): """ return item in self.items - def __hash__(self): + def __hash__(self) -> int: """ Builds the hash of this :class:`FrameSet` for equality checking and to allow use as a dictionary key. @@ -664,7 +666,7 @@ def __hash__(self): """ return hash(self.frange) | hash(self.items) | hash(self.order) - def __lt__(self, other): + def __lt__(self, other: typing.Any) -> typing.Any: """ Check if self < other via a comparison of the contents. If other is not a :class:`FrameSet`, but is a set, frozenset, or is iterable, it will be @@ -694,7 +696,7 @@ def __lt__(self, other): return self.items < other.items or ( self.items == other.items and self.order < other.order) - def __le__(self, other): + def __le__(self, other: typing.Any) -> typing.Any: """ Check if `self` <= `other` via a comparison of the contents. If `other` is not a :class:`FrameSet`, but is a set, frozenset, or @@ -712,7 +714,7 @@ def __le__(self, other): return NotImplemented return self.items <= other.items - def __eq__(self, other): + def __eq__(self, other: typing.Any) -> typing.Any: """ Check if `self` == `other` via a comparison of the hash of their contents. @@ -734,7 +736,7 @@ def __eq__(self, other): that = hash(other.items) | hash(other.order) return this == that - def __ne__(self, other): + def __ne__(self, other: typing.Any) -> typing.Any: """ Check if `self` != `other` via a comparison of the hash of their contents. @@ -753,7 +755,7 @@ def __ne__(self, other): return not is_equals return is_equals - def __ge__(self, other): + def __ge__(self, other: typing.Any) -> typing.Any: """ Check if `self` >= `other` via a comparison of the contents. If `other` is not a :class:`FrameSet`, but is a set, frozenset, or @@ -771,7 +773,7 @@ def __ge__(self, other): return NotImplemented return self.items >= other.items - def __gt__(self, other): + def __gt__(self, other: typing.Any) -> typing.Any: """ Check if `self` > `other` via a comparison of the contents. If `other` is not a :class:`FrameSet`, but is a set, frozenset, or @@ -801,7 +803,7 @@ def __gt__(self, other): return self.items > other.items or ( self.items == other.items and self.order > other.order) - def __and__(self, other): + def __and__(self, other: typing.Any) -> typing.Any: """ Overloads the ``&`` operator. Returns a new :class:`FrameSet` that holds only the @@ -826,7 +828,7 @@ def __and__(self, other): __rand__ = __and__ - def __sub__(self, other): + def __sub__(self, other: typing.Any) -> typing.Any: """ Overloads the ``-`` operator. Returns a new :class:`FrameSet` that holds only the @@ -848,7 +850,7 @@ def __sub__(self, other): return NotImplemented return self.from_iterable(self.items - other.items, sort=True) - def __rsub__(self, other): + def __rsub__(self, other: typing.Any) -> typing.Any: """ Overloads the ``-`` operator. Returns a new :class:`FrameSet` that holds only the @@ -870,7 +872,7 @@ def __rsub__(self, other): return NotImplemented return self.from_iterable(other.items - self.items, sort=True) - def __or__(self, other): + def __or__(self, other: typing.Any) -> typing.Any: """ Overloads the ``|`` operator. Returns a new :class:`FrameSet` that holds all the @@ -895,7 +897,7 @@ def __or__(self, other): __ror__ = __or__ - def __xor__(self, other): + def __xor__(self, other: typing.Any) -> typing.Any: """ Overloads the ``^`` operator. Returns a new :class:`FrameSet` that holds all the @@ -919,7 +921,7 @@ def __xor__(self, other): __rxor__ = __xor__ - def isdisjoint(self, other) -> bool | NotImplemented: # type: ignore + def isdisjoint(self, other: typing.Any) -> bool | NotImplemented: # type: ignore """ Check if the contents of :class:self has no common intersection with the contents of :class:other. @@ -936,7 +938,7 @@ def isdisjoint(self, other) -> bool | NotImplemented: # type: ignore return NotImplemented return self.items.isdisjoint(other.items) - def issubset(self, other) -> bool | NotImplemented: # type: ignore + def issubset(self, other: typing.Any) -> bool | NotImplemented: # type: ignore """ Check if the contents of `self` is a subset of the contents of `other.` @@ -951,9 +953,9 @@ def issubset(self, other) -> bool | NotImplemented: # type: ignore other = self._cast_to_frameset(other) if other is NotImplemented: return NotImplemented - return self.items <= other.items + return self.items <= other.items # type: ignore - def issuperset(self, other) -> bool | NotImplemented: # type: ignore + def issuperset(self, other: typing.Any) -> bool | NotImplemented: # type: ignore """ Check if the contents of `self` is a superset of the contents of `other.` @@ -968,9 +970,9 @@ def issuperset(self, other) -> bool | NotImplemented: # type: ignore other = self._cast_to_frameset(other) if other is NotImplemented: return NotImplemented - return self.items >= other.items + return self.items >= other.items # type: ignore - def union(self, *other) -> FrameSet: + def union(self, *other: typing.Any) -> FrameSet: """ Returns a new :class:`FrameSet` with the elements of `self` and of `other`. @@ -984,7 +986,7 @@ def union(self, *other) -> FrameSet: from_frozenset = self.items.union(*(set(o) for o in other)) return self.from_iterable(from_frozenset, sort=True) - def intersection(self, *other) -> FrameSet: + def intersection(self, *other: typing.Any) -> FrameSet: """ Returns a new :class:`FrameSet` with the elements common to `self` and `other`. @@ -998,7 +1000,7 @@ def intersection(self, *other) -> FrameSet: from_frozenset = self.items.intersection(*(set(o) for o in other)) return self.from_iterable(from_frozenset, sort=True) - def difference(self, *other) -> FrameSet: + def difference(self, *other: typing.Any) -> FrameSet: """ Returns a new :class:`FrameSet` with elements in `self` but not in `other`. @@ -1012,7 +1014,7 @@ def difference(self, *other) -> FrameSet: from_frozenset = self.items.difference(*(set(o) for o in other)) return self.from_iterable(from_frozenset, sort=True) - def symmetric_difference(self, other) -> FrameSet: + def symmetric_difference(self, other: typing.Any) -> FrameSet: """ Returns a new :class:`FrameSet` that contains all the elements in either `self` or `other`, but not both. @@ -1043,7 +1045,7 @@ def copy(self) -> FrameSet: return fs @classmethod - def _maxSizeCheck(cls, obj: int | float | decimal.Decimal | Sized): + def _maxSizeCheck(cls, obj: int | float | decimal.Decimal | Sized) -> None: """ Raise a MaxSizeException if ``obj`` exceeds MAX_FRAME_SIZE @@ -1114,7 +1116,7 @@ def padFrameRange(cls, frange: str, zfill: int, decimal_places: int | None = Non str: """ - def _do_pad(match): + def _do_pad(match: typing.Any) -> str: """ Substitutes padded for unpadded frames. """ @@ -1156,7 +1158,7 @@ def _parse_frange_part(cls, frange: str) -> tuple[int, int, str, int]: end = normalizeFrame(end) if end is not None else start chunk = normalizeFrame(chunk) if chunk is not None else 1 - if end > start and chunk is not None and chunk < 0: + if end > start and chunk is not None and chunk < 0: # type: ignore[operator] msg = 'Could not parse "{0}: chunk can not be negative' raise ParseException(msg.format(frange)) @@ -1165,10 +1167,10 @@ def _parse_frange_part(cls, frange: str) -> tuple[int, int, str, int]: msg = 'Could not parse "{0}": chunk cannot be 0' raise ParseException(msg.format(frange)) - return start, end, modifier, abs(chunk) + return start, end, modifier, abs(chunk) # type: ignore @staticmethod - def _build_frange_part(start: object, stop: object, stride: int | decimal.Decimal | None, zfill: int = 0) -> str: + def _build_frange_part(start: object, stop: object, stride: int|float|decimal.Decimal|None, zfill: int = 0) -> str: """ Private method: builds a proper and padded frame range string. @@ -1202,7 +1204,7 @@ def _build_frange_part_decimal( min_stride: decimal.Decimal, max_stride: decimal.Decimal, zfill: int = 0 - ): + ) -> str: """ Private method: builds a proper and padded subframe range string from decimal values. @@ -1243,11 +1245,14 @@ def _build_frange_part_decimal( delta = decimal.Decimal(1).scaleb(exponent) stop += delta.copy_sign(stop) - start, stop = normalizeFrames([start, stop]) + start, stop = normalizeFrames([start, stop]) # type:ignore[assignment] return FrameSet._build_frange_part(start, stop, stride, zfill=zfill) @staticmethod - def framesToFrameRanges(frames: typing.Iterable, zfill: int = 0) -> typing.Iterator[str]: + def framesToFrameRanges( + frames: typing.Iterable[typing.Any], + zfill: int = 0 + ) -> typing.Iterator[str]: """ Converts a sequence of frames to a series of padded frame range strings. @@ -1370,7 +1375,12 @@ def framesToFrameRanges(frames: typing.Iterable, zfill: int = 0) -> typing.Itera yield _build(curr_start, curr_frame, curr_stride, zfill) @staticmethod - def framesToFrameRange(frames: typing.Iterable, sort: bool = True, zfill: int = 0, compress: bool = False) -> str: + def framesToFrameRange( + frames: typing.Iterable[typing.Any], + sort: bool = True, + zfill: int = 0, + compress: bool = False + ) -> str: """ Converts an iterator of frames into a frame range string. diff --git a/src/fileseq/utils.py b/src/fileseq/utils.py index f6e833f..d180f41 100644 --- a/src/fileseq/utils.py +++ b/src/fileseq/utils.py @@ -1,7 +1,9 @@ """ utils - General tools of use to fileseq operations. """ +from __future__ import annotations +import collections.abc import decimal import os import typing @@ -15,7 +17,11 @@ FILESYSTEM_ENCODING = sys.getfilesystemencoding() or 'utf-8' -def quantize(number, decimal_places, rounding=decimal.ROUND_HALF_EVEN): +def quantize( + number: decimal.Decimal, + decimal_places: int, + rounding: str = decimal.ROUND_HALF_EVEN + ) -> decimal.Decimal: """ Round a decimal value to given number of decimal places @@ -35,7 +41,7 @@ def quantize(number, decimal_places, rounding=decimal.ROUND_HALF_EVEN): return nq -def lenRange(start, stop, step=1): +def lenRange(start: int, stop: int, step: int = 1) -> int: """ Get the length of values for a given range, exclusive of the stop @@ -67,7 +73,7 @@ class xrange2(object): __slots__ = ['_len', '_islice', '_start', '_stop', '_step'] - def __init__(self, start, stop=None, step=1): + def __init__(self, start: int, stop: typing.Optional[int] = None, step: int = 1): if stop is None: start, stop = 0, start @@ -77,31 +83,31 @@ def __init__(self, start, stop=None, step=1): self._stop = stop self._step = step - def __repr__(self): + def __repr__(self) -> str: if self._step == 1: return 'range({}, {})'.format(self._start, self._stop) else: return 'range({}, {}, {})'.format(self._start, self._stop, self._step) - def __len__(self): + def __len__(self) -> int: return self._len - def __next__(self): + def __next__(self) -> int: return next(self._islice) - def __iter__(self): + def __iter__(self) -> typing.Iterable[typing.Any]: return self._islice.__iter__() @property - def start(self): + def start(self) -> int: return self._start @property - def stop(self): + def stop(self) -> int: return self._stop @property - def step(self): + def step(self) -> int: return self._step @@ -117,42 +123,43 @@ def step(self): class _islice(object): - def __init__(self, gen, start, stop, step=1): + def __init__(self, gen: typing.Iterable[typing.Any], start: int, stop: int, step: int = 1): self._gen = gen self._start = start self._stop = stop self._step = step - def __len__(self): + def __len__(self) -> int: return lenRange(self._start, self._stop, self._step) - def __next__(self): - return next(self._gen) + def __next__(self) -> typing.Any: + # noinspection PyTypeChecker + return next(self._gen) # type:ignore - def __iter__(self): + def __iter__(self) -> typing.Iterable[typing.Any]: return self._gen.__iter__() @property - def start(self): + def start(self) -> int: return self._start @property - def stop(self): + def stop(self) -> int: return self._stop @property - def step(self): + def step(self) -> int: return self._step class _xfrange(_islice): - def __len__(self): + def __len__(self) -> int: stop = self._stop + (1 if self._start <= self._stop else -1) return lenRange(self._start, stop, self._step) -def xfrange(start, stop, step=1, maxSize=-1): +def xfrange(start: int, stop: int, step: int = 1, maxSize: int = -1) -> typing.Generator[typing.Any, None, None]: """ Returns a generator that yields the frames from start to stop, inclusive. In other words it adds or subtracts a frame, as necessary, to return the @@ -173,7 +180,7 @@ def xfrange(start, stop, step=1, maxSize=-1): if not step: raise ValueError('xfrange() step argument must not be zero') - start, stop, step = normalizeFrames([start, stop, step]) + start, stop, step = normalizeFrames([start, stop, step]) # type:ignore[assignment] if start <= stop: step = abs(step) @@ -193,14 +200,14 @@ def xfrange(start, stop, step=1, maxSize=-1): # generator expression to get a proper Generator if isinstance(start, int): offset = step // abs(step) - gen = (f for f in range(start, stop + offset, step)) + gen = (f for f in range(start, stop + offset, step)) # type:ignore else: gen = (start + i * step for i in range(size)) - return _xfrange(gen, start, stop, step) + return _xfrange(gen, start, stop, step) # type:ignore -def batchFrames(start, stop, batch_size): +def batchFrames(start: int, stop: int, batch_size: int) -> typing.Iterable[typing.Any]: """ Returns a generator that yields batches of frames from start to stop, inclusive. Each batch value is a ``range`` generator object, also providing start, stop, and @@ -229,7 +236,7 @@ def batchFrames(start, stop, batch_size): yield xfrange(i, sub_stop) -def batchIterable(it, batch_size): +def batchIterable(it: typing.Iterable[typing.Any], batch_size: int) -> typing.Iterable[typing.Any]: """ Returns a generator that yields batches of items returned by the given iterable. The last batch frame length may be smaller if the batches cannot be divided evenly. @@ -248,20 +255,20 @@ def batchIterable(it, batch_size): # known length, then we have to use a less efficient # method that builds results by exhausting the generator try: - length = len(it) + length = len(it) # type:ignore except TypeError: for b in _batchGenerator(it, batch_size): yield b return # We can use the known length to yield slices - for start in xrange(0, length, batch_size): + for start in xrange(0, length, batch_size): # type:ignore stop = start + batch_size gen = islice(it, start, stop) yield _islice(gen, start, stop) -def _batchGenerator(gen, batch_size): +def _batchGenerator(gen: typing.Iterable[typing.Any], batch_size: int) -> typing.Generator[typing.Any, None, None]: """ A batching generator function that handles a generator type, where the length isn't known. @@ -283,7 +290,7 @@ def _batchGenerator(gen, batch_size): yield batch -def normalizeFrame(frame): +def normalizeFrame(frame: int | float | decimal.Decimal | str) -> int | float | decimal.Decimal | None: """ Convert a frame number to the most appropriate type - the most compact type that doesn't affect precision, for example numbers that convert exactly @@ -316,12 +323,12 @@ def normalizeFrame(frame): try: frame = decimal.Decimal(frame) except decimal.DecimalException: - return frame + return frame # type:ignore[return-value] else: return normalizeFrame(frame) -def normalizeFrames(frames: typing.Iterable[typing.Any]) -> list: +def normalizeFrames(frames: typing.Iterable[typing.Any]) -> list[int | float | decimal.Decimal]: """ Convert a sequence of frame numbers to the most appropriate type for the overall sequence, where all members of the result are of the same type. @@ -364,7 +371,10 @@ def normalizeFrames(frames: typing.Iterable[typing.Any]) -> list: return frames -def unique(seen, *iterables): +def unique( + seen: typing.Set[typing.Any], + *iterables: typing.Iterable[typing.Any] + ) -> typing.Generator[typing.Any, None, None]: """ Get the unique items in iterables while preserving order. Note that this mutates the seen set provided only when the returned generator is used. @@ -382,7 +392,7 @@ def unique(seen, *iterables): return (i for i in chain(*iterables) if i not in seen and not _add(i)) -def pad(number, width=0, decimal_places=None): +def pad(number: typing.Any, width: typing.Optional[int] = 0, decimal_places: typing.Optional[int] = None) -> str: """ Return the zero-padded string of a given number. @@ -404,7 +414,7 @@ def pad(number, width=0, decimal_places=None): number = round(number) or 0 except TypeError: pass - return str(number).partition(".")[0].zfill(width) + return str(number).partition(".")[0].zfill(width) # type:ignore[arg-type] # USD ultimately uses vsnprintf to format floats for templateAssetPath: # _DeriveClipTimeString -> TfStringPrintf -> ArchVStringPrintf -> ArchVsnprintf -> vsnprintf @@ -424,7 +434,7 @@ def pad(number, width=0, decimal_places=None): return ".".join(parts) -def _getPathSep(path): +def _getPathSep(path: str) -> str: """ Abstracts returning the appropriate path separator for the given path string. @@ -445,26 +455,26 @@ def _getPathSep(path): _STR_TYPES = frozenset((str, bytes)) -def asString(obj): +def asString(obj: object) -> str: """ - Ensure an object is either explicitly str or unicode + Ensure an object is explicitly str type and not some derived type that can change semantics. - If the object is unicode, return unicode. + If the object is str, return str. Otherwise, return the string conversion of the object. Args: - obj: Object to return as str or unicode + obj: Object to return as str Returns: - str or unicode: + str: """ typ = type(obj) # explicit type check as faster path if typ in _STR_TYPES: if typ is bytes: - obj = os.fsdecode(obj) - return obj + obj = os.fsdecode(obj) # type: ignore + return obj # type: ignore # derived type check elif isinstance(obj, bytes): obj = obj.decode(FILESYSTEM_ENCODING)