Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selecting checked codes #123

Merged
merged 9 commits into from Jul 19, 2015
@@ -38,6 +38,9 @@ docs/_*
.pydevproject
.settings

# PyCharm files
.idea

# virtualenv
venv/

@@ -5,6 +5,11 @@ Release Notes
Current Development Version
---------------------------

New Features

* Added support for more flexible error selections using ``--ignore``,
``--select``, ``--convention``, ``--add-ignore`` and ``--add-select``.

Bug Fixes

* Property setter and deleter methods are now treated as private and do not
@@ -6,21 +6,44 @@ Usage
Usage: pep257 [options] [<file|dir>...]

Options:
--version show program's version number and exit
-h, --help show this help message and exit
-e, --explain show explanation of each error
-s, --source show source for each error
--ignore=<codes> ignore a list comma-separated error codes, for
example: --ignore=D101,D202
--match=<pattern> check only files that exactly match <pattern> regular
--version show program's version number and exit
-h, --help show this help message and exit
-e, --explain show explanation of each error
-s, --source show source for each error
--select=<codes> choose the basic list of checked errors by specifying
which errors to check for (with a list of comma-
separated error codes). for example:
--select=D101,D202
--ignore=<codes> choose the basic list of checked errors by specifying
which errors to ignore (with a list of comma-separated
error codes). for example: --ignore=D101,D202
--convention=<name> choose the basic list of checked errors by specifying
an existing convention. for example:
--convention=pep257
--add-select=<codes> amend the list of errors to check for by specifying
more error codes to check.
--add-ignore=<codes> amend the list of errors to check for by specifying
more error codes to ignore.
--match=<pattern> check only files that exactly match <pattern> regular
expression; default is --match='(?!test_).*\.py' which
matches files that don't start with 'test_' but end
with '.py'
--match-dir=<pattern>
--match-dir=<pattern>
search only dirs that exactly match <pattern> regular
expression; default is --match-dir='[^\.].*', which
matches all dirs that don't start with a dot
-d, --debug print debug information
-v, --verbose print status information
--count print total number of errors to stdout
-d, --debug print debug information
-v, --verbose print status information
--count print total number of errors to stdout

Return Code
^^^^^^^^^^^

+--------------+--------------------------------------------------------------+
| 0 | Success - no violations |
+--------------+--------------------------------------------------------------+
| 1 | Some code violations were found |
+--------------+--------------------------------------------------------------+
| 2 | Illegal usage - see error message |
+--------------+--------------------------------------------------------------+

120 pep257.py
@@ -20,6 +20,8 @@
from itertools import takewhile, dropwhile, chain
from optparse import OptionParser
from re import compile as re
import itertools

try: # Python 3.x
from ConfigParser import RawConfigParser
except ImportError: # Python 2.x
@@ -61,6 +63,9 @@ def next(obj, default=nothing):
__all__ = ('check', 'collect')

PROJECT_CONFIG = ('setup.cfg', 'tox.ini', '.pep257')
NO_VIOLATIONS_RETURN_CODE = 0
VIOLATIONS_RETURN_CODE = 1
INVALID_OPTIONS_RETURN_CODE = 2


def humanize(string):
@@ -549,7 +554,7 @@ def create_group(cls, prefix, name):
def get_error_codes(cls):
for group in cls.groups:
for error in group.errors:
yield error
yield error.code

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


class Conventions(object):
pep257 = set(ErrorRegistry.get_error_codes())


def get_option_parser():
parser = OptionParser(version=__version__,
usage='Usage: pep257 [options] [<file|dir>...]')
parser.config_options = ('explain', 'source', 'ignore', 'match',
'match-dir', 'debug', 'verbose', 'count')
parser.config_options = ('explain', 'source', 'ignore', 'match', 'select',
'match-dir', 'debug', 'verbose', 'count',
'convention')
option = parser.add_option
option('-e', '--explain', action='store_true',
help='show explanation of each error')
option('-s', '--source', action='store_true',
help='show source for each error')
option('--select', metavar='<codes>', default='',
help='choose the basic list of checked errors by specifying which '
'errors to check for (with a list of comma-separated error '
'codes). for example: --select=D101,D202')
option('--ignore', metavar='<codes>', default='',
help='ignore a list comma-separated error codes, '
'for example: --ignore=D101,D202')
help='choose the basic list of checked errors by specifying which '
'errors to ignore (with a list of comma-separated error '
'codes). for example: --ignore=D101,D202')
option('--convention', metavar='<name>', default='',
help='choose the basic list of checked errors by specifying an '
'existing convention. for example: --convention=pep257')
option('--add-select', metavar='<codes>', default='',
help='amend the list of errors to check for by specifying more '
'error codes to check.')
option('--add-ignore', metavar='<codes>', default='',
help='amend the list of errors to check for by specifying more '
'error codes to ignore.')
option('--match', metavar='<pattern>', default='(?!test_).*\.py',
help="check only files that exactly match <pattern> regular "
"expression; default is --match='(?!test_).*\.py' which "
@@ -665,25 +689,34 @@ def collect(names, match=lambda name: True, match_dir=lambda name: True):
yield name


def check(filenames, ignore=()):
def check(filenames, select=None, ignore=None):
"""Generate PEP 257 errors that exist in `filenames` iterable.
Skips errors with error-codes defined in `ignore` iterable.
Only returns errors with error-codes defined in `checked_codes` iterable.
Example
-------
>>> check(['pep257.py'], ignore=['D100'])
>>> check(['pep257.py'], checked_codes=['D100'])
<generator object check at 0x...>
"""
if select and ignore:
raise ValueError('Cannot pass both select and ignore. They are '
'mutually exclusive.')
elif select or ignore:
checked_codes = (select or
set(ErrorRegistry.get_error_codes()) - set(ignore))
else:
checked_codes = Conventions.pep257

for filename in filenames:
log.info('Checking file %s.', filename)
try:
with tokenize_open(filename) as file:
source = file.read()
for error in PEP257Checker().check_source(source, filename):
code = getattr(error, 'code', None)
if code is not None and code not in ignore:
if code in checked_codes:
yield error
except (EnvironmentError, AllError):
yield sys.exc_info()[1]
@@ -693,7 +726,7 @@ def check(filenames, ignore=()):

def get_options(args, opt_parser):
config = RawConfigParser()
parent = tail = args and os.path.abspath(os.path.commonprefix(args))
parent = tail = os.path.abspath(os.path.commonprefix(args))
while tail:
for fn in PROJECT_CONFIG:
full_path = os.path.join(parent, fn)
@@ -714,7 +747,7 @@ def get_options(args, opt_parser):
pep257_section = 'pep257'
for opt in config.options(pep257_section):
if opt.replace('_', '-') not in opt_parser.config_options:
print("Unknown option '{}' ignored".format(opt))
log.warning("Unknown option '{}' ignored".format(opt))
continue
normalized_opt = opt.replace('-', '_')
opt_type = option_list[normalized_opt]
@@ -733,19 +766,57 @@ def get_options(args, opt_parser):
return options


def setup_stream_handler(options):
def setup_stream_handlers(options):
"""Setup logging stream handlers according to the options."""
class StdoutFilter(logging.Filter):
def filter(self, record):
return record.levelno in (logging.DEBUG, logging.INFO)

if log.handlers:
for handler in log.handlers:
log.removeHandler(handler)
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(logging.WARNING)

stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.WARNING)
stdout_handler.addFilter(StdoutFilter())
if options.debug:
stream_handler.setLevel(logging.DEBUG)
stdout_handler.setLevel(logging.DEBUG)
elif options.verbose:
stream_handler.setLevel(logging.INFO)
stdout_handler.setLevel(logging.INFO)
else:
stdout_handler.setLevel(logging.WARNING)
log.addHandler(stdout_handler)

stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setLevel(logging.WARNING)
log.addHandler(stderr_handler)


def get_checked_error_codes(options):
codes = set(ErrorRegistry.get_error_codes())
if options.ignore:
checked_codes = codes - set(options.ignore.split(','))
elif options.select:
checked_codes = set(options.select.split(','))
elif options.convention:
checked_codes = getattr(Conventions, options.convention)
else:
stream_handler.setLevel(logging.WARNING)
log.addHandler(stream_handler)
checked_codes = Conventions.pep257
checked_codes -= set(options.add_ignore.split(','))
checked_codes |= set(options.add_select.split(','))
return checked_codes - set('')


def validate_options(options):
mutually_exclusive = ('ignore', 'select', 'convention')
for opt1, opt2 in itertools.permutations(mutually_exclusive, 2):
if getattr(options, opt1) and getattr(options, opt2):
log.error('Cannot pass both {0} and {1}. They are '
'mutually exclusive.'.format(opt1, opt2))
return False
if options.convention and not hasattr(Conventions, options.convention):
return False
return True


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

collected = collect(arguments or ['.'],
match=re(options.match + '$').match,
@@ -770,12 +843,13 @@ def run_pep257():
Error.explain = options.explain
Error.source = options.source
collected = list(collected)
errors = check(collected, ignore=options.ignore.split(','))
code = 0
checked_codes = get_checked_error_codes(options)
errors = check(collected, select=checked_codes)
code = NO_VIOLATIONS_RETURN_CODE
count = 0
for error in errors:
sys.stderr.write('%s\n' % error)
code = 1
code = VIOLATIONS_RETURN_CODE
count += 1
if options.count:
print(count)
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.