diff --git a/music21/common/classTools.py b/music21/common/classTools.py index fb6dae8d4..96afcb0c2 100644 --- a/music21/common/classTools.py +++ b/music21/common/classTools.py @@ -230,7 +230,10 @@ def classToClassStr(classObj: type) -> str: return str(classObj).rsplit('.', maxsplit=1)[-1][:-2] -def getClassSet(instance, classNameTuple=None): +def getClassSet( + instance: object, + classNameTuple: tuple[str, ...]|None = None +) -> frozenset[str|type]: ''' Return the classSet for an instance (whether a Music21Object or something else). See base.Music21Object.classSet for more details. @@ -280,7 +283,11 @@ def getClassSet(instance, classNameTuple=None): @contextlib.contextmanager -def tempAttribute(obj, attribute: str, new_val=TEMP_ATTRIBUTE_SENTINEL): +def tempAttribute( + obj: object, + attribute: str, + new_val: t.Any = TEMP_ATTRIBUTE_SENTINEL +) -> t.Generator[None, None, None]: ''' Temporarily set an attribute in an object to another value and then restore it afterwards. @@ -309,7 +316,7 @@ def tempAttribute(obj, attribute: str, new_val=TEMP_ATTRIBUTE_SENTINEL): setattr(obj, attribute, tempStorage) @contextlib.contextmanager -def saveAttributes(obj, *attributeList: str) -> t.Generator[None, None, None]: +def saveAttributes(obj: object, *attributeList: str) -> t.Generator[None, None, None]: ''' Save a number of attributes in an object and then restore them afterwards. diff --git a/music21/common/decorators.py b/music21/common/decorators.py index 1317d680a..a71e37455 100644 --- a/music21/common/decorators.py +++ b/music21/common/decorators.py @@ -10,23 +10,41 @@ # ------------------------------------------------------------------------------ from __future__ import annotations +from collections.abc import Callable from functools import wraps +import typing as t import warnings from music21 import exceptions21 __all__ = ['optional_arg_decorator', 'deprecated', 'cacheMethod'] + +class _DeprecatedCallInfo(t.TypedDict): + calledAlready: bool + message: str + + +class _HasCache(t.Protocol): + '''An object exposing the `._cache` dict that :func:`cacheMethod` reads and writes.''' + _cache: dict[str, t.Any] + + +_CacheP = t.ParamSpec('_CacheP') +_CacheReturn = t.TypeVar('_CacheReturn') +_CacheInstance = t.TypeVar('_CacheInstance', bound=_HasCache) + + # from Ryne Everett # http://stackoverflow.com/questions/3888158/python-making-decorators-with-optional-arguments -def optional_arg_decorator(fn): +def optional_arg_decorator(fn: Callable[..., t.Any]) -> Callable[..., t.Any]: ''' a decorator for decorators. Allows them to either have or not have arguments. ''' @wraps(fn) - def wrapped_decorator(*arguments, **keywords): + def wrapped_decorator(*arguments: t.Any, **keywords: t.Any) -> t.Any: is_bound_method = hasattr(arguments[0], fn.__name__) if arguments else False klass = None @@ -42,7 +60,7 @@ def wrapped_decorator(*arguments, **keywords): return fn(arguments[0]) else: - def real_decorator(toBeDecorated): + def real_decorator(toBeDecorated: Callable[..., t.Any]) -> t.Any: if is_bound_method: return fn(klass, toBeDecorated, *arguments, **keywords) else: @@ -52,7 +70,12 @@ def real_decorator(toBeDecorated): @optional_arg_decorator -def deprecated(method, startDate=None, removeDate=None, message=None): +def deprecated( + method: Callable[..., t.Any], + startDate: str|None = None, + removeDate: str|None = None, + message: str|None = None +) -> Callable[..., t.Any]: ''' Decorator that marks a function as deprecated and should not be called. @@ -109,7 +132,7 @@ def deprecated(method, startDate=None, removeDate=None, message=None): else: funcName = method.__name__ - method._isDeprecated = True + method.__dict__['_isDeprecated'] = True if startDate is not None: startDate = ' on ' + startDate @@ -125,11 +148,11 @@ def deprecated(method, startDate=None, removeDate=None, message=None): message = 'Find alternative methods.' m = f'{funcName} was deprecated{startDate} and will disappear {removeDate}. {message}' - callInfo = {'calledAlready': False, - 'message': m} + callInfo: _DeprecatedCallInfo = {'calledAlready': False, + 'message': m} @wraps(method) - def func_wrapper(*arguments, **keywords): + def func_wrapper(*arguments: t.Any, **keywords: t.Any) -> t.Any: if len(arguments) > 1 and arguments[1] in ( '_ipython_canary_method_should_not_exist_', '_repr_mimebundle_', @@ -153,7 +176,9 @@ def func_wrapper(*arguments, **keywords): return func_wrapper -def cacheMethod(method): +def cacheMethod( + method: Callable[t.Concatenate[_CacheInstance, _CacheP], _CacheReturn] +) -> Callable[t.Concatenate[_CacheInstance, _CacheP], _CacheReturn]: ''' A decorator for music21Objects or other objects that assumes that there is a ._cache Dictionary in the instance @@ -176,7 +201,11 @@ def cacheMethod(method): funcName = method.__name__ @wraps(method) - def inner(instance, *arguments, **keywords): + def inner( + instance: _CacheInstance, + *arguments: _CacheP.args, + **keywords: _CacheP.kwargs + ) -> _CacheReturn: if funcName in instance._cache: return instance._cache[funcName] diff --git a/music21/common/enums.py b/music21/common/enums.py index fce3232e1..f23b4f735 100644 --- a/music21/common/enums.py +++ b/music21/common/enums.py @@ -15,7 +15,7 @@ # when Python 3.12 is minimum, will not need StrEnumMeta at all -- contains will work. class StrEnumMeta(EnumType): - def __contains__(cls, item): + def __contains__(cls, item: object) -> bool: if isinstance(item, str): if item in cls.__members__.values(): return True @@ -33,7 +33,7 @@ class ContainsMeta(EnumType): This will be removed when Python 3.12 is the minimum version for music21. ''' - def __contains__(cls, item): + def __contains__(cls, item: object) -> bool: try: cls(item) # pylint: disable=no-value-for-parameter return True @@ -46,12 +46,12 @@ class ContainsEnum(IntEnum, metaclass=ContainsMeta): ''' An IntEnum that allows "in" checks against the values of the enum. ''' - def __repr__(self): + def __repr__(self) -> str: val = super().__repr__() return re.sub(r'(\d+)', lambda m: f'0x{int(m.group(1)):X}', val) @classmethod - def hasValue(cls, val): + def hasValue(cls, val: object) -> bool: return val in cls._value2member_map_ @@ -97,13 +97,13 @@ class BooleanEnum(Enum): True ''' @staticmethod - def is_bool_tuple(v): + def is_bool_tuple(v: object) -> bool: if isinstance(v, tuple) and len(v) == 2 and isinstance(v[0], bool): return True else: return False - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): return super().__eq__(other) v = self.value @@ -115,13 +115,13 @@ def __eq__(self, other): return v[1] == other return False - def __bool__(self): + def __bool__(self) -> bool: v = self.value if self.is_bool_tuple(v): return v[0] return bool(self.value) - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}.{self.name}>' @@ -139,10 +139,10 @@ class StrEnum(str, Enum, metaclass=StrEnumMeta): will no longer be used internally and will eventually become deprecated (2027?) and removed (2030?). ''' - def __repr__(self): + def __repr__(self) -> str: return f'<{self.__class__.__name__}.{self.name}>' - def __str__(self): + def __str__(self) -> str: ''' >>> from music21.common.enums import OffsetSpecial >>> str(OffsetSpecial.AT_END) diff --git a/music21/common/fileTools.py b/music21/common/fileTools.py index 8fabcb5ef..08193f95f 100644 --- a/music21/common/fileTools.py +++ b/music21/common/fileTools.py @@ -14,6 +14,7 @@ from __future__ import annotations import codecs +from collections.abc import Sequence import contextlib # for with statements import gzip import io @@ -36,7 +37,7 @@ @contextlib.contextmanager -def cd(targetDir): +def cd(targetDir: str|pathlib.Path) -> t.Generator[None, None, None]: ''' Useful for a temporary cd for use in a `with` statement: @@ -74,7 +75,7 @@ def readPickleGzip(filePath: str|pathlib.Path) -> t.Any: restorePathClassesAfterUnpickling() return newMdb -def readFileEncodingSafe(filePath, firstGuess='utf-8') -> str: +def readFileEncodingSafe(filePath: str|pathlib.Path, firstGuess: str = 'utf-8') -> str: # noinspection PyShadowingNames r''' Slow, but will read a file of unknown encoding as safely as possible using @@ -127,7 +128,7 @@ def readFileEncodingSafe(filePath, firstGuess='utf-8') -> str: _storedPathlibClasses = {'posixPath': pathlib.PosixPath, 'windowsPath': pathlib.WindowsPath} -def preparePathClassesForUnpickling(): +def preparePathClassesForUnpickling() -> None: ''' When we need to unpickle a function that might have relative paths (like some music21 stream options), Windows chokes if the PosixPath @@ -135,25 +136,26 @@ def preparePathClassesForUnpickling(): ''' from music21.common.misc import getPlatform platform = getPlatform() + # deliberately swap the pathlib classes so cross-platform pickles can load if platform == 'win': - pathlib.PosixPath = pathlib.WindowsPath + pathlib.PosixPath = pathlib.WindowsPath # type: ignore[misc, assignment] else: - pathlib.WindowsPath = pathlib.PosixPath + pathlib.WindowsPath = pathlib.PosixPath # type: ignore[misc, assignment] -def restorePathClassesAfterUnpickling(): +def restorePathClassesAfterUnpickling() -> None: ''' After unpickling, leave pathlib alone. ''' from music21.common.misc import getPlatform platform = getPlatform() if platform == 'win': - pathlib.PosixPath = _storedPathlibClasses['posixPath'] + pathlib.PosixPath = _storedPathlibClasses['posixPath'] # type: ignore[misc, assignment] else: - pathlib.WindowsPath = _storedPathlibClasses['windowsPath'] + pathlib.WindowsPath = _storedPathlibClasses['windowsPath'] # type: ignore[misc, assignment] -def runSubprocessCapturingStderr(subprocessCommand): +def runSubprocessCapturingStderr(subprocessCommand: Sequence[str|pathlib.Path]) -> None: ''' Run a subprocess command, capturing stderr and only show the error if an exception is raised. diff --git a/music21/common/formats.py b/music21/common/formats.py index 71fc79856..7d9bf7cce 100644 --- a/music21/common/formats.py +++ b/music21/common/formats.py @@ -86,7 +86,7 @@ def findSubConverterForFormat(fmt: str) -> type[SubConverter]|None: # @deprecated('May 2014', '[soonest possible]', 'Moved to converter') -def findFormat(fmt): +def findFormat(fmt: str) -> tuple[str|None, str|None]: ''' Given a format defined either by a format name, abbreviation, or an extension, return the regularized format name as well as @@ -224,7 +224,7 @@ def findInputExtension(fmt: str) -> tuple[str, ...]: # @deprecated('May 2014', '[soonest possible]', 'Moved to converter') -def findFormatFile(fp): +def findFormatFile(fp: str|pathlib.Path) -> str|None: r''' Given a file path (relative or absolute) return the format @@ -249,7 +249,7 @@ def findFormatFile(fp): # @deprecated('May 2014', '[soonest possible]', 'Moved to converter') -def findFormatExtFile(fp): +def findFormatExtFile(fp: str|pathlib.Path) -> tuple[str|None, str|None]: r''' Given a file path (relative or absolute) find format and extension used (not the output extension) @@ -286,7 +286,7 @@ def findFormatExtFile(fp): # @deprecated('May 2014', '[soonest possible]', 'Moved to converter') -def findFormatExtURL(url): +def findFormatExtURL(url: str) -> tuple[str|None, str|None]: ''' Given a URL, attempt to find the extension. This may scrub arguments in a URL, or simply look at the last characters. diff --git a/music21/common/misc.py b/music21/common/misc.py index 219ea8b24..2bd50af94 100644 --- a/music21/common/misc.py +++ b/music21/common/misc.py @@ -13,7 +13,7 @@ ''' from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence import copy import os import platform @@ -41,6 +41,7 @@ from functools import cache if t.TYPE_CHECKING: + from music21 import pitch _T = t.TypeVar('_T') # ----------------------------------------------------------------------------- @@ -105,7 +106,7 @@ def unique(originalList: Iterable, *, key: Callable|None = None) -> list: # provide warning strings to users for use in conditional imports -def getMissingImportStr(modNameList): +def getMissingImportStr(modNameList: Sequence[str]) -> str|None: ''' Given a list of missing module names, returns a nicely-formatted message to the user that gives instructions on how to expand music21 with optional packages. @@ -200,7 +201,7 @@ def sortModules(moduleList: Iterable[t.Any]) -> list[object]: # ---------------------------- -def pitchList(pitchL): +def pitchList(pitchL: Iterable[pitch.Pitch]) -> str: ''' utility method that replicates the previous behavior of lists of pitches. @@ -254,7 +255,12 @@ def runningInMarimo() -> bool: weakref.ref, property, } -def defaultDeepcopy(obj: t.Any, memo=None, *, ignoreAttributes: Iterable[str] = ()): +def defaultDeepcopy( + obj: t.Any, + memo: dict[int, t.Any]|None = None, + *, + ignoreAttributes: Iterable[str] = () +) -> t.Any: ''' Unfortunately, it is not possible to do something like:: diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 4bf457813..722d60143 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -23,7 +23,7 @@ from music21.common.types import OffsetQLIn, OffsetQL if TYPE_CHECKING: - from collections.abc import Sequence, Collection + from collections.abc import Callable, Sequence, Collection __all__ = [ @@ -359,7 +359,7 @@ def opFrac(num: OffsetQLIn) -> OffsetQL: def mixedNumeral(expr: numbers.Real, - limitDenominator=defaults.limitOffsetDenominator): + limitDenominator: int = defaults.limitOffsetDenominator) -> str: ''' Returns a string representing a mixedNumeral form of a number @@ -472,7 +472,7 @@ def roundToHalfInteger(num: float|int) -> float|int: return intVal + floatVal -def addFloatPrecision(x, grain=1e-2) -> float|Fraction: +def addFloatPrecision(x: str|float, grain: float = 1e-2) -> float|Fraction: ''' Given a value that suggests a floating point fraction, like 0.33, return a Fraction or float that provides greater specification, such as Fraction(1, 3) @@ -656,7 +656,7 @@ def decimalToTuplet(decNum: float) -> tuple[int, int]: Traceback (most recent call last): ZeroDivisionError: number must be greater than zero ''' - def findSimpleFraction(inner_working): + def findSimpleFraction(inner_working: float) -> tuple[int, int]: ''' Utility function. ''' @@ -681,15 +681,15 @@ def findSimpleFraction(inner_working): if iy == 0: raise ValueError('No such luck') - jy *= multiplier - my_gcd = gcd(int(jy), int(iy)) - jy = jy / my_gcd - iy = iy / my_gcd + jyFloat = jy * multiplier + my_gcd = gcd(int(jyFloat), int(iy)) + numerator = jyFloat / my_gcd + denominator = iy / my_gcd if flipNumerator is False: - return (int(jy), int(iy)) + return (int(numerator), int(denominator)) else: - return (int(iy), int(jy)) + return (int(denominator), int(numerator)) def unitNormalizeProportion(values: Sequence[int|float]) -> list[float]: @@ -755,7 +755,7 @@ def unitBoundaryProportion( def weightedSelection(values: list[int], weights: list[int|float], - randomGenerator=None) -> int: + randomGenerator: Callable[[], float]|None = None) -> int: ''' Given a list of values and an equal-sized list of weights, return a randomly selected value using the weight. @@ -853,7 +853,7 @@ def approximateGCD(values: Collection[int|float|Fraction], grain: float = 1e-4) -def contiguousList(inputListOrTuple) -> bool: +def contiguousList(inputListOrTuple: Sequence[int]) -> bool: ''' returns bool True or False if a list containing ints contains only contiguous (increasing) values @@ -930,7 +930,7 @@ def groupContiguousIntegers(src: list[int]) -> list[list[int]]: # noinspection SpellCheckingInspection -def fromRoman(num: str, *, strictModern=False) -> int: +def fromRoman(num: str, *, strictModern: bool = False) -> int: ''' Convert a Roman numeral (upper or lower) to an int @@ -1035,7 +1035,7 @@ def toRoman(num: int) -> str: return result -def ordinalAbbreviation(value: int, plural=False) -> str: +def ordinalAbbreviation(value: int, plural: bool = False) -> str: ''' Return the ordinal abbreviations for integers @@ -1093,14 +1093,14 @@ class Test(unittest.TestCase): Tests not requiring file output. ''' - def setUp(self): + def setUp(self) -> None: pass - def testToRoman(self): + def testToRoman(self) -> None: for src, dst in [(1, 'I'), (3, 'III'), (5, 'V')]: self.assertEqual(dst, toRoman(src)) - def testOrdinalsToNumbers(self): + def testOrdinalsToNumbers(self) -> None: self.assertEqual(ordinalsToNumbers['unison'], 1) self.assertEqual(ordinalsToNumbers['Unison'], 1) self.assertEqual(ordinalsToNumbers['first'], 1) @@ -1112,7 +1112,7 @@ def testOrdinalsToNumbers(self): self.assertEqual(ordinalsToNumbers['Eighth'], 8) self.assertEqual(ordinalsToNumbers['8th'], 8) - def testWeightedSelection(self): + def testWeightedSelection(self) -> None: # test equal selection for j in range(10): x = 0 diff --git a/music21/common/objects.py b/music21/common/objects.py index 573511763..15cde92ae 100644 --- a/music21/common/objects.py +++ b/music21/common/objects.py @@ -20,9 +20,11 @@ 'Timer', ] +from collections.abc import Callable, Iterator import collections import inspect import time +import typing as t import weakref @@ -64,15 +66,17 @@ class RelativeCounter(collections.Counter): ''' # pylint:disable=abstract-method - def __iter__(self): + def __iter__(self) -> Iterator[t.Any]: sortedKeys = sorted(super().__iter__(), key=lambda x: self[x], reverse=True) yield from sortedKeys - def items(self): + # deliberately a generator (not dict's re-iterable ItemsView) so that the + # pairs follow the most-common-first order of the overridden __iter__. + def items(self) -> Iterator[tuple[t.Any, t.Any]]: # type: ignore[override] for k in self: yield k, self[k] - def asProportion(self): + def asProportion(self) -> RelativeCounter: selfLen = sum(self[x] for x in self) outDict = {} for y in self: @@ -81,7 +85,7 @@ def asProportion(self): new = self.__class__(outDict) return new - def asPercentage(self): + def asPercentage(self) -> RelativeCounter: selfLen = sum(self[x] for x in self) outDict = {} for y in self: @@ -99,19 +103,19 @@ class defaultlist(list): >>> a[5] True ''' - def __init__(self, fx): + def __init__(self, fx: Callable[[], t.Any]) -> None: super().__init__() self._fx = fx - def _fill(self, index): + def _fill(self, index: int) -> None: while len(self) <= index: self.append(self._fx()) - def __setitem__(self, index, value): + def __setitem__(self, index: int, value: t.Any) -> None: # type: ignore[override] self._fill(index) list.__setitem__(self, index, value) - def __getitem__(self, index): + def __getitem__(self, index: int) -> t.Any: # type: ignore[override] self._fill(index) return list.__getitem__(self, index) @@ -136,10 +140,10 @@ class SingletonCounter: >>> v2 > v1 True ''' - def __init__(self): + def __init__(self) -> None: pass - def __call__(self): + def __call__(self) -> int: post = _singletonCounter['value'] _singletonCounter['value'] += 1 return post @@ -191,7 +195,7 @@ class SlottedObjectMixin: # SPECIAL METHODS # - def __getstate__(self): + def __getstate__(self) -> dict[str, t.Any]: if getattr(self, '__dict__', None) is not None: state = self.__dict__.copy() else: @@ -206,11 +210,11 @@ def __getstate__(self): state[slot] = sValue return state - def __setstate__(self, state): + def __setstate__(self, state: dict[str, t.Any]) -> None: for slot, value in state.items(): setattr(self, slot, value) - def _getSlotsRecursive(self): + def _getSlotsRecursive(self) -> set[str]: ''' Find all slots recursively. @@ -237,7 +241,7 @@ def _getSlotsRecursive(self): ['_editorial', '_style', 'direction', 'funkiness', 'groovability', 'id', 'independentAngle', 'number', 'type'] ''' - slots = set() + slots: set[str] = set() for cls in self.__class__.mro(): slots.update(getattr(cls, '__slots__', ())) return slots @@ -253,7 +257,7 @@ class EqualSlottedObjectMixin(SlottedObjectMixin): ''' __slots__: tuple[str, ...] = () - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if type(self) is not type(other): return False for thisSlot in self._getSlotsRecursive(): @@ -263,7 +267,7 @@ def __eq__(self, other): return False return True - def __ne__(self, other): + def __ne__(self, other: object) -> bool: ''' Defining __ne__ explicitly so that it inherits the same as __eq__ ''' @@ -275,7 +279,7 @@ def __ne__(self, other): class FrozenObject(EqualSlottedObjectMixin): __slots__: tuple[str, ...] = () - def _check_init(self, key=None) -> bool: + def _check_init(self, key: str|None = None) -> bool: if key == '__class__': return True if not getattr(self, 'frozen', True): @@ -288,24 +292,24 @@ def _check_init(self, key=None) -> bool: return True raise TypeError(f'This {self.__class__.__name__} instance is immutable.') - def __setattr__(self, key: str, value): + def __setattr__(self, key: str, value: t.Any) -> None: self._check_init(key) super().__setattr__(key, value) - def __delattr__(self, key: str): + def __delattr__(self, key: str) -> None: self._check_init(key) super().__delattr__(key) - def __setitem__(self, key, value): + def __setitem__(self, key: t.Any, value: t.Any) -> None: if hasattr(super(), '__setitem__'): self._check_init() - super().__setitem__(key, value) + super().__setitem__(key, value) # type: ignore[misc] raise TypeError(f'{self.__class__} object is not subscriptable') - def __delitem__(self, key): + def __delitem__(self, key: t.Any) -> None: if hasattr(super(), '__delitem__'): self._check_init() - super().__delitem__(key) + super().__delitem__(key) # type: ignore[misc] raise TypeError(f'{self.__class__} object is not subscriptable') def __hash__(self) -> int: @@ -341,13 +345,13 @@ class Timer: >>> stopTime < 1 True ''' - def __init__(self): + def __init__(self) -> None: # start on init - self._tStart = time.time() - self._tDif = 0 - self._tStop = None + self._tStart: float = time.time() + self._tDif: float = 0 + self._tStop: float|None = None - def start(self): + def start(self) -> None: ''' Explicit start method; will clear previous values. @@ -357,16 +361,16 @@ def start(self): self._tStop = None # show that a new run has started so __call__ works self._tDif = 0 - def stop(self): + def stop(self) -> None: self._tStop = time.time() self._tDif = self._tStop - self._tStart - def clear(self): + def clear(self) -> None: + self._tStart = time.time() self._tStop = None self._tDif = 0 - self._tStart = None - def __call__(self): + def __call__(self) -> float: ''' Reports current time or, if stopped, stopped time. ''' @@ -377,7 +381,7 @@ def __call__(self): timeDifference = self._tDif return timeDifference - def __str__(self): + def __str__(self) -> str: if self._tStop is None: # if not stopped yet timeDifference = time.time() - self._tStart else: diff --git a/music21/common/parallel.py b/music21/common/parallel.py index 9d0802b77..d10c50933 100644 --- a/music21/common/parallel.py +++ b/music21/common/parallel.py @@ -16,18 +16,20 @@ 'safeToParallize', ] +from collections.abc import Callable, Sequence import multiprocessing import os +import typing as t import unittest -def runParallel(iterable, - parallelFunction, +def runParallel(iterable: Sequence[t.Any], + parallelFunction: Callable[..., t.Any], *, - updateFunction=None, - updateMultiply=3, - unpackIterable=False, - updateSendsIterable=False): + updateFunction: Callable[..., t.Any]|bool|None = None, + updateMultiply: int = 3, + unpackIterable: bool = False, + updateSendsIterable: bool = False) -> list[t.Any]: ''' runs parallelFunction over iterable in parallel, optionally calling updateFunction after each common.cpus * updateMultiply calls. @@ -124,13 +126,13 @@ def runParallel(iterable, # if there is no need for updates, run at max speed # -- do the whole list at once. - resultsList = [] + resultsList: list[t.Any] = [] - def callUpdate(ii): + def callUpdate(ii: int) -> None: if updateFunction is True: tasksDone = min([ii, iterLength]) print(f'Done {tasksDone} tasks of {iterLength}') - elif updateFunction not in (False, None): + elif callable(updateFunction): for thisPosition in range(ii - (updateMultiply * numCpus), ii): if thisPosition < 0: continue @@ -166,13 +168,13 @@ def callUpdate(ii): return resultsList -def runNonParallel(iterable, - parallelFunction, +def runNonParallel(iterable: Sequence[t.Any], + parallelFunction: Callable[..., t.Any], *, - updateFunction=None, - updateMultiply=3, - unpackIterable=False, - updateSendsIterable=False): + updateFunction: Callable[..., t.Any]|bool|None = None, + updateMultiply: int = 3, + unpackIterable: bool = False, + updateSendsIterable: bool = False) -> list[t.Any]: ''' This is intended to be a perfect drop in replacement for runParallel, except that it runs on one core only, and not in parallel. @@ -180,16 +182,16 @@ def runNonParallel(iterable, Used automatically if we're already in a parallelized function. ''' iterLength = len(iterable) - resultsList = [] + resultsList: list[t.Any] = [] - def callUpdate(ii): + def callUpdate(ii: int) -> None: if ii % updateMultiply != 0: return if updateFunction is True: tasksDone = min([ii, iterLength]) print(f'Done {tasksDone} tasks of {iterLength}') - elif updateFunction not in (False, None): + elif callable(updateFunction): for thisPosition in range(ii - updateMultiply, ii): if thisPosition < 0: continue @@ -218,7 +220,7 @@ def callUpdate(ii): return resultsList -def cpus(): +def cpus() -> int: ''' Returns the number of CPUs or if >= 3, one less (to leave something out for multiprocessing) ''' @@ -255,23 +257,23 @@ def safeToParallize() -> bool: # pickleable testing functions. -def _countN(fn): +def _countN(filename: str) -> int: from music21 import corpus - c = corpus.parse(fn) + c = corpus.parse(filename) return len(c.recurse().notes) -def _countUnpacked(i, fn): +def _countUnpacked(i: int, filename: str) -> bool: if i >= 3: return False - if fn not in ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel']: + if filename not in ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel']: return False return True class Test(unittest.TestCase): # pylint: disable=redefined-outer-name - def x_figure_out_segfault_testMultiprocess(self): + def x_figure_out_segfault_testMultiprocess(self) -> None: files = ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel'] # for importing into testSingleCoreAll we need the full path to the modules from music21.common.parallel import _countN, _countUnpacked @@ -291,13 +293,15 @@ def x_figure_out_segfault_testMultiprocess(self): self.assertNotIn(False, passed) # testing functions - def _customUpdate1(self, i, total, output): + def _customUpdate1(self, i: int, total: int, output: int) -> None: self.assertEqual(total, 3) self.assertLess(i, 3) self.assertIn(output, [165, 50, 131]) - def _customUpdate2(self, i, unused_total, unused_output, fn): - self.assertIn(fn, ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel']) + def _customUpdate2( + self, i: int, unused_total: int, unused_output: int, filename: str + ) -> None: + self.assertIn(filename, ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel']) if __name__ == '__main__': diff --git a/music21/common/pathTools.py b/music21/common/pathTools.py index 610926d68..fffc7601d 100644 --- a/music21/common/pathTools.py +++ b/music21/common/pathTools.py @@ -200,7 +200,7 @@ def cleanpath(path: str|pathlib.Path, *, class Test(unittest.TestCase): - def testGetSourcePath(self): + def testGetSourcePath(self) -> None: fp = getSourceFilePath() self.assertIsInstance(fp, pathlib.Path) diff --git a/music21/common/stringTools.py b/music21/common/stringTools.py index bcb943af3..8adb6c9c0 100644 --- a/music21/common/stringTools.py +++ b/music21/common/stringTools.py @@ -28,6 +28,8 @@ 'ParenthesesMatch', ] +from collections.abc import Iterable +import typing as t import dataclasses import hashlib import random @@ -162,7 +164,11 @@ def camelCaseToHyphen(usrStr: str, replacement: str = '-') -> str: return re.sub('([a-z0-9])([A-Z])', r'\1' + replacement + r'\2', s1).lower() -def spaceCamelCase(usrStr: str, replaceUnderscore=True, fixMeList=None) -> str: +def spaceCamelCase( + usrStr: str, + replaceUnderscore: bool = True, + fixMeList: Iterable[str]|None = None +) -> str: ''' Given a camel-cased string, or a mixture of numbers and characters, create a space separated string. @@ -200,6 +206,7 @@ def spaceCamelCase(usrStr: str, replaceUnderscore=True, fixMeList=None) -> str: post: list[str] = [] # do not split these + fixupList: Iterable[str] if fixMeList is None: fixupList = ('PMFC',) else: @@ -243,7 +250,7 @@ def spaceCamelCase(usrStr: str, replaceUnderscore=True, fixMeList=None) -> str: return postStr -def getMd5(value=None) -> str: +def getMd5(value: str|bytes|None = None) -> str: # noinspection SpellCheckingInspection ''' Return an md5 hash from a string. If no value is given then @@ -254,18 +261,16 @@ def getMd5(value=None) -> str: ''' if value is None: value = str(time.time()) + str(random.random()) + if isinstance(value, str): + value = value.encode('UTF-8') m = hashlib.md5() - try: - m.update(value) - except TypeError: # unicode - m.update(value.encode('UTF-8')) - + m.update(value) return m.hexdigest() -def formatStr(msg, - *rest_of_message, - **keywords) -> str: +def formatStr(msg: object, + *rest_of_message: object, + **keywords: object) -> str: ''' DEPRECATED: do not use. May be removed at any time. @@ -277,20 +282,20 @@ def formatStr(msg, test 1 2 3 ''' - msg = [msg, *rest_of_message] - for i in range(len(msg)): - x = msg[i] + msgList: list[t.Any] = [msg, *rest_of_message] + for i in range(len(msgList)): + x = msgList[i] if isinstance(x, bytes): - msg[i] = x.decode('utf-8') + msgList[i] = x.decode('utf-8') if not isinstance(x, str): try: - msg[i] = repr(x) + msgList[i] = repr(x) except TypeError: try: - msg[i] = x.decode('utf-8') + msgList[i] = x.decode('utf-8') except AttributeError: - msg[i] = '' - return ' '.join(msg) + '\n' + msgList[i] = '' + return ' '.join(msgList) + '\n' def stripAccents(inputString: str) -> str: diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index 0d73c021c..d3b8c456f 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -770,8 +770,9 @@ def parseURL( raise ConverterException(f'cannot determine file format of url: {url}') else: unused_formatType, ext = common.findFormat(format) - if ext is None: - ext = '.txt' + + if ext is None: + ext = '.txt' directory = environLocal.getRootTempDir() fp = self._getDownloadFp(directory, ext, url) # returns pathlib.Path diff --git a/music21/roman.py b/music21/roman.py index 191d23868..733261bd2 100644 --- a/music21/roman.py +++ b/music21/roman.py @@ -3889,7 +3889,7 @@ def isMixture(self, if evaluateSecondaryNumeral and self.secondaryRomanNumeral: return self.secondaryRomanNumeral.isMixture(evaluateSecondaryNumeral=True) - if (not self.isTriad) and (not self.isSeventh): + if (not self.isTriad()) and (not self.isSeventh()): return False if not self.key or not isinstance(self.key, key.Key): @@ -4455,6 +4455,12 @@ def testMixture(self): # True, minor key: self.assertTrue(RomanNumeral(fig, 'a').isMixture()) + # Anything that is neither a triad nor a seventh returns False early. + rn = romanNumeralFromChord(chord.Chord('C4 D4 E4')) + self.assertFalse(rn.isTriad()) + self.assertFalse(rn.isSeventh()) + self.assertFalse(rn.isMixture()) + def testMinorTonic7InMajor(self): rn = RomanNumeral('i7', 'C') pitchStrings = [p.name for p in rn.pitches] diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py index 3b87d2675..27f0af181 100644 --- a/music21/scale/__init__.py +++ b/music21/scale/__init__.py @@ -684,7 +684,7 @@ def write(self, if fmt is not None: fileFormat, ext = common.findFormat(fmt) if fp is None: - fpLocal: str|pathlib.Path|IOBase = environLocal.getTempFile(ext) + fpLocal: str|pathlib.Path|IOBase = environLocal.getTempFile(ext or '') else: fpLocal = fp