Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public variables #5

Merged
merged 8 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
224 changes: 219 additions & 5 deletions PySide6-stubs/QtWidgets.pyi

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions scripts/apply_public_variables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from typing import Dict, Optional, Union

import pathlib, json

import libcst
import libcst as cst
import libcst.matchers as matchers

JSON_INPUT_FNAME = pathlib.Path(__file__).parent / 'public-variables.json'

class TypingTransformer(cst.CSTTransformer):
"""TypingTransformer that visits classes and methods."""

def __init__(self, mod_name: str, d: Dict[str, str]) -> None:
super().__init__()
self.mod_name = mod_name
self.full_name_stack = [mod_name]
self.fqn_class_pub_var = d
self.visited_attributes = []


def visit_ClassDef(self, node: cst.ClassDef) -> Optional[bool]:
"""Put a class on top of the stack when visiting."""
self.full_name_stack.append( node.name.value )
return True


def leave_AnnAssign(self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign ) \
-> cst.AnnAssign:
fqn_class = '.'.join(self.full_name_stack)
if not fqn_class in self.fqn_class_pub_var:
return updated_node


attr_ann_type_dict = self.fqn_class_pub_var[fqn_class]
attr_name = original_node.target.value
self.visited_attributes.append(attr_name)
if attr_name not in attr_ann_type_dict:
# we have no info about this attribute
return updated_node

ann_value = original_node.annotation.annotation.value
if ann_value == attr_ann_type_dict[attr_name]:
# we agree with annotation
return updated_node

# let's update the annotation
print(f'Fixing {fqn_class}.{attr_name} from annotation "{ann_value}" to "{attr_ann_type_dict[attr_name]}"')
return updated_node.with_changes(
annotation=updated_node.annotation.with_changes(
annotation=updated_node.annotation.annotation.with_changes(
value=attr_ann_type_dict[attr_name]
) ) )

def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) \
-> Union[cst.BaseStatement, cst.FlattenSentinel[cst.BaseStatement], cst.RemovalSentinel, ]:
fqn_class = '.'.join(self.full_name_stack)
self.full_name_stack.pop()

# no variables to adjust
if not fqn_class in self.fqn_class_pub_var:
return updated_node

attr_ann_type_dict = self.fqn_class_pub_var[fqn_class]

nonAnnotatedAttributes = set()

for class_content in updated_node.body.body:
if matchers.matches(class_content, matchers.SimpleStatementLine(body=[matchers.Assign()])):
nonAnnotatedAttributes.add(class_content.body[0].targets[0].target.value)


missingPubVar = sorted(set(attr_ann_type_dict.keys()) - nonAnnotatedAttributes - set(self.visited_attributes))

if not missingPubVar:
# all public variables are already there
return updated_node

pre_body = []
for pub_var in missingPubVar:
print(f'Class {fqn_class}: adding public variable {pub_var}: {attr_ann_type_dict[pub_var]}')
pre_body.append(libcst.parse_statement(f'{pub_var}: {attr_ann_type_dict[pub_var]}'))
pre_body.insert(0, libcst.EmptyLine(indent=False, newline=libcst.Newline()))
pre_body.append(libcst.EmptyLine(indent=False, newline=libcst.Newline()))

if isinstance(updated_node.body, libcst.SimpleStatementSuite):
# the class is a single ellipsis, we need to create a full indented body
return updated_node.with_changes(
body=libcst.IndentedBlock(body=pre_body)
)

# regular class
return updated_node.with_changes(
body=updated_node.body.with_changes(
body=tuple(pre_body) + updated_node.body.body
)
)


def apply_public_variables_for_module(module_path: str, d: Dict[str, str]) -> None:
if module_path.name.startswith('_'):
return

module_name = module_path.stem

print('Fixing ', module_name)
with open(module_path, "r", encoding="utf-8") as fhandle:
stub_tree = cst.parse_module(fhandle.read())

transformer = TypingTransformer(module_name, d)
modified_tree = stub_tree.visit(transformer)

with open(module_path, "w", encoding="utf-8") as fhandle:
fhandle.write(modified_tree.code)


def main():
with open(JSON_INPUT_FNAME, 'r') as f:
d = json.load(f)

for fpath in (pathlib.Path(__file__).parent.parent / 'PySide6-stubs').glob('QtWidgets.pyi'):
apply_public_variables_for_module(fpath, d)



if __name__ == '__main__':
# auto_test()
main()
80 changes: 80 additions & 0 deletions scripts/collect_public_variables..py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Dict, Type

import importlib, json, pathlib

from PySide6.QtWidgets import QApplication

JSON_OUTPUT_FNAME = pathlib.Path(__file__).parent / 'public-variables.json'

def collect_public_variables_for_module(module_name: str, d: Dict[str, str]) -> None:
'''Load module, inspect all attribute types and fill dict with information'''
if module_name.startswith('_'):
return

print('Processing %s' % module_name)
try:
m = importlib.import_module(f'PySide6.{module_name}')
except ModuleNotFoundError:
print('... Module not available!')
# platform-specific modules can not be imported for example on other platforms
return

for class_name, class_type in m.__dict__.items():
if class_name.startswith('_'):
continue

collect_public_variables_for_class(f'{module_name}.{class_name}', class_type, d)

def collect_public_variables_for_class(class_fqn: str, class_type: Type, d: Dict[str, str]) -> None:
# we only care about classes
try:
class_members = class_type.__dict__.items()
except AttributeError:
# this is not a class
return

instance = None
for class_attr_name, class_attr_value in class_members:
if class_attr_name.startswith('_'):
continue

if class_attr_value.__class__.__name__ == 'getset_descriptor':

# create the instance on-demand
if instance is None:
try:
instance = class_type()
except Exception:
# we can not work without the instance
return

attr_of_instance = getattr(instance, class_attr_name)
if attr_of_instance == None:
continue
typename = attr_of_instance.__class__.__name__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do something like this to extend the types with modules:

typename = attr_of_instance.__class__.__name__
modulename = attr_of_instance.__class__.__module__
if modulename != "builtins":
    typename = f'{modulename}.{typename}'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I did not know about that one. It will solve several cases, but not nested class. So, all enums can not be typed for example, because they always lie within another class.

Still, it's better than nothing, I'll have an attempt at it.

Copy link
Contributor

@boldar99 boldar99 Sep 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe using __class__.__qualname__ could solve this problem?


try:
pub_var_dict = d[class_fqn]
except KeyError:
pub_var_dict = {}
d[class_fqn] = pub_var_dict
pub_var_dict[class_attr_name] = typename
else:
# try if it is a subclass
collect_public_variables_for_class(f'{class_fqn}.{class_attr_name}', class_attr_value, d)


def main():
application = QApplication(['-platform', 'minimal']) # needed for instancing QWidgets
d = {}
for fpath in (pathlib.Path(__file__).parent.parent / 'PySide6-stubs').glob('*.pyi'):
module_name = fpath.stem
collect_public_variables_for_module(module_name, d)

with open(JSON_OUTPUT_FNAME, 'w') as f:
json.dump(d, f, indent=4)



if __name__ == '__main__':
main()
Loading