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

Commit 95fb2ec

Browse files
GideonKoenigMasaraGideonKoeniglars-reimann
authored
feat: 434 generate constant annotations (#440)
* feat: Started issue #434 - not working yet! * feat: A bit more code cleanup - still not working tho! * added function: determine_constant_parameter() cleaned and refactored _generate_annotations module still missing test data - waiting for a response from Lars * fixed init file * Prepared the test file for the incoming testdata and cleaned some code in the determine_constant_parameters() function. * test: data for unused and constant annotations (#439) * test: api data for unused annotations * test: usage data for unused annotations * test: api data for constant annotations * test: usage data for constant annotations * refactor: rename files to prevent mapping to incorrect schema * Finished the test file - has to be run on a different machine, since tests dont seem to work for me. * Removed unnecessary function call * Fixed spelling error * Fixed more issues * Restructured test, since the preprocessing steps are skipped otherwise. * More issues fixed * style: apply automatic fixes of linters * Fixed typos * Fixed return type of __determine_constant_parameters * Added debug message when adding default values * style: apply automatic fixes of linters * Fixed test data - Since the usage entries were all identical, the preprocessing didnt work properly. * Adjust output format of values * style: apply automatic fixes of linters * Changed output format - proper formation should be part of the gathering function * Clean merge comments * Improved documentation * Added the processing of the DefaultValue so the dictinary that is return is already matching the desired format. * Update package-parser.iml * Update usage_data.json * Update package-parser.iml * Update package-parser.iml * Fixed bug in output formatting * style: apply automatic fixes of linters * Seperate formatting steps into functions, since they are gonna be needed in the future * Fixed result typing * Fixed typing issues * style: apply automatic fixes of linters Co-authored-by: Arsam Islami <[email protected]> Co-authored-by: GideonKoenig <[email protected]> Co-authored-by: Lars Reimann <[email protected]> Co-authored-by: GideonKoenig <[email protected]>
1 parent f6fc3fa commit 95fb2ec

File tree

5 files changed

+253
-3
lines changed

5 files changed

+253
-3
lines changed

package-parser/package-parser.iml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
<orderEntry type="jdk" jdkName="Poetry (api-editor)" jdkType="Python SDK" />
1212
<orderEntry type="sourceFolder" forTests="false" />
1313
</component>
14-
</module>
14+
</module>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._generate_annotations import generate_annotations
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import json
2+
from io import TextIOWrapper
3+
from pathlib import Path
4+
from typing import Any
5+
6+
from package_parser.commands.find_usages import (
7+
ClassUsage,
8+
FunctionUsage,
9+
UsageStore,
10+
ValueUsage,
11+
)
12+
from package_parser.commands.get_api import API
13+
from package_parser.utils import parent_qname
14+
15+
16+
def generate_annotations(
17+
api_file: TextIOWrapper, usages_file: TextIOWrapper, out_dir: Path
18+
):
19+
with api_file:
20+
api_json = json.load(api_file)
21+
api = API.from_json(api_json)
22+
23+
with usages_file:
24+
usages_json = json.load(usages_file)
25+
usages = UsageStore.from_json(usages_json)
26+
27+
# out_dir.mkdir(parents=True, exist_ok=True)
28+
# base_file_name = api_file.name.replace("__api.json", "")
29+
30+
__preprocess_usages(usages, api)
31+
constant_parameters = __find_constant_parameters(usages, api)
32+
return constant_parameters
33+
34+
35+
def __preprocess_usages(usages: UsageStore, api: API) -> None:
36+
__remove_internal_usages(usages, api)
37+
__add_unused_api_elements(usages, api)
38+
__add_implicit_usages_of_default_value(usages, api)
39+
40+
41+
def __remove_internal_usages(usages: UsageStore, api: API) -> None:
42+
"""
43+
Removes usages of internal parts of the API. It might incorrectly remove some calls to methods that are inherited
44+
from internal classes into a public class but these are just fit/predict/etc., i.e. something we want to keep
45+
unchanged anyway.
46+
47+
:param usages: Usage store
48+
:param api: Description of the API
49+
"""
50+
51+
# Internal classes
52+
for class_qname in list(usages.class_usages.keys()):
53+
if not api.is_public_class(class_qname):
54+
print(f"Removing usages of internal class {class_qname}")
55+
usages.remove_class(class_qname)
56+
57+
# Internal functions
58+
for function_qname in list(usages.function_usages.keys()):
59+
if not api.is_public_function(function_qname):
60+
print(f"Removing usages of internal function {function_qname}")
61+
usages.remove_function(function_qname)
62+
63+
# Internal parameters
64+
parameter_qnames = set(api.parameters().keys())
65+
66+
for parameter_qname in list(usages.parameter_usages.keys()):
67+
function_qname = parent_qname(parameter_qname)
68+
if parameter_qname not in parameter_qnames or not api.is_public_function(
69+
function_qname
70+
):
71+
print(f"Removing usages of internal parameter {parameter_qname}")
72+
usages.remove_parameter(parameter_qname)
73+
74+
75+
def __add_unused_api_elements(usages: UsageStore, api: API) -> None:
76+
"""
77+
Adds unused API elements to the UsageStore. When a class, function or parameter is not used, it is not content of
78+
the UsageStore, so we need to add it.
79+
80+
:param usages: Usage store
81+
:param api: Description of the API
82+
"""
83+
84+
# Public classes
85+
for class_qname in api.classes:
86+
if api.is_public_class(class_qname):
87+
usages.init_class(class_qname)
88+
89+
# Public functions
90+
for function in api.functions.values():
91+
if api.is_public_function(function.qname):
92+
usages.init_function(function.qname)
93+
94+
# "Public" parameters
95+
for parameter in function.parameters:
96+
parameter_qname = f"{function.qname}.{parameter.name}"
97+
usages.init_parameter(parameter_qname)
98+
usages.init_value(parameter_qname)
99+
100+
101+
def __add_implicit_usages_of_default_value(usages: UsageStore, api: API) -> None:
102+
"""
103+
Adds the implicit usages of a parameters default value. When a function is called and a parameter is used with its
104+
default value, that usage of a value is not part of the UsageStore, so we need to add it.
105+
106+
:param usages: Usage store
107+
:param api: Description of the API
108+
"""
109+
110+
for parameter_qname, parameter_usage_list in list(usages.parameter_usages.items()):
111+
default_value = api.get_default_value(parameter_qname)
112+
if default_value is None:
113+
continue
114+
115+
function_qname = parent_qname(parameter_qname)
116+
function_usage_list = usages.function_usages[function_qname]
117+
118+
locations_of_implicit_usages_of_default_value = set(
119+
[it.location for it in function_usage_list]
120+
) - set([it.location for it in parameter_usage_list])
121+
122+
for location in locations_of_implicit_usages_of_default_value:
123+
usages.add_value_usage(parameter_qname, default_value, location)
124+
125+
126+
def __find_constant_parameters(
127+
usages: UsageStore, api: API
128+
) -> dict[str, dict[str, str]]:
129+
"""
130+
Returns all parameters that are only ever assigned a single value.
131+
132+
:param usages: Usage store
133+
"""
134+
135+
result = {}
136+
137+
for parameter_qname in list(usages.parameter_usages.keys()):
138+
139+
if len(usages.value_usages[parameter_qname].values()) == 0:
140+
continue
141+
142+
if len(usages.value_usages[parameter_qname].keys()) == 1:
143+
target_name = __qname_to_target_name(api, parameter_qname)
144+
default_type, default_value = __get_default_type_from_value(
145+
str(usages.most_common_value(parameter_qname))
146+
)
147+
print(target_name)
148+
result[target_name] = {
149+
"target": target_name,
150+
"defaultType": default_type,
151+
"defaultValue": default_value,
152+
}
153+
154+
print(json.dumps(result))
155+
return result
156+
157+
158+
def __qname_to_target_name(api: API, qname: str) -> str:
159+
target_elements = qname.split(".")
160+
161+
package_name = api.package
162+
module_name = class_name = function_name = parameter_name = ""
163+
164+
if ".".join(target_elements) in api.parameters().keys():
165+
parameter_name = "/" + target_elements.pop()
166+
if ".".join(target_elements) in api.functions.keys():
167+
function_name = f"/{target_elements.pop()}"
168+
if ".".join(target_elements) in api.classes.keys():
169+
class_name = f"/{target_elements.pop()}"
170+
if ".".join(target_elements) in api.modules.keys():
171+
module_name = "/" + ".".join(target_elements)
172+
173+
return package_name + module_name + class_name + function_name + parameter_name
174+
175+
176+
def __get_default_type_from_value(default_value: str) -> tuple[str, str]:
177+
default_value = str(default_value)[1:-1]
178+
179+
if default_value == "null":
180+
default_type = "none"
181+
elif default_value == "True" or default_value == "False":
182+
default_type = "boolean"
183+
elif default_value.isnumeric():
184+
default_type = "number"
185+
default_value = default_value
186+
else:
187+
default_type = "string"
188+
default_value = default_value
189+
190+
return default_type, default_value
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
import os
3+
4+
import pytest
5+
from package_parser.commands.find_usages._model import UsageStore
6+
from package_parser.commands.generate_annotations._generate_annotations import (
7+
generate_annotations,
8+
)
9+
10+
# Expected output:
11+
# @Unused annotations should be created for the following declarations:
12+
#
13+
# test.Unused_Class
14+
# test.unused_global_function
15+
# test.Commonly_Used_Class.unused_method
16+
#
17+
# @Constant annotations should be created for the following parameters:
18+
#
19+
# test.commonly_used_global_function.useless_required_parameter (with value "'blup'")
20+
# test.commonly_used_global_function.unused_optional_parameter (with value "'bla'", i.e. the default value)
21+
# test.commonly_used_global_function.useless_optional_parameter (with value "'bla'")
22+
23+
24+
def test_determination_of_constant_parameters():
25+
26+
expected = {
27+
"test/test/commonly_used_global_function/useless_required_parameter": {
28+
"target": "test/test/commonly_used_global_function/useless_required_parameter",
29+
"defaultType": "string",
30+
"defaultValue": "blup",
31+
},
32+
"test/test/commonly_used_global_function/unused_optional_parameter": {
33+
"target": "test/test/commonly_used_global_function/unused_optional_parameter",
34+
"defaultType": "string",
35+
"defaultValue": "bla",
36+
},
37+
"test/test/commonly_used_global_function/useless_optional_parameter": {
38+
"target": "test/test/commonly_used_global_function/useless_optional_parameter",
39+
"defaultType": "string",
40+
"defaultValue": "bla",
41+
},
42+
}
43+
44+
api_json_path = os.path.join(
45+
os.getcwd(), "tests", "data", "constant", "api_data.json"
46+
)
47+
usages_json_path = os.path.join(
48+
os.getcwd(), "tests", "data", "constant", "usage_data.json"
49+
)
50+
51+
api_file = open(api_json_path)
52+
usages_file = open(usages_json_path)
53+
54+
constant_parameters = generate_annotations(api_file, usages_file, "/.")
55+
56+
api_file.close()
57+
usages_file.close()
58+
59+
assert constant_parameters == expected

package-parser/tests/data/constant/usage_data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
},
2626
{
2727
"file": "test.py",
28-
"line": 1,
29-
"column": 1
28+
"line": 2,
29+
"column": 2
3030
}
3131
]
3232
},

0 commit comments

Comments
 (0)