Skip to content

Commit

Permalink
✨ feature: add pep701 support and remove python3.8
Browse files Browse the repository at this point in the history
  • Loading branch information
zrr1999 committed Mar 13, 2024
1 parent c451510 commit e350a82
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 166 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
target-version: [py38, py39, py310, py311, py312]
target-version: [py39, py310, py311, py312]
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repos:
- id: ruff-format

- repo: https://github.com/pdm-project/pdm
rev: 2.10.4
rev: 2.11.1
hooks:
- id: pdm-lock-check

Expand Down
291 changes: 132 additions & 159 deletions pdm.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyfuture/codemod/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .pep604 import TransformUnionTypesCommand
from .pep622 import TransformMatchCommand
from .pep695 import TransformTypeParametersCommand
from .pep701 import TransformFStringCommand
1 change: 1 addition & 0 deletions pyfuture/codemod/pep701/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .fstring import TransformFStringCommand
126 changes: 126 additions & 0 deletions pyfuture/codemod/pep701/fstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from __future__ import annotations

from typing import Any

import libcst as cst
from libcst import (
Arg,
ClassDef,
FunctionDef,
Index,
Name,
SimpleStatementLine,
Subscript,
SubscriptElement,
)
from libcst.codemod import (
CodemodContext,
VisitorBasedCodemodCommand,
)
from libcst.codemod.visitors import AddImportsVisitor
from libcst.metadata import ScopeProvider

from ..utils import gen_type_param


class TransformFStringCommand(VisitorBasedCodemodCommand):
"""
Remove f-string from node, and return a new node with the formatted string.
Example:
>>> transformer = TransformFStringCommand(CodemodContext())
>>> module = cst.parse_module(\"""
... name = "world"
... x = f"hello {name}"
... y = f"hello {"world"}"
... \"""
... )
>>> new_module = transformer.transform_module(module)
>>> print(new_module.code)
name = "world"
x = "hello {:}".format(name)
y = "hello {:}".format("world")
>>> module = cst.parse_module(\"""
... result = 3.1415926
... x = f"result: {result:.2f}"
... y = f"result: {3.1415926:.2f}"
... \"""
... )
>>> new_module = transformer.transform_module(module)
>>> print(new_module.code)
result = 3.1415926
x = "result: {:.2f}".format(result)
y = "result: {:.2f}".format(3.1415926)
"""

METADATA_DEPENDENCIES = (ScopeProvider,)

def __init__(self, context: CodemodContext) -> None:
self.node_to_wrapper: dict[FunctionDef | ClassDef, Any] = {}
super().__init__(context)

def remove_type_parameters[T: FunctionDef | ClassDef](
self, node: T, prefix: str = "", suffix: str = ""
) -> tuple[list[SimpleStatementLine], T]:
type_params = node.type_parameters
if type_params is None:
return [], node
statements = []
new_node = node.with_changes(type_parameters=None)

Check warning on line 69 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L65-L69

Added lines #L65 - L69 were not covered by tests

slices = []
for type_param in type_params.params:
new_name = type_param.param.name.with_changes(value=f"{prefix}{type_param.param.name.value}{suffix}")

Check warning on line 73 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L71-L73

Added lines #L71 - L73 were not covered by tests

AddImportsVisitor.add_needed_import(self.context, "typing", type_param.param.__class__.__name__)
statements.append(gen_type_param(type_param.param, new_name))
slices.append(

Check warning on line 77 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L75-L77

Added lines #L75 - L77 were not covered by tests
SubscriptElement(
slice=Index(value=new_name),
),
)

if isinstance(new_node, ClassDef) and slices:
AddImportsVisitor.add_needed_import(self.context, "typing", "Generic")
generic_base = Arg(

Check warning on line 85 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L83-L85

Added lines #L83 - L85 were not covered by tests
value=Subscript(
value=Name(
value="Generic",
lpar=[],
rpar=[],
),
slice=slices,
)
)
new_node = new_node.with_changes(bases=[*new_node.bases, generic_base])

Check warning on line 95 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L95

Added line #L95 was not covered by tests

