Browse Source

typehints

master
Nils 2 years ago
parent
commit
6b68d8eba8
  1. 2
      template/engine/api.py
  2. 13
      template/engine/duration.py
  3. 2
      template/engine/input_midi.py
  4. 2
      template/engine/metronome.py
  5. 59
      template/engine/pitch.py
  6. 8
      template/engine/sequencer.py
  7. 13
      template/mypy.ini
  8. 3
      template/qtgui/about.py
  9. 2
      template/qtgui/helper.py
  10. 8
      template/qtgui/mainwindow.py
  11. 1
      template/qtgui/menu.py
  12. 2
      template/qtgui/nsmclient.py
  13. 22
      template/start.py

2
template/engine/api.py

@ -282,7 +282,7 @@ class Callbacks(object):
func(channel)
def startEngine(nsmClient):
def startEngine(nsmClient, additionalData:dict={}):
"""
This function gets called after initializing the GUI, calfbox
and loading saved data from a file.

13
template/engine/duration.py

@ -26,9 +26,11 @@ This file handles various durations and their conversions.
import logging; logger = logging.getLogger(__name__); logger.info("import")
from typing import Union
from engine.config import METADATA
#Suggested import in other files:
#from template.engine.duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128
#import template.engine.duration as duration
@ -36,8 +38,7 @@ from engine.config import METADATA
MAXIMUM_TICK_DURATION = 2**31-1
D4 = METADATA["quarterNoteInTicks"]
D4:int = METADATA["quarterNoteInTicks"] #type: ignore
D8 = int(D4 / 2)
D16 = int(D8 / 2)
D32 = int(D16 / 2)
@ -69,7 +70,7 @@ D_STACCATO = 1
D_TENUTO = 2
D_TIE = 3
def _baseDurationToTraditionalNumber(baseDuration):
def _baseDurationToTraditionalNumber(baseDuration:int)->int:
"""4 = Quarter, 8 = Eighth, 0 = Brevis, -1= Longa
Created in the loop below on startup"""
if baseDuration == D4:
@ -119,7 +120,7 @@ traditionalNumberToBaseDuration = {}
for baseDuration in baseDurations:
traditionalNumberToBaseDuration[_baseDurationToTraditionalNumber(baseDuration)] = baseDuration
def jackBBTicksToDuration(beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks):
def jackBBTicksToDuration(beatTypeAsTraditionalNumber:int, jackTicks:int, jackBeatTicks:int)->Union[float, None]:
if None in (beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks):
return None
else:
@ -129,7 +130,7 @@ def jackBBTicksToDuration(beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks)
return jackTicks * factor
def lyDurationLogToTicks(durationLog):
def lyDurationLogToTicks(durationLog:int)->int:
return 2**(8 - durationLog) * 6
ticksToLyDurationLogDict = {
@ -147,7 +148,7 @@ ticksToLyDurationLogDict = {
D256: 8, #1/256
}
def ticksToLilypond(ticks):
def ticksToLilypond(ticks:int)->int:
if ticks in ticksToLyDurationLogDict:
return ticksToLyDurationLogDict[ticks]
else:

2
template/engine/input_midi.py

@ -257,7 +257,7 @@ class MidiProcessor(object):
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = _printer
else:
try:
del self.callbacks[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)]
del self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)]
except KeyError:
pass

2
template/engine/metronome.py

@ -75,7 +75,7 @@ class Metronome(object):
def soundStresses(self, value:bool):
self._soundStresses = value
self.generate(self._cachedData)
self.generate(self._cachedData, self.label)
def generate(self, data, label:str):
"""Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions)

59
template/engine/pitch.py

@ -28,11 +28,18 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
from collections import defaultdict
from typing import Dict, Tuple, List, DefaultDict
#Third Party Modules
#Template Modules
#Our modules
class KeySignature(object):
"""For tests and typechecking. The real one is in Laborejo/engine/items.py"""
def __init__(self, root, deviationFromMajorScale):
self.root:int = root
self.deviationFromMajorScale:List = deviationFromMajorScale
self.keysigList:tuple = tuple()
#Constants
OCTAVE = 350
@ -41,21 +48,21 @@ MAX = 3140
MIN = 0
#Without a keysignature
def plain(pitch):
def plain(pitch:int)->int:
""" Extract the note from a note-number, without any octave but with the tailing zero.
This means we double-use the lowest octave as abstract version."""
#Dividing through the octave, 350, results in the number of the octave and the note as remainder.
return divmod(pitch, 350)[1]
def octave(pitch):
def octave(pitch:int)->int:
"""Return the octave of given note. Lowest 0 is X,,,"""
return divmod(pitch, 350)[0]
def toOctave(pitch, octave):
def toOctave(pitch:int, octave:int)->int:
"""Take a plain note and give the octave variant. Starts with 0"""
return pitch + octave * 350
def mirror(pitch, axis):
def mirror(pitch:int, axis:int)->int:
"""Calculate the distance between the pitch and the axis-pitch and
set the new pitch twice as far, which creates a mirror effect:
Half the distancen is object->mirror and then mirror->object on the
@ -68,12 +75,12 @@ def mirror(pitch, axis):
#1420 + 200 = 1620
return pitch + 2 * (axis - pitch)
def diatonicIndex(pitch):
def diatonicIndex(pitch:int)->int:
"""Return an int between 0 and 6, resembling the diatonic position
of the given pitch without octave, accidentals. 0 is c"""
return divmod(plain(toWhite[pitch]), 50)[0]
def absoluteDiatonicIndex(pitch):
def absoluteDiatonicIndex(pitch:int)->int:
"""Like diatonicIndex but works from pitch 20 which gets index 0
middle c is 28
tuning a is 33
@ -81,13 +88,13 @@ def absoluteDiatonicIndex(pitch):
(not traditional interval steps, real step counting from 1)"""
return divmod(toWhite[pitch], 50)[0]
def distanceInDiatonicSteps(first, second):
def distanceInDiatonicSteps(first:int, second:int)->int:
"""root is a pitch like 1720. Pitch as well
Returns not a signed int. If the first is lower than the second
you get a negative return value."""
return absoluteDiatonicIndex(first) - absoluteDiatonicIndex(second)
def diatonicIndexToPitch(index, octave):
def diatonicIndexToPitch(index:int, octave:int)->int:
"""supports indices from - to +. """
while index < 0:
index += 7 #plus one octave. index -1 becomes b
@ -97,7 +104,7 @@ def diatonicIndexToPitch(index, octave):
octave += 1
return toOctave(index * 50 + 20, octave) #0 is cesces, 20 is c
def upStepsFromRoot(root, pitch):
def upStepsFromRoot(root:int, pitch:int)->int:
"""Like diatonicIndex but assumes a different root than C. So it is:
'stepcount upward in white keys from root to pitch'.
It is always assumed it should go up.
@ -113,7 +120,7 @@ def upStepsFromRoot(root, pitch):
p += 7
return p - r
def fromMidi(midipitch, keysig):
def fromMidi(midipitch:int, keysig:KeySignature)->int:
"""Convert a midi pitch to internal pitch.
Nearest to pillar of fifth"""
if (midipitch, keysig) in cache_fromMidi:
@ -162,7 +169,7 @@ def fromMidi(midipitch, keysig):
def halfToneDistanceFromC(pitch):
def halfToneDistanceFromC(pitch:int)->int:
"""Return the half-tone step distance from C. The "sounding" interval"""
return {
#00 : 10, # ceses,,, -> bes
@ -208,7 +215,7 @@ def halfToneDistanceFromC(pitch):
}[plain(pitch)]
def sharpen(pitch):
def sharpen(pitch:int)->int:
"""Sharpen the pitch until double crossed"""
sharper = pitch + 10
if toWhite[sharper] == toWhite[pitch]: #still the same base note?
@ -216,7 +223,7 @@ def sharpen(pitch):
else:
return pitch #too sharp, do nothing.
def flatten(pitch):
def flatten(pitch:int)->int:
"""Flatten the pitch until double flat"""
flatter = pitch - 10
if toWhite[flatter] == toWhite[pitch]: #still the same base note?
@ -224,7 +231,7 @@ def flatten(pitch):
else:
return pitch #too flat, do nothing.
def interval(pitch1, pitch2):
def interval(pitch1:int, pitch2:int)->Tuple[int, int]:
"""Return the distance between two pitches as steps in the pillar of fifths.
Intervals are tuplets with two members x = 1,0 #fifth in the same octave
x[0] = interval. Steps in the pillar of fifths.
@ -235,7 +242,7 @@ def interval(pitch1, pitch2):
else:
return (pillarOfFifth.index(plain(pitch2)) - pillarOfFifth.index(plain(pitch1)), octave(pitch2 - pitch1))
def intervalUp(pitch, interval, midiIn = False):
def intervalUp(pitch:int, interval:Tuple[int, int], midiIn:bool = False)->int:
"""Return a pitch which is _interval_ higher than the given pitch"""
octv = octave(pitch)
indexNumber = pillarOfFifth.index(plain(pitch))
@ -245,7 +252,7 @@ def intervalUp(pitch, interval, midiIn = False):
targetPitch += interval[1]*350
return targetPitch
def intervalDown(pitch, interval):
def intervalDown(pitch:int, interval:Tuple[int, int])->int:
"""Return a pitch which is _interval_ lower than the given pitch
intervalUp(20, (-12,1)) #c,,, to deses,,"""
@ -257,18 +264,18 @@ def intervalDown(pitch, interval):
targetPitch -= interval[1]*350
return targetPitch
def intervalAutomatic(originalPitch, rootPitch, targetPitch):
def intervalAutomatic(originalPitch:int, rootPitch:int, targetPitch:int)->int:
"""Return the original pitch transposed by the interval
between rootPitch and targetPitch"""
iv = interval(rootPitch, targetPitch)
if rootPitch >= targetPitch:
return intervalDown(originalPitch, iv)
elif rootPitch < targetPitch:
else: #rootPitch < targetPitch
return intervalUp(originalPitch, iv)
#With a Key Signature
def toScale(pitch, keysig):
def toScale(pitch:int, keysig:KeySignature)->int:
"""Return a pitch which is the in-scale variant of the given one.
Needs a Key Signature as second parameter"""
if (pitch, keysig) in cache_toScale:
@ -281,7 +288,7 @@ def toScale(pitch, keysig):
cache_toScale[(pitch, keysig)] = value
return value
def diffToKey(pitch, keysig):
def diffToKey(pitch:int, keysig:KeySignature)->int:
"""Return if a note is natural, sharp, flat etc.
Same syntax as Key Signature:
-20 double flat, 0 natural, +20 d-sharp."""
@ -329,7 +336,7 @@ pillarOfFifth = [
]
def midiPitchLimiter(pitch, transpose):
def midiPitchLimiter(pitch:int, transpose:int)->int:
if pitch + transpose < 0:
logger.warning(f"Tranpose lead to a note below midi value 0: {pitch}. Limiting to 0. Please fix manually")
return 0
@ -339,7 +346,7 @@ def midiPitchLimiter(pitch, transpose):
else:
return pitch + transpose
def midiChannelLimiter(value):
def midiChannelLimiter(value:int)->int:
"""makes sure that a midi channel is in range 0-15"""
if value > 15:
logger.warning("Midi Channel bigger 15 detected: {}. Limiting to 15. Please fix manually".format(value))
@ -1057,8 +1064,8 @@ P_Bb = P_Hb = 270 - 10
#These are mostly values that are called multiple thousand times per track for every callback
pitch2ly = dict((ly2pitch[k], k) for k in ly2pitch)
cache_toScale = {} #filled dynamically
cache_fromMidi = {} #filled dynamically
cache_toScale: Dict[ Tuple[int, KeySignature], int] = {} #filled dynamically. int is a pitch, object is a keysignature. value is pitch
cache_fromMidi: Dict[ Tuple[int, KeySignature], int] = {} #filled dynamically midipitch:keysig . type is (midipitch, keysig) : internal pitch
toMidi = {} #filled for all pitches on startup, below
toWhite = {} #filled for all pitches on startup, below
tonalDistanceFromC = {} #filled for all pitches on startup, below
@ -1161,10 +1168,10 @@ midi_notenames_gm_drums = [str(i) for i in range(0,35)] + [
] + [str(i) for i in range(87,128)]
def _defaultSimpleNoteNames():
def _defaultSimpleNoteNames()->list:
return midi_notenames_english
simpleNoteNames = defaultdict(_defaultSimpleNoteNames)
simpleNoteNames:DefaultDict[str,list] = defaultdict(_defaultSimpleNoteNames)
simpleNoteNames["German"] = midi_notenames_german
simpleNoteNames["English"] = midi_notenames_english
simpleNoteNames["Lilypond"] = midi_notenames_lilypond

8
template/engine/sequencer.py

@ -23,7 +23,7 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
from typing import List, Dict, Tuple, Iterable
from typing import List, Dict, Tuple, Iterable, Union
#Third Party Modules
from calfbox import cbox
@ -90,7 +90,7 @@ class Score(Data):
#Tracks
def addTrack(self, name:str=""):
"""Create and add a new track. Not an existing one"""
track = Score.TrackClass(parentData=self, name=name)
track = Score.TrackClass(parentData=self, name=name) # type: ignore # mypy doesn't know that this is an injected class variable
assert track.sequencerInterface
self.tracks.append(track)
return track
@ -361,7 +361,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
self._enabled = bool(enabled)
cbox.Document.get_song().update_playback()
@_Interface.name.setter
@_Interface.name.setter # type: ignore
def name(self, value):
if not value in (track.sequencerInterface.name for track in self.parentData.tracks):
self._name = self._isNameAvailable(value)
@ -667,7 +667,7 @@ class TempoMap(object):
currentValue, OLD_timesigNum, OLD_timesigDenom = self._tempoMap[0]
self.setTempoMap({0:(currentValue, timesigNum, timesigDenom)})
def getQuarterNotesPerMinute(self)->float:
def getQuarterNotesPerMinute(self)->Union[float, None]:
"""This assumes there is only one tempo point"""
if self.isTransportMaster:
assert len(self._tempoMap) == 1, len(self._tempoMap)

13
template/mypy.ini

@ -0,0 +1,13 @@
[mypy]
[mypy-*.calfbox.*]
ignore_missing_imports = True
[mypy-calfbox.*]
ignore_missing_imports = True
[mypy-compiledprefix.*]
ignore_missing_imports = True
[mypy-*.designer.*]
ignore_missing_imports = True

3
template/qtgui/about.py

@ -23,6 +23,7 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Lib
from random import choice
from typing import List
#System Wide Modules
from PyQt5 import QtCore, QtWidgets, QtGui
@ -49,7 +50,7 @@ class About(QtWidgets.QDialog):
METADATA["showAboutDialogFirstStart"]
"""
didYouKnow = [] #Will be filled by the non-template part of the program
didYouKnow:List[str] = [] #Will be filled by the non-template part of the program
def __init__(self, mainWindow):
#super(DidYouKnow, self).__init__(parent, QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.CustomizeWindowHint|QtCore.Qt.X11BypassWindowManagerHint)

2
template/qtgui/helper.py

@ -253,7 +253,7 @@ class ToggleSwitch(QtWidgets.QAbstractButton):
}
self._track_opacity = 0.7
@QtCore.pyqtProperty(int)
@property
def offset(self):
return self._offset

8
template/qtgui/mainwindow.py

@ -61,13 +61,13 @@ api.session.eventLoop = EventLoop()
#Language variants like de_AT.UTF-8 will be detected automatically and will result in Qt language detection as "German"
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
logger.info("{}: Language set to {}".format(METADATA["name"], language))
if language in METADATA["supportedLanguages"]:
if language in METADATA["supportedLanguages"]: # type: ignore
templateTranslator = QtCore.QTranslator()
templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL
templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") # type: ignore #colon to make it a resource URL
qtApp.installTranslator(templateTranslator)
otherTranslator = QtCore.QTranslator()
otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") #colon to make it a resource URL
otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") # type: ignore #colon to make it a resource URL
qtApp.installTranslator(otherTranslator)
else:
@ -148,7 +148,7 @@ class MainWindow(QtWidgets.QMainWindow):
if api.session.guiWasSavedAsNSMVisible:
self.showGUI()
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) # type: ignore #mypy cannot handle METADATA
if settings.contains("showAboutDialog") and settings.value("showAboutDialog", type=bool):
QtCore.QTimer.singleShot(100, self.about.show) #Qt Event loop is not ready at that point. We need to wait for the paint event. This is not to stall for time: Using the event loop guarantees that it exists
elif not self.nsmClient.sessionName == "NOT-A-SESSION": #standalone mode

1
template/qtgui/menu.py

@ -211,7 +211,6 @@ class Menu(object):
menuAction.setVisible(False)
def removeSubmenu(self, submenuAction:str):
menuAction = getattr(self.ui, submenu).menuAction()
raise NotImplementedError #TODO
def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False, startChecked=False):

2
template/qtgui/nsmclient.py

@ -28,7 +28,7 @@ OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWA
"""
import logging;
logger = None #filled by init with prettyName
logger: logging.Logger #filled by init with client logger.
import struct
import socket

22
template/start.py

@ -32,8 +32,7 @@ Same with the tests if jack or nsm are running.
#But every bit helps when hunting bugs.
import faulthandler; faulthandler.enable()
from engine.config import * #includes METADATA only. No other environmental setup is executed.
from engine.config import METADATA #includes METADATA only. No other environmental setup is executed.
from template.qtgui.chooseSessionDirectory import ChooseSessionDirectory
from template.qtgui.helper import setPaletteAndFont #our error boxes shall look like the rest of the program
@ -93,7 +92,8 @@ logger.info(f"Script dir: {get_script_dir()}")
logger.info(f"Python Version {sys.version}")
try:
from compiledprefix import prefix
from compiledprefix import prefix as prefix_import
prefix:str = prefix_import
compiledVersion = True
logger.info("Compiled prefix found: {}".format(prefix))
except ModuleNotFoundError as e:
@ -101,22 +101,22 @@ except ModuleNotFoundError as e:
logger.info("Compiled version: {}".format(compiledVersion))
cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"]
cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] # type: ignore
logger.info("Our calfbox extension cpython library should be named " + cboxSharedObjectVersionedName)
#ZippApp with compiledprefix.py
if compiledVersion:
PATHS={ #this gets imported
"root": "",
"bin": os.path.join(prefix, "bin"),
"doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]),
"desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), #not ~/Desktop but our desktop file
"share": os.path.join(prefix, "share", METADATA["shortName"]),
"templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"),
"bin": os.path.join(prefix, "bin"), # type: ignore
"doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), # type: ignore
"desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), # type: ignore #not ~/Desktop but our desktop file
"share": os.path.join(prefix, "share", METADATA["shortName"]), # type: ignore
"templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), # type: ignore
#"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH
}
cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName)
cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) # type: ignore
_root = os.path.dirname(__file__)
_root = os.path.abspath(os.path.join(_root, ".."))
@ -301,7 +301,7 @@ def profiler(*pargs, **kwds):
#Catch Exceptions even if PyQt crashes.
import sys
sys._excepthook = sys.excepthook
sys._excepthook = sys.excepthook # type: ignore #mypy thinks that doesn't exist, eventhough we set it ourselves right here.
def exception_hook(exctype, value, traceback):
"""This hook purely exists to call sys.exit(1) even on a Qt crash
so that atexit gets triggered"""

Loading…
Cancel
Save