1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: configure.py 4# Purpose: Installation and Configuration Utilities 5# 6# Authors: Christopher Ariza 7# 8# Copyright: Copyright © 2011-2019 Michael Scott Cuthbert and the music21 Project 9# License: BSD, see license.txt 10# ------------------------------------------------------------------------------ 11import os 12import pathlib 13import re 14import time 15import sys 16import unittest 17import textwrap 18import webbrowser 19 20from typing import List 21 22from importlib import reload # Python 3.4 23 24import io 25 26# assume that we will manually add this dir to sys.path top get access to 27# all modules before installation 28from music21 import common 29from music21 import environment 30from music21 import exceptions21 31 32_MOD = 'configure' 33environLocal = environment.Environment(_MOD) 34 35_DOC_IGNORE_MODULE_OR_PACKAGE = True 36IGNORECASE = re.RegexFlag.IGNORECASE 37 38# ------------------------------------------------------------------------------ 39# match finale name, which may be directory or something else 40reFinaleApp = re.compile(r'Finale.*\.app', IGNORECASE) 41reSibeliusApp = re.compile(r'Sibelius\.app', IGNORECASE) 42reFinaleExe = re.compile(r'Finale.*\.exe', IGNORECASE) 43reSibeliusExe = re.compile(r'Sibelius\.exe', IGNORECASE) 44reFinaleReaderApp = re.compile(r'Finale Reader\.app', IGNORECASE) 45reMuseScoreApp = re.compile(r'MuseScore.*\.app', IGNORECASE) 46reMuseScoreExe = re.compile(r'Musescore.*\\bin\\MuseScore.*\.exe', IGNORECASE) 47 48urlMusic21 = 'https://web.mit.edu/music21' 49urlMuseScore = 'http://musescore.org' 50urlGettingStarted = 'https://web.mit.edu/music21/doc/' 51urlMusic21List = 'https://groups.google.com/g/music21list' 52 53LINE_WIDTH = 78 54 55# ------------------------------------------------------------------------------ 56# class Action(threading.Thread): 57# ''' 58# A thread-based action for performing remote actions, like downloading 59# or opening in a webbrowser. 60# ''' 61# def __init__ (self, prompt, timeOutTime): 62# super().__init__() 63# self.status = None 64# 65# def run(self): 66# pass 67 68 69# ------------------------------------------------------------------------------ 70 71 72def writeToUser(msg, wrapLines=True, linesPerPage=20): 73 ''' 74 Display a message to the user, handling multiple lines as necessary and wrapping text 75 ''' 76 # wrap everything to 60 lines 77 if common.isListLike(msg): 78 lines = msg 79 else: 80 # divide into lines if lines breaks are already in place 81 lines = msg.split('\n') 82 # print(lines) 83 post = [] 84 if wrapLines: 85 for sub in lines: 86 if sub == '': 87 post.append('') 88 elif sub == ' ': 89 post.append(' ') 90 else: 91 # concatenate lines 92 post += textwrap.wrap(sub, LINE_WIDTH) 93 else: 94 post = lines 95 96 # print(post) 97 lineCount = 0 98 for i, line in enumerate(post): 99 if line == '': # treat an empty line as a break 100 line = '\n' 101 # if first and there is more than one line 102 elif i == 0 and len(post) > 1: 103 # add a leading space 104 line = f'\n{line} \n' 105 # if only one line 106 elif i == 0 and len(post) == 1: 107 line = f'\n{line} ' 108 elif i < len(post) - 1: # if not last 109 line = f'{line} \n' 110 else: # if last, add trailing space, do not add trailing return 111 line = f'{line} ' 112 if lineCount > 0 and lineCount % linesPerPage == 0: 113 # ask user to continue 114 d = AnyKey(promptHeader='Pausing for page.') 115 d.askUser() 116 sys.stdout.write(line) 117 sys.stdout.flush() 118 lineCount += 1 119 120 121def getSitePackages(): 122 import distutils.sysconfig 123 return distutils.sysconfig.get_python_lib() 124 125 126def findInstallations(): 127 ''' 128 Find all music21 references found in site packages, or 129 possibly look at the running code as well. 130 ''' 131 found = [] 132 sitePackages = getSitePackages() 133 for fn in sorted(os.listdir(sitePackages)): 134 if fn.startswith('music21'): 135 found.append(os.path.join(sitePackages, fn)) 136 try: 137 # see if we can import music21 138 import music21 # pylint: disable=redefined-outer-name 139 found.append(music21.__path__[0]) # list, get first item 140 except ImportError: 141 pass 142 return found 143 144 145def findInstallationsEggInfo(): 146 ''' 147 Find all music21 eggs found in site packages, or possibly look 148 at the running code as well. 149 ''' 150 found = findInstallations() 151 # only get those that end w/ egg-info 152 post = [] 153 for fp in found: 154 unused_dir, fn = os.path.split(fp) 155 if fn.endswith('egg-info') or fn.endswith('egg'): 156 post.append(fn) 157 return post 158 159 160def findInstallationsEggInfoStr(): 161 ''' 162 Return a string presentation, or the string None 163 ''' 164 found = findInstallationsEggInfo() 165 if not found: 166 return 'None' 167 else: 168 return ','.join(found) 169 170 171def getUserData(): 172 ''' 173 Return a dictionary with user data 174 ''' 175 post = {} 176 try: 177 import music21 # pylint: disable=redefined-outer-name 178 post['music21.version'] = music21.VERSION_STR 179 except ImportError: 180 post['music21.version'] = 'None' 181 182 post['music21 egg-info current'] = findInstallationsEggInfoStr() 183 184 if hasattr(os, 'uname'): 185 uname = os.uname() 186 post['os.uname'] = f'{uname[0]}, {uname[2]}, {uname[4]}' 187 else: # catch all 188 post['os.uname'] = 'None' 189 190 post['time.gmtime'] = time.strftime('%a, %d %b %Y %H:%M:%S', time.gmtime()) 191 post['time.timezone'] = time.timezone 192 193 tzname = time.tzname 194 if len(tzname) == 2 and tzname[1] not in [None, 'None', '']: 195 post['time.tzname'] = tzname[1] 196 else: 197 post['time.tzname'] = tzname[0] 198 return post 199 200 201def _crawlPathUpward(start, target): 202 ''' 203 Ascend up paths given a start; return when target file has been found. 204 ''' 205 lastDir = start 206 thisDir = lastDir 207 match = None 208 # first, ascend upward 209 while True: 210 environLocal.printDebug(f'at dir: {thisDir}') 211 if match is not None: 212 break 213 for fn in sorted(os.listdir(thisDir)): 214 if fn == target: 215 match = os.path.join(thisDir, fn) 216 break 217 lastDir = thisDir 218 thisDir, junk = os.path.split(thisDir) 219 if thisDir == lastDir: # at top level 220 break 221 return match 222 223 224def findSetup(): 225 ''' 226 Find the setup.py script and returns the path to the setup.py file. 227 ''' 228 # find setup.py 229 # look in current directory and ascending 230 match = _crawlPathUpward(start=os.getcwd(), target='setup.py') 231 # if no match, search downward if music21 is in this directory 232 if match is None: 233 if 'music21' in os.listdir(os.getcwd()): 234 sub = os.path.join(os.getcwd(), 'music21') 235 if 'setup.py' in os.listdir(sub): 236 match = os.path.join(sub, 'setup.py') 237 238 # if still not found, try to get from importing music21. 239 # this may not be correct, as this might be a previous music21 installation 240 # if match is None: 241 # try: 242 # import music21 243 # fpMusic21 = music21.__path__[0] # list, get first item 244 # except ImportError: 245 # fpMusic21 = None 246 # if fpMusic21 is not None: 247 # match = _crawlPathUpward(start=fpMusic21, target='setup.py') 248 249 environLocal.printDebug([f'found setup.py: {match}']) 250 return match 251 252 253# ------------------------------------------------------------------------------ 254# error objects, not exceptions 255class DialogError: 256 ''' 257 DialogError is a normal object, not an Exception. 258 ''' 259 def __init__(self, src=None): 260 self.src = src 261 262 def __repr__(self): 263 return f'<music21.configure.{self.__class__.__name__}: {self.src}>' 264 265 266class KeyInterruptError(DialogError): 267 ''' 268 Subclass of DialogError that deals with Keyboard Interruptions. 269 ''' 270 271 def __init__(self, src=None): 272 super().__init__(src=src) 273 274 275class IncompleteInput(DialogError): 276 ''' 277 Subclass of DialogError that runs when the user has provided 278 incomplete input that cannot be understood. 279 ''' 280 281 def __init__(self, src=None): 282 super().__init__(src=src) 283 284 285class NoInput(DialogError): 286 ''' 287 Subclass of DialogError for when the user has provided no input, and there is not a default. 288 ''' 289 290 def __init__(self, src=None): 291 super().__init__(src=src) 292 293 294class BadConditions(DialogError): 295 ''' 296 Subclass of DialogError for when the user's system does support the 297 action of the dialog: something is missing or 298 otherwise prohibits operation. 299 ''' 300 301 def __init__(self, src=None): 302 super().__init__(src=src) 303 304 305# ------------------------------------------------------------------------------ 306class DialogException(exceptions21.Music21Exception, DialogError): 307 pass 308 309# ------------------------------------------------------------------------------ 310 311 312class Dialog: 313 ''' 314 Model a dialog as a question and response. Have different subclasses for 315 different types of questions. Store all in a Conversation, or multiple dialog passes. 316 317 A `default`, if provided, is returned if the users provides no input and just enters return. 318 319 The `tryAgain` option determines if, if a user provides incomplete or no response, 320 and there is no default (for no response), whether the user is given another chance 321 to provide valid input. 322 323 The `promptHeader` is a string header that is placed in front of any common header 324 for this dialog. 325 ''' 326 327 def __init__(self, default=None, tryAgain=True, promptHeader=None): 328 # store the result obtained from the user 329 self._result = None 330 # store a previously entered value, permitting undoing an action 331 self._resultPrior = None 332 # set the default 333 # parse the default to permit expressive flexibility 334 defaultCooked = self._parseUserInput(default) 335 # if not any class of error: 336 # environLocal.printDebug(['Dialog: defaultCooked:', defaultCooked]) 337 338 if not isinstance(defaultCooked, DialogError): 339 self._default = defaultCooked 340 else: 341 # default is None by default; this cannot be a default value then 342 self._default = None 343 # if we try again 344 self._tryAgain = tryAgain 345 self._promptHeader = promptHeader 346 347 # how many times to ask the user again and again for the same thing 348 self._maxAttempts = 8 349 350 # set platforms this dialog should run in 351 self._platforms = ['win', 'darwin', 'nix'] 352 353 def _writeToUser(self, msg): 354 '''Write output to user. Call module-level function 355 ''' 356 writeToUser(msg) 357 358 def _readFromUser(self): 359 '''Collect from user; return None if an empty response. 360 ''' 361 # noinspection PyBroadException 362 try: 363 post = input() 364 return post 365 except KeyboardInterrupt: 366 # store as own class so as a subclass of dialog error 367 return KeyInterruptError() 368 except Exception: # pylint: disable=broad-except 369 return DialogError() 370 371 def prependPromptHeader(self, msg): 372 '''Add a message to the front of the stored prompt header. 373 374 375 >>> d = configure.Dialog() 376 >>> d.prependPromptHeader('test') 377 >>> d._promptHeader 378 'test' 379 380 >>> d = configure.Dialog(promptHeader='this is it') 381 >>> d.prependPromptHeader('test') 382 >>> d._promptHeader 383 'test this is it' 384 ''' 385 msg = msg.strip() 386 if self._promptHeader is not None: 387 self._promptHeader = f'{msg} {self._promptHeader}' 388 else: 389 self._promptHeader = msg 390 391 def appendPromptHeader(self, msg): 392 ''' 393 394 >>> d = configure.Dialog() 395 >>> d.appendPromptHeader('test') 396 >>> d._promptHeader 397 'test' 398 399 >>> d = configure.Dialog(promptHeader='this is it') 400 >>> d.appendPromptHeader('test') 401 >>> d._promptHeader 402 'this is it test' 403 ''' 404 msg = msg.strip() 405 if self._promptHeader is not None: 406 self._promptHeader = f'{self._promptHeader} {msg}' 407 else: 408 self._promptHeader = msg 409 410 def _askTryAgain(self, default=True, force=None): 411 ''' 412 What to do if input is incomplete 413 414 >>> prompt = configure.YesOrNo(default=True) 415 >>> prompt._askTryAgain(force='yes') 416 True 417 >>> prompt._askTryAgain(force='n') 418 False 419 >>> prompt._askTryAgain(force='') # gets default 420 True 421 >>> prompt._askTryAgain(force='blah') # error gets false 422 False 423 ''' 424 # need to call a yes or no on using default 425 d = YesOrNo(default=default, tryAgain=False, 426 promptHeader='Your input was not understood. Try Again?') 427 d.askUser(force=force) 428 post = d.getResult() 429 # if any errors are found, return False 430 if isinstance(post, DialogError): 431 return False 432 else: 433 return post 434 435 def _rawQueryPrepareHeader(self, msg=''): 436 '''Prepare the header, given a string. 437 438 >>> from music21 import configure 439 >>> d = configure.Dialog() 440 >>> d._rawQueryPrepareHeader('test') 441 'test' 442 >>> d = configure.Dialog(promptHeader='what are you doing?') 443 >>> d._rawQueryPrepareHeader('test') 444 'what are you doing? test' 445 ''' 446 if self._promptHeader is not None: 447 header = self._promptHeader.strip() 448 if header.endswith('?') or header.endswith('.'): 449 div = '' 450 else: 451 div = ':' 452 453 if self._promptHeader.endswith('\n\n'): 454 div += '\n\n' 455 elif self._promptHeader.endswith('\n'): 456 div += '\n' 457 else: 458 div += ' ' 459 msg = f'{header}{div}{msg}' 460 return msg 461 462 def _rawQueryPrepareFooter(self, msg=''): 463 '''Prepare the end of the query message 464 ''' 465 if self._default is not None: 466 msg = msg.strip() 467 if msg.endswith(':'): 468 div = ':' 469 msg = msg[:-1] 470 msg.strip() 471 else: 472 div = '' 473 default = self._formatResultForUser(self._default) 474 # leave a space at end 475 msg = f'{msg} (default is {default}){div} ' 476 return msg 477 478 def _rawIntroduction(self): 479 '''Return a multiline presentation of an introduction. 480 ''' 481 return None 482 483 def _rawQuery(self): 484 '''Return a multiline presentation of the question. 485 ''' 486 pass 487 488 def _formatResultForUser(self, result): 489 ''' 490 For various result options, we may need to at times convert the internal 491 representation of the result into something else. For example, we might present 492 the user with 'Yes' or 'No' but store the result as True or False. 493 ''' 494 # override in subclass 495 return result 496 497 def _parseUserInput(self, raw): 498 ''' 499 Translate string to desired output. Pass None through 500 (as no input), convert '' to None, and pass all other 501 outputs as IncompleteInput objects. 502 ''' 503 return raw 504 505 def _evaluateUserInput(self, raw): 506 ''' 507 Evaluate the user's string entry after parsing; do not return None: 508 either return a valid response, default if available, or IncompleteInput object. 509 ''' 510 pass 511 # define in subclass 512 513 def _preAskUser(self, force=None): 514 ''' 515 Call this method immediately before calling askUser. 516 Can be used for configuration getting additional information. 517 ''' 518 pass 519 # define in subclass 520 521 def askUser(self, force=None, *, skipIntro=False): 522 ''' 523 Ask the user, display the query. The force argument can 524 be provided to test. Sets self._result; does not return a value. 525 ''' 526 # if an introduction is defined, try to use it 527 intro = self._rawIntroduction() # pylint: disable=assignment-from-none 528 if intro is not None and not skipIntro: 529 self._writeToUser(intro) 530 531 # always call preAskUser: can customize in subclass. must return True 532 # or False. if False, askUser cannot continue 533 post = self._preAskUser(force=force) # pylint: disable=assignment-from-no-return 534 if post is False: 535 self._result = BadConditions() 536 return 537 538 # ten attempts; not using a while so will ultimately break 539 for i in range(self._maxAttempts): 540 # in some cases, the query might not be able to be formed: 541 # for example, in selecting values from a list, and not having 542 # any values. thus, query may be an error 543 query = self._rawQuery() # pylint: disable=assignment-from-no-return 544 if isinstance(query, DialogError): 545 # set result as error 546 self._result = query 547 break 548 549 if force is None: 550 self._writeToUser(query) 551 rawInput = self._readFromUser() 552 else: 553 environLocal.printDebug(['writeToUser:', query]) 554 rawInput = force 555 556 # rawInput here could be an error or a value 557 # environLocal.printDebug(['received as rawInput', rawInput]) 558 # check for errors and handle 559 if isinstance(rawInput, KeyInterruptError): 560 # set as result KeyInterruptError 561 self._result = rawInput 562 break 563 564 # need to not catch no NoInput nor IncompleteInput classes, as they 565 # will be handled in evaluation 566 # pylint: disable=assignment-from-no-return 567 cookedInput = self._evaluateUserInput(rawInput) 568 # environLocal.printDebug(['post _evaluateUserInput() cookedInput', cookedInput]) 569 570 # if no default and no input, we get here (default supplied in 571 # evaluate 572 if isinstance(cookedInput, (NoInput, IncompleteInput)): 573 # set result to these objects whether or not try again 574 self._result = cookedInput 575 if self._tryAgain: 576 # only returns True or False 577 if self._askTryAgain(): 578 pass 579 else: # this will keep whatever the cooked was 580 break 581 else: 582 break 583 else: 584 # should be in proper format after evaluation 585 self._result = cookedInput 586 break 587 # self._result may still be None 588 589 def getResult(self, simulate=True): 590 ''' 591 Return the result, or None if not set. This may also do a 592 processing routine that is part of the desired result. 593 ''' 594 return self._result 595 596 def _performAction(self, simulate=False): 597 ''' 598 does nothing; redefine in subclass 599 ''' 600 pass 601 602 def performAction(self, simulate=False): 603 ''' 604 After getting a result, the query might require an action 605 to be performed. If result is None, this will use whatever 606 value is found in _result. 607 608 If simulate is True, no action will be taken. 609 ''' 610 dummy = self.getResult() 611 if isinstance(self._result, DialogError): 612 environLocal.printDebug( 613 f'performAction() called, but result is an error: {self._result}') 614 self._writeToUser(['No action taken.', ' ']) 615 616 elif simulate: # do not operate 617 environLocal.printDebug( 618 f'performAction() called, but in simulation mode: {self._result}') 619 else: 620 try: 621 self._performAction(simulate=simulate) 622 except DialogException: # pylint: disable=catching-non-exception 623 # in some cases, the action selected requires exciting the 624 # configuration assistant 625 # pylint: disable=raising-non-exception,raise-missing-from 626 raise DialogException('perform action raised a dialog exception') 627 628 629# ------------------------------------------------------------------------------ 630class AnyKey(Dialog): 631 ''' 632 Press any key to continue 633 ''' 634 635 def __init__(self, default=None, tryAgain=False, promptHeader=None): 636 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 637 638 def _rawQuery(self): 639 ''' 640 Return a multiline presentation of the question. 641 ''' 642 msg = 'Press return to continue.' 643 msg = self._rawQueryPrepareHeader(msg) 644 # footer provides default; here, ignore 645 # msg = self._rawQueryPrepareFooter(msg) 646 return msg 647 648 def _parseUserInput(self, raw): 649 ''' 650 Always returns True 651 ''' 652 return True 653 654 655# ------------------------------------------------------------------------------ 656class YesOrNo(Dialog): 657 ''' 658 Ask a yes or no question. 659 660 >>> d = configure.YesOrNo(default=True) 661 >>> d.askUser('yes') # force arg for testing 662 >>> d.getResult() 663 True 664 665 >>> d = configure.YesOrNo(tryAgain=False) 666 >>> d.askUser('junk') # force arg for testing 667 >>> d.getResult() 668 <music21.configure.IncompleteInput: junk> 669 ''' 670 671 def __init__(self, default=None, tryAgain=True, promptHeader=None): 672 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 673 674 def _formatResultForUser(self, result): 675 ''' 676 For various result options, we may need to at times convert 677 the internal representation of the result into something else. 678 For example, we might present the user with 'Yes' or 'No' but 679 store the result as True or False. 680 ''' 681 if result is True: 682 return 'Yes' 683 elif result is False: 684 return 'No' 685 # while a result might be an error object, this method should probably 686 # never be called with such objects. 687 else: 688 raise DialogException(f'attempting to format result for user: {result}') 689 690 def _rawQuery(self): 691 ''' 692 Return a multiline presentation of the question. 693 694 >>> d = configure.YesOrNo(default=True) 695 >>> d._rawQuery() 696 'Enter Yes or No (default is Yes): ' 697 >>> d = configure.YesOrNo(default=False) 698 >>> d._rawQuery() 699 'Enter Yes or No (default is No): ' 700 701 >>> d = configure.YesOrNo(default=True, promptHeader='Would you like more time?') 702 >>> d._rawQuery() 703 'Would you like more time? Enter Yes or No (default is Yes): ' 704 ''' 705 msg = 'Enter Yes or No: ' 706 msg = self._rawQueryPrepareHeader(msg) 707 msg = self._rawQueryPrepareFooter(msg) 708 return msg 709 710 def _parseUserInput(self, raw): 711 ''' 712 Translate string to desired output. Pass None and '' (as no input), as 713 NoInput objects, and pass all other outputs as IncompleteInput objects. 714 715 >>> d = configure.YesOrNo() 716 >>> d._parseUserInput('y') 717 True 718 >>> d._parseUserInput('') 719 <music21.configure.NoInput: None> 720 >>> d._parseUserInput('asdf') 721 <music21.configure.IncompleteInput: asdf> 722 ''' 723 if raw is None: 724 return NoInput() 725 # string; 726 raw = str(raw) 727 raw = raw.strip() 728 raw = raw.lower() 729 if raw == '': 730 return NoInput() 731 732 if raw in ['yes', 'y', '1', 'true']: 733 return True 734 elif raw in ['no', 'n', '0', 'false']: 735 return False 736 # if no match, or an empty string 737 return IncompleteInput(raw) 738 739 def _evaluateUserInput(self, raw): 740 ''' 741 Evaluate the user's string entry after parsing; 742 do not return None: either return a valid response, 743 default if available, IncompleteInput, NoInput objects. 744 745 >>> d = configure.YesOrNo() 746 >>> d._evaluateUserInput('y') 747 True 748 >>> d._evaluateUserInput('False') 749 False 750 >>> d._evaluateUserInput('') # there is no default, 751 <music21.configure.NoInput: None> 752 >>> d._evaluateUserInput('wer') # there is no default, 753 <music21.configure.IncompleteInput: wer> 754 755 >>> d = configure.YesOrNo('yes') 756 >>> d._evaluateUserInput('') # there is a default 757 True 758 >>> d._evaluateUserInput('wbf') # there is a default 759 <music21.configure.IncompleteInput: wbf> 760 761 >>> d = configure.YesOrNo('n') 762 >>> d._evaluateUserInput('') # there is a default 763 False 764 >>> d._evaluateUserInput(None) # None is processed as NoInput 765 False 766 >>> d._evaluateUserInput('blah') # None is processed as NoInput 767 <music21.configure.IncompleteInput: blah> 768 ''' 769 rawParsed = self._parseUserInput(raw) 770 # means no answer: return default 771 if isinstance(rawParsed, NoInput): 772 if self._default is not None: 773 return self._default 774 # could be IncompleteInput, NoInput, or a proper, valid answer 775 return rawParsed 776 777 778# ------------------------------------------------------------------------------ 779class AskOpenInBrowser(YesOrNo): 780 ''' 781 Ask the user if the want to open a URL in a browser. 782 783 784 >>> d = configure.AskOpenInBrowser('http://mit.edu/music21') 785 ''' 786 787 def __init__(self, urlTarget, default=True, tryAgain=True, 788 promptHeader=None, prompt=None): 789 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 790 791 self._urlTarget = urlTarget 792 # try to directly set prompt header 793 if prompt is not None: 794 # override whatever is already in the prompt 795 self._promptHeader = prompt 796 else: # else, append 797 msg = f'Open the following URL ({self._urlTarget}) in a web browser?\n' 798 self.appendPromptHeader(msg) 799 800 def _performAction(self, simulate=False): 801 '''The action here is to open the stored URL in a browser, if the user agrees. 802 ''' 803 result = self.getResult() 804 if result is True: 805 webbrowser.open_new(self._urlTarget) 806 elif result is False: 807 pass 808 # self._writeToUser(['No URL is opened.', ' ']) 809 810 # perform action 811 812 813class AskInstall(YesOrNo): 814 ''' 815 Ask the user if they want to move music21 to the normal place... 816 ''' 817 def __init__(self, default=True, tryAgain=True, 818 promptHeader=None): 819 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 820 821 # define platforms that this will run on 822 self._platforms = ['darwin', 'nix'] 823 824 msg = ( 825 'Would you like to install music21 in the normal ' 826 + 'place for Python packages (i.e., site-packages)?' 827 ) 828 self.appendPromptHeader(msg) 829 830 def _performActionNix(self, simulate=False): 831 fp = findSetup() 832 if fp is None: 833 return None 834 835 self._writeToUser(['You must authorize writing in the following directory:', 836 getSitePackages(), 837 ' ', 838 'Please provide your user password to complete this operation.', 839 '']) 840 841 stdoutSrc = sys.stdout 842 # stderrSrc = sys.stderr 843 844 fileLikeOpen = io.StringIO() 845 sys.stdout = fileLikeOpen 846 847 directory, unused_fn = os.path.split(fp) 848 pyPath = sys.executable 849 cmd = f'cd {directory!r}; sudo {pyPath!r} setup.py install' 850 post = os.system(cmd) 851 852 fileLikeOpen.close() 853 sys.stdout = stdoutSrc 854 # sys.stderr = stderrSrc 855 return post 856 857 def _performAction(self, simulate=False): 858 '''The action here is to install in site packages, if the user agrees. 859 ''' 860 result = self.getResult() 861 if result is not True: 862 return None 863 864 platform = common.getPlatform() 865 if platform == 'win': 866 post = None 867 elif platform == 'darwin': 868 post = self._performActionNix() 869 elif platform == 'nix': 870 post = self._performActionNix() 871 else: 872 post = self._performActionNix() 873 return post 874 875 876class AskSendInstallationReport(YesOrNo): 877 ''' 878 Ask the user if they want to send a report 879 regarding their system and usage. 880 ''' 881 def __init__(self, default=True, tryAgain=True, 882 promptHeader=None, additionalEntries=None): 883 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 884 885 if additionalEntries is None: 886 additionalEntries = {} 887 self._additionalEntries = additionalEntries 888 889 msg = ('Would you like to send a pre-formatted email to music21 regarding your ' 890 'installation? Installation reports help us make music21 work better for you') 891 self.appendPromptHeader(msg) 892 893 def _getMailToStr(self): 894 # noinspection PyListCreation 895 body = [] 896 body.append('Please send the following email; your return email address ' 897 'will never be used in any way.') 898 body.append('') 899 body.append('The following information on your installation ' 900 'will be used only for research.') 901 body.append('') 902 903 userData = getUserData() 904 # add any additional entries; this is used for adding the original egg info 905 userData.update(self._additionalEntries) 906 for key in sorted(userData): 907 body.append(f'{key} // {userData[key]}') 908 body.append('python version:') 909 body.append(sys.version) 910 911 body.append('') 912 body.append('Below, please provide a few words about what sorts of tasks ' 913 'or problems you plan to explore with music21. Any information on ' 914 'your background is also appreciated (e.g., amateur musician, ' 915 'computer programmer, professional music researcher). Thanks!') 916 body.append('') 917 918 platform = common.getPlatform() 919 if platform == 'win': # need to add proper return carriage for win 920 body = '%0D%0A'.join(body) 921 else: 922 body = '\n'.join(body) 923 924 msg = f'''mailto:music21stats@gmail.com?subject=music21 Installation Report&body={body}''' 925 return msg # pass this to webbrowser 926 927 def _performAction(self, simulate=False): 928 ''' 929 The action here is to open the stored URL in a browser, if the user agrees. 930 ''' 931 result = self.getResult() 932 if result is True: 933 webbrowser.open(self._getMailToStr()) 934 935 936# ------------------------------------------------------------------------------ 937class SelectFromList(Dialog): 938 ''' 939 General class to select values from a list. 940 941 >>> d = configure.SelectFromList() # empty selection list 942 >>> d.askUser('no') # results in bad condition 943 >>> d.getResult() 944 <music21.configure.BadConditions: None> 945 946 >>> d = configure.SelectFromList() # empty selection list 947 >>> def validResults(force=None): 948 ... return range(5) 949 >>> d._getValidResults = validResults # provide alt function for testing 950 >>> d.askUser(2) # results in bad condition 951 >>> d.getResult() 952 2 953 ''' 954 955 def __init__(self, default=None, tryAgain=True, promptHeader=None): 956 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 957 958 def _getValidResults(self, force=None): 959 ''' 960 Return a list of valid results that are possible and should be displayed to the user. 961 ''' 962 # this might need to be cached 963 # customize in subclass 964 if force is not None: 965 return force 966 else: 967 return [] 968 969 def _formatResultForUser(self, result): 970 '''Reduce each complete file path to stub, or otherwise compact display 971 ''' 972 return result 973 974 def _askFillEmptyList(self, default=None, force=None): 975 ''' 976 What to do if the selection list is empty. Only return True or False: 977 if we should continue or not. 978 979 >>> prompt = configure.SelectFromList(default=True) 980 >>> prompt._askFillEmptyList(force='yes') 981 True 982 >>> prompt._askFillEmptyList(force='n') 983 False 984 >>> prompt._askFillEmptyList(force='') # no default, returns False 985 False 986 >>> prompt._askFillEmptyList(force='blah') # error gets false 987 False 988 ''' 989 # this does not do anything: customize in subclass 990 d = YesOrNo(default=default, 991 tryAgain=False, 992 promptHeader='The selection list is empty. Try Again?') 993 d.askUser(force=force) 994 post = d.getResult() 995 # if any errors are found, return False 996 if isinstance(post, DialogError): 997 return False 998 else: # must be True or False 999 if post not in [True, False]: 1000 # this should never happen... 1001 raise DialogException( 1002 '_askFillEmptyList(): sub-command returned non True/False value') 1003 return post 1004 1005 def _preAskUser(self, force=None): 1006 ''' 1007 Before we ask user, we need to to run _askFillEmptyList list if the list is empty. 1008 1009 >>> d = configure.SelectFromList() 1010 >>> d._preAskUser('no') # force for testing 1011 False 1012 >>> d._preAskUser('yes') # force for testing 1013 True 1014 >>> d._preAskUser('') # no default, returns False 1015 False 1016 >>> d._preAskUser('x') # bad input returns False 1017 False 1018 ''' 1019 options = self._getValidResults() 1020 if not options: 1021 # must return True/False, 1022 post = self._askFillEmptyList(force=force) 1023 return post 1024 else: # if we have options, return True 1025 return True 1026 1027 def _rawQuery(self, force=None): 1028 ''' 1029 Return a multiline presentation of the question. 1030 1031 >>> d = configure.SelectFromList() 1032 >>> d._rawQuery(['a', 'b', 'c']) 1033 ['[1] a', '[2] b', '[3] c', ' ', 'Select a number from the preceding options: '] 1034 1035 >>> d = configure.SelectFromList(default=1) 1036 >>> d._default 1037 1 1038 >>> d._rawQuery(['a', 'b', 'c']) 1039 ['[1] a', '[2] b', '[3] c', ' ', 1040 'Select a number from the preceding options (default is 1): '] 1041 ''' 1042 head = [] 1043 i = 1 1044 options = self._getValidResults(force=force) 1045 # if no options, cannot form query: return bad conditions 1046 if not options: 1047 return BadConditions('no options available') 1048 1049 for entry in options: 1050 sub = self._formatResultForUser(entry) 1051 head.append(f'[{i}] {sub}') 1052 i += 1 1053 1054 tail = 'Select a number from the preceding options: ' 1055 tail = self._rawQueryPrepareHeader(tail) 1056 tail = self._rawQueryPrepareFooter(tail) 1057 return head + [' ', tail] 1058 1059 def _parseUserInput(self, raw): 1060 ''' 1061 Convert all values to an integer, or return NoInput or IncompleteInput. 1062 Do not yet evaluate whether the number is valid in the context of the selection choices. 1063 1064 >>> d = configure.SelectFromList() 1065 ''' 1066 # environLocal.printDebug(['SelectFromList', '_parseUserInput', 'raw', raw]) 1067 if raw is None: 1068 return NoInput() 1069 if raw == '': 1070 return NoInput() 1071 # accept yes as 1 1072 1073 if raw in ['yes', 'y', '1', 'true']: 1074 post = 1 1075 else: # try to convert string into a number 1076 try: 1077 post = int(raw) 1078 # catch all problems 1079 except (ValueError, TypeError, ZeroDivisionError): 1080 return IncompleteInput(raw) 1081 return post 1082 1083 def _evaluateUserInput(self, raw): 1084 rawParsed = self._parseUserInput(raw) 1085 1086 # means no answer: return default 1087 if isinstance(rawParsed, NoInput): 1088 if self._default is not None: 1089 return self._default 1090 1091 # could be IncompleteInput, NoInput, or a proper, valid answer 1092 return rawParsed 1093 1094 1095class AskAutoDownload(SelectFromList): 1096 ''' 1097 General class to select values from a list. 1098 ''' 1099 1100 def __init__(self, default=1, tryAgain=True, promptHeader=None): 1101 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 1102 1103 def _rawIntroduction(self): 1104 '''Return a multiline presentation of an introduction. 1105 ''' 1106 return ['The BSD-licensed music21 software is distributed with a corpus of encoded ' 1107 'compositions which are distributed with the permission of the encoders ' 1108 '(and, where needed, the composers or arrangers) and where permitted under ' 1109 'United States copyright law. Some encodings included in the corpus may not ' 1110 'be used for commercial uses or have other restrictions: please see the ' 1111 'licenses embedded in individual compositions or directories for more details.', 1112 ' ', 1113 'In addition to the corpus distributed with music21, other pieces are not ' 1114 'included in this distribution, but are indexed as links to other web sites ' 1115 'where they can be downloaded (the "virtual corpus"). If you would like, music21 ' 1116 'can help your computer automatically resolve these links and bring them to your ' 1117 'hard drive for analysis. ' 1118 # 'See corpus/virtual.py for a list of sites that music21 ' 1119 # 'might index.', 1120 ' ', 1121 'To the best of our knowledge, the music (if not the encodings) in the corpus are ' 1122 'either out of copyright in the United States and/or are licensed for ' 1123 'non-commercial use. These works, along with any works linked to in the virtual ' 1124 'corpus, may or may not be free in your jurisdiction. If you believe this message ' 1125 'to be in error regarding one or more works please contact ' 1126 'Michael Cuthbert at cuthbert@mit.edu.', 1127 ' ', 1128 'Would you like to:' 1129 ] 1130 1131 def _getValidResults(self, force=None): 1132 '''Just return number options 1133 ''' 1134 if force is not None: 1135 return force 1136 else: 1137 return [ 1138 'Acknowledge these terms and allow music21 to aid in finding pieces in the corpus', 1139 'Acknowledge these terms and block the virtual corpus', 1140 'Do not agree to these terms and will not use music21 (agreeing to the terms of ' 1141 + 'the corpus is mandatory for using the system).' 1142 ] 1143 1144 def _evaluateUserInput(self, raw): 1145 '''Evaluate the user's string entry after parsing; do not return None: 1146 either return a valid response, default if available, IncompleteInput, NoInput objects. 1147 ''' 1148 rawParsed = self._parseUserInput(raw) 1149 # if NoInput: and a default, return default 1150 if isinstance(rawParsed, NoInput): 1151 if self._default is not None: 1152 # do not return the default, as this here is a number 1153 # and proper results are file paths. thus, set rawParsed 1154 # to default; will get converted later 1155 rawParsed = self._default 1156 1157 # could be IncompleteInput, NoInput, or a proper, valid answer 1158 if isinstance(rawParsed, DialogError): # keep as is 1159 return rawParsed 1160 1161 if 1 <= rawParsed <= 3: 1162 return rawParsed 1163 else: 1164 return IncompleteInput(rawParsed) 1165 1166 def _performAction(self, simulate=False): 1167 ''' 1168 override base. 1169 ''' 1170 result = self.getResult() 1171 if result in [1, 2, 3]: 1172 # noinspection PyTypeChecker 1173 reload(environment) 1174 # us = environment.UserSettings() 1175 if result == 1: 1176 # calling this function will check to see if a file is created 1177 environment.set('autoDownload', 'allow') 1178 # us['autoDownload'] = 'allow' # automatically writes 1179 elif result == 2: 1180 # us['autoDownload'] = 'deny' # automatically writes 1181 environment.set('autoDownload', 'deny') 1182 elif result == 3: 1183 raise DialogException('user selected an option that terminates installer.') 1184 1185 if result in [1, 2]: 1186 self._writeToUser([f"Auto Download set to: {environment.get('autoDownload')}", ' ']) 1187 1188 1189class SelectFilePath(SelectFromList): 1190 ''' 1191 General class to select values from a list. 1192 ''' 1193 1194 def __init__(self, default=None, tryAgain=True, promptHeader=None): 1195 super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader) 1196 1197 def _getAppOSIndependent(self, comparisonFunction, path0: str, post: List[str], 1198 *, 1199 glob: str = '**/*'): 1200 ''' 1201 Uses comparisonFunction to see if a file in path0 matches 1202 the RE embedded in comparisonFunction and if so manipulate the list 1203 in post 1204 1205 comparisonFunction = function (lambda function on path returning True/False) 1206 path0 = os-specific string 1207 post = list of matching results to fill 1208 glob = string to glob for (default **/*, but **/*.exe on Windows and * on Mac). 1209 ''' 1210 path0_as_path = pathlib.Path(path0) 1211 for path1 in path0_as_path.glob(glob): 1212 if comparisonFunction(str(path1)): 1213 post.append(str(path1)) 1214 1215 def _getDarwinApp(self, comparisonFunction) -> List[str]: 1216 ''' 1217 Provide a comparison function that returns True or False based on the file name. 1218 This looks at everything in Applications, as well as every directory in Applications 1219 ''' 1220 post: List[str] = [] 1221 for path0 in ('/Applications', common.cleanpath('~/Applications')): 1222 self._getAppOSIndependent(comparisonFunction, path0, post, glob='*') 1223 return post 1224 1225 def _getWinApp(self, comparisonFunction) -> List[str]: 1226 ''' 1227 Provide a comparison function that returns True or False based on the file name. 1228 ''' 1229 # provide a similar method to _getDarwinApp 1230 post: List[str] = [] 1231 environKeys = ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432') 1232 for possibleEnvironKey in environKeys: 1233 if possibleEnvironKey not in os.environ: 1234 continue # Many do not define ProgramW6432 1235 environPath = os.environ[possibleEnvironKey] 1236 if environPath == '': 1237 continue 1238 self._getAppOSIndependent(comparisonFunction, environPath, post, glob='**/*.exe') 1239 1240 return post 1241 1242 def _evaluateUserInput(self, raw): 1243 ''' 1244 Evaluate the user's string entry after parsing; 1245 do not return None: either return a valid response, default if available, 1246 IncompleteInput, NoInput objects. 1247 1248 Here, we convert the user-selected number into a file path 1249 ''' 1250 rawParsed = self._parseUserInput(raw) 1251 # if NoInput: and a default, return default 1252 if isinstance(rawParsed, NoInput): 1253 if self._default is not None: 1254 # do not return the default, as this here is a number 1255 # and proper results are file paths. thus, set rawParsed 1256 # to default; will get converted later 1257 rawParsed = self._default 1258 1259 # could be IncompleteInput, NoInput, or a proper, valid answer 1260 if isinstance(rawParsed, DialogError): # keep as is 1261 return rawParsed 1262 1263 # else, translate a number into a file path; assume zero is 1 1264 options = self._getValidResults() 1265 if 1 <= rawParsed <= len(options): 1266 return options[rawParsed - 1] 1267 else: 1268 return IncompleteInput(rawParsed) 1269 1270 1271class SelectMusicXMLReader(SelectFilePath): 1272 ''' 1273 Select a MusicXML Reader by presenting a user a list of options. 1274 ''' 1275 1276 def __init__(self, default=None, tryAgain=True, promptHeader=None): 1277 SelectFilePath.__init__(self, 1278 default=default, 1279 tryAgain=tryAgain, 1280 promptHeader=promptHeader) 1281 1282 # define platforms that this will run on 1283 self._platforms = ['darwin', 'win'] 1284 1285 def _rawIntroduction(self): 1286 ''' 1287 Return a multiline presentation of an introduction. 1288 ''' 1289 return [ 1290 'Defining an XML Reader permits automatically opening ' 1291 + 'music21-generated MusicXML in an editor for display and manipulation when calling ' 1292 + 'the show() method. Setting this option is highly recommended. ', 1293 ' ' 1294 ] 1295 1296 def _getMusicXMLReaderDarwin(self): 1297 ''' 1298 Get all possible MusicXML Reader paths on Darwin (i.e., macOS) 1299 ''' 1300 def comparisonFinale(x): 1301 return reFinaleApp.search(x) is not None 1302 1303 def comparisonMuseScore(x): 1304 return reMuseScoreApp.search(x) is not None 1305 1306 def comparisonFinaleReader(x): 1307 return reFinaleReaderApp.search(x) is not None 1308 1309 def comparisonSibelius(x): 1310 return reSibeliusApp.search(x) is not None 1311 1312 # order here results in ranks 1313 results = self._getDarwinApp(comparisonMuseScore) 1314 results += self._getDarwinApp(comparisonFinale) 1315 results += self._getDarwinApp(comparisonFinaleReader) 1316 results += self._getDarwinApp(comparisonSibelius) 1317 1318 # de-duplicate 1319 res = [] 1320 for one_path in results: 1321 if one_path not in res: 1322 res.append(one_path) 1323 1324 return res 1325 1326 def _getMusicXMLReaderWin(self): 1327 ''' 1328 Get all possible MusicXML Reader paths on Windows 1329 ''' 1330 def comparisonFinale(x): 1331 return reFinaleExe.search(x) is not None 1332 1333 def comparisonMuseScore(x): 1334 return reMuseScoreExe.search(x) is not None and 'crash-reporter' not in x 1335 1336 def comparisonSibelius(x): 1337 return reSibeliusExe.search(x) is not None 1338 1339 # order here results in ranks 1340 results = self._getWinApp(comparisonMuseScore) 1341 results += self._getWinApp(comparisonFinale) 1342 results += self._getWinApp(comparisonSibelius) 1343 1344 # de-duplicate (Windows especially can put the same environment var twice) 1345 res = [] 1346 for one_path in results: 1347 if one_path not in res: 1348 res.append(one_path) 1349 1350 return res 1351 1352 def _getMusicXMLReaderNix(self): 1353 ''' 1354 Get all possible MusicXML Reader paths on Unix 1355 ''' 1356 return [] 1357 1358 def _getValidResults(self, force=None): 1359 ''' 1360 Return a list of valid results that are possible and 1361 should be displayed to the user. 1362 These will be processed by _formatResultForUser before usage. 1363 ''' 1364 # customize in subclass 1365 if force is not None: 1366 return force 1367 1368 platform = common.getPlatform() 1369 if platform == 'win': 1370 post = self._getMusicXMLReaderWin() 1371 elif platform == 'darwin': 1372 post = self._getMusicXMLReaderDarwin() 1373 elif platform == 'nix': 1374 post = self._getMusicXMLReaderNix() 1375 else: 1376 post = self._getMusicXMLReaderNix() 1377 return post 1378 1379 def _askFillEmptyList(self, default=None, force=None): 1380 ''' 1381 If we do not have an musicxml readers, ask user if they want to download. 1382 ''' 1383 urlTarget = urlMuseScore 1384 1385 # this does not do anything: customize in subclass 1386 d = AskOpenInBrowser( 1387 urlTarget=urlTarget, 1388 default=True, 1389 tryAgain=False, 1390 promptHeader='No available MusicXML readers are found on your system. ' 1391 + 'We recommend downloading and installing a reader before continuing.\n\n') 1392 d.askUser(force=force) 1393 post = d.getResult() 1394 # can call regardless of result; will only function if result is True 1395 d.performAction() 1396 # if any errors are found, return False; this will end execution of 1397 # askUser and return a BadConditions error 1398 if isinstance(post, DialogError): 1399 return False 1400 else: # must be True or False 1401 # if user selected to open web page, give them time to download 1402 # and install; so ask if ready to continue 1403 if post is True: 1404 for dummy in range(self._maxAttempts): 1405 d = YesOrNo(default=True, tryAgain=False, 1406 promptHeader='Are you ready to continue?') 1407 d.askUser(force=force) 1408 post = d.getResult() 1409 if post is True: 1410 break 1411 elif isinstance(post, DialogError): 1412 break 1413 1414 return post 1415 1416 def _performAction(self, simulate=False): 1417 ''' 1418 The action here is to open the stored URL in a browser, if the user agrees. 1419 ''' 1420 result = self.getResult() 1421 if result is not None and not isinstance(result, DialogError): 1422 # noinspection PyTypeChecker 1423 reload(environment) 1424 # us = environment.UserSettings() 1425 # us['musicxmlPath'] = result # automatically writes 1426 environment.set('musicxmlPath', result) 1427 musicXmlNew = environment.get('musicxmlPath') 1428 self._writeToUser([f'MusicXML Reader set to: {musicXmlNew}', ' ']) 1429 1430 1431# ------------------------------------------------------------------------------ 1432class ConfigurationAssistant: 1433 ''' 1434 Class for managing numerous configuration tasks. 1435 ''' 1436 def __init__(self, simulate=False): 1437 self._simulate = simulate 1438 self._platform = common.getPlatform() 1439 1440 # get and store if there is a current egg-info files 1441 self._lastEggInfo = findInstallationsEggInfoStr() 1442 1443 # add dialogs to list 1444 self._dialogs = [] 1445 self.getDialogs() 1446 1447 def getDialogs(self): 1448 if 'site-packages' not in common.getSourceFilePath().parts: 1449 d = AskInstall(default=True) 1450 self._dialogs.append(d) 1451 1452 d = SelectMusicXMLReader(default=1) 1453 self._dialogs.append(d) 1454 1455 d = AskAutoDownload(default=True) 1456 self._dialogs.append(d) 1457 1458 # provide original egg info files 1459 additionalEntries = {'music21 egg-info previous': self._lastEggInfo} 1460 d = AskSendInstallationReport(default=True, additionalEntries=additionalEntries) 1461 self._dialogs.append(d) 1462 1463 d = AskOpenInBrowser( 1464 urlTarget=urlMusic21List, 1465 prompt='The music21 discussion group provides a forum for ' 1466 + 'asking questions and getting help. Would you like to see the ' 1467 + 'music21 discussion list or sign up for updates?') 1468 self._dialogs.append(d) 1469 1470 # note: this is the on-line URL: 1471 # might be better to find local documentation 1472 d = AskOpenInBrowser( 1473 urlTarget=urlGettingStarted, 1474 prompt='Would you like to view the music21 documentation in a web browser?') 1475 self._dialogs.append(d) 1476 1477 d = AnyKey(promptHeader='The music21 Configuration Assistant is complete.') 1478 self._dialogs.append(d) 1479 1480 def _introduction(self): 1481 msg = [] 1482 msg.append('Welcome the music21 Configuration Assistant. You will be guided ' 1483 + 'through a number of questions to install and setup music21. ' 1484 + 'Simply pressing return at a prompt will select a default, if available.') 1485 msg.append('') # will cause a line break 1486 msg.append('You may run this configuration again at a later time ' 1487 + 'by running music21/configure.py.') 1488 msg.append(' ') # will cause a blank line 1489 1490 writeToUser(msg) 1491 1492 def _conclusion(self): 1493 pass 1494 1495 def _hr(self): 1496 ''' 1497 Draw a line 1498 ''' 1499 msg = [] 1500 msg.append('_' * LINE_WIDTH) 1501 msg.append(' ') # add a space 1502 writeToUser(msg) 1503 1504 def run(self, forceList=None): 1505 ''' 1506 The forceList, if provided, is a list of string arguments 1507 passed in order to the included dialogs. Used for testing. 1508 ''' 1509 if forceList is None: 1510 forceList = [] 1511 self._hr() 1512 self._introduction() 1513 1514 for i, d in enumerate(self._dialogs): 1515 # if this platform is not in those defined for the dialog, continue 1516 if self._platform not in d._platforms: 1517 continue 1518 1519 self._hr() 1520 if len(forceList) > i: 1521 force = forceList[i] 1522 else: 1523 force = None 1524 1525 d.askUser(force=force) 1526 unused_post = d.getResult() 1527 # post may be an error; no problem calling perform action anyways 1528 try: 1529 d.performAction(simulate=self._simulate) 1530 except DialogException: 1531 # a user may have selected an option that requires breaking 1532 break 1533 1534 # self._hr() 1535 self._conclusion() 1536 1537 1538# ------------------------------------------------------------------------------ 1539# for time-out gather of arguments: possibly look at: 1540# https://code.activestate.com/recipes/576780/ 1541# http://www.garyrobinson.net/2009/10/non-blocking-raw_input-for-python.html 1542# class Prompt(threading.Thread): 1543# def __init__ (self, prompt, timeOutTime): 1544# super().__init__() 1545# self.status = None 1546# self.timeLeft = timeOutTime 1547# self.prompt = prompt 1548# 1549# def removeTime(self, value): 1550# self.timeLeft -= value 1551# 1552# def printPrompt(self): 1553# sys.stdout.write('%s: ' % self.prompt) 1554# 1555# def run(self): 1556# self.printPrompt() # print on first call 1557# self.status = input() 1558# 1559# 1560# def getResponseOrTimeout(prompt='provide a value', timeOutTime=16): 1561# 1562# current = Prompt(prompt=prompt, timeOutTime=timeOutTime) 1563# current.start() 1564# reportInterval = 4 1565# updateInterval = 1 1566# intervalCount = 0 1567# 1568# post = None 1569# while True: 1570# # for host in range(60, 70): 1571# if not current.isAlive() or current.status is not None: 1572# break 1573# if current.timeLeft <= 0: 1574# break 1575# time.sleep(updateInterval) 1576# current.removeTime(updateInterval) 1577# 1578# if intervalCount % reportInterval == reportInterval - 1: 1579# sys.stdout.write('\n time out in %s seconds\n' % current.timeLeft) 1580# current.printPrompt() 1581# 1582# intervalCount += 1 1583# # for o in objList: 1584# # can have timeout argument, otherwise blocks 1585# # o.join() # wait until the thread terminates 1586# 1587# post = current.status 1588# # this thread will remain active until the user provides values 1589# 1590# if post == None: 1591# print('got no value') 1592# else: 1593# print('got: %s' % post) 1594# ------------------------------------------------------------------------------ 1595# define presented order in documentation 1596_DOC_ORDER = [] 1597 1598 1599class TestUserInput(unittest.TestCase): # pragma: no cover 1600 1601 def testYesOrNo(self): 1602 print() 1603 print('starting: YesOrNo()') 1604 d = YesOrNo() 1605 d.askUser() 1606 print('getResult():', d.getResult()) 1607 1608 print() 1609 print('starting: YesOrNo(default=True)') 1610 d = YesOrNo(default=True) 1611 d.askUser() 1612 print('getResult():', d.getResult()) 1613 1614 print() 1615 print('starting: YesOrNo(default=False)') 1616 d = YesOrNo(default=False) 1617 d.askUser() 1618 print('getResult():', d.getResult()) 1619 1620 def testSelectMusicXMLReader(self): 1621 print() 1622 print('starting: SelectMusicXMLReader()') 1623 d = SelectMusicXMLReader() 1624 d.askUser() 1625 print('getResult():', d.getResult()) 1626 1627 def testSelectMusicXMLReaderDefault(self): 1628 print() 1629 print('starting: SelectMusicXMLReader(default=1)') 1630 d = SelectMusicXMLReader(default=1) 1631 d.askUser() 1632 print('getResult():', d.getResult()) 1633 1634 def testOpenInBrowser(self): 1635 print() 1636 d = AskOpenInBrowser('http://mit.edu/music21') 1637 d.askUser() 1638 print('getResult():', d.getResult()) 1639 d.performAction() 1640 1641 def testSelectMusicXMLReader2(self): 1642 print() 1643 print('starting: SelectMusicXMLReader()') 1644 d = SelectMusicXMLReader() 1645 d.askUser() 1646 print('getResult():', d.getResult()) 1647 d.performAction() 1648 1649 print() 1650 print('starting: SelectMusicXMLReader() w/o results') 1651 d = SelectMusicXMLReader() 1652 # force request to user by returning no valid results 1653 1654 def getValidResults(force=None): 1655 return [] 1656 1657 d._getValidResults = getValidResults 1658 d.askUser() 1659 print('getResult():', d.getResult()) 1660 d.performAction() 1661 1662 def testConfigurationAssistant(self): 1663 print('Running ConfigurationAssistant all') 1664 configAsst = ConfigurationAssistant(simulate=True) 1665 configAsst.run() 1666 1667 1668class Test(unittest.TestCase): 1669 1670 def testYesOrNo(self): 1671 from music21 import configure 1672 d = configure.YesOrNo(default=True, tryAgain=False, 1673 promptHeader='Are you ready to continue?') 1674 d.askUser('n') 1675 self.assertEqual(str(d.getResult()), 'False') 1676 d.askUser('y') 1677 self.assertEqual(str(d.getResult()), 'True') 1678 d.askUser('') # gets default 1679 self.assertEqual(str(d.getResult()), 'True') 1680 d.askUser('blah') # gets default 1681 self.assertEqual(str(d.getResult()), '<music21.configure.IncompleteInput: blah>') 1682 1683 d = configure.YesOrNo(default=None, tryAgain=False, 1684 promptHeader='Are you ready to continue?') 1685 d.askUser('n') 1686 self.assertEqual(str(d.getResult()), 'False') 1687 d.askUser('y') 1688 self.assertEqual(str(d.getResult()), 'True') 1689 d.askUser('') # gets default 1690 self.assertEqual(str(d.getResult()), '<music21.configure.NoInput: None>') 1691 d.askUser('blah') # gets default 1692 self.assertEqual(str(d.getResult()), '<music21.configure.IncompleteInput: blah>') 1693 1694 def testSelectFromList(self): 1695 from music21 import configure 1696 d = configure.SelectFromList(default=1) 1697 self.assertEqual(d._default, 1) 1698 1699 def testSelectMusicXMLReaders(self): 1700 from music21 import configure 1701 d = configure.SelectMusicXMLReader() 1702 # force request to user by returning no valid results 1703 1704 def getValidResults(force=None): 1705 return [] 1706 1707 d._getValidResults = getValidResults 1708 d.askUser(force='n', skipIntro=True) # reject option to open in a browser 1709 post = d.getResult() 1710 # returns a bad condition b/c there are no options and user entered 'n' 1711 self.assertIsInstance(post, configure.BadConditions) 1712 1713 def testRe(self): 1714 g = reFinaleApp.search('Finale 2011.app') 1715 self.assertEqual(g.group(0), 'Finale 2011.app') 1716 1717 self.assertEqual(reFinaleApp.search('final blah 2011'), None) 1718 1719 g = reFinaleApp.search('Finale.app') 1720 self.assertEqual(g.group(0), 'Finale.app') 1721 1722 self.assertEqual(reFinaleApp.search('Final Cut 2017.app'), None) 1723 1724 def testConfigurationAssistant(self): 1725 unused_ca = ConfigurationAssistant(simulate=True) 1726 1727 def testAskInstall(self): 1728 unused_d = AskInstall() 1729 # d.askUser() 1730 # d.getResult() 1731 # d.performAction() 1732 1733 def testGetUserData(self): 1734 unused_d = AskSendInstallationReport() 1735 # d.askUser() 1736 # d.getResult() 1737 # d.performAction() 1738 1739 def testGetUserData2(self): 1740 unused_d = AskAutoDownload() 1741 # d.askUser() 1742 # d.getResult() 1743 # d.performAction() 1744 1745 def testAnyKey(self): 1746 unused_d = AnyKey() 1747 # d.askUser() 1748 # d.getResult() 1749 # d.performAction() 1750 1751 1752def run(): 1753 ca = ConfigurationAssistant() 1754 ca.run() 1755 1756 1757if __name__ == '__main__': 1758 if len(sys.argv) == 1: # normal conditions 1759 # music21.mainTest(Test) 1760 run() 1761 1762 else: 1763 # only if running tests 1764 t = Test() 1765 te = TestUserInput() 1766 1767 if len(sys.argv) < 2 or sys.argv[1] in ['all', 'test']: 1768 import music21 1769 music21.mainTest(Test) 1770 1771 # arg[1] is test to launch 1772 elif sys.argv[1] == 'te': 1773 # run test user input 1774 getattr(te, sys.argv[2])() 1775 # just run named Test 1776 elif hasattr(t, sys.argv[1]): 1777 getattr(t, sys.argv[1])() 1778