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

feat: 434 generate constant annotations #440

Merged
merged 41 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fefda4b
feat: Started issue #434 - not working yet!
Masara Apr 8, 2022
5e21e98
feat: A bit more code cleanup - still not working tho!
Masara Apr 12, 2022
6ac7e3d
added function: determine_constant_parameter()
Apr 15, 2022
d87bcde
fixed init file
Apr 15, 2022
514555d
Prepared the test file for the incoming testdata and cleaned some cod…
Apr 17, 2022
c3fc405
test: data for unused and constant annotations (#439)
lars-reimann Apr 17, 2022
075fa2b
Finished the test file - has to be run on a different machine, since …
Apr 17, 2022
25c3f5f
Merge branch 'main' into #434_generate_constant_annotations
GideonKoenig Apr 17, 2022
781dc28
Removed unnecessary function call
Apr 17, 2022
ae8f762
Merge branch '#434_generate_constant_annotations' of github.com:lars-…
Apr 17, 2022
61edf3c
Fixed spelling error
Apr 17, 2022
725de51
Fixed more issues
Apr 17, 2022
4a466eb
Restructured test, since the preprocessing steps are skipped otherwise.
Apr 17, 2022
3a134d2
More issues fixed
Apr 17, 2022
3ddb04e
style: apply automatic fixes of linters
GideonKoenig Apr 17, 2022
90d057f
Fixed typos
Apr 17, 2022
768b1ed
Merge branch '#434_generate_constant_annotations' of github.com:lars-…
Apr 17, 2022
32d9ffb
Fixed return type of __determine_constant_parameters
Apr 17, 2022
9a07c26
Added debug message when adding default values
Apr 17, 2022
bf2b58d
style: apply automatic fixes of linters
GideonKoenig Apr 17, 2022
b4ec6b4
Fixed test data - Since the usage entries were all identical, the pre…
Apr 17, 2022
3ce3539
Merge branch '#434_generate_constant_annotations' of github.com:lars-…
Apr 17, 2022
3b5193d
Adjust output format of values
Apr 17, 2022
3eb4b16
style: apply automatic fixes of linters
GideonKoenig Apr 17, 2022
bae4f6c
Changed output format - proper formation should be part of the gather…
Apr 17, 2022
5cfb180
Merge branch '#434_generate_constant_annotations' of github.com:lars-…
Apr 17, 2022
f771ed8
Clean merge comments
Apr 17, 2022
6d0ba51
Improved documentation
Apr 20, 2022
8b1b8b4
Added the processing of the DefaultValue so the dictinary that is ret…
Apr 20, 2022
d98281c
Update package-parser.iml
GideonKoenig Apr 20, 2022
6516d19
Update usage_data.json
GideonKoenig Apr 20, 2022
f0a33d1
Update package-parser.iml
GideonKoenig Apr 20, 2022
c30c8aa
Update package-parser.iml
GideonKoenig Apr 20, 2022
503aa17
Fixed bug in output formatting
Apr 20, 2022
ac37f23
style: apply automatic fixes of linters
GideonKoenig Apr 20, 2022
1d2092e
Seperate formatting steps into functions, since they are gonna be nee…
Apr 20, 2022
d2c19ad
Merge branch '#434_generate_constant_annotations' of github.com:lars-…
Apr 20, 2022
c1a0874
Fixed result typing
Apr 20, 2022
34c4113
Fixed typing issues
Apr 20, 2022
6a60f33
style: apply automatic fixes of linters
GideonKoenig Apr 20, 2022
552839c
Merge branch 'main' into #434_generate_constant_annotations
GideonKoenig Apr 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-parser/package-parser.iml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
<orderEntry type="jdk" jdkName="Poetry (api-editor)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._generate_annotations import generate_annotations
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import json
from io import TextIOWrapper
from pathlib import Path
from typing import Any

from package_parser.commands.find_usages import (
ClassUsage,
FunctionUsage,
UsageStore,
ValueUsage,
)
from package_parser.commands.get_api import API
from package_parser.utils import parent_qname


def generate_annotations(
api_file: TextIOWrapper, usages_file: TextIOWrapper, out_dir: Path
):
with api_file:
api_json = json.load(api_file)
api = API.from_json(api_json)

with usages_file:
usages_json = json.load(usages_file)
usages = UsageStore.from_json(usages_json)

# out_dir.mkdir(parents=True, exist_ok=True)
# base_file_name = api_file.name.replace("__api.json", "")

__preprocess_usages(usages, api)
constant_parameters = __find_constant_parameters(usages, api)
return constant_parameters


def __preprocess_usages(usages: UsageStore, api: API) -> None:
__remove_internal_usages(usages, api)
__add_unused_api_elements(usages, api)
__add_implicit_usages_of_default_value(usages, api)


def __remove_internal_usages(usages: UsageStore, api: API) -> None:
"""
Removes usages of internal parts of the API. It might incorrectly remove some calls to methods that are inherited
from internal classes into a public class but these are just fit/predict/etc., i.e. something we want to keep
unchanged anyway.

:param usages: Usage store
:param api: Description of the API
"""

# Internal classes
for class_qname in list(usages.class_usages.keys()):
if not api.is_public_class(class_qname):
print(f"Removing usages of internal class {class_qname}")
usages.remove_class(class_qname)

# Internal functions
for function_qname in list(usages.function_usages.keys()):
if not api.is_public_function(function_qname):
print(f"Removing usages of internal function {function_qname}")
usages.remove_function(function_qname)

# Internal parameters
parameter_qnames = set(api.parameters().keys())

for parameter_qname in list(usages.parameter_usages.keys()):
function_qname = parent_qname(parameter_qname)
if parameter_qname not in parameter_qnames or not api.is_public_function(
function_qname
):
print(f"Removing usages of internal parameter {parameter_qname}")
usages.remove_parameter(parameter_qname)


def __add_unused_api_elements(usages: UsageStore, api: API) -> None:
"""
Adds unused API elements to the UsageStore. When a class, function or parameter is not used, it is not content of
the UsageStore, so we need to add it.

:param usages: Usage store
:param api: Description of the API
"""

# Public classes
for class_qname in api.classes:
if api.is_public_class(class_qname):
usages.init_class(class_qname)

# Public functions
for function in api.functions.values():
if api.is_public_function(function.qname):
usages.init_function(function.qname)

# "Public" parameters
for parameter in function.parameters:
parameter_qname = f"{function.qname}.{parameter.name}"
usages.init_parameter(parameter_qname)
usages.init_value(parameter_qname)


def __add_implicit_usages_of_default_value(usages: UsageStore, api: API) -> None:
"""
Adds the implicit usages of a parameters default value. When a function is called and a parameter is used with its
default value, that usage of a value is not part of the UsageStore, so we need to add it.

:param usages: Usage store
:param api: Description of the API
"""

for parameter_qname, parameter_usage_list in list(usages.parameter_usages.items()):
default_value = api.get_default_value(parameter_qname)
if default_value is None:
continue

function_qname = parent_qname(parameter_qname)
function_usage_list = usages.function_usages[function_qname]

locations_of_implicit_usages_of_default_value = set(
[it.location for it in function_usage_list]
) - set([it.location for it in parameter_usage_list])

for location in locations_of_implicit_usages_of_default_value:
usages.add_value_usage(parameter_qname, default_value, location)


def __find_constant_parameters(
usages: UsageStore, api: API
) -> dict[str, dict[str, str]]:
"""
Returns all parameters that are only ever assigned a single value.

:param usages: Usage store
"""

result = {}

for parameter_qname in list(usages.parameter_usages.keys()):

if len(usages.value_usages[parameter_qname].values()) == 0:
continue

if len(usages.value_usages[parameter_qname].keys()) == 1:
target_name = __qname_to_target_name(api, parameter_qname)
default_type, default_value = __get_default_type_from_value(
str(usages.most_common_value(parameter_qname))
)
print(target_name)
result[target_name] = {
"target": target_name,
"defaultType": default_type,
"defaultValue": default_value,
}

print(json.dumps(result))
return result


def __qname_to_target_name(api: API, qname: str) -> str:
target_elements = qname.split(".")

package_name = api.package
module_name = class_name = function_name = parameter_name = ""

if ".".join(target_elements) in api.parameters().keys():
parameter_name = "/" + target_elements.pop()
if ".".join(target_elements) in api.functions.keys():
function_name = f"/{target_elements.pop()}"
if ".".join(target_elements) in api.classes.keys():
class_name = f"/{target_elements.pop()}"
if ".".join(target_elements) in api.modules.keys():
module_name = "/" + ".".join(target_elements)

return package_name + module_name + class_name + function_name + parameter_name


def __get_default_type_from_value(default_value: str) -> tuple[str, str]:
default_value = str(default_value)[1:-1]

if default_value == "null":
default_type = "none"
elif default_value == "True" or default_value == "False":
default_type = "boolean"
elif default_value.isnumeric():
default_type = "number"
default_value = default_value
else:
default_type = "string"
default_value = default_value

return default_type, default_value
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import json
import os

import pytest
from package_parser.commands.find_usages._model import UsageStore
from package_parser.commands.generate_annotations._generate_annotations import (
generate_annotations,
)

# Expected output:
# @Unused annotations should be created for the following declarations:
#
# test.Unused_Class
# test.unused_global_function
# test.Commonly_Used_Class.unused_method
#
# @Constant annotations should be created for the following parameters:
#
# test.commonly_used_global_function.useless_required_parameter (with value "'blup'")
# test.commonly_used_global_function.unused_optional_parameter (with value "'bla'", i.e. the default value)
# test.commonly_used_global_function.useless_optional_parameter (with value "'bla'")


def test_determination_of_constant_parameters():

expected = {
"test/test/commonly_used_global_function/useless_required_parameter": {
"target": "test/test/commonly_used_global_function/useless_required_parameter",
"defaultType": "string",
"defaultValue": "blup",
},
"test/test/commonly_used_global_function/unused_optional_parameter": {
"target": "test/test/commonly_used_global_function/unused_optional_parameter",
"defaultType": "string",
"defaultValue": "bla",
},
"test/test/commonly_used_global_function/useless_optional_parameter": {
"target": "test/test/commonly_used_global_function/useless_optional_parameter",
"defaultType": "string",
"defaultValue": "bla",
},
}

api_json_path = os.path.join(
os.getcwd(), "tests", "data", "constant", "api_data.json"
)
usages_json_path = os.path.join(
os.getcwd(), "tests", "data", "constant", "usage_data.json"
)

api_file = open(api_json_path)
usages_file = open(usages_json_path)

constant_parameters = generate_annotations(api_file, usages_file, "/.")

api_file.close()
usages_file.close()

assert constant_parameters == expected
4 changes: 2 additions & 2 deletions package-parser/tests/data/constant/usage_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
},
{
"file": "test.py",
"line": 1,
"column": 1
"line": 2,
"column": 2
}
]
},
Expand Down