diff --git a/parso/normalizer.py b/parso/normalizer.py index f26669f..0d8d204 100644 --- a/parso/normalizer.py +++ b/parso/normalizer.py @@ -1,10 +1,34 @@ +from contextlib import contextmanager + + class Normalizer(object): + def __init__(self, config): + self._config = config + self.issues = [] + + @contextmanager + def visit_node(self): + yield + + def normalize(self, leaf): + return leaf.prefix + leaf.value + + +class NormalizerConfig(object): + normalizer_class = Normalizer + + def create_normalizer(self): + if self.normalizer_class is None: + return None + + return self.normalizer_class(self) + @classmethod def register_rule(cls, rule): """ Use it as a class decorator: - >>> normalizer = Normalizer() + >>> normalizer = NormalizerConfig() >>> @normalizer.register_rule ... class MyRule(Rule): ... error_code = 42 @@ -16,12 +40,6 @@ class Normalizer(object): rules.append(rule) return rule - def normalize(self, leaf): - return leaf.prefix + leaf.value - - def iter_errors(self, leaf): - return iter([]) - class Error(object): def __init__(self, leaf, code, message): diff --git a/parso/python/normalizer.py b/parso/python/normalizer.py index 647b1ce..375860f 100644 --- a/parso/python/normalizer.py +++ b/parso/python/normalizer.py @@ -1,6 +1,9 @@ -from parso.normalizer import Normalizer, Error +from contextlib import contextmanager +from parso.normalizer import Normalizer, Rule, NormalizerConfig +IMPORT_TYPES = ('import_name', 'import_from') + class CompressNormalizer(Normalizer): """ Removes comments and whitespace. @@ -9,16 +12,114 @@ class CompressNormalizer(Normalizer): return leaf.prefix + leaf.value +class WhitespaceInfo(object): + def __init__(self, leaf): + parts = list(leaf._split_prefix()) + ''' + ' ': 'spaces', + '#': 'comment', + '\\': 'backslash', + '\f': 'formfeed', + '\n': 'newline', + '\r': 'newline', + '\t': 'tabs', +''' + for part in parts: + if part.type: + part + self.newline_count = 2 + self.indentation = ' ' + self.trailing_whitespace = [] + self.comment_whitespace = [] + class PEP8Normalizer(Normalizer): + def __init__(self, config): + super(PEP8Normalizer, self).__init__(config) + self.indentation = 0 + + @contextmanager + def visit_node(self, state, node): + typ = node.type + + if typ in 'import_name': + names = node.get_defined_names() + if len(names) > 1: + for name in names[:1]: + self.log_error(401, 'Multiple imports on one line', name) + elif typ == 'lambdef': + if node.parent.type == 'expr_stmt': + self.log_error(731, 'Do not assign a lambda expression, use a def', node) + elif typ == 'try_stmt': + for child in node.children: + # Here we can simply check if it's an except, because otherwise + # it would be an except_clause. + if child == 'except': + self.log_error(722, 'Do not use bare except, specify exception instead', node) + elif typ == 'comparison': + odd = False + for child in node.children: + if odd: + if child not in ('is', '=='): + break + else: + if child.type == 'atom_expr': + break + trailer = child.children[-1] + atom = child.children[-1] + if not (trailer.type == 'trailer' and atom.type == 'name' + and atom.value == 'type'): + break + odd = not odd + else: + self.log_error(721, "Do not compare types, use 'isinstance()", node) + + if typ in IMPORT_TYPES: + module = node.parent + if module.type == 'file_input': + index = module.children.index(node) + for child in module.children[:index]: + if child.type not in IMPORT_TYPES: + self.log_error(402, 'Module level import not at top of file', node) + break + + if typ == 'suite': + self.indentation += 1 + yield + if typ == 'suite': + self.indentation -= 1 + + def normalize(self, leaf): + typ = leaf.type + if typ == 'name' and leaf.value in ('l', 'O', 'I'): + if leaf.is_definition(): + message = "Do not define %s named 'l', 'O', or 'I' one line" + if leaf.parent.type == 'class' and leaf.parent.name == leaf: + self.log_error(742, message % 'classes', leaf) + elif leaf.parent.type == 'function' and leaf.parent.name == leaf: + self.log_error(743, message % 'function', leaf) + else: + self.log_error(741, message % 'variables', leaf) + + for part in leaf._split_prefix(): + part + return leaf.value + + +class PEP8NormalizerConfig(NormalizerConfig): + normalizer_class = PEP8Normalizer """ Normalizing to PEP8. Not really implemented, yet. """ - def normalize(self, leaf): - return leaf.value - - def iter_errors(self, leaf): - return iter([]) -class Rule(): +@PEP8NormalizerConfig.register_rule +class FooRule(Rule): pass + + +@PEP8NormalizerConfig.register_rule +class BlankLineAtEnd(Rule): + code = 'W391' + message = 'blank line at end of file' + + leaf_event = ['endmarker'] diff --git a/parso/python/tree.py b/parso/python/tree.py index 207481c..8f0b7fd 100644 --- a/parso/python/tree.py +++ b/parso/python/tree.py @@ -65,7 +65,7 @@ class PythonMixin(object): Some Python specific utitilies. """ __slots__ = () - default_normalizer = normalizer.PEP8Normalizer() + default_normalizer_config = normalizer.PEP8NormalizerConfig() def get_definition(self): if self.type in ('newline', 'endmarker'): diff --git a/parso/tree.py b/parso/tree.py index 295f759..ec51a45 100644 --- a/parso/tree.py +++ b/parso/tree.py @@ -22,7 +22,7 @@ class NodeOrLeaf(object): The base class for nodes and leaves. """ __slots__ = () - default_normalizer = None + default_normalizer_config = None def get_root_node(self): """ @@ -154,22 +154,34 @@ class NodeOrLeaf(object): e.g. a statement. """ - def normalize(self, normalizer=None): + def _get_normalizer(self, normalizer_config): + if normalizer_config is None: + normalizer_config = self.default_normalizer_config + if normalizer_config is None: + raise ValueError("You need to specify a normalizer, because " + "there's no default normalizer for this tree.") + return normalizer_config.create_normalizer() + + def normalize(self, normalizer_config=None): """ The returned code will be normalized, e.g. PEP8 for Python. """ - if normalizer is None: - normalizer = self.default_normalizer - if normalizer is None: - raise ValueError("You need to specify a normalizer, because " - "there's no default normalizer for this tree.") + normalizer = self._get_normalizer(normalizer_config) + return self._normalize(normalizer) + def _normalize(self, normalizer): try: children = self.children except AttributeError: return normalizer.normalize(self) else: - return ''.join(child.normalize(normalizer) for child in children) + with normalizer.visit_node(self): + return ''.join(child._normalize(normalizer) for child in children) + + def _get_normalize_errors(self, normalizer_config=None): + normalizer = self._get_normalizer(normalizer_config) + self._normalize(normalizer) + return normalizer.issues