timeit

Програмиране с Python

Курс във Факултета по Математика и Информатика към СУ

Решение на Статичен анализ на python код от Светомир Стоименов

Обратно към всички решения

Към профила на Светомир Стоименов

Резултати

  • 9 точки от тестове
  • 0 бонус точки
  • 9 точки общо
  • 10 успешни тест(а)
  • 1 неуспешни тест(а)

Код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import re
import itertools
from collections import defaultdict
import ast


DEFAULT_RULES = {'line_length': 79, 'forbid_semicolons': True,
                 'indentation_size': 4, 'forbid_trailing_whitespace': True}


def secure_rules(**rules):
    secured_rules = rules.copy()
    for key in DEFAULT_RULES:
        if secured_rules.get(key) is None:
            secured_rules[key] = DEFAULT_RULES[key]
    return secured_rules


def error_str(message, actual, allowed):
    return message + ' ({} > {})'.format(actual, allowed)


def critic(code, **rules):
    errors = defaultdict(list)

    def add_errors(new_raw_errs, message, allowed):
        for number, value in new_raw_errs.items():
            errors[number].append(error_str(message, value, allowed))

    rules = secure_rules(**rules)
    analyzer = AstAnalyzer(code, **rules)
    for number in analyzer.trailing_whitespace_lines:
        errors[number].append('trailing whitespace')
    for number in analyzer.multiple_expression_lines:
        errors[number].append('multiple expressions on the same line')
    for number, indent in analyzer.bad_indentation.items():
        err = 'indentation is {} instead of {}'.format(indent, analyzer.indent)
        errors[number].append(err)
    add_errors(analyzer.too_long_lines, 'line too long', rules['line_length'])
    add_errors(analyzer.nesting_too_deep, 'nesting too deep',
               analyzer.max_nesting)
    add_errors(analyzer.too_long_functions, 'method with too many lines',
               analyzer.max_lines)
    add_errors(analyzer.too_many_args_functions, 'too many arguments',
               analyzer.max_arity)
    add_errors(analyzer.too_many_methods_classes, 'too many methods in class',
               analyzer.methods_per_class)
    return dict(errors)


class DummyNode:
    def __init__(self, body, name='dummy'):
        self.body = body
        self.lineno = body[0].lineno - 1
        self.name = name

    def __str__(self):
        return '<_DummyNode.{} at {}>'.format(self.name, hex(id(self)))


class Block(ast.While, ast.For, ast.Try, ast.ExceptHandler, ast.If, DummyNode,
            ast.With, ast.Module, ast.FunctionDef, ast.ClassDef):
    pass


def is_block(obj):
    return issubclass(Block, obj.__class__)


def inner_nodes(node):
    if not is_block(node):
        return
    for inner_node in node.body:
        yield inner_node
        if getattr(inner_node, 'orelse', []):
            yield DummyNode(inner_node.orelse, 'Else')
        yield from getattr(inner_node, 'handlers', [])
        if getattr(inner_node, 'finalbody', []):
            yield DummyNode(inner_node.finalbody, 'Finally')


def node_lines(node):
    return sum(map(node_lines, inner_nodes(node))) + 1


class AstAnalyzer(ast.NodeVisitor):
    def __init__(self, code, **rules):
        self.code = code
        self.max_arity = rules.get('max_arity')
        self.max_lines = rules.get('max_lines_per_function')
        self.methods_per_class = rules.get('methods_per_class')
        self.max_nesting = rules.get('max_nesting')
        self.forbid_semicolon = rules.get('forbid_semicolons')
        self.indent = rules.get('indentation_size')
        self.max_line_length = rules.get('line_length')
        self.forbid_trailing = rules.get('forbid_trailing_whitespace')
        self.node_nesting = defaultdict(lambda: -1)
        self.too_many_methods_classes = {}
        self.too_long_functions = {}
        self.too_many_args_functions = {}
        self.visit(ast.parse(self.code))

    def procces_node(self, node):
        nesting = not isinstance(node, ast.ClassDef)
        for inner in inner_nodes(node):
            self.node_nesting[inner] = self.node_nesting[node] + nesting
            if isinstance(inner, DummyNode):
                self.procces_node(inner)

    def generic_visit(self, node):
        self.procces_node(node)
        ast.NodeVisitor.generic_visit(self, node)

    def visit_FunctionDef(self, node):
        lines = node_lines(node) - 1
        if self.max_lines is not None and lines > self.max_lines:
            self.too_long_functions[node.lineno] = lines
        if self.max_arity is not None and len(node.args.args) > self.max_arity:
            self.too_many_args_functions[node.lineno] = len(node.args.args)
        self.generic_visit(node)

    def visit_ClassDef(self, node):
        funcs = sum(inner.__class__ == ast.FunctionDef for inner in node.body)
        if self.methods_per_class is not None:
            if funcs > self.methods_per_class:
                self.too_many_methods_classes[node.lineno] = funcs
        self.generic_visit(node)

    def first_node_on(self, line):
        nodes = [node for node in self.node_nesting
                 if getattr(node, 'lineno', 0) == line]
        return min(nodes, key=lambda x: getattr(x, 'col_offset', 0))

    @property
    def nesting_too_deep(self):
        if self.max_nesting is None:
            return {}
        lines = {}
        for key, value in self.node_nesting.items():
            if value > self.max_nesting:
                lines[key.lineno] = value
        return lines

    @property
    def multiple_expression_lines(self):
        if not self.forbid_semicolon:
            return set()
        lines = [getattr(node, 'lineno', 0) for node in self.node_nesting]
        return set(line for line in lines if lines.count(line) > 1)

    @property
    def bad_indentation(self):
        lines = {}
        for node in self.node_nesting:
            if not hasattr(node, 'lineno') or not hasattr(node, 'col_offset'):
                continue
            node = self.first_node_on(node.lineno)
            miss = node.col_offset - self.node_nesting[node] * self.indent
            if miss % 4 > 0:
                lines[node.lineno] = miss + self.indent
        return lines

    @property
    def too_long_lines(self):
        long_lines = {}
        for line, number in zip(self.code.splitlines(), itertools.count(1)):
            if len(line) > self.max_line_length:
                long_lines[number] = len(line)
        return long_lines

    @property
    def trailing_whitespace_lines(self):
        if self.forbid_trailing is None:
            return set()
        line_numbers = set()
        for line, number in zip(self.code.splitlines(), itertools.count(1)):
            if re.search(r'^\s*(.*?)(\s*)$', line).group(2):
                line_numbers.add(number)
        return line_numbers

Лог от изпълнението

......F....
======================================================================
FAIL: test_multiple_issues_all_over_the_place (test.TestCritic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/data/rails/pyfmi-2016/releases/20160307095126/lib/language/python/runner.py", line 67, in thread
    raise result
AssertionError: Items in the first set but not the second:
'method with too many lines (13 > 5)'
'too many arguments (7 > 4)'
Items in the second set but not the first:
'too many arguments(7 > 4)'

----------------------------------------------------------------------
Ran 11 tests in 0.133s

FAILED (failures=1)

История (1 версия и 0 коментара)

Светомир обнови решението на 18.05.2016 10:48 (преди над 1 година)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import re
import itertools
from collections import defaultdict
import ast


DEFAULT_RULES = {'line_length': 79, 'forbid_semicolons': True,
                 'indentation_size': 4, 'forbid_trailing_whitespace': True}


def secure_rules(**rules):
    secured_rules = rules.copy()
    for key in DEFAULT_RULES:
        if secured_rules.get(key) is None:
            secured_rules[key] = DEFAULT_RULES[key]
    return secured_rules


def error_str(message, actual, allowed):
    return message + ' ({} > {})'.format(actual, allowed)


def critic(code, **rules):
    errors = defaultdict(list)

    def add_errors(new_raw_errs, message, allowed):
        for number, value in new_raw_errs.items():
            errors[number].append(error_str(message, value, allowed))

    rules = secure_rules(**rules)
    analyzer = AstAnalyzer(code, **rules)
    for number in analyzer.trailing_whitespace_lines:
        errors[number].append('trailing whitespace')
    for number in analyzer.multiple_expression_lines:
        errors[number].append('multiple expressions on the same line')
    for number, indent in analyzer.bad_indentation.items():
        err = 'indentation is {} instead of {}'.format(indent, analyzer.indent)
        errors[number].append(err)
    add_errors(analyzer.too_long_lines, 'line too long', rules['line_length'])
    add_errors(analyzer.nesting_too_deep, 'nesting too deep',
               analyzer.max_nesting)
    add_errors(analyzer.too_long_functions, 'method with too many lines',
               analyzer.max_lines)
    add_errors(analyzer.too_many_args_functions, 'too many arguments',
               analyzer.max_arity)
    add_errors(analyzer.too_many_methods_classes, 'too many methods in class',
               analyzer.methods_per_class)
    return dict(errors)


class DummyNode:
    def __init__(self, body, name='dummy'):
        self.body = body
        self.lineno = body[0].lineno - 1
        self.name = name

    def __str__(self):
        return '<_DummyNode.{} at {}>'.format(self.name, hex(id(self)))


class Block(ast.While, ast.For, ast.Try, ast.ExceptHandler, ast.If, DummyNode,
            ast.With, ast.Module, ast.FunctionDef, ast.ClassDef):
    pass


def is_block(obj):
    return issubclass(Block, obj.__class__)


def inner_nodes(node):
    if not is_block(node):
        return
    for inner_node in node.body:
        yield inner_node
        if getattr(inner_node, 'orelse', []):
            yield DummyNode(inner_node.orelse, 'Else')
        yield from getattr(inner_node, 'handlers', [])
        if getattr(inner_node, 'finalbody', []):
            yield DummyNode(inner_node.finalbody, 'Finally')


def node_lines(node):
    return sum(map(node_lines, inner_nodes(node))) + 1


class AstAnalyzer(ast.NodeVisitor):
    def __init__(self, code, **rules):
        self.code = code
        self.max_arity = rules.get('max_arity')
        self.max_lines = rules.get('max_lines_per_function')
        self.methods_per_class = rules.get('methods_per_class')
        self.max_nesting = rules.get('max_nesting')
        self.forbid_semicolon = rules.get('forbid_semicolons')
        self.indent = rules.get('indentation_size')
        self.max_line_length = rules.get('line_length')
        self.forbid_trailing = rules.get('forbid_trailing_whitespace')
        self.node_nesting = defaultdict(lambda: -1)
        self.too_many_methods_classes = {}
        self.too_long_functions = {}
        self.too_many_args_functions = {}
        self.visit(ast.parse(self.code))

    def procces_node(self, node):
        nesting = not isinstance(node, ast.ClassDef)
        for inner in inner_nodes(node):
            self.node_nesting[inner] = self.node_nesting[node] + nesting
            if isinstance(inner, DummyNode):
                self.procces_node(inner)

    def generic_visit(self, node):
        self.procces_node(node)
        ast.NodeVisitor.generic_visit(self, node)

    def visit_FunctionDef(self, node):
        lines = node_lines(node) - 1
        if self.max_lines is not None and lines > self.max_lines:
            self.too_long_functions[node.lineno] = lines
        if self.max_arity is not None and len(node.args.args) > self.max_arity:
            self.too_many_args_functions[node.lineno] = len(node.args.args)
        self.generic_visit(node)

    def visit_ClassDef(self, node):
        funcs = sum(inner.__class__ == ast.FunctionDef for inner in node.body)
        if self.methods_per_class is not None:
            if funcs > self.methods_per_class:
                self.too_many_methods_classes[node.lineno] = funcs
        self.generic_visit(node)

    def first_node_on(self, line):
        nodes = [node for node in self.node_nesting
                 if getattr(node, 'lineno', 0) == line]
        return min(nodes, key=lambda x: getattr(x, 'col_offset', 0))

    @property
    def nesting_too_deep(self):
        if self.max_nesting is None:
            return {}
        lines = {}
        for key, value in self.node_nesting.items():
            if value > self.max_nesting:
                lines[key.lineno] = value
        return lines

    @property
    def multiple_expression_lines(self):
        if not self.forbid_semicolon:
            return set()
        lines = [getattr(node, 'lineno', 0) for node in self.node_nesting]
        return set(line for line in lines if lines.count(line) > 1)

    @property
    def bad_indentation(self):
        lines = {}
        for node in self.node_nesting:
            if not hasattr(node, 'lineno') or not hasattr(node, 'col_offset'):
                continue
            node = self.first_node_on(node.lineno)
            miss = node.col_offset - self.node_nesting[node] * self.indent
            if miss % 4 > 0:
                lines[node.lineno] = miss + self.indent
        return lines

    @property
    def too_long_lines(self):
        long_lines = {}
        for line, number in zip(self.code.splitlines(), itertools.count(1)):
            if len(line) > self.max_line_length:
                long_lines[number] = len(line)
        return long_lines

    @property
    def trailing_whitespace_lines(self):
        if self.forbid_trailing is None:
            return set()
        line_numbers = set()
        for line, number in zip(self.code.splitlines(), itertools.count(1)):
            if re.search(r'^\s*(.*?)(\s*)$', line).group(2):
                line_numbers.add(number)
        return line_numbers