Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit 110e49f

Browse files
committed
Merge pull request #123 from Nurdok/checked-codes
Selecting checked codes
2 parents 4fe27bd + a5b7d21 commit 110e49f

File tree

5 files changed

+266
-53
lines changed

5 files changed

+266
-53
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ docs/_*
3838
.pydevproject
3939
.settings
4040

41+
# PyCharm files
42+
.idea
43+
4144
# virtualenv
4245
venv/
4346

docs/release_notes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Release Notes
55
Current Development Version
66
---------------------------
77

8+
New Features
9+
10+
* Added support for more flexible error selections using ``--ignore``,
11+
``--select``, ``--convention``, ``--add-ignore`` and ``--add-select``.
12+
813
Bug Fixes
914

1015
* Property setter and deleter methods are now treated as private and do not

docs/snippets/cli.rst

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,44 @@ Usage
66
Usage: pep257 [options] [<file|dir>...]
77
88
Options:
9-
--version show program's version number and exit
10-
-h, --help show this help message and exit
11-
-e, --explain show explanation of each error
12-
-s, --source show source for each error
13-
--ignore=<codes> ignore a list comma-separated error codes, for
14-
example: --ignore=D101,D202
15-
--match=<pattern> check only files that exactly match <pattern> regular
9+
--version show program's version number and exit
10+
-h, --help show this help message and exit
11+
-e, --explain show explanation of each error
12+
-s, --source show source for each error
13+
--select=<codes> choose the basic list of checked errors by specifying
14+
which errors to check for (with a list of comma-
15+
separated error codes). for example:
16+
--select=D101,D202
17+
--ignore=<codes> choose the basic list of checked errors by specifying
18+
which errors to ignore (with a list of comma-separated
19+
error codes). for example: --ignore=D101,D202
20+
--convention=<name> choose the basic list of checked errors by specifying
21+
an existing convention. for example:
22+
--convention=pep257
23+
--add-select=<codes> amend the list of errors to check for by specifying
24+
more error codes to check.
25+
--add-ignore=<codes> amend the list of errors to check for by specifying
26+
more error codes to ignore.
27+
--match=<pattern> check only files that exactly match <pattern> regular
1628
expression; default is --match='(?!test_).*\.py' which
1729
matches files that don't start with 'test_' but end
1830
with '.py'
19-
--match-dir=<pattern>
31+
--match-dir=<pattern>
2032
search only dirs that exactly match <pattern> regular
2133
expression; default is --match-dir='[^\.].*', which
2234
matches all dirs that don't start with a dot
23-
-d, --debug print debug information
24-
-v, --verbose print status information
25-
--count print total number of errors to stdout
35+
-d, --debug print debug information
36+
-v, --verbose print status information
37+
--count print total number of errors to stdout
38+
39+
Return Code
40+
^^^^^^^^^^^
41+
42+
+--------------+--------------------------------------------------------------+
43+
| 0 | Success - no violations |
44+
+--------------+--------------------------------------------------------------+
45+
| 1 | Some code violations were found |
46+
+--------------+--------------------------------------------------------------+
47+
| 2 | Illegal usage - see error message |
48+
+--------------+--------------------------------------------------------------+
2649

pep257.py

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from itertools import takewhile, dropwhile, chain
2121
from optparse import OptionParser
2222
from re import compile as re
23+
import itertools
24+
2325
try: # Python 3.x
2426
from ConfigParser import RawConfigParser
2527
except ImportError: # Python 2.x
@@ -61,6 +63,9 @@ def next(obj, default=nothing):
6163
__all__ = ('check', 'collect')
6264

6365
PROJECT_CONFIG = ('setup.cfg', 'tox.ini', '.pep257')
66+
NO_VIOLATIONS_RETURN_CODE = 0
67+
VIOLATIONS_RETURN_CODE = 1
68+
INVALID_OPTIONS_RETURN_CODE = 2
6469

6570

6671
def humanize(string):
@@ -549,7 +554,7 @@ def create_group(cls, prefix, name):
549554
def get_error_codes(cls):
550555
for group in cls.groups:
551556
for error in group.errors:
552-
yield error
557+
yield error.code
553558

554559
@classmethod
555560
def to_rst(cls):
@@ -612,19 +617,38 @@ def to_rst(cls):
612617
'"signature"')
613618

614619

620+
class Conventions(object):
621+
pep257 = set(ErrorRegistry.get_error_codes())
622+
623+
615624
def get_option_parser():
616625
parser = OptionParser(version=__version__,
617626
usage='Usage: pep257 [options] [<file|dir>...]')
618-
parser.config_options = ('explain', 'source', 'ignore', 'match',
619-
'match-dir', 'debug', 'verbose', 'count')
627+
parser.config_options = ('explain', 'source', 'ignore', 'match', 'select',
628+
'match-dir', 'debug', 'verbose', 'count',
629+
'convention')
620630
option = parser.add_option
621631
option('-e', '--explain', action='store_true',
622632
help='show explanation of each error')
623633
option('-s', '--source', action='store_true',
624634
help='show source for each error')
635+
option('--select', metavar='<codes>', default='',
636+
help='choose the basic list of checked errors by specifying which '
637+
'errors to check for (with a list of comma-separated error '
638+
'codes). for example: --select=D101,D202')
625639
option('--ignore', metavar='<codes>', default='',
626-
help='ignore a list comma-separated error codes, '
627-
'for example: --ignore=D101,D202')
640+
help='choose the basic list of checked errors by specifying which '
641+
'errors to ignore (with a list of comma-separated error '
642+
'codes). for example: --ignore=D101,D202')
643+
option('--convention', metavar='<name>', default='',
644+
help='choose the basic list of checked errors by specifying an '
645+
'existing convention. for example: --convention=pep257')
646+
option('--add-select', metavar='<codes>', default='',
647+
help='amend the list of errors to check for by specifying more '
648+
'error codes to check.')
649+
option('--add-ignore', metavar='<codes>', default='',
650+
help='amend the list of errors to check for by specifying more '
651+
'error codes to ignore.')
628652
option('--match', metavar='<pattern>', default='(?!test_).*\.py',
629653
help="check only files that exactly match <pattern> regular "
630654
"expression; default is --match='(?!test_).*\.py' which "
@@ -665,25 +689,34 @@ def collect(names, match=lambda name: True, match_dir=lambda name: True):
665689
yield name
666690

667691

668-
def check(filenames, ignore=()):
692+
def check(filenames, select=None, ignore=None):
669693
"""Generate PEP 257 errors that exist in `filenames` iterable.
670694
671-
Skips errors with error-codes defined in `ignore` iterable.
695+
Only returns errors with error-codes defined in `checked_codes` iterable.
672696
673697
Example
674698
-------
675-
>>> check(['pep257.py'], ignore=['D100'])
699+
>>> check(['pep257.py'], checked_codes=['D100'])
676700
<generator object check at 0x...>
677701
678702
"""
703+
if select and ignore:
704+
raise ValueError('Cannot pass both select and ignore. They are '
705+
'mutually exclusive.')
706+
elif select or ignore:
707+
checked_codes = (select or
708+
set(ErrorRegistry.get_error_codes()) - set(ignore))
709+
else:
710+
checked_codes = Conventions.pep257
711+
679712
for filename in filenames:
680713
log.info('Checking file %s.', filename)
681714
try:
682715
with tokenize_open(filename) as file:
683716
source = file.read()
684717
for error in PEP257Checker().check_source(source, filename):
685718
code = getattr(error, 'code', None)
686-
if code is not None and code not in ignore:
719+
if code in checked_codes:
687720
yield error
688721
except (EnvironmentError, AllError):
689722
yield sys.exc_info()[1]
@@ -693,7 +726,7 @@ def check(filenames, ignore=()):
693726

694727
def get_options(args, opt_parser):
695728
config = RawConfigParser()
696-
parent = tail = args and os.path.abspath(os.path.commonprefix(args))
729+
parent = tail = os.path.abspath(os.path.commonprefix(args))
697730
while tail:
698731
for fn in PROJECT_CONFIG:
699732
full_path = os.path.join(parent, fn)
@@ -714,7 +747,7 @@ def get_options(args, opt_parser):
714747
pep257_section = 'pep257'
715748
for opt in config.options(pep257_section):
716749
if opt.replace('_', '-') not in opt_parser.config_options:
717-
print("Unknown option '{}' ignored".format(opt))
750+
log.warning("Unknown option '{}' ignored".format(opt))
718751
continue
719752
normalized_opt = opt.replace('-', '_')
720753
opt_type = option_list[normalized_opt]
@@ -733,19 +766,57 @@ def get_options(args, opt_parser):
733766
return options
734767

735768

736-
def setup_stream_handler(options):
769+
def setup_stream_handlers(options):
770+
"""Setup logging stream handlers according to the options."""
771+
class StdoutFilter(logging.Filter):
772+
def filter(self, record):
773+
return record.levelno in (logging.DEBUG, logging.INFO)
774+
737775
if log.handlers:
738776
for handler in log.handlers:
739777
log.removeHandler(handler)
740-
stream_handler = logging.StreamHandler(sys.stdout)
741-
stream_handler.setLevel(logging.WARNING)
778+
779+
stdout_handler = logging.StreamHandler(sys.stdout)
780+
stdout_handler.setLevel(logging.WARNING)
781+
stdout_handler.addFilter(StdoutFilter())
742782
if options.debug:
743-
stream_handler.setLevel(logging.DEBUG)
783+
stdout_handler.setLevel(logging.DEBUG)
744784
elif options.verbose:
745-
stream_handler.setLevel(logging.INFO)
785+
stdout_handler.setLevel(logging.INFO)
786+
else:
787+
stdout_handler.setLevel(logging.WARNING)
788+
log.addHandler(stdout_handler)
789+
790+
stderr_handler = logging.StreamHandler(sys.stderr)
791+
stderr_handler.setLevel(logging.WARNING)
792+
log.addHandler(stderr_handler)
793+
794+
795+
def get_checked_error_codes(options):
796+
codes = set(ErrorRegistry.get_error_codes())
797+
if options.ignore:
798+
checked_codes = codes - set(options.ignore.split(','))
799+
elif options.select:
800+
checked_codes = set(options.select.split(','))
801+
elif options.convention:
802+
checked_codes = getattr(Conventions, options.convention)
746803
else:
747-
stream_handler.setLevel(logging.WARNING)
748-
log.addHandler(stream_handler)
804+
checked_codes = Conventions.pep257
805+
checked_codes -= set(options.add_ignore.split(','))
806+
checked_codes |= set(options.add_select.split(','))
807+
return checked_codes - set('')
808+
809+
810+
def validate_options(options):
811+
mutually_exclusive = ('ignore', 'select', 'convention')
812+
for opt1, opt2 in itertools.permutations(mutually_exclusive, 2):
813+
if getattr(options, opt1) and getattr(options, opt2):
814+
log.error('Cannot pass both {0} and {1}. They are '
815+
'mutually exclusive.'.format(opt1, opt2))
816+
return False
817+
if options.convention and not hasattr(Conventions, options.convention):
818+
return False
819+
return True
749820

750821

751822
def run_pep257():
@@ -754,12 +825,14 @@ def run_pep257():
754825
# setup the logger before parsing the config file, so that command line
755826
# arguments for debug / verbose will be printed.
756827
options, arguments = opt_parser.parse_args()
757-
setup_stream_handler(options)
828+
setup_stream_handlers(options)
758829
# We parse the files before opening the config file, since it changes where
759830
# we look for the file.
760831
options = get_options(arguments, opt_parser)
832+
if not validate_options(options):
833+
return INVALID_OPTIONS_RETURN_CODE
761834
# Setup the handler again with values from the config file.
762-
setup_stream_handler(options)
835+
setup_stream_handlers(options)
763836

764837
collected = collect(arguments or ['.'],
765838
match=re(options.match + '$').match,
@@ -770,12 +843,13 @@ def run_pep257():
770843
Error.explain = options.explain
771844
Error.source = options.source
772845
collected = list(collected)
773-
errors = check(collected, ignore=options.ignore.split(','))
774-
code = 0
846+
checked_codes = get_checked_error_codes(options)
847+
errors = check(collected, select=checked_codes)
848+
code = NO_VIOLATIONS_RETURN_CODE
775849
count = 0
776850
for error in errors:
777851
sys.stderr.write('%s\n' % error)
778-
code = 1
852+
code = VIOLATIONS_RETURN_CODE
779853
count += 1
780854
if options.count:
781855
print(count)

0 commit comments

Comments
 (0)