1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: testLint.py 4# Purpose: Controller for all lint based testing. 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2009-2010, 2015 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12 13# this requires pylint to be installed and available from the command line 14import argparse 15import os 16 17from music21 import common 18from music21.test import commonTest 19 20try: 21 # noinspection PyPackageRequirements 22 from pylint.lint import Run as pylintRun 23except ImportError: 24 pylintRun = None 25 26 27# see feature list here: 28# http://docs.pylint.org/features.html 29 30# W0511: Used when a warning note as FIXME or XXX is detected. 31# W0404: Reimport %r (imported line %s) Used when a module is re-imported multiple times. 32 33# we do this all the time in unit tests 34# R0201: Method could be a function Used when a method doesn't 35# use its bound instance, and so could be written as a function. 36# R0904: Too many public methods (%s/%s) Used when class has too many public methods, 37# try to reduce this to get a more simple (and so easier to use) class. 38# E1101: %s %r has no %r member Used when a variable is accessed for an non-existent member. 39# R0914: Too many local variables (%s/%s) Used when a function or method has 40# too many local variables. 41# many of our test use many local variables 42# R0903: Too few public methods (%s/%s) Used when class has too few public methods, 43# so be sure it's really worth it. 44# R0911: Too many return statements (%s/%s) Used when a function or method has 45# too many return statement, making it hard to follow. 46 47 48def main(fnAccept=None, strict=False): 49 ''' 50 `fnAccept` is a list of one or more files to test. Otherwise runs all. 51 ''' 52 poolSize = common.cpus() 53 54 if pylintRun is None: 55 print("make sure that 'sudo pip3 install pylint' is there. exiting.") 56 return 57 58 mg = commonTest.ModuleGather() 59 60 fnPathReject = [ 61 # 'demos/', 62 # 'test/timeGraphs.py', 63 '/ext/', 64 # 'bar.py', # used to crash pylint... 65 # 'repeat.py', # used to hang pylint... 66 # 'spanner.py', # used to hang pylint... 67 ] 68 69 disable_unless_strict = [ 70 'too-many-statements', # someday 71 'too-many-arguments', # definitely! but takes too long to get a fix now... 72 'too-many-public-methods', # maybe, look 73 'too-many-branches', # yes, someday 74 'too-many-lines', # yes, someday. 75 'too-many-return-statements', # we'll see 76 'too-many-instance-attributes', # maybe later 77 'inconsistent-return-statements', # would be nice 78 'protected-access', # this is an important one, but for now we do a lot of 79 # x = copy.deepcopy(self); x._volume = ... which is not a problem... 80 # also, test suites need to be exempt. 81 'keyword-arg-before-vararg', # a good thing to check for new code, but 82 # requires rewriting function signatures in old code 83 84 ] 85 disable = [ # These also need to be changed in MUSIC21BASE/.pylintrc 86 'arguments-differ', # -- no -- should be able to add additional arguments so long 87 # as initial ones are the same. 88 'arguments-renamed', # not an issue 89 90 'multiple-imports', # import os, sys -- fine... 91 'redefined-variable-type', # would be good, but currently 92 # lines like: if x: y = note.Note() ; else: y = note.Rest() 93 # triggers this, even though y doesn't change. 94 'no-else-return', # these are unnecessary but can help show the flow of thinking. 95 'cyclic-import', # we use these inside functions when there's a deep problem. 96 'unnecessary-pass', # nice, but not really a problem... 97 'locally-disabled', # test for this later, but hopefully will know what 98 # they're doing 99 'consider-using-get', # if it can figure out that the default value is something 100 # simple, we will turn back on, but until then, no. 101 'chained-comparison', # sometimes simpler that way 102 # 'duplicate-code', # needs to ignore strings -- keeps getting doctests... 103 'too-many-ancestors', # -- 8 is okay. 104 'fixme', # known... 105 'superfluous-parens', # nope -- if they make things clearer... 106 'no-member', # important, but too many false positives 107 'too-many-locals', # no 108 'bad-whitespace', # maybe later, but "bad" isn't something I necessarily agree with 109 'bad-continuation', # never remove -- this is a good thing many times. 110 'unpacking-non-sequence', # gets it wrong too often. 111 112 # AbstractDiatonicScale.__eq__ shows how this 113 # can be fine... 114 'too-many-boolean-expressions', 115 116 'misplaced-comparison-constant', # sometimes 2 < x is what we want 117 'unsubscriptable-object', # unfortunately, thinks that Streams are unsubscriptable. 118 119 # sometimes .keys() is a good test against 120 # changing the dictionary size while iterating. 121 'consider-iterating-dictionary', 122 'consider-using-dict-items', # readability improvement depends on excellent variable names 123 124 'invalid-name', # these are good music21 names; fix the regexp instead... 125 'no-self-use', # maybe later 126 'too-few-public-methods', # never remove or set to 1 127 128 'trailing-whitespace', # should ignore blank lines with tabs 129 130 # just because something is easy to detect doesn't make it bad. 131 'trailing-newlines', 132 133 'missing-docstring', # gets too many well-documented properties 134 'star-args', # no problem with them... 135 'unused-argument', 136 'import-self', # fix is either to get rid of it or move away many tests... 137 138 'simplifiable-if-statement', # NO! NO! NO! 139 # if (x or y and z and q): return True, else: return False, 140 # is a GREAT paradigm -- over "return (x or y and z and q)" and 141 # assuming that it returns a bool... it's no slower than 142 # the simplification and it's so much clearer. 143 'consider-using-enumerate', # good when i used only once, but 144 # x[i] = y[i] is a nice paradigm, even if one can be simplified out. 145 'not-callable', # false positives, for instance on x.next() 146 147 'raise-missing-from', # later. 148 149 ] 150 if not strict: 151 disable = disable + disable_unless_strict 152 153 goodNameRx = {'argument-rgx': r'[a-z_][A-Za-z0-9_]{2,30}$', 154 'attr-rgx': r'[a-z_][A-Za-z0-9_]{2,30}$', 155 'class-rgx': r'[A-Z_][A-Za-z0-9_]{2,30}$', 156 'function-rgx': r'[a-z_][A-Za-z0-9_]{2,30}$', 157 'method-rgx': r'[a-z_][A-Za-z0-9_]{2,30}$', 158 'module-rgx': r'(([a-z_][a-zA-Z0-9_]*)|([A-Z][a-zA-Z0-9]+))$', 159 'variable-rgx': r'[a-z_][A-Za-z0-9_]{2,30}$', 160 } 161 162 maxArgs = 7 if not strict else 5 163 maxBranches = 20 if not strict else 10 164 165 cmd = ['--output-format=parseable', 166 r'--dummy-variables-rgx="_$|dummy|unused|i$|j$|junk|counter"', 167 '--docstring-min-length=3', 168 '--ignore-docstrings=yes', 169 '--min-similarity-lines=8', 170 '--max-args=' + str(maxArgs), # should be 5 later, but baby steps 171 '--bad-names="foo,shit,fuck,stuff"', # definitely allow "bar" for barlines 172 '--reports=n', 173 '--max-branches=' + str(maxBranches), 174 '-j ' + str(poolSize), # multiprocessing! 175 r'--ignore-long-lines="converter\.parse"', # some tiny notation... 176 '--max-line-length=100', 177 ] 178 for gn, gnv in goodNameRx.items(): 179 cmd.append('--' + gn + '="' + gnv + '"') 180 181 for pyLintId in disable: 182 cmd.append(f'--disable={pyLintId}') 183 184 # add entire package 185 acceptable = [] 186 for fp in mg.modulePaths: 187 rejectIt = False 188 for rejectPath in fnPathReject: 189 if rejectPath in fp: 190 rejectIt = True 191 break 192 if rejectIt: 193 continue 194 if fnAccept: 195 rejectIt = True 196 for acceptableName in fnAccept: 197 if acceptableName in fp: 198 rejectIt = False 199 break 200 if rejectIt: 201 continue 202 203 acceptable.append(fp) 204 205 cmdFile = cmd + acceptable 206 if not acceptable: 207 print('No matching files were found.') 208 return 209 210 # print(fnAccept) 211 # print(' '.join(cmdFile)) 212 # print(fp) 213 214 try: 215 # noinspection PyArgumentList,PyCallingNonCallable 216 pylintRun(cmdFile, exit=False) 217 except TypeError: 218 # noinspection PyCallingNonCallable 219 pylintRun(cmdFile, do_exit=False) # renamed in recent versions 220 221 222def argRun(): 223 parser = argparse.ArgumentParser( 224 description='Run pylint on music21 according to style guide.') 225 parser.add_argument('files', metavar='filename', type=str, nargs='*', 226 help='Files to parse (default nearly all)') 227 parser.add_argument('--strict', action='store_true', 228 help='Run the file in strict mode') 229 args = parser.parse_args() 230 # print(args.files) 231 # print(args.strict) 232 files = args.files if args.files else None 233 if files: 234 filesMid = [os.path.abspath(f) for f in files] 235 files = [] 236 for f in filesMid: 237 if os.path.exists(f): 238 files.append(f) 239 else: 240 print('skipping ' + f + ': no matching file') 241 main(files, args.strict) 242 243 244if __name__ == '__main__': 245 argRun() 246 # main(sys.argv[1:]) 247 # if len(sys.argv) >= 2: 248 # test.main(sys.argv[1:], restoreEnvironmentDefaults=True) 249 # else: 250 # test.main(restoreEnvironmentDefaults=True) 251 # 252 253