return statements, new_node

Check warning on line 97 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L97

Added line #L97 was not covered by tests

def leave_FormattedString(self, original_node: cst.FormattedString, updated_node: cst.FormattedString):
expressions = []
string = ""
start = updated_node.start.strip("f")
end = updated_node.end

for node in updated_node.parts:
if isinstance(node, cst.FormattedStringExpression):
expressions.append(node.expression)
format_spec_str = ""
if (format_spec := node.format_spec) is not None:
for format_spec_node in format_spec:
if isinstance(format_spec_node, cst.FormattedStringText):
format_spec_str += format_spec_node.value
else:
raise NotImplementedError(f"Unknown node type: {format_spec_node}")

Check warning on line 114 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L114

Added line #L114 was not covered by tests
string += f"{{:{format_spec_str}}}"
elif isinstance(node, cst.FormattedStringText):
string += node.value
else:
raise NotImplementedError(f"Unknown node type: {node}")

Check warning on line 119 in pyfuture/codemod/pep701/fstring.py

View check run for this annotation

Codecov / codecov/patch

pyfuture/codemod/pep701/fstring.py#L119

Added line #L119 was not covered by tests
return cst.Call(
func=cst.Attribute(
value=cst.SimpleString(value=f"{start}{string}{end}"),
attr=cst.Name(value="format"),
),
args=[cst.Arg(value=expression) for expression in expressions],
)
4 changes: 4 additions & 0 deletions pyfuture/codemod/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class RuleSet(Enum):
pep622 = "pep622"
# python 3.12+
pep695 = "pep695"
pep701 = "pep701"


def get_transformers(rule_sets: list[RuleSet] | RuleSet) -> Iterable[type[Codemod]]:
Expand All @@ -27,6 +28,7 @@ def get_transformers(rule_sets: list[RuleSet] | RuleSet) -> Iterable[type[Codemo
from .pep604 import TransformUnionTypesCommand
from .pep622 import TransformMatchCommand
from .pep695 import TransformTypeParametersCommand
from .pep701 import TransformFStringCommand

if not isinstance(rule_sets, list):
rule_sets = [rule_sets]
Expand All @@ -39,6 +41,8 @@ def get_transformers(rule_sets: list[RuleSet] | RuleSet) -> Iterable[type[Codemo
yield TransformMatchCommand
case RuleSet.pep695:
yield TransformTypeParametersCommand
case RuleSet.pep701:
yield TransformFStringCommand
case _: # pragma: no cover
raise ValueError(f"Unknown rule set: {rule_set}")

Expand Down
6 changes: 3 additions & 3 deletions pyfuture/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test(x: __test_T) -> __test_T:
def transfer_code(
code: str,
*,
target: tuple[int, int] = (3, 8),
target: tuple[int, int] = (3, 9),
) -> str:
"""
Transfer code to specified target version of python.
Expand All @@ -66,7 +66,7 @@ def test(x: __test_T) -> __test_T:
assert target[0] == 3, "Only support python3"
transformers = []
if target[1] < 12:
transformers.extend(get_transformers(RuleSet.pep695))
transformers.extend(get_transformers([RuleSet.pep695, RuleSet.pep701]))
if target[1] < 10:
transformers.extend(get_transformers([RuleSet.pep622, RuleSet.pep604]))
new_code = apply_transformer(
Expand All @@ -80,7 +80,7 @@ def transfer_file(
src_file: Path,
tgt_file: Path,
*,
target: tuple[int, int] = (3, 8),
target: tuple[int, int] = (3, 9),
):
"""
Transfer code from src_file and write to tgt_file.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ authors = [
{name = "Zhan Rongrui", email = "2742392377@qq.com"},
]
dependencies = [
"libcst>=1.1.0",
"libcst>=1.2.0",
"typer>=0.9.0",
"loguru>=0.7.2",
"rich>=13.7.0",
]
requires-python = ">=3.8"
requires-python = ">=3.9"
readme = "README.md"
license = {text = "MIT"}

Expand Down

0 comments on commit e350a82

Please sign in to comment.