Advertisement
Guest User

Untitled

a guest
Jul 18th, 2019
94
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.41 KB | None | 0 0
  1. import optparse
  2. import tokenize
  3. import warnings
  4.  
  5. # Polyfill stdin loading/reading lines
  6. # https://gitlab.com/pycqa/flake8-polyfill/blob/1.0.1/src/flake8_polyfill/stdin.py#L52-57
  7. try:
  8.     from flake8.engine import pep8
  9.     stdin_get_value = pep8.stdin_get_value
  10.     readlines = pep8.readlines
  11. except ImportError:
  12.     from flake8 import utils
  13.     import pycodestyle
  14.     stdin_get_value = utils.stdin_get_value
  15.     readlines = pycodestyle.readlines
  16.  
  17. from flake8_quotes.__about__ import __version__
  18. from flake8_quotes.docstring_detection import get_docstring_tokens
  19.  
  20.  
  21. class QuoteChecker(object):
  22.     name = __name__
  23.     version = __version__
  24.  
  25.     INLINE_QUOTES = {
  26.         # When user wants only single quotes
  27.         '\'': {
  28.             'good_single': '\'',
  29.             'bad_single': '"',
  30.         },
  31.         # When user wants only double quotes
  32.         '"': {
  33.             'good_single': '"',
  34.             'bad_single': '\'',
  35.         },
  36.     }
  37.     # Provide aliases for Windows CLI support
  38.     #   https://github.com/zheller/flake8-quotes/issues/49
  39.     INLINE_QUOTES['single'] = INLINE_QUOTES['\'']
  40.     INLINE_QUOTES['double'] = INLINE_QUOTES['"']
  41.  
  42.     MULTILINE_QUOTES = {
  43.         '\'': {
  44.             'good_multiline': '\'\'\'',
  45.             'bad_multiline': '"""',
  46.         },
  47.         '"': {
  48.             'good_multiline': '"""',
  49.             'bad_multiline': '\'\'\'',
  50.         },
  51.     }
  52.     # Provide Windows CLI and multi-quote aliases
  53.     MULTILINE_QUOTES['single'] = MULTILINE_QUOTES['\'']
  54.     MULTILINE_QUOTES['double'] = MULTILINE_QUOTES['"']
  55.     MULTILINE_QUOTES['\'\'\''] = MULTILINE_QUOTES['\'']
  56.     MULTILINE_QUOTES['"""'] = MULTILINE_QUOTES['"']
  57.  
  58.     DOCSTRING_QUOTES = {
  59.         '\'': {
  60.             'good_docstring': '\'\'\'',
  61.             'bad_docstring': '"""',
  62.         },
  63.         '"': {
  64.             'good_docstring': '"""',
  65.             'bad_docstring': '\'\'\'',
  66.         },
  67.     }
  68.     # Provide Windows CLI and docstring-quote aliases
  69.     DOCSTRING_QUOTES['single'] = DOCSTRING_QUOTES['\'']
  70.     DOCSTRING_QUOTES['double'] = DOCSTRING_QUOTES['"']
  71.     DOCSTRING_QUOTES['\'\'\''] = DOCSTRING_QUOTES['\'']
  72.     DOCSTRING_QUOTES['"""'] = DOCSTRING_QUOTES['"']
  73.  
  74.     def __init__(self, tree, filename='(none)'):
  75.         self.filename = filename
  76.  
  77.     @staticmethod
  78.     def _register_opt(parser, *args, **kwargs):
  79.         """
  80.        Handler to register an option for both Flake8 3.x and 2.x.
  81.  
  82.        This is based on:
  83.        https://github.com/PyCQA/flake8/blob/3.0.0b2/docs/source/plugin-development/cross-compatibility.rst#option-handling-on-flake8-2-and-3
  84.  
  85.        It only supports `parse_from_config` from the original function and it
  86.        uses the `Option` object returned to get the string.
  87.        """
  88.         try:
  89.             # Flake8 3.x registration
  90.             parser.add_option(*args, **kwargs)
  91.         except (optparse.OptionError, TypeError):
  92.             # Flake8 2.x registration
  93.             parse_from_config = kwargs.pop('parse_from_config', False)
  94.             option = parser.add_option(*args, **kwargs)
  95.             if parse_from_config:
  96.                 parser.config_options.append(option.get_opt_string().lstrip('-'))
  97.  
  98.     @classmethod
  99.     def add_options(cls, parser):
  100.         cls._register_opt(parser, '--quotes', action='store',
  101.                           parse_from_config=True, type='choice',
  102.                           choices=sorted(cls.INLINE_QUOTES.keys()),
  103.                           help='Deprecated alias for `--inline-quotes`')
  104.         cls._register_opt(parser, '--inline-quotes', default='\'',
  105.                           action='store', parse_from_config=True, type='choice',
  106.                           choices=sorted(cls.INLINE_QUOTES.keys()),
  107.                           help='Quote to expect in all files (default: \')')
  108.         cls._register_opt(parser, '--multiline-quotes', default=None, action='store',
  109.                           parse_from_config=True, type='choice',
  110.                           choices=sorted(cls.MULTILINE_QUOTES.keys()),
  111.                           help='Quote to expect in all files (default: """)')
  112.         cls._register_opt(parser, '--docstring-quotes', default=None, action='store',
  113.                           parse_from_config=True, type='choice',
  114.                           choices=sorted(cls.DOCSTRING_QUOTES.keys()),
  115.                           help='Quote to expect in all files (default: """)')
  116.         cls._register_opt(parser, '--avoid-escape', default=None, action='store_true',
  117.                           parse_from_config=True,
  118.                           help='Avoiding escaping same quotes in inline strings (enabled by default)')
  119.         cls._register_opt(parser, '--no-avoid-escape', dest='avoid_escape', default=None, action='store_false',
  120.                           parse_from_config=False,
  121.                           help='Disable avoiding escaping same quotes in inline strings')
  122.  
  123.     @classmethod
  124.     def parse_options(cls, options):
  125.         # Define our default config
  126.         # cls.config = {good_single: ', good_multiline: ''', bad_single: ", bad_multiline: """}
  127.         cls.config = {}
  128.         cls.config.update(cls.INLINE_QUOTES['\''])
  129.         cls.config.update(cls.MULTILINE_QUOTES['"""'])
  130.         cls.config.update(cls.DOCSTRING_QUOTES['"""'])
  131.  
  132.         # If `options.quotes` was specified, then use it
  133.         if hasattr(options, 'quotes') and options.quotes is not None:
  134.             # https://docs.python.org/2/library/warnings.html#warnings.warn
  135.             warnings.warn('flake8-quotes has deprecated `quotes` in favor of `inline-quotes`. '
  136.                           'Please update your configuration')
  137.             cls.config.update(cls.INLINE_QUOTES[options.quotes])
  138.         # Otherwise, use the supported `inline_quotes`
  139.         else:
  140.             # cls.config = {good_single: ', good_multiline: """, bad_single: ", bad_multiline: '''}
  141.             #   -> {good_single: ", good_multiline: """, bad_single: ', bad_multiline: '''}
  142.             cls.config.update(cls.INLINE_QUOTES[options.inline_quotes])
  143.  
  144.         # If multiline quotes was specified, overload our config with those options
  145.         if hasattr(options, 'multiline_quotes') and options.multiline_quotes is not None:
  146.             # cls.config = {good_single: ', good_multiline: """, bad_single: ", bad_multiline: '''}
  147.             #   -> {good_single: ', good_multiline: ''', bad_single: ", bad_multiline: """}
  148.             cls.config.update(cls.MULTILINE_QUOTES[options.multiline_quotes])
  149.  
  150.         # If docstring quotes was specified, overload our config with those options
  151.         if hasattr(options, 'docstring_quotes') and options.docstring_quotes is not None:
  152.             cls.config.update(cls.DOCSTRING_QUOTES[options.docstring_quotes])
  153.  
  154.         # If avoid escaped specified, add to config
  155.         if hasattr(options, 'avoid_escape') and options.avoid_escape is not None:
  156.             cls.config.update({'avoid_escape': options.avoid_escape})
  157.         else:
  158.             cls.config.update({'avoid_escape': True})
  159.  
  160.     def get_file_contents(self):
  161.         if self.filename in ('stdin', '-', None):
  162.             return stdin_get_value().splitlines(True)
  163.         else:
  164.             return readlines(self.filename)
  165.  
  166.     def run(self):
  167.         file_contents = self.get_file_contents()
  168.  
  169.         noqa_line_numbers = self.get_noqa_lines(file_contents)
  170.         errors = self.get_quotes_errors(file_contents)
  171.  
  172.         for error in errors:
  173.             if error.get('line') not in noqa_line_numbers:
  174.                 yield (error.get('line'), error.get('col'), error.get('message'), type(self))
  175.  
  176.     def get_noqa_lines(self, file_contents):
  177.         tokens = [Token(t) for t in tokenize.generate_tokens(lambda L=iter(file_contents): next(L))]
  178.         return [token.start_row
  179.                 for token in tokens
  180.                 if token.type == tokenize.COMMENT and token.string.endswith('noqa')]
  181.  
  182.     def get_quotes_errors(self, file_contents):
  183.         tokens = [Token(t) for t in tokenize.generate_tokens(lambda L=iter(file_contents): next(L))]
  184.         docstring_tokens = get_docstring_tokens(tokens)
  185.  
  186.         for token in tokens:
  187.  
  188.             if token.type != tokenize.STRING:
  189.                 # ignore non strings
  190.                 continue
  191.  
  192.             # Remove any prefixes in strings like `u` from `u"foo"`
  193.             # DEV: `last_quote_char` is 1 character, even for multiline strings
  194.             #   `"foo"`   -> `"foo"`
  195.             #   `b"foo"`  -> `"foo"`
  196.             #   `br"foo"` -> `"foo"`
  197.             #   `b"""foo"""` -> `"""foo"""`
  198.             last_quote_char = token.string[-1]
  199.             first_quote_index = token.string.index(last_quote_char)
  200.             prefix = token.string[:first_quote_index].lower()
  201.             unprefixed_string = token.string[first_quote_index:]
  202.  
  203.             # Determine if our string is multiline-based
  204.             #   "foo"[0] * 3 = " * 3 = """
  205.             #   "foo"[0:3] = "fo
  206.             #   """foo"""[0:3] = """
  207.             is_docstring = token in docstring_tokens
  208.             is_multiline_string = unprefixed_string[0] * 3 == unprefixed_string[0:3]
  209.             start_row, start_col = token.start
  210.  
  211.             # If our string is a docstring
  212.             # DEV: Docstring quotes must come before multiline quotes as it can as a multiline quote
  213.             if is_docstring:
  214.                 if self.config['good_docstring'] in unprefixed_string:
  215.                     continue
  216.  
  217.                 yield {
  218.                     'message': 'Q002 Remove bad quotes from docstring',
  219.                     'line': start_row,
  220.                     'col': start_col,
  221.                 }
  222.             # Otherwise if our string is multiline
  223.             elif is_multiline_string:
  224.                 # If our string is or containing a known good string, then ignore it
  225.                 #   (""")foo""" -> good (continue)
  226.                 #   '''foo(""")''' -> good (continue)
  227.                 #   (''')foo''' -> possibly bad
  228.                 if self.config['good_multiline'] in unprefixed_string:
  229.                     continue
  230.  
  231.                 # Output our error
  232.                 yield {
  233.                     'message': 'Q001 Remove bad quotes from multiline string',
  234.                     'line': start_row,
  235.                     'col': start_col,
  236.                 }
  237.             # Otherwise (string is inline quote)
  238.             else:
  239.                 #   'This is a string'       -> Good
  240.                 #   'This is a "string"'     -> Good
  241.                 #   'This is a \"string\"'   -> Good
  242.                 #   'This is a \'string\''   -> Bad (Q003)  Escaped inner quotes
  243.                 #   '"This" is a \'string\'' -> Good        Changing outer quotes would not avoid escaping
  244.                 #   "This is a string"       -> Bad (Q000)
  245.                 #   "This is a 'string'"     -> Good        Avoids escaped inner quotes
  246.                 #   "This is a \"string\""   -> Bad (Q000)
  247.                 #   "\"This\" is a 'string'" -> Good
  248.                
  249.                 string_contents = unprefixed_string[1:-1]
  250.                
  251.                 # If string preferred type, check for escapes
  252.                 if last_quote_char == self.config['good_single']:
  253.                     if not self.config['avoid_escape'] or 'r' in prefix:
  254.                         continue
  255.                     if self.config['good_single'] in string_contents and not self.config['bad_single'] in string_contents:
  256.                         yield {
  257.                             'message': 'Q003 Change outer quotes to avoid escaping inner quotes',
  258.                             'line': start_row,
  259.                             'col': start_col,
  260.                         }
  261.                     continue
  262.                
  263.                 # If not preferred type, only allow use to avoid escapes.
  264.                 if not self.config['good_single'] in string_contents:
  265.                     yield {
  266.                         'message': 'Q000 Remove bad quotes',
  267.                         'line': start_row,
  268.                         'col': start_col,
  269.                     }
  270.        
  271.  
  272.  
  273. class Token:
  274.     """Python 2 and 3 compatible token"""
  275.     def __init__(self, token):
  276.         self.token = token
  277.  
  278.     @property
  279.     def type(self):
  280.         return self.token[0]
  281.  
  282.     @property
  283.     def string(self):
  284.         return self.token[1]
  285.  
  286.     @property
  287.     def start(self):
  288.         return self.token[2]
  289.  
  290.     @property
  291.     def start_row(self):
  292.         return self.token[2][0]
  293.  
  294.     @property
  295.     def start_col(self):
  296.         return self.token[2][1]
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement