Skip to content

Commit

Permalink
use more v3.10 features. TypeGuards (#1447)
Browse files Browse the repository at this point in the history
* Use v3.10 features

* Add typeguards to isIterable isNum

* common.classTools: isInt (int not True, False), holdsType (does it hold a particular type?)

* RepeatBracket change defaults

* isNum type to Rational not Number
  • Loading branch information
mscuthbert authored Oct 6, 2022
1 parent 5417b3c commit bbd29fe
Show file tree
Hide file tree
Showing 29 changed files with 687 additions and 590 deletions.
9 changes: 4 additions & 5 deletions music21/abcFormat/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,11 +799,10 @@ def testLyrics(self):
assert s is not None

# s.show()
# self.assertEqual(len(s.parts), 3)
# self.assertEqual(len(s.parts[0].notesAndRests), 6)
# self.assertEqual(len(s.parts[1].notesAndRests), 20)
# self.assertEqual(len(s.parts[2].notesAndRests), 6)
#
# self.assertEqual(len(s.parts), 3)
# self.assertEqual(len(s.parts[0].notesAndRests), 6)
# self.assertEqual(len(s.parts[1].notesAndRests), 20)
# self.assertEqual(len(s.parts[2].notesAndRests), 6)
# s.show()
# s.show('midi')

Expand Down
34 changes: 17 additions & 17 deletions music21/articulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,23 +668,23 @@ def testBasic(self):
self.assertEqual(a.bendAlter, None)


# def testArticulationEquality(self):
# a1 = Accent()
# a2 = Accent()
# a3 = StrongAccent()
# a4 = StrongAccent()
#
# self.assertEqual(a1, a2)
# self.assertEqual(a3, a4)
#
# # in order lists
# self.assertEqual([a1, a3], [a2, a4])
#
# self.assertEqual(set([a1, a3]), set([a1, a3]))
# self.assertEqual(set([a1, a3]), set([a3, a1]))
#
# # comparison of sets of different objects do not pass
# # self.assertEqual(list(set([a1, a3])), list(set([a2, a4])))
# def testArticulationEquality(self):
# a1 = Accent()
# a2 = Accent()
# a3 = StrongAccent()
# a4 = StrongAccent()
#
# self.assertEqual(a1, a2)
# self.assertEqual(a3, a4)
#
# # in order lists
# self.assertEqual([a1, a3], [a2, a4])
#
# self.assertEqual(set([a1, a3]), set([a1, a3]))
# self.assertEqual(set([a1, a3]), set([a3, a1]))
#
# # comparison of sets of different objects do not pass
# # self.assertEqual(list(set([a1, a3])), list(set([a2, a4])))


# ------------------------------------------------------------------------------
Expand Down
8 changes: 2 additions & 6 deletions music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import builtins
from collections.abc import Generator, Iterable
import copy
import fractions # for type annotation only
from importlib.util import find_spec
import typing as t
from typing import overload # Pycharm can't do alias
Expand All @@ -57,7 +56,7 @@
from music21 import defaults
from music21.derivation import Derivation
from music21.duration import Duration, DurationException
from music21.editorial import Editorial
from music21.editorial import Editorial # import class directly to not conflict with property.
from music21 import environment
from music21 import exceptions21
from music21 import prebase
Expand All @@ -68,6 +67,7 @@
from music21 import tie

if TYPE_CHECKING:
import fractions
from music21 import meter
from music21 import stream
from music21 import spanner
Expand Down Expand Up @@ -685,8 +685,6 @@ def editorial(self) -> Editorial:

@editorial.setter
def editorial(self, ed: Editorial):
# Dev note: because the property "editorial" shadows module editorial,
# typing has to be in quotes.
self._editorial = ed

@property
Expand Down Expand Up @@ -743,8 +741,6 @@ def style(self) -> Style:

@style.setter
def style(self, newStyle: Style | None):
# Dev note: because property style shadows module style,
# typing has to be in quotes.
self._style = newStyle

# convenience.
Expand Down
10 changes: 6 additions & 4 deletions music21/braille/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# ------------------------------------------------------------------------------
from __future__ import annotations

import typing as t
import unittest

# from music21 import articulations
from music21 import articulations
from music21 import clef
from music21 import duration
Expand All @@ -22,12 +22,14 @@
from music21 import expressions
from music21 import interval
from music21 import note
from music21 import tempo # for typing

from music21.braille import lookup
from music21.common import stringTools

# Add aliases to lookup tables ONLY if it will be used in many different contexts
if t.TYPE_CHECKING:
from music21 import tempo

# Add aliases to lookup tables ONLY if it will be used in many contexts
# if it is used in only one function, make the alias there.
alphabet = lookup.alphabet
ascii_chars = lookup.ascii_chars
Expand Down Expand Up @@ -680,7 +682,7 @@ def noteToBraille(
# Note: beamStatus is a helper that we hope to remove
# when moving all the translation features to an object class.
#
# Currently does not allow parallelization of parsing.
# Currently, does not allow parallelization of parsing.
music21Note.editorial.brailleEnglish = []

for keyword in TEMPORARY_ATTRIBUTES:
Expand Down
16 changes: 8 additions & 8 deletions music21/braille/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2763,8 +2763,8 @@ def xtest_example13_11(self):
ml[1].append(spanner.Slur(ml[1].notes[0], ml[1].notes[1]))
ml[3].insert(0.0, expressions.TextExpression('rit.'))
self.s = bm
# self.b = '''
# '''
# self.b = '''
# '''

def test_example13_14(self):
bm = converter.parse("tinynotation: 3/4 e'4 e' f'# g'2. f'#8 d' a d' e' c'# d'2.").flatten()
Expand Down Expand Up @@ -2895,8 +2895,8 @@ def xtest_example13_19(self):
ml[0].insert(0.0, expressions.TextExpression('very sweetly'))
ml[-1].rightBarline = None
self.s = bm
# self.b = '''
# '''
# self.b = '''
# '''

def test_example13_26(self):
bm = converter.parse('''
Expand Down Expand Up @@ -3287,8 +3287,8 @@ def xtest_example15_10(self):
bm.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
bm.getElementsByClass(stream.Measure)[-1].rightBarline = None
self.s = bm
# self.b = '''
# '''
# self.b = '''
# '''

def test_example15_11(self):
bm = converter.parse('''
Expand Down Expand Up @@ -3370,8 +3370,8 @@ def xtest_example16_6(self):
bm.makeNotation(inPlace=True, cautionaryNotImmediateRepeat=False)
bm.getElementsByClass(stream.Measure)[-1].rightBarline = None
self.s = bm
# self.b = '''
# '''
# self.b = '''
# '''

def test_example16_15(self):
bm = converter.parse('''
Expand Down
2 changes: 1 addition & 1 deletion music21/clef.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from music21 import base
from music21 import exceptions21
from music21 import environment
from music21 import pitch # for typing only
from music21 import pitch
from music21 import style


Expand Down
97 changes: 88 additions & 9 deletions music21/common/classTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,43 @@
# ------------------------------------------------------------------------------
from __future__ import annotations

from collections.abc import Iterable, Collection
import contextlib
import numbers
import typing as t

# from music21 import exceptions21
__all__ = [
'isNum', 'isListLike', 'isIterable', 'classToClassStr', 'getClassSet',
'holdsType',
'isNum', 'isInt', 'isListLike', 'isIterable', 'classToClassStr', 'getClassSet',
'tempAttribute', 'saveAttributes',
]


def isNum(usrData: t.Any) -> bool:
_T = t.TypeVar('_T')


def isInt(usrData: t.Any) -> t.TypeGuard[int]:
'''
Check if usrData is an integer and not True or False.
>>> common.isInt(3)
True
>>> common.isInt(False)
False
>>> common.isInt(2.0)
False
'''
return isinstance(usrData, int) and usrData is not True and usrData is not False


def isNum(usrData: t.Any) -> t.TypeGuard[numbers.Rational]:
'''
check if usrData is a number (float, int, long, Decimal),
return boolean
return boolean and if True casts the value as a Rational number
unlike `isinstance(usrData, Number)` does not return True for `True, False`.
unlike `isinstance(usrData, Rational)` does not return True for `True, False`.
Does not use `isinstance(usrData, Number)` which is 2-6 times slower
Does not use `isinstance(usrData, Rational)` which is 2-6 times slower
than calling this function (except in the case of Fraction, when
it's 6 times faster, but that's rarer). (6 times slower on Py3.4, now
only 2x slower in Python 3.10)
Expand Down Expand Up @@ -66,7 +85,7 @@ def isNum(usrData: t.Any) -> bool:
return False


def isListLike(usrData: t.Any) -> bool:
def isListLike(usrData: t.Any) -> t.TypeGuard[list | tuple]:
'''
Returns True if is a List or Tuple or their subclasses.
Expand All @@ -91,10 +110,10 @@ def isListLike(usrData: t.Any) -> bool:
return isinstance(usrData, (list, tuple))


def isIterable(usrData: t.Any) -> bool:
def isIterable(usrData: t.Any) -> t.TypeGuard[Iterable]:
'''
Returns True if is the object can be iter'd over
and is NOT a string
and is NOT a string. Marks it as an Iterable for type checking.
>>> common.isIterable([5, 10])
True
Expand Down Expand Up @@ -126,6 +145,64 @@ def isIterable(usrData: t.Any) -> bool:
return False


def holdsType(usrData: t.Any, checkType: type[_T]) -> t.TypeGuard[Collection[_T]]:
'''
Returns True if usrData is a Collection of type checkType.
This reads an item from usrData, so don't use it on something
where iterating destroys the type.
>>> y = [1, 2, 3]
>>> common.classTools.holdsType(y, int)
True
>>> common.classTools.holdsType(5, int)
False
>>> common.classTools.holdsType(['hello'], str)
True
Empty iterators hold the type:
>>> common.classTools.holdsType([], float)
True
Note that a mixed collection holds whatever is first
>>> common.classTools.holdsType((4, 'hello'), int)
True
>>> common.classTools.holdsType((4, 'hello'), str)
False
Works on sets with arbitrary order:
>>> common.classTools.holdsType({2, 10}, int)
True
Intelligent collections will not have their position affected.
>>> m = stream.Measure([note.Note('C'), note.Rest()])
>>> common.classTools.holdsType(m, note.GeneralNote)
True
>>> next(iter(m))
<music21.note.Note C>
>>> r = range(1, 100)
>>> common.classTools.holdsType(r, int)
True
>>> next(iter(r))
1
New in v9.
'''
if not isIterable(usrData):
return False
try:
first = next(iter(usrData))
return isinstance(first, checkType)
except StopIteration:
return True



def classToClassStr(classObj: type) -> str:
'''Convert a class object to a class string.
Expand Down Expand Up @@ -157,6 +234,8 @@ def getClassSet(instance, classNameTuple=None):
True
>>> 'object' in cs
True
>>> note.Note in cs
False
To save time (this IS a performance-critical operation), classNameTuple
can be passed a tuple of names such as ('Pitch', 'object') that
Expand Down
2 changes: 1 addition & 1 deletion music21/common/numberTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ def lcm(filterList: Iterable[int]) -> int:
common.lcm({3, 5, 6})
30
Deprecated in v9 since Python 3.9 is the minimum version
Deprecated in v9 since Python 3.10 is the minimum version
and math.lcm works in C and is faster
'''
def _lcm(a, b):
Expand Down
Loading

0 comments on commit bbd29fe

Please sign in to comment.