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