protoerror 3.20.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
protoerror/question.py ADDED
@@ -0,0 +1,138 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2022 by Helmut Konrad Fahrendholz. All rights reserved.
5
+ # This file is property of Helmut Konrad Fahrendholz. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import dataclasses
11
+ import enum
12
+ import re
13
+
14
+ import iamraw
15
+ import utilo
16
+
17
+ import protoerror
18
+
19
+ QUESTION_PATTERN = r'^QUESTION_(?P<number>\d{2,5})$'
20
+
21
+
22
+ class Answer(enum.Enum):
23
+ OPEN = enum.auto()
24
+ YES = enum.auto()
25
+ NO = enum.auto()
26
+ UNDECIDED = enum.auto()
27
+
28
+
29
+ class Action(enum.Enum):
30
+ PAGE = enum.auto()
31
+ DOCU = enum.auto()
32
+
33
+
34
+ @dataclasses.dataclass(unsafe_hash=True)
35
+ class Question(iamraw.Solution):
36
+ msgid: str = None
37
+ title: str = None
38
+ description: str = None
39
+ status: Answer = Answer.OPEN
40
+
41
+ action: Action = None
42
+ finding: int = None
43
+ yes: callable = None
44
+ no: callable = None
45
+
46
+
47
+ Questions = list[Question]
48
+
49
+
50
+ def parse_questions(module) -> Questions:
51
+ if isinstance(module, str):
52
+ module = protoerror.utils.module_fromname(module)
53
+ enabler = parse_enabler(module)
54
+ result = []
55
+ for name, value in vars(module).items():
56
+ matched = re.match(QUESTION_PATTERN, name)
57
+ if not matched:
58
+ continue
59
+ number = matched['number']
60
+ try:
61
+ title, message = value.split('\n\n', maxsplit=1)
62
+ except ValueError:
63
+ title, message = value.strip(), ''
64
+ item = protoerror.Question(
65
+ title=title,
66
+ msgid=number,
67
+ description=message,
68
+ )
69
+ try:
70
+ selected = enabler[number]
71
+ except KeyError:
72
+ pass
73
+ else:
74
+ item.action = selected[0]
75
+ item.finding = selected[1]
76
+ item.yes = selected[2]
77
+ item.no = selected[3] # pylint:disable=C0103
78
+
79
+ result.append(item)
80
+ # sort ascending
81
+ result.sort(key=lambda x: x.msgid)
82
+ return result
83
+
84
+
85
+ ENABLE_PATTERN = r'^QUESTION_(?P<number>\d{2,5})_ENABLE$'
86
+
87
+
88
+ def parse_enabler(module) -> int:
89
+ result = {}
90
+ for name, value in vars(module).items():
91
+ matched = re.match(ENABLE_PATTERN, name)
92
+ if not matched:
93
+ continue
94
+ number = matched['number']
95
+ result[number] = value
96
+ return result
97
+
98
+
99
+ def pagemore(finding_count: int, yes: callable = None, no: callable = None):
100
+ return (Action.PAGE, finding_count, yes, no)
101
+
102
+
103
+ def documore(finding_count: int, yes: callable = None, no: callable = None):
104
+ return (Action.DOCU, finding_count, yes, no)
105
+
106
+
107
+ def answer_questions(path: str, questions: Questions) -> iamraw.Findings:
108
+ findings = [item.content for item in protoerror.findings_from_path(path)]
109
+ findings = utilo.flat(findings)
110
+ grouped = protoerror.byid(findings)
111
+ result = []
112
+ for question in questions:
113
+ try:
114
+ selected = grouped[question.msgid]
115
+ except KeyError:
116
+ # no potential message available
117
+ continue
118
+ if question.action == Action.PAGE:
119
+ bypage = protoerror.bypage(selected)
120
+ for paged in bypage:
121
+ if len(paged.content) < question.finding:
122
+ continue
123
+ solution = iamraw.Finding(
124
+ location=iamraw.Location.from_page(paged.page),
125
+ msgid=question.msgid,
126
+ solution=question,
127
+ )
128
+ result.append(solution)
129
+ elif question.action == Action.DOCU:
130
+ if len(selected) < question.finding:
131
+ continue
132
+ solution = iamraw.Finding(
133
+ location=protoerror.OVERVIEW,
134
+ msgid=question.msgid,
135
+ solution=question,
136
+ )
137
+ result.append(solution)
138
+ return result
protoerror/report.py ADDED
@@ -0,0 +1,210 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2022 by Helmut Konrad Fahrendholz. All rights reserved.
5
+ # This file is property of Helmut Konrad Fahrendholz. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+ """Report Solution
10
+ ===============
11
+
12
+ example
13
+ -------
14
+
15
+ .. code-block::none
16
+
17
+ <<titlepage>>
18
+ ~7777:headline~
19
+ ...
20
+ ...
21
+ ...
22
+ ~7779:next headline~
23
+ ...
24
+ ...
25
+ ...
26
+ <<bibliography>>
27
+
28
+ cli
29
+ ---
30
+
31
+ decider show list [--short]
32
+ show titlepage
33
+ show 7777
34
+
35
+ """
36
+
37
+ import functools
38
+ import importlib
39
+ import os
40
+ import sys
41
+ import textwrap
42
+
43
+ import utilo
44
+
45
+ import protoerror
46
+
47
+
48
+ def integrate(root: str, features: str):
49
+ hook = integrate_cli
50
+ run_ = functools.partial(run, root=root, features=features)
51
+ return hook, run_
52
+
53
+
54
+ def show_list(root, features, short: bool = False):
55
+ print_features(root, features, choice=None, short=short)
56
+
57
+
58
+ def show_feature(root, features, choice, short: bool = False):
59
+ print_features(root, features, choice, short)
60
+
61
+
62
+ def show_solution(root, features, choice, short: bool = False):
63
+ parsed = parse_features(root, features)
64
+ if not parsed:
65
+ return
66
+ parsed = [(checkers, solutions) for (_, (checkers, solutions)) in parsed]
67
+ for checkers, solutions in parsed:
68
+ for check in sorted(checkers):
69
+ if check != choice:
70
+ continue
71
+ print_solution(check, solutions, short=short, single=True)
72
+ return
73
+ utilo.error(f'could not find solution: {choice}')
74
+
75
+
76
+ def print_features(root, features, choice, short: bool = False):
77
+ parsed = parse_features(root, features)
78
+ if not parsed:
79
+ return
80
+ hit = False
81
+ for title, (checkers, solutions) in parsed:
82
+ if choice and title != choice:
83
+ continue
84
+ hit = True
85
+ utilo.log(f'<<{title}>>')
86
+ for check in sorted(checkers):
87
+ print_solution(check, solutions, short=short)
88
+ utilo.log()
89
+ if not hit:
90
+ utilo.error(f'could not find feature: {choice}')
91
+ sys.exit(utilo.FAILURE)
92
+
93
+
94
+ def print_solution(
95
+ check,
96
+ solutions,
97
+ *,
98
+ short: bool = False,
99
+ single: bool = False,
100
+ ):
101
+ indent = '\t' if short and not single else ''
102
+ try:
103
+ title, description = solutions[check]
104
+ except KeyError:
105
+ utilo.log(f'{indent}~{check}:NO_SOLUTION~')
106
+ return
107
+ if single:
108
+ utilo.log(title)
109
+ else:
110
+ utilo.log(f'{indent}~{check}:{title}~')
111
+ if single:
112
+ utilo.log()
113
+ if not short:
114
+ prefix = '\t' if not single else ''
115
+ utilo.log(textwrap.indent(description.strip(), prefix))
116
+ utilo.log()
117
+
118
+
119
+ def run(args, root, features):
120
+ if 'show' not in args:
121
+ return
122
+ assert os.path.exists(root), str(root)
123
+ choice = args['show']
124
+ short = args.get('short', False)
125
+ if 'list' in choice:
126
+ show_list(root, features, short=short)
127
+ elif utilo.isnumber(choice):
128
+ choice = int(choice)
129
+ show_solution(root, features, choice, short=short)
130
+ else:
131
+ show_feature(root, features, choice)
132
+ sys.exit(utilo.SUCCESS)
133
+
134
+
135
+ def integrate_cli(parser):
136
+ sub = parser.add_subparsers(help='show additional linter information')
137
+ show = sub.add_parser('show')
138
+ show.add_argument('show', nargs='?', default='list')
139
+ show.add_argument('--short', action='store_true', help='display fewer content') # yapf:disable
140
+
141
+
142
+ def parse_features(root: str, features: str):
143
+ if isinstance(features, str):
144
+ features = [features]
145
+ collected = [parse_python(root, feature) for feature in features]
146
+ result = []
147
+ for collect in collected:
148
+ for (name, item) in collect:
149
+ parsed = parse_feature(item.__name__)
150
+ if parsed is None:
151
+ continue
152
+ result.append((name, parsed))
153
+ result = sorted(result, key=lambda x: x[0])
154
+ if not result:
155
+ return result
156
+ # merge duplicated groups to single group
157
+ grouped = [result[0]]
158
+ for name, data in result[1:]:
159
+ before = grouped[-1]
160
+ if name == before[0]:
161
+ # add before
162
+ before[1][0].extend(data[0])
163
+ before[1][1].update(data[1])
164
+ else:
165
+ # new group
166
+ grouped.append((name, data))
167
+ return grouped
168
+
169
+
170
+ def parse_python(root: str, feature: str) -> list:
171
+ """Load python code from path."""
172
+ # TODO: THERE MUST BE A BETTER WAY
173
+ collected = [
174
+ item.replace('.py', '')
175
+ for item in os.listdir(os.path.join(root, feature.replace('.', '/')))
176
+ if '__init__' not in item and item.endswith('.py')
177
+ ]
178
+ result = []
179
+ for item in collected:
180
+ current = importlib.import_module(
181
+ feature + '.' + item,
182
+ feature,
183
+ )
184
+ result.append((item, current))
185
+ return result
186
+
187
+
188
+ def parse_feature(path: str):
189
+ # assert os.path.exists(path), str(path)
190
+ checkers = protoerror.parse_checkers(path)
191
+ checkers = [item.msgid for item in checkers]
192
+ solutions = protoerror.parse_solutions(path)
193
+ solutions = {
194
+ msgid(item.msgid): (item.title, item.description) for item in solutions
195
+ }
196
+ if not checkers and not solutions:
197
+ return None
198
+ return checkers, solutions
199
+
200
+
201
+ def msgid(item: str) -> int:
202
+ """\
203
+ >>> msgid('W1020')
204
+ 1020
205
+ >>> msgid('333')
206
+ 333
207
+ """
208
+ if item[0].isalpha():
209
+ return int(item[1:])
210
+ return int(item)
@@ -0,0 +1,58 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2021-2022 by Helmut Konrad Fahrendholz. All rights reserved.
5
+ # This file is property of Helmut Konrad Fahrendholz. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import collections
11
+ import re
12
+
13
+ Step = collections.namedtuple('Step', 'msgid title description')
14
+ Steps = list[Step]
15
+ Feature = collections.namedtuple('Feature', 'title solutions')
16
+ Report = collections.namedtuple('Report', 'name features')
17
+ Reports = list[Report]
18
+
19
+ TITLE = re.compile(r'^<<(?P<title>.+)>>\n', re.MULTILINE)
20
+ HEADLINE = re.compile(r'~(?P<id>\d{2,6}):(?P<headline>.+?)~\n')
21
+
22
+
23
+ def parses(content: str, name: str = '', active: set = None) -> Report:
24
+ splitted = TITLE.split(content)
25
+ splitted = [item for item in splitted if item]
26
+ features = []
27
+ for title, area in xsome(splitted, count=2):
28
+ solutions = parse_steps(area.strip(), active=active)
29
+ if not solutions:
30
+ continue
31
+ features.append(Feature(title=title, solutions=solutions))
32
+ result = Report(name=name, features=features)
33
+ return result
34
+
35
+
36
+ def parse_steps(content: str, active: set = None) -> Steps:
37
+ splitted = HEADLINE.split(content)
38
+ splitted = [item for item in splitted if item]
39
+ result = []
40
+ for msgid, headline, description in xsome(splitted, count=3):
41
+ msgid = int(msgid)
42
+ if active and msgid not in active:
43
+ # skip solution if solution is not selected
44
+ continue
45
+ description = description.strip()
46
+ result.append(Step(msgid, headline, description))
47
+ return result
48
+
49
+
50
+ def xsome(items, count: 1):
51
+ # TODO: MOVE TO UTILA
52
+ collected = []
53
+ for item in items:
54
+ collected.append(item)
55
+ if len(collected) != count:
56
+ continue
57
+ yield collected
58
+ collected = []
protoerror/simple.py ADDED
@@ -0,0 +1,85 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2022 by Helmut Konrad Fahrendholz. All rights reserved.
5
+ # This file is property of Helmut Konrad Fahrendholz. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import functools
11
+
12
+ import iamraw
13
+ import utilo
14
+
15
+ import protoerror
16
+
17
+ ResultType = tuple[str, str]
18
+ ResultDefault = ('user', 'developer') # pylint:disable=C0103
19
+
20
+
21
+ def run(
22
+ modulename,
23
+ driver=None,
24
+ location: iamraw.Location = None,
25
+ document: iamraw.DocInfo = None,
26
+ findings_merge: bool = True,
27
+ before_dump: callable = None,
28
+ ):
29
+ if not location:
30
+ location = protoerror.OVERVIEW
31
+ # create linter
32
+ linter = protoerror.from_modules(
33
+ modules=modulename,
34
+ document=document,
35
+ )
36
+ # run linter
37
+ result = linting(
38
+ linter=linter,
39
+ checkers=linter.checkers,
40
+ driver=driver,
41
+ location=location,
42
+ )
43
+ if findings_merge:
44
+ result = protoerror.merge_findings(result)
45
+ if before_dump:
46
+ result = utilo.pass_required(
47
+ before_dump,
48
+ result=result,
49
+ driver=driver,
50
+ linter=linter,
51
+ )
52
+ # dump results
53
+ user, developer = protoerror.dump_result(
54
+ result,
55
+ checkers=linter.checkers,
56
+ )
57
+ return user, developer
58
+
59
+
60
+ def linting(
61
+ linter: protoerror.Linter,
62
+ checkers: 'protoerror.Validators',
63
+ driver,
64
+ location: iamraw.Location,
65
+ ):
66
+ failure = []
67
+ for checker in checkers:
68
+ utilo.debug(checker.__name__)
69
+ call = functools.partial(
70
+ linter.add_finding,
71
+ msgid=checker.msgid,
72
+ location=location,
73
+ )
74
+ try:
75
+ checker(call, driver)
76
+ except Exception as error: # pylint:disable=broad-except
77
+ utilo.error(error)
78
+ failure.append(error)
79
+ else:
80
+ utilo.debug('done')
81
+ if failure:
82
+ first_failure = failure[0]
83
+ raise RuntimeError('linter step error') from first_failure
84
+ result = linter.result(unique=True)
85
+ return result