Skip to content
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
13 changes: 10 additions & 3 deletions music21/common/classTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
49 changes: 39 additions & 10 deletions music21/common/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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_',
Expand All @@ -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
Expand All @@ -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]

Expand Down
20 changes: 10 additions & 10 deletions music21/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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_


Expand Down Expand Up @@ -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
Expand All @@ -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}>'


Expand All @@ -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)
Expand Down
20 changes: 11 additions & 9 deletions music21/common/fileTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from __future__ import annotations

import codecs
from collections.abc import Sequence
import contextlib # for with statements
import gzip
import io
Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -127,33 +128,34 @@ 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
is not defined, but usually can still unpickle easily.
'''
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.
Expand Down
8 changes: 4 additions & 4 deletions music21/common/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions music21/common/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,6 +41,7 @@
from functools import cache

if t.TYPE_CHECKING:
from music21 import pitch
_T = t.TypeVar('_T')

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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::

Expand Down
Loading
Loading