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

Support traversal of TypeAlias for Python 3.12 #115

Merged
merged 4 commits into from
Apr 19, 2024

Conversation

ericbn
Copy link
Contributor

@ericbn ericbn commented Apr 10, 2024

Also update tox and GitHub Actions to use Python 3.12 by default.

Also update tox and GitHub Actions to use Python 3.12 by default.
@coveralls
Copy link

coveralls commented Apr 12, 2024

Pull Request Test Coverage Report for Build 8745231098

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 82 of 83 (98.8%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.002%) to 98.261%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/ssort/_parsing.py 8 9 88.89%
Totals Coverage Status
Change from base Build 7702612545: 0.002%
Covered Lines: 1356
Relevant Lines: 1380

💛 - Coveralls

@ericbn ericbn force-pushed the add-type-alias-support branch from c1c52f5 to 6e765f4 Compare April 13, 2024 02:31
Copy link
Contributor Author

@ericbn ericbn left a comment

Choose a reason for hiding this comment

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

I've added a commit with the proposed fixes.

src/ssort/_parsing.py Show resolved Hide resolved
if sys.version_info >= (3, 12):
for type_param in node.type_params:
yield from get_requirements(type_param)
scope.update(type_param.name for type_param in node.type_params) # type: ignore[attr-defined]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added the # type: ignore[attr-defined] here an in a couple of lines below because mypy complained that "type_param" has no attribute "name". But type_param is TypeVar | ParamSpec | TypeVarTuple and all the latter have a name.

Comment on lines 227 to 228
if not requirement.deferred:
requirement = dataclasses.replace(requirement, deferred=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've copied the two lines above from the similar code in _get_requirements_for_function_def. I don't know what a referred requirement is or if this code is needed here.

Copy link
Owner

Choose a reason for hiding this comment

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

This is good. Might be worth factoring out a _get_scope_from_type_params function.

Copy link
Owner

Choose a reason for hiding this comment

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

A deferred requirement is one that is captured by a statement but not used immediately.

For example here, a has an immediate requirement on b:

a = b

and here the requirement is deferred:

def a():
    return b

@type_parameter_syntax
def test_type_var_bindings():
node = _parse("type RecursiveList[T] = T | list[RecursiveList[T]]")
assert list(get_bindings(node)) == ["RecursiveList"]
Copy link
Owner

Choose a reason for hiding this comment

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

I'm surprised that this all just worked without any changes to _bindings.py. Nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. “Type aliases can refer to themselves” (as defined in the PEP) works because _get_requirements_for_type_alias does scope.add(node.name.id).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only new binding introduced here is for TypeAliases, which have a Name and is resolved by the existing _get_bindings_for_name function. Type parameters, which are TypeVar | ParamSpec | TypeVarTuple, are treated for TypeAlias | FunctionDef | AsyncFunctionDef | ClassDef as arguments are treated for FunctionDef | AsyncFunctionDef | Lambda: they are added into the current scope for their usages to be resolved.

@ericbn ericbn force-pushed the add-type-alias-support branch from 43c277b to d062f04 Compare April 14, 2024 02:42
Copy link
Contributor Author

@ericbn ericbn left a comment

Choose a reason for hiding this comment

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

Thank you for the review again. I've added a new commit with the latest suggestion, plus extracted a couple more functions.

if token.string == ")":
depth -= 1

def _optional_nested_brackets(open: str, close: str):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've extracted the repeated code here into _optional_nested_brackets.

Copy link
Owner

Choose a reason for hiding this comment

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

Would you mind hoisting it out of split_class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm thinking of reverting this refactoring of _optional_nested_brackets and leaving a more complete refactoring of split_class for a latter PR. Agree?

Copy link
Owner

Choose a reason for hiding this comment

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

Curious to see it. Go ahead.

@@ -32,6 +33,13 @@ def get_requirements(node: ast.AST) -> Iterable[Requirement]:
yield from get_requirements(child)


def _get_requirements_from_nodes(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've hopefully made the code a little more simple with a _get_requirements_from_nodes function.

Comment on lines +66 to +69
scope = _get_scope_from_type_params(node.type_params)
for requirement in _get_requirements_from_nodes(node.type_params):
if requirement.name not in scope:
yield requirement
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's now using the scope from type params to filter their own requirements. This adds the compliance to:

Type parameters are visible to other type parameters declared elsewhere in the list. This allows type parameters to use other type parameters within their definition.

for the PEP. It's not considering any order for sake of simplicity, leaving the more strict rules to the compiler or type checkers.

Copy link
Owner

Choose a reason for hiding this comment

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

Yup, had read that as scope being updated from left to right but I think you are right.

Also extract _get_requirements_from_nodes, _get_scope_from_type_params
and _optional_nested_brackets functions.
@ericbn ericbn force-pushed the add-type-alias-support branch from d062f04 to ac899c9 Compare April 14, 2024 02:59

scope = set(CLASS_BUILTINS)
for requirement in _get_requirements_from_nodes(node.bases):
if requirement.name not in scope:
Copy link
Owner

Choose a reason for hiding this comment

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

Good spot.

@bwhmather
Copy link
Owner

I think this is just about done. Should have time this evening to do a full read through then will merge and bake a release.

Copy link
Owner

@bwhmather bwhmather left a comment

Choose a reason for hiding this comment

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

No more comments from me. Looks good. Hoist or revert _optional_nested_brackets and will merge when next at the keyboard.

@ericbn
Copy link
Contributor Author

ericbn commented Apr 18, 2024

Cool! I've reverted _optional_nested_brackets. The reason I didn't want to hoist it was because I thought it would have a weird signature, as if depends on both token and tokens, and mutates token:

    token = _optional_nested_brackets(token, tokens, "[", "]")
    token = _optional_nested_brackets(token, tokens, "(", ")")

Instead, I think this would be better refactored as a having split_class become a class and _optional_nested_brackets a private method, with token and tokens being instance attributes. I tried a similar effect first by having _optional_nested_brackets as a closure.

Copy link
Owner

@bwhmather bwhmather left a comment

Choose a reason for hiding this comment

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

Thank you. Merging and releasing.

@bwhmather bwhmather enabled auto-merge April 19, 2024 05:44
@bwhmather bwhmather merged commit 2025243 into bwhmather:master Apr 19, 2024
22 checks passed
@ericbn ericbn deleted the add-type-alias-support branch April 19, 2024 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants