Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import optparse
- import tokenize
- import warnings
- # Polyfill stdin loading/reading lines
- # https://gitlab.com/pycqa/flake8-polyfill/blob/1.0.1/src/flake8_polyfill/stdin.py#L52-57
- try:
- from flake8.engine import pep8
- stdin_get_value = pep8.stdin_get_value
- readlines = pep8.readlines
- except ImportError:
- from flake8 import utils
- import pycodestyle
- stdin_get_value = utils.stdin_get_value
- readlines = pycodestyle.readlines
- from flake8_quotes.__about__ import __version__
- from flake8_quotes.docstring_detection import get_docstring_tokens
- class QuoteChecker(object):
- name = __name__
- version = __version__
- INLINE_QUOTES = {
- # When user wants only single quotes
- '\'': {
- 'good_single': '\'',
- 'bad_single': '"',
- },
- # When user wants only double quotes
- '"': {
- 'good_single': '"',
- 'bad_single': '\'',
- },
- }
- # Provide aliases for Windows CLI support
- # https://github.com/zheller/flake8-quotes/issues/49
- INLINE_QUOTES['single'] = INLINE_QUOTES['\'']
- INLINE_QUOTES['double'] = INLINE_QUOTES['"']
- MULTILINE_QUOTES = {
- '\'': {
- 'good_multiline': '\'\'\'',
- 'bad_multiline': '"""',
- },
- '"': {
- 'good_multiline': '"""',
- 'bad_multiline': '\'\'\'',
- },
- }
- # Provide Windows CLI and multi-quote aliases
- MULTILINE_QUOTES['single'] = MULTILINE_QUOTES['\'']
- MULTILINE_QUOTES['double'] = MULTILINE_QUOTES['"']
- MULTILINE_QUOTES['\'\'\''] = MULTILINE_QUOTES['\'']
- MULTILINE_QUOTES['"""'] = MULTILINE_QUOTES['"']
- DOCSTRING_QUOTES = {
- '\'': {
- 'good_docstring': '\'\'\'',
- 'bad_docstring': '"""',
- },
- '"': {
- 'good_docstring': '"""',
- 'bad_docstring': '\'\'\'',
- },
- }
- # Provide Windows CLI and docstring-quote aliases
- DOCSTRING_QUOTES['single'] = DOCSTRING_QUOTES['\'']
- DOCSTRING_QUOTES['double'] = DOCSTRING_QUOTES['"']
- DOCSTRING_QUOTES['\'\'\''] = DOCSTRING_QUOTES['\'']
- DOCSTRING_QUOTES['"""'] = DOCSTRING_QUOTES['"']
- def __init__(self, tree, filename='(none)'):
- self.filename = filename
- @staticmethod
- def _register_opt(parser, *args, **kwargs):
- """
- Handler to register an option for both Flake8 3.x and 2.x.
- This is based on:
- https://github.com/PyCQA/flake8/blob/3.0.0b2/docs/source/plugin-development/cross-compatibility.rst#option-handling-on-flake8-2-and-3
- It only supports `parse_from_config` from the original function and it
- uses the `Option` object returned to get the string.
- """
- try:
- # Flake8 3.x registration
- parser.add_option(*args, **kwargs)
- except (optparse.OptionError, TypeError):
- # Flake8 2.x registration
- parse_from_config = kwargs.pop('parse_from_config', False)
- option = parser.add_option(*args, **kwargs)
- if parse_from_config:
- parser.config_options.append(option.get_opt_string().lstrip('-'))
- @classmethod
- def add_options(cls, parser):
- cls._register_opt(parser, '--quotes', action='store',
- parse_from_config=True, type='choice',
- choices=sorted(cls.INLINE_QUOTES.keys()),
- help='Deprecated alias for `--inline-quotes`')
- cls._register_opt(parser, '--inline-quotes', default='\'',
- action='store', parse_from_config=True, type='choice',
- choices=sorted(cls.INLINE_QUOTES.keys()),
- help='Quote to expect in all files (default: \')')
- cls._register_opt(parser, '--multiline-quotes', default=None, action='store',
- parse_from_config=True, type='choice',
- choices=sorted(cls.MULTILINE_QUOTES.keys()),
- help='Quote to expect in all files (default: """)')
- cls._register_opt(parser, '--docstring-quotes', default=None, action='store',
- parse_from_config=True, type='choice',
- choices=sorted(cls.DOCSTRING_QUOTES.keys()),
- help='Quote to expect in all files (default: """)')
- cls._register_opt(parser, '--avoid-escape', default=None, action='store_true',
- parse_from_config=True,
- help='Avoiding escaping same quotes in inline strings (enabled by default)')
- cls._register_opt(parser, '--no-avoid-escape', dest='avoid_escape', default=None, action='store_false',
- parse_from_config=False,
- help='Disable avoiding escaping same quotes in inline strings')
- @classmethod
- def parse_options(cls, options):
- # Define our default config
- # cls.config = {good_single: ', good_multiline: ''', bad_single: ", bad_multiline: """}
- cls.config = {}
- cls.config.update(cls.INLINE_QUOTES['\''])
- cls.config.update(cls.MULTILINE_QUOTES['"""'])
- cls.config.update(cls.DOCSTRING_QUOTES['"""'])
- # If `options.quotes` was specified, then use it
- if hasattr(options, 'quotes') and options.quotes is not None:
- # https://docs.python.org/2/library/warnings.html#warnings.warn
- warnings.warn('flake8-quotes has deprecated `quotes` in favor of `inline-quotes`. '
- 'Please update your configuration')
- cls.config.update(cls.INLINE_QUOTES[options.quotes])
- # Otherwise, use the supported `inline_quotes`
- else:
- # cls.config = {good_single: ', good_multiline: """, bad_single: ", bad_multiline: '''}
- # -> {good_single: ", good_multiline: """, bad_single: ', bad_multiline: '''}
- cls.config.update(cls.INLINE_QUOTES[options.inline_quotes])
- # If multiline quotes was specified, overload our config with those options
- if hasattr(options, 'multiline_quotes') and options.multiline_quotes is not None:
- # cls.config = {good_single: ', good_multiline: """, bad_single: ", bad_multiline: '''}
- # -> {good_single: ', good_multiline: ''', bad_single: ", bad_multiline: """}
- cls.config.update(cls.MULTILINE_QUOTES[options.multiline_quotes])
- # If docstring quotes was specified, overload our config with those options
- if hasattr(options, 'docstring_quotes') and options.docstring_quotes is not None:
- cls.config.update(cls.DOCSTRING_QUOTES[options.docstring_quotes])
- # If avoid escaped specified, add to config
- if hasattr(options, 'avoid_escape') and options.avoid_escape is not None:
- cls.config.update({'avoid_escape': options.avoid_escape})
- else:
- cls.config.update({'avoid_escape': True})
- def get_file_contents(self):
- if self.filename in ('stdin', '-', None):
- return stdin_get_value().splitlines(True)
- else:
- return readlines(self.filename)
- def run(self):
- file_contents = self.get_file_contents()
- noqa_line_numbers = self.get_noqa_lines(file_contents)
- errors = self.get_quotes_errors(file_contents)
- for error in errors:
- if error.get('line') not in noqa_line_numbers:
- yield (error.get('line'), error.get('col'), error.get('message'), type(self))
- def get_noqa_lines(self, file_contents):
- tokens = [Token(t) for t in tokenize.generate_tokens(lambda L=iter(file_contents): next(L))]
- return [token.start_row
- for token in tokens
- if token.type == tokenize.COMMENT and token.string.endswith('noqa')]
- def get_quotes_errors(self, file_contents):
- tokens = [Token(t) for t in tokenize.generate_tokens(lambda L=iter(file_contents): next(L))]
- docstring_tokens = get_docstring_tokens(tokens)
- for token in tokens:
- if token.type != tokenize.STRING:
- # ignore non strings
- continue
- # Remove any prefixes in strings like `u` from `u"foo"`
- # DEV: `last_quote_char` is 1 character, even for multiline strings
- # `"foo"` -> `"foo"`
- # `b"foo"` -> `"foo"`
- # `br"foo"` -> `"foo"`
- # `b"""foo"""` -> `"""foo"""`
- last_quote_char = token.string[-1]
- first_quote_index = token.string.index(last_quote_char)
- prefix = token.string[:first_quote_index].lower()
- unprefixed_string = token.string[first_quote_index:]
- # Determine if our string is multiline-based
- # "foo"[0] * 3 = " * 3 = """
- # "foo"[0:3] = "fo
- # """foo"""[0:3] = """
- is_docstring = token in docstring_tokens
- is_multiline_string = unprefixed_string[0] * 3 == unprefixed_string[0:3]
- start_row, start_col = token.start
- # If our string is a docstring
- # DEV: Docstring quotes must come before multiline quotes as it can as a multiline quote
- if is_docstring:
- if self.config['good_docstring'] in unprefixed_string:
- continue
- yield {
- 'message': 'Q002 Remove bad quotes from docstring',
- 'line': start_row,
- 'col': start_col,
- }
- # Otherwise if our string is multiline
- elif is_multiline_string:
- # If our string is or containing a known good string, then ignore it
- # (""")foo""" -> good (continue)
- # '''foo(""")''' -> good (continue)
- # (''')foo''' -> possibly bad
- if self.config['good_multiline'] in unprefixed_string:
- continue
- # Output our error
- yield {
- 'message': 'Q001 Remove bad quotes from multiline string',
- 'line': start_row,
- 'col': start_col,
- }
- # Otherwise (string is inline quote)
- else:
- # 'This is a string' -> Good
- # 'This is a "string"' -> Good
- # 'This is a \"string\"' -> Good
- # 'This is a \'string\'' -> Bad (Q003) Escaped inner quotes
- # '"This" is a \'string\'' -> Good Changing outer quotes would not avoid escaping
- # "This is a string" -> Bad (Q000)
- # "This is a 'string'" -> Good Avoids escaped inner quotes
- # "This is a \"string\"" -> Bad (Q000)
- # "\"This\" is a 'string'" -> Good
- string_contents = unprefixed_string[1:-1]
- # If string preferred type, check for escapes
- if last_quote_char == self.config['good_single']:
- if not self.config['avoid_escape'] or 'r' in prefix:
- continue
- if self.config['good_single'] in string_contents and not self.config['bad_single'] in string_contents:
- yield {
- 'message': 'Q003 Change outer quotes to avoid escaping inner quotes',
- 'line': start_row,
- 'col': start_col,
- }
- continue
- # If not preferred type, only allow use to avoid escapes.
- if not self.config['good_single'] in string_contents:
- yield {
- 'message': 'Q000 Remove bad quotes',
- 'line': start_row,
- 'col': start_col,
- }
- class Token:
- """Python 2 and 3 compatible token"""
- def __init__(self, token):
- self.token = token
- @property
- def type(self):
- return self.token[0]
- @property
- def string(self):
- return self.token[1]
- @property
- def start(self):
- return self.token[2]
- @property
- def start_row(self):
- return self.token[2][0]
- @property
- def start_col(self):
- return self.token[2][1]
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement