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

Commit 956ff32

Browse files
authored
Merge branch 'main' into 436-create-a-json-file-that-integrates-the-dictionaries-from-and
2 parents 6b5a4f2 + 95fb2ec commit 956ff32

File tree

9 files changed

+373
-3
lines changed

9 files changed

+373
-3
lines changed

package-parser/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ out/
3030

3131
# VSCode Settings
3232
.vscode/
33+
34+
# IntelliJ/Pycharm settings
35+
.idea/

package-parser/package-parser.iml

+1-1
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>
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from ._combine import write_json
2+
from ._generate_annotations import generate_annotations
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import json
2+
import re
3+
from typing import Dict, List, Tuple
4+
5+
6+
def generate_unused_annotations(in_file_path: str):
7+
"""
8+
Returns a Dict of unused functions or classes
9+
10+
:param in_file_path: JSON file that contains a list of unused functions or classes
11+
"""
12+
13+
with open(in_file_path, "r", encoding="UTF-8") as in_file:
14+
data = json.load(in_file)
15+
16+
unuseds: Dict[str, Dict[str, str]] = {}
17+
for name in data:
18+
formatted_name = format_name(name)
19+
unuseds[formatted_name] = {"target": formatted_name}
20+
21+
return unuseds
22+
23+
24+
def format_name(name: str):
25+
if name is None:
26+
return None
27+
28+
parts = re.split("\\.", name)
29+
newname = "sklearn/" + parts[0]
30+
31+
if len(parts) == 1:
32+
return newname
33+
34+
slash = False
35+
for part in parts[1:-1]:
36+
if not slash and re.match("^_{0,2}[A-Z]", part):
37+
slash = True
38+
if slash:
39+
newname += "/" + part
40+
else:
41+
newname += "." + part
42+
43+
newname += "/" + parts[-1]
44+
return newname
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import pytest
2+
from package_parser.commands.generate_annotations._generate_unused_annotations import (
3+
format_name,
4+
generate_unused_annotations,
5+
)
6+
7+
EXPECTED_VALUE = {
8+
"sklearn/sklearn.base/_BaseEstimator/__setstate__": {
9+
"target": "sklearn/sklearn.base/_BaseEstimator/__setstate__"
10+
},
11+
"sklearn/sklearn.base/is_regressor": {
12+
"target": "sklearn/sklearn.base/is_regressor"
13+
},
14+
"sklearn/sklearn.cluster._agglomerative/linkage_tree": {
15+
"target": "sklearn/sklearn.cluster._agglomerative/linkage_tree"
16+
},
17+
"sklearn/sklearn.cluster._kmeans/MiniBatchKMeans/init_size_": {
18+
"target": "sklearn/sklearn.cluster._kmeans/MiniBatchKMeans/init_size_"
19+
},
20+
}
21+
22+
23+
def test_format_underscores():
24+
assert (
25+
format_name("sklearn.cluster._kmeans._MiniBatchKMeans.random_state_")
26+
== "sklearn/sklearn.cluster._kmeans/_MiniBatchKMeans/random_state_"
27+
)
28+
29+
30+
def test_format_uppercase():
31+
assert (
32+
format_name("sklearn.cluster._kmeans.MiniBatchKMeans.random_state_")
33+
== "sklearn/sklearn.cluster._kmeans/MiniBatchKMeans/random_state_"
34+
)
35+
36+
37+
def test_format_normal():
38+
assert (
39+
format_name("sklearn.cluster._mean_shift.get_bin_seeds")
40+
== "sklearn/sklearn.cluster._mean_shift/get_bin_seeds"
41+
)
42+
43+
44+
def test_format_one_part():
45+
assert format_name("test") == "sklearn/test"
46+
47+
48+
def test_format_none():
49+
assert format_name(None) is None
50+
51+
52+
def test_format_empty():
53+
assert format_name("") == "sklearn/"
54+
55+
56+
def test_generate():
57+
assert (
58+
generate_unused_annotations(
59+
"tests/commands/generate_annotations/unused_functions_list.json"
60+
)
61+
== EXPECTED_VALUE
62+
)
63+
64+
65+
def test_generate_bad_path():
66+
with pytest.raises(FileNotFoundError):
67+
generate_unused_annotations("aaaaaaaaaaaAAAAAAAAAAAA")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
"sklearn.base._BaseEstimator.__setstate__",
3+
"sklearn.base.is_regressor",
4+
"sklearn.cluster._agglomerative.linkage_tree",
5+
"sklearn.cluster._kmeans.MiniBatchKMeans.init_size_"
6+
]

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

+2-2
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)