-
-
Notifications
You must be signed in to change notification settings - Fork 654
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
[internal] Add infrastructure to support deprecating field names #13666
Changes from 2 commits
4e696c8
7d7b002
6ddf8b1
22edec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
from collections import defaultdict | ||
from dataclasses import dataclass | ||
from io import BytesIO | ||
from typing import DefaultDict, cast | ||
from typing import DefaultDict, Tuple, cast | ||
|
||
from colors import green, red | ||
|
||
|
@@ -380,11 +380,118 @@ def should_be_renamed(token: tokenize.TokenInfo) -> bool: | |
) | ||
|
||
|
||
# ------------------------------------------------------------------------------------------ | ||
# Rename deprecated field types fixer | ||
# ------------------------------------------------------------------------------------------ | ||
|
||
|
||
class RenameDeprecatedFieldsRequest(DeprecationFixerRequest): | ||
pass | ||
|
||
|
||
class RenamedFieldTypes(FrozenDict[Tuple[str, str], str]): | ||
"""Deprecated field type names for target to new names.""" | ||
|
||
|
||
@rule | ||
def determine_renamed_field_types( | ||
target_types: RegisteredTargetTypes, union_membership: UnionMembership | ||
) -> RenamedFieldTypes: | ||
renamed_field_types = {} | ||
for tgt in target_types.types: | ||
for field_type in tgt.class_field_types(union_membership): | ||
if field_type.deprecated_alias is None: | ||
continue | ||
renamed_field_types[(tgt.alias, field_type.deprecated_alias)] = field_type.alias | ||
if tgt.deprecated_alias is not None: | ||
renamed_field_types[ | ||
(tgt.deprecated_alias, field_type.deprecated_alias) | ||
] = field_type.alias | ||
return RenamedFieldTypes(renamed_field_types) | ||
|
||
|
||
@rule(desc="Check for deprecated field type names", level=LogLevel.DEBUG) | ||
def maybe_rename_deprecated_fields( | ||
request: RenameDeprecatedFieldsRequest, | ||
renamed_field_types: RenamedFieldTypes, | ||
) -> RewrittenBuildFile: | ||
target = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe call this |
||
level = 0 | ||
tokens = iter(request.tokenize()) | ||
applied_renames: set[tuple[str, str]] = set() | ||
|
||
def parse_level(token: tokenize.TokenInfo) -> bool: | ||
nonlocal target | ||
nonlocal level | ||
|
||
if target is None or token.type is not tokenize.OP: # type: ignore[unreachable] | ||
return False | ||
if token.string == "(": # type: ignore[unreachable] | ||
level += 1 | ||
elif token.string == ")": | ||
level -= 1 | ||
if level < 1: | ||
target = None | ||
else: | ||
return False | ||
return True | ||
|
||
def next_op_is(string: str) -> bool: | ||
for next_token in tokens: | ||
if next_token.type is tokenize.NL: | ||
continue | ||
parse_level(next_token) | ||
return next_token.type is tokenize.OP and next_token.string == string | ||
return False | ||
|
||
def should_be_renamed(token: tokenize.TokenInfo) -> bool: | ||
nonlocal target | ||
nonlocal level | ||
|
||
if parse_level(token): | ||
return False | ||
|
||
if token.type is not tokenize.NAME: | ||
return False | ||
|
||
if target is None and next_op_is("("): | ||
target = token.string | ||
level = 1 | ||
return False | ||
|
||
if level > 1 or not next_op_is("="): | ||
return False | ||
|
||
return (target, token.string) in renamed_field_types | ||
|
||
updated_text_lines = list(request.lines) | ||
for token in tokens: | ||
if not should_be_renamed(token): | ||
continue | ||
line_index = token.start[0] - 1 | ||
line = request.lines[line_index] | ||
prefix = line[: token.start[1]] | ||
suffix = line[token.end[1] :] | ||
new_symbol = renamed_field_types[(cast(str, target), token.string)] | ||
applied_renames.add((f"{target}.{token.string}", f"{target}.{new_symbol}")) | ||
updated_text_lines[line_index] = f"{prefix}{new_symbol}{suffix}" | ||
|
||
return RewrittenBuildFile( | ||
request.path, | ||
tuple(updated_text_lines), | ||
change_descriptions=tuple( | ||
f"Rename `{request.red(deprecated)}` to `{request.green(new)}`" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about this? (Still using colors)
|
||
for deprecated, new in sorted(applied_renames) | ||
), | ||
) | ||
|
||
|
||
def rules(): | ||
return ( | ||
*collect_rules(), | ||
*pex.rules(), | ||
UnionRule(RewrittenBuildFileRequest, RenameDeprecatedTargetsRequest), | ||
UnionRule(RewrittenBuildFileRequest, RenameDeprecatedFieldsRequest), | ||
# NB: We want this to come at the end so that running Black happens after all our | ||
# deprecation fixers. | ||
UnionRule(RewrittenBuildFileRequest, FormatWithBlackRequest), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
FrozenDict[str, FrozenDict[str, str]]
, which maps target types to field renames. Then you can rewrite the implementation I think to more eagerly exit if the target type is not a match?I'm not sure I followed the level code correctly...the level should be 1. A nested target should not exist - targets are always top-level.
At this point, might also be worth switching to a frozen dataclass rather than subclassing FrozenDict so that you can give a name to the variable like
target_types_to_field_renames
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're referring to eagerly exit out of
should_be_renamed
, correct?OK, will try it out.. see what it'll look like (not convinced it will win much, but perhaps the data structure will be easier to comprehend?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, there's a check that we only ever operate when
level==1
.This is to protect against constructs, such as this, as an example:
As we do not want to change
my_field
here... The level code might be overkill, but it plugs a potential hole.And, as I write this, I realize I don't only look at the field name, but the enclosing target name as well (
setup_py
in this example) and as such, it would be safe to ignore the level all together.. thanks for the nudge, will get rid of it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, wait, no, the level is important for this reason..
We must keep track of when we're seeing fields in
setup_py
, which we should ignore, and when we're back inpython_distritbution
, so we know thatold_field
is deprecated and should be replaced.It's hard to make this distinction unless I keep track of when I enter and exit constructs, such as
setup_py
there..There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made some refactorings that will hopefully be easier to follow now.