Commit 712aab3d authored by Denis Khachko's avatar Denis Khachko

Merge branch 'python' of git.lpm.org.ru:niisi/kumir2

parents 80d80d2d 177995e1
[Desktop Entry]
Type=Application
Exec=kumir2-python-teacher %U
Icon=kumir2-python-teacher
Name=Kumir-Python Teacher Edition
Name[ru]=Кумир-Python (режим учителя)
Categories=Qt;KDE;Education;X-KDE-Edu-Misc;Math;
X-Kumir-MDI=true
[Desktop Entry]
Type=Application
Exec=kumir2-python %U
Icon=kumir2-python
Name=Kumir-Python
Name[ru]=Кумир-Python
Categories=Qt;KDE;Education;X-KDE-Edu-Misc;Math;
# coding=utf-8
"""
Analizer instance module
NOTE: Each analizer instance runs in it's own interpreter
"""
import copy
import os
from check_syntax import *
from check_syntax import pep8_wrapper
from kumir_constants import *
import color_marking
import static_analisys
SOURCE_DIR_NAME = ""
SOURCE_TEXT = ""
LINE_PROPERTIES = []
LINE_RANKS = []
ERRORS = []
USE_PEP8 = False
class Error:
# VY: В соответствии с PEP-8 и Google Coding Standards, имена классов - CamelCase
# В PyCharm есть функция "умного" переименования имен: Refactor -> Rename... (Shift+F6)
# Проверь также и другие файлы
""" One error message """
def __init__(self, line_no, start_pos, length, message):
"""
line_no -- line number (int)
start_pos -- start position from 0 (int)
length -- error block length (int)
message -- a string (ASCII symbols only) identificating error message
"""
assert isinstance(line_no, int)
assert isinstance(start_pos, int)
assert isinstance(length, int)
assert isinstance(message, str)
self.line_no = line_no
self.start_pos = start_pos
self.length = length
self.message = message
def __eq__(self, other):
return self.line_no == other.line_no and self.start_pos == other.start_pos
def set_source_dir_name(path):
"""
Set the source file location (directory name) to help searching imported modules
path -- directory path in platform dependent format (str)
"""
assert isinstance(path, str)
global SOURCE_DIR_NAME
SOURCE_DIR_NAME = path
def _make_syntax_checks(text):
global ERRORS
global USE_PEP8
checkers = [pylint_wrapper, pyflakes_wrapper]
if USE_PEP8:
checkers += [pep8_wrapper]
for checker in checkers:
checker.set_source_text(text)
errors = checker.get_errors()
for error in errors:
error.origin_name = checker.name
if error not in ERRORS:
ERRORS += [error]
def set_use_pep8(use):
global USE_PEP8
USE_PEP8 = use
def set_source_text(text):
"""
Set the source text and require complete analisis
text -- complete python program source; line delimiter is '\n' (str)
"""
assert isinstance(text, str)
global SOURCE_TEXT
global LINE_PROPERTIES
global LINE_RANKS
global ERRORS
SOURCE_TEXT = text
ERRORS.clear()
# noinspection PyBroadException
try:
color_marking.set_color_marks_and_ranks(SOURCE_TEXT)
LINE_RANKS = color_marking.get_ranks()
LINE_PROPERTIES = color_marking.get_colors()
except Exception as e:
pass
if SOURCE_TEXT:
_make_syntax_checks(SOURCE_TEXT)
for error in get_errors():
if 0 <= error.line_no <= len(LINE_PROPERTIES):
props = LINE_PROPERTIES[error.line_no]
start = error.start_pos
end = start + error.length
if end <= len(props):
for i in range(start, end):
props[i] |= LxTypeError
def set_source_text_old(text):
"""
Set the source text and require complete analisis
text -- complete python program source; line delimiter is '\n' (str)
"""
assert isinstance(text, str)
global SOURCE_TEXT
SOURCE_TEXT = text
global LINE_PROPERTIES
global LINE_RANKS
static_analisys.clear_errors()
static_analisys.run_static_analisys(text)
color_marking.set_color_marks_and_ranks(SOURCE_TEXT)
LINE_RANKS = color_marking.get_ranks()
# VY: Результат -- это список пар чисел
# каждое число -- это не количество отступов, а ожидаемое (логически, а не по количеству подсчитанных отступов)
# изменение отступа. В качестве признака можно использовать, например, двоеточие в конце строки.
# Эти значения используются редактором для того, чтобы автоматически вставлять нужное количество отступов при
# нажатии Enter.
#
# Пример:
# def sign(x): # rank = ( 0, +1) # увеличиваем отступ следующей строки, поскольку в конце строки двоеточие
# print(x) # rank = ( 0, 0) # отступы не меняются
# if x < 0: # rank = ( 0, +1) # увеличиваем отступ следующей строки, поскольку в конце строки двоеточие
# return -1 # rank = ( 0, -1) # в блоке кода после return не может быть ничего, поэтому уменьшаем отступ
# elif x > 0: return 1 # rank = ( 0, 0) # в данном случае elif ... : и return компенсируют друг друга
# else: # rank = ( 0, +1) # увеличиваем отступ следующей строки, поскольку в конце строки двоеточие
# return 0 # rank = ( 0, -1) # в блоке кода после return не может быть ничего, поэтому уменьшаем отступ
#
# Таким образом, для данной программы ranks должно быть:
# line_ranks = [ (0,1), (0,0), (0,1), (0,-1), (0,0), (0,1), (0,-1) ]
#
# Замечание. Похоже, в Python первое число во всех парах, равно 0. Тем не менее, необходимо соблюдать именно такой
# интерфейс, который разработан с учетом других ЯП. Например, в Паскале, возможны такие числа:
#
# program MySuperProgram; { rank = ( 0, 0) }
# var { rank = (-1, +1) -- почему в начале -1 -- смотри ниже, станет понятно }
# i: Integer; { rank = ( 0, 0) }
# s: String; { rank = ( 0, 0) }
# begin { rank = (-1, +1) -- begin должен сместиться влево на 1 уровень с var и program,
# но следующая строка -- уже с отступом на +1. Аналогично и с var,
# поскольку, в общем случае, описание переменных может следовать
# после группы описания const.
# WriteLn('Hello!'); { rank = ( 0, 0) }
# end. { rank = (-1, 0) -- end сам по себе сдвигается влево (-1), следующие -- на том же уровне
LINE_PROPERTIES = color_marking.get_colors()
for error in get_errors():
if 0 <= error.line_no <= len(LINE_PROPERTIES):
props = LINE_PROPERTIES[error.line_no]
start = error.start_pos
end = start + error.length
for i in range(start, end):
props[i] |= LxTypeError
def get_errors():
"""
Get a list of errors generated while 'set_source_text'
returns a list of Error class instances
"""
global ERRORS
return ERRORS
def get_line_properties():
"""
Get a list of line highlight properties generated while 'set_source_text'
returns a list:
- each item corresponds one text line
- each item is a list:
- each item corresponds one character in line
- each item is an integer number, see 'kumir_constants.py'
"""
global LINE_PROPERTIES
return LINE_PROPERTIES
def get_line_ranks():
"""
Get a list of line indentation ranks generated while 'set_source_text'
returns a list:
- each item corresponds one text line
- each item is a tuple (start, end), where:
- start is a line start indentation rank
- end is a terminal indentation rank
"""
global LINE_RANKS
return LINE_RANKS
def get_line_property(line_no, line_text):
"""
Get line property of one text line, currently editing and possible not complete
line_no -- editable line number (from 0)
line_text -- one text line while in edit progress
returns a list:
- each item corresponds one character in line
- each item is an integer number, see 'kumir_constants.py'
"""
assert isinstance(line_no, int)
assert isinstance(line_text, str)
global LINE_RANKS
changes = color_marking.is_change_rank(line_text)
print(LINE_RANKS[line_no])
LINE_RANKS[line_no] = (changes[0] + LINE_RANKS[line_no][0], changes[1] + LINE_RANKS[line_no][1])
print(LINE_RANKS[line_no])
return [LxTypeEmpty] * len(str)
def __run_test(test_name):
# base = os.path.dirname(os.path.abspath(__file__)) + "/"
base = ""
source_file = open(base + test_name, 'r')
set_source_text(source_file.read())
source_file.close()
class LinePropPrintHex(int):
def __repr__(self):
return "0x%x" % self
lines = SOURCE_TEXT.split('\n')
ranks = get_line_ranks()
props = [[LinePropPrintHex(item) for item in row] for row in get_line_properties()]
errors = get_errors()
assert len(lines) == len(ranks) == len(props)
print("\nBegin test ", test_name, "========================")
for no in range(0, len(lines)):
out = "{:2d}: {!s:<30} # {!s:<8} {}".format(no+1, lines[no], ranks[no], props[no])
print(out)
if errors:
print("\n")
for error in errors:
error_message = error.message
if error.id:
error_message += " [" + error.id + "]"
print("Error: ", error_message)
line = lines[error.line_no]
out = "{:2d}: {:s}".format(error.line_no+1, line)
print(out)
out = " " + " " * error.start_pos + "^" * error.length
print(out)
print("End test ", test_name, "==========================")
if __name__ == "__main__":
TESTS = [
"MyTests/test1.py",
"MyTests/test2.py",
"MyTests/test3.py",
"MyTests/test4.py",
"MyTests/test5.py",
"MyTests/undefined_names.py",
"MyTests/reference_before_assignment.py",
"MyTests/structure_syntax_check.py",
"MyTests/arguments_count_mismatch.py",
"MyTests/arguments_types_mismatch.py",
"MyTests/legacy_syntax.py",
# "MyTests/sa.py",
# "MyTests/st_an.py",
]
for test in TESTS:
__run_test(test)
__all__ = ["pyflakes_wrapper", "pylint_wrapper"]
class Error:
""" One error message """
def __init__(self, line_no, start_pos, length, message, internal_id=""):
"""
line_no -- line number (int)
start_pos -- start position from 0 (int)
length -- error block length (int)
message -- a string (ASCII symbols only) identificating error message
"""
assert isinstance(line_no, int)
assert isinstance(start_pos, int)
assert isinstance(length, int)
assert isinstance(message, str)
self.line_no = line_no
self.start_pos = start_pos
self.length = length
self.message = message
self.id = internal_id
self.origin_name = ""
def __eq__(self, other):
return self.line_no == other.line_no and self.start_pos == other.start_pos
\ No newline at end of file
# coding=utf-8
from check_syntax.error import Error
import pep8
import re
name = "PEP-8"
description = {
"generic": "PEP-8 checker",
"russian": "Анализатор PEP-8"
}
priority = 0.0
try:
import _kumir
debug = _kumir.debug
except ImportError:
debug = print
class Report(pep8.BaseReport):
def __init__(self, options):
super().__init__(options)
self.errors = []
def stop(self):
super().stop()
def print_statistics(self, prefix=''):
super().print_statistics(prefix)
def start(self):
super().start()
self.errors.clear()
def print_benchmark(self):
super().print_benchmark()
def increment_logical_line(self):
super().increment_logical_line()
def get_file_results(self):
return super().get_file_results()
def init_file(self, filename, lines, expected, line_offset):
super().init_file(filename, lines, expected, line_offset)
def get_count(self, prefix=''):
return super().get_count(prefix)
def error(self, line_number, offset, text, check):
assert isinstance(text, str)
first_space_pos = text.find(' ')
msg_id = text[:first_space_pos]
message = text[first_space_pos+1:].capitalize()
self.errors += [Error(line_number-1, offset, 1, message, msg_id)]
return super().error(line_number, offset, text, check)
def get_statistics(self, prefix=''):
return super().get_statistics(prefix)
def improve_errors_positions(self, source_text):
lines = source_text.split('\n')
for error in self.errors:
line = lines[error.line_no]
found_exact_position = False
if error.start_pos < len(line):
found_exact_position = "line" not in error.message
if not found_exact_position:
# can't determine exact error position, so
# mark whole line (except leading spaces)
error.start_pos = 0
error.length = len(line) - error.start_pos
def set_source_text(text):
global _reporter
assert isinstance(text, str)
text_lines = [line + '\n' for line in text.split('\n')]
text_lines[-1] = text_lines[-1][:-1]
if not text_lines[-1]:
text_lines = text_lines[:-1]
# debug("Text lines: " + str(text_lines))
style_guide = pep8.StyleGuide(parse_argv=False, config_file=False)
options = style_guide.options
_reporter = Report(options)
options.report = _reporter
style_guide.input_file(None, text_lines)
_reporter.improve_errors_positions(text)
def get_errors():
global _reporter
return _reporter.errors
\ No newline at end of file
# coding=utf-8
from pyflakes.api import check
from pyflakes.reporter import Reporter as ReporterBase
from check_syntax.error import Error
from pyflakes.messages import *
name = "PyFlakes"
description = {
"generic": "PyFlakes error checker",
"russian": "Анализатор PyFlakes"
}
priority = 5.0
class Reporter(ReporterBase):
def __init__(self):
self.errors = []
self.reset()
def unexpectedError(self, filename, msg):
pass
def syntaxError(self, filename, msg, lineno, offset, text):
if text[offset] == ' ':
start = 0
length = len(text)
if text.endswith('\n'):
length -= 1
else:
start = offset
length = 1
message = str(msg).capitalize()
self.errors += [Error(lineno - 1, start, length, message)]
def flake(self, message):
if isinstance(message, UndefinedName):
self.errors += [Error(message.lineno - 1, message.col, len(message.message_args[0]), "Undefined name")]
def reset(self):
self.errors.clear()
def improve_errors_positions(self, source_text):
lines = source_text.split('\n')
for error in self.errors:
line = lines[error.line_no]
assert isinstance(line, str)
if error.start_pos >= len(line):
error.start_pos = 0
for char in line:
if ' ' == char:
error.start_pos += 1
else:
break
error.length = len(line) - error.start_pos
_reporter = Reporter()
def set_source_text(text):
global _reporter
assert isinstance(_reporter, Reporter)
_reporter.reset()
check(text, "<stdin>", _reporter)
_reporter.improve_errors_positions(text)
def get_errors():
global _reporter
return _reporter.errors
\ No newline at end of file
# coding=utf-8
from astroid.builder import AstroidBuilder
from logilab.common.interface import implements
from pylint import lint, utils
from pylint.interfaces import *
from pylint.reporters import BaseReporter
from check_syntax.error import Error
import re
name = "PyLint"
description = {
"generic": "PyLint error checker",
"russian": "Анализатор PyLint"
}
_not_errors = [
"C0103", # Invalid module name
"C0304", # Final newline missing
"C0111", # Missing module docstring
"W0622", # Redefining built-in ...
"W0401", # Wildcard import
"W0614", # Unused import ... from wildcard import
"W0613", # Unused argument
"W0621", # Redefining name from outer scope
"W0612", # Undefined variable
"W0704", # Except doesn't do anything
]
priority = 10.0
_reporter = None
_linter = None
_walker = None
_token_checkers = None
_raw_checkers = None
class Reporter(BaseReporter):
__implements__ = IReporter
def __init__(self, output=None):
super().__init__(output)
self.errors = []
def set_output(self, output=None):
super().set_output(output)
def _display(self, layout):
pass
def writeln(self, string=''):
super().writeln(string)
def on_set_current_module(self, module, filepath):
super().on_set_current_module(module, filepath)
def display_results(self, layout):
super().display_results(layout)
def on_close(self, stats, previous_stats):
super().on_close(stats, previous_stats)
def add_message(self, msg_id, location, msg):
# TODO select only error messages
if msg_id[0] not in "C" and msg_id not in _not_errors:
self.errors += [Error(location[3]-1, location[4], 1, msg, msg_id)]
def reset(self):
self.errors.clear()
def improve_errors_positions(self, source_text):
lines = source_text.split('\n')
for error in self.errors:
line = lines[error.line_no]
name_match = re.search(r"\s'(\w+)'", error.message)
found_exact_position = False
search_literal = False
if not name_match: # second chance...
name_match = re.search(r'"(\S+)"\sis\b', error.message)
search_literal = True
if name_match is not None:
a0, b0 = name_match.span(0)
a1, b1 = name_match.span(1)
lexem_to_find = error.message[a1:b1]
if search_literal:
lexem_match = re.search(r"([\"']"+lexem_to_find+r"[\"'])", line, error.start_pos)
else: