Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit b9031b5

Browse files
GideonKoenigGideonKoenig
and
GideonKoenig
authored
feat: Automatic generation of Required Annotations (#452)
* Code should be working - Still need to write a test * Add tests and prepare for merge with issue #441 * Still testing * get required test funktioniert jetzt * Test done and working * Satisfy Linter * style: apply automatic fixes of linters * Bug fix * Implemented the new AnnotationStore object Improved auxiliary functions Added ParameterInfo Class to communicate the return values of functions better Migrated said ParameterInfo and ParameterType classes to annotation_model.py Test is now working - all is good * Update package-parser.iml * Renamed function to better communicate it's function * Delete package-parser.iml * Satisfy Linter -> Fixed typing * style: apply automatic fixes of linters * Remove unnecessary import * Further seperate the replaceable math part of determining if a parameter should be optional or required for more clarity * Function namechange Co-authored-by: GideonKoenig <[email protected]> Co-authored-by: GideonKoenig <[email protected]>
1 parent 5fc20e3 commit b9031b5

File tree

5 files changed

+525
-31
lines changed

5 files changed

+525
-31
lines changed

package-parser/package_parser/commands/generate_annotations/generate_annotations.py

+90-29
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from package_parser.models.annotation_models import (
99
AnnotationStore,
1010
ConstantAnnotation,
11+
ParameterInfo,
12+
ParameterType,
13+
RequiredAnnotation,
1114
UnusedAnnotation,
1215
)
1316
from package_parser.utils import parent_qname
@@ -22,10 +25,9 @@ def generate_annotations(
2225
:param api_file: API file
2326
:param usages_file: UsageStore file
2427
:param output_file: Output file
25-
:return: None
2628
"""
2729
if api_file is None or usages_file is None or output_file is None:
28-
raise ValueError("api_file, usages_file, and output_file must be specified.")
30+
raise ValueError("Api_file, usages_file, and output_file must be specified.")
2931

3032
with api_file:
3133
api_json = json.load(api_file)
@@ -36,7 +38,11 @@ def generate_annotations(
3638
usages = UsageStore.from_json(usages_json)
3739

3840
annotations = AnnotationStore()
39-
annotation_functions = [__get_unused_annotations, __get_constant_annotations]
41+
annotation_functions = [
42+
__get_unused_annotations,
43+
__get_constant_annotations,
44+
__get_required_annotations,
45+
]
4046

4147
__generate_annotation_dict(api, usages, annotations, annotation_functions)
4248

@@ -60,29 +66,21 @@ def __get_constant_annotations(
6066
usages: UsageStore, api: API, annotations: AnnotationStore
6167
) -> None:
6268
"""
63-
Returns all parameters that are only ever assigned a single value.
69+
Collect all parameters that are only ever assigned a single value.
6470
:param usages: UsageStore object
6571
:param api: API object for usages
66-
:return: {"constant": dict[str, dict[str, str]]}
72+
:param annotations: AnnotationStore, that holds all annotations
6773
"""
68-
for parameter_qname in list(usages.parameter_usages.keys()):
69-
if len(usages.value_usages[parameter_qname].values()) == 0:
70-
continue
71-
72-
if len(usages.value_usages[parameter_qname].keys()) == 1:
73-
if usages.most_common_value(parameter_qname) is None:
74-
continue
75-
76-
target_name = __qname_to_target_name(api, parameter_qname)
77-
default_type, default_value = __get_default_type_from_value(
78-
str(usages.most_common_value(parameter_qname))
79-
)
74+
for qname in list(usages.parameter_usages.keys()):
75+
parameter_info = __get_parameter_info(qname, usages)
8076

77+
if parameter_info.type == ParameterType.Constant:
78+
formatted_name = __qname_to_target_name(api, qname)
8179
annotations.constant.append(
8280
ConstantAnnotation(
83-
target=target_name,
84-
defaultType=default_type,
85-
defaultValue=default_value,
81+
target=formatted_name,
82+
defaultValue=parameter_info.value,
83+
defaultType=parameter_info.value_type,
8684
)
8785
)
8886

@@ -91,10 +89,10 @@ def __get_unused_annotations(
9189
usages: UsageStore, api: API, annotations: AnnotationStore
9290
) -> None:
9391
"""
94-
Returns all parameters that are never used.
92+
Collect all parameters, functions and classes that are never used.
9593
:param usages: UsageStore object
9694
:param api: API object for usages
97-
:return: {"unused": dict[str, dict[str, str]]}
95+
:param annotations: AnnotationStore, that holds all annotations
9896
"""
9997
for parameter_name in list(api.parameters().keys()):
10098
if (
@@ -121,9 +119,31 @@ def __get_unused_annotations(
121119
annotations.unused.append(UnusedAnnotation(formatted_name))
122120

123121

122+
def __get_required_annotations(
123+
usages: UsageStore, api: API, annotations: AnnotationStore
124+
) -> None:
125+
"""
126+
Collects all parameters that are currently optional but should be required to be assign a value
127+
:param usages: Usage store
128+
:param api: Description of the API
129+
:param annotations: AnnotationStore, that holds all annotations
130+
"""
131+
parameters = api.parameters()
132+
optional_parameter = [
133+
(it, parameters[it])
134+
for it in parameters
135+
if parameters[it].default_value is not None
136+
]
137+
for qname, _ in optional_parameter:
138+
139+
if __get_parameter_info(qname, usages).type is ParameterType.Required:
140+
formatted_name = __qname_to_target_name(api, qname)
141+
annotations.requireds.append(RequiredAnnotation(formatted_name))
142+
143+
124144
def __qname_to_target_name(api: API, qname: str) -> str:
125145
"""
126-
Formats the given name to the wanted format. This method is to be removed as soon as the UsageStore is updated to
146+
Formats the given name to the output format. This method is to be removed as soon as the UsageStore is updated to
127147
use the new format.
128148
:param api: API object
129149
:param qname: Name pre-formatting
@@ -149,21 +169,17 @@ def __qname_to_target_name(api: API, qname: str) -> str:
149169
return package_name + module_name + class_name + function_name + parameter_name
150170

151171

152-
def __get_default_type_from_value(default_value: str) -> tuple[str, str]:
153-
default_value = str(default_value)[1:-1]
154-
172+
def __get_default_type_from_value(default_value: str) -> str:
155173
if default_value == "null":
156174
default_type = "none"
157175
elif default_value == "True" or default_value == "False":
158176
default_type = "boolean"
159177
elif default_value.isnumeric():
160178
default_type = "number"
161-
default_value = default_value
162179
else:
163180
default_type = "string"
164-
default_value = default_value
165181

166-
return default_type, default_value
182+
return default_type
167183

168184

169185
def _preprocess_usages(usages: UsageStore, api: API) -> None:
@@ -255,3 +271,48 @@ def __add_implicit_usages_of_default_value(usages: UsageStore, api: API) -> None
255271

256272
for location in locations_of_implicit_usages_of_default_value:
257273
usages.add_value_usage(parameter_qname, default_value, location)
274+
275+
276+
def __get_parameter_info(qname: str, usages: UsageStore) -> ParameterInfo:
277+
"""
278+
Returns a ParameterInfo object, that contains the type of the parameter, the value that is associated with it, and the values type
279+
:param qname: name of the parameter
280+
:param usages: UsageStore
281+
:return ParameterInfo
282+
"""
283+
values = [(it[0], len(it[1])) for it in usages.value_usages[qname].items()]
284+
285+
if len(values) == 0:
286+
return ParameterInfo(ParameterType.Unused)
287+
elif len(values) == 1:
288+
value = values[0][0]
289+
if value[0] == "'":
290+
value = value[1:-1]
291+
return ParameterInfo(
292+
ParameterType.Constant, value, __get_default_type_from_value(value)
293+
)
294+
295+
if __is_required(values):
296+
return ParameterInfo(ParameterType.Required)
297+
298+
value = max(values, key=lambda item: item[1])[0]
299+
if value[0] == "'":
300+
value = value[1:-1]
301+
return ParameterInfo(
302+
ParameterType.Optional, value, __get_default_type_from_value(value)
303+
)
304+
305+
306+
def __is_required(values: list[tuple[str, int]]) -> bool:
307+
"""
308+
This replaceable function determines how to differentiate between an optional and a required parameter
309+
:param values: List of all associated values and the amount they get used with
310+
:return True means the parameter should be required, False means it should be optional
311+
"""
312+
n = len(values)
313+
m = sum([count for value, count in values])
314+
315+
seconds_most_used_value_tupel, most_used_value_tupel = sorted(
316+
values, key=lambda tup: tup[1]
317+
)[-2:]
318+
return most_used_value_tupel[1] - seconds_most_used_value_tupel[1] <= m / n

package-parser/package_parser/models/annotation_models.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
from enum import Enum
23

34

45
@dataclasses.dataclass
@@ -103,3 +104,21 @@ def to_json(self) -> dict:
103104
annotation.target: annotation.to_json() for annotation in self.enums
104105
},
105106
}
107+
108+
109+
class ParameterType(Enum):
110+
Constant = 0
111+
Optional = 1
112+
Required = 2
113+
Unused = 3
114+
115+
116+
class ParameterInfo:
117+
type: ParameterType
118+
value: str
119+
value_type: str
120+
121+
def __init__(self, parameter_type, value="", value_type=""):
122+
self.type = parameter_type
123+
self.value = value
124+
self.value_type = value_type

package-parser/tests/commands/generate_annotations/test_generate_annotations.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from package_parser.commands.find_usages import UsageStore
77
from package_parser.commands.generate_annotations.generate_annotations import (
88
__get_constant_annotations,
9+
__get_required_annotations,
910
__get_unused_annotations,
1011
__qname_to_target_name,
1112
_preprocess_usages,
@@ -28,6 +29,7 @@
2829
},
2930
}
3031

32+
3133
CONSTANT_EXPECTED: dict[str, dict[str, str]] = {
3234
"test/test/commonly_used_global_function/unused_optional_parameter": {
3335
"defaultType": "string",
@@ -44,9 +46,25 @@
4446
"defaultValue": "blup",
4547
"target": "test/test/commonly_used_global_function/useless_required_parameter",
4648
},
49+
"test/test/commonly_used_global_required_and_optional_function/constant_parameter": {
50+
"defaultType": "string",
51+
"defaultValue": "bockwurst",
52+
"target": "test/test/commonly_used_global_required_and_optional_function/constant_parameter",
53+
},
54+
}
55+
56+
REQUIREDS_EXPECTED: dict[str, dict[str, str]] = {
57+
"test/test/commonly_used_global_required_and_optional_function/optional_that_should_be_required": {
58+
"target": "test/test/commonly_used_global_required_and_optional_function/optional_that_should_be_required"
59+
},
60+
"test/test/commonly_used_global_required_and_optional_function/commonly_used_barely_required": {
61+
"target": "test/test/commonly_used_global_required_and_optional_function/commonly_used_barely_required"
62+
},
63+
"test/test/commonly_used_global_function/useful_optional_parameter": {
64+
"target": "test/test/commonly_used_global_function/useful_optional_parameter"
65+
},
4766
}
4867

49-
REQUIREDS_EXPECTED: dict[str, dict[str, str]] = {}
5068
OPTIONALS_EXPECTED: dict[str, dict[str, str]] = {}
5169
BOUNDARIES_EXPECTED: dict[str, dict[str, str]] = {}
5270
ENUMS_EXPECTED: dict[str, dict[str, str]] = {}
@@ -123,6 +141,16 @@ def test_get_constant():
123141
} == CONSTANT_EXPECTED
124142

125143

144+
def test_get_required():
145+
usages, api, usages_file, api_file, usages_json_path, api_json_path = setup()
146+
annotations = AnnotationStore()
147+
_preprocess_usages(usages, api)
148+
__get_required_annotations(usages, api, annotations)
149+
assert {
150+
annotation.target: annotation.to_json() for annotation in annotations.requireds
151+
} == REQUIREDS_EXPECTED
152+
153+
126154
def test_generate():
127155
usages, api, usages_file, api_file, usages_json_path, api_json_path = setup()
128156
out_file_path = os.path.join(

package-parser/tests/data/api_data.json

+86-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"functions": [
1616
"test.unused_global_function",
1717
"test.rarely_used_global_function",
18-
"test.commonly_used_global_function"
18+
"test.commonly_used_global_function",
19+
"test.commonly_used_global_required_and_optional_function"
1920
]
2021
}
2122
],
@@ -156,6 +157,90 @@
156157
"description": "",
157158
"docstring": "",
158159
"source_code": ""
160+
},
161+
{
162+
"name": "commonly_used_global_required_and_optional_function",
163+
"unique_name": "commonly_used_global_required_and_optional_function",
164+
"qname": "test.commonly_used_global_required_and_optional_function",
165+
"unique_qname": "test.commonly_used_global_required_and_optional_function",
166+
"decorators": [],
167+
"parameters": [
168+
{
169+
"name": "optional_that_should_be_required",
170+
"default_value": "'brains'",
171+
"is_public": true,
172+
"assigned_by": "POSITION_OR_NAME",
173+
"docstring": {
174+
"type": "str",
175+
"description": ""
176+
}
177+
},
178+
{
179+
"name": "required_that_should_be_required",
180+
"default_value": null,
181+
"is_public": true,
182+
"assigned_by": "POSITION_OR_NAME",
183+
"docstring": {
184+
"type": "str",
185+
"description": ""
186+
}
187+
},
188+
{
189+
"name": "required_that_should_be_optional",
190+
"default_value": null,
191+
"is_public": true,
192+
"assigned_by": "POSITION_OR_NAME",
193+
"docstring": {
194+
"type": "str",
195+
"description": ""
196+
}
197+
},
198+
{
199+
"name": "optional_that_should_be_optional",
200+
"default_value": "'captain_morgan'",
201+
"is_public": true,
202+
"assigned_by": "POSITION_OR_NAME",
203+
"docstring": {
204+
"type": "str",
205+
"description": ""
206+
}
207+
},
208+
{
209+
"name": "commonly_used_almost_required",
210+
"default_value": "'marvel'",
211+
"is_public": true,
212+
"assigned_by": "POSITION_OR_NAME",
213+
"docstring": {
214+
"type": "str",
215+
"description": ""
216+
}
217+
},
218+
{
219+
"name": "commonly_used_barely_required",
220+
"default_value": "'otto'",
221+
"is_public": true,
222+
"assigned_by": "POSITION_OR_NAME",
223+
"docstring": {
224+
"type": "str",
225+
"description": ""
226+
}
227+
},
228+
{
229+
"name": "constant_parameter",
230+
"default_value": null,
231+
"is_public": true,
232+
"assigned_by": "POSITION_OR_NAME",
233+
"docstring": {
234+
"type": "str",
235+
"description": ""
236+
}
237+
}
238+
],
239+
"results": [],
240+
"is_public": true,
241+
"description": "",
242+
"docstring": "",
243+
"source_code": ""
159244
}
160245
]
161246
}

0 commit comments

Comments
 (0)