From de2073e42253c5b43a65b59b04b8bbc822605c6c Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 16:13:36 -0500 Subject: [PATCH 1/7] replace Optional[T] and Union[T, None] with T | None --- src/fixit/api.py | 18 ++-- src/fixit/cli.py | 10 +-- src/fixit/config.py | 30 ++++--- src/fixit/engine.py | 2 +- src/fixit/ftypes.py | 40 ++++----- src/fixit/lsp.py | 6 +- src/fixit/rule.py | 24 +++--- src/fixit/rules/deprecated_abc_import.py | 2 +- src/fixit/rules/no_namedtuple.py | 2 +- src/fixit/rules/no_static_if_condition.py | 2 +- ...py => replace_optional_type_annotation.py} | 82 +++++++++++-------- src/fixit/rules/rewrite_to_comprehension.py | 2 +- src/fixit/rules/use_fstring.py | 4 +- src/fixit/tests/ftypes.py | 2 +- .../upgrade/deprecated_testcase_keywords.py | 4 +- src/fixit/util.py | 2 +- 16 files changed, 123 insertions(+), 109 deletions(-) rename src/fixit/rules/{replace_union_with_optional.py => replace_optional_type_annotation.py} (51%) diff --git a/src/fixit/api.py b/src/fixit/api.py index 32fb79b3..c125c94e 100644 --- a/src/fixit/api.py +++ b/src/fixit/api.py @@ -100,7 +100,7 @@ def fixit_bytes( *, config: Config, autofix: bool = False, - metrics_hook: Optional[MetricsHook] = None, + metrics_hook: MetricsHook | None = None, ) -> Generator[Result, bool, Optional[FileContent]]: """ Lint raw bytes content representing a single path, using the given configuration. @@ -154,8 +154,8 @@ def fixit_stdin( path: Path, *, autofix: bool = False, - options: Optional[Options] = None, - metrics_hook: Optional[MetricsHook] = None, + options: Options | None = None, + metrics_hook: MetricsHook | None = None, ) -> Generator[Result, bool, None]: """ Wrapper around :func:`fixit_bytes` for formatting content from STDIN. @@ -187,8 +187,8 @@ def fixit_file( path: Path, *, autofix: bool = False, - options: Optional[Options] = None, - metrics_hook: Optional[MetricsHook] = None, + options: Options | None = None, + metrics_hook: MetricsHook | None = None, ) -> Generator[Result, bool, None]: """ Lint a single file on disk, detecting and generating appropriate configuration. @@ -223,8 +223,8 @@ def _fixit_file_wrapper( path: Path, *, autofix: bool = False, - options: Optional[Options] = None, - metrics_hook: Optional[MetricsHook] = None, + options: Options | None = None, + metrics_hook: MetricsHook | None = None, ) -> List[Result]: """ Wrapper because generators can't be pickled or used directly via multiprocessing @@ -239,9 +239,9 @@ def fixit_paths( paths: Iterable[Path], *, autofix: bool = False, - options: Optional[Options] = None, + options: Options | None = None, parallel: bool = True, - metrics_hook: Optional[MetricsHook] = None, + metrics_hook: MetricsHook | None = None, ) -> Generator[Result, bool, None]: """ Lint multiple files or directories, recursively expanding each path. diff --git a/src/fixit/cli.py b/src/fixit/cli.py index 84bd344a..02939870 100644 --- a/src/fixit/cli.py +++ b/src/fixit/cli.py @@ -89,11 +89,11 @@ def f(v: int) -> str: @click.option("--print-metrics", is_flag=True, help="Print metrics of this run") def main( ctx: click.Context, - debug: Optional[bool], - config_file: Optional[Path], + debug: bool | None, + config_file: Path | None, tags: str, rules: str, - output_format: Optional[OutputFormat], + output_format: OutputFormat | None, output_template: str, print_metrics: bool, ) -> None: @@ -258,8 +258,8 @@ def fix( def lsp( ctx: click.Context, stdio: bool, - tcp: Optional[int], - ws: Optional[int], + tcp: int | None, + ws: int | None, debounce_interval: float, ) -> None: """ diff --git a/src/fixit/config.py b/src/fixit/config.py index ededa89f..7be4c516 100644 --- a/src/fixit/config.py +++ b/src/fixit/config.py @@ -62,7 +62,7 @@ class ConfigError(ValueError): - def __init__(self, msg: str, config: Optional[RawConfig] = None): + def __init__(self, msg: str, config: RawConfig | None = None): super().__init__(msg) self.config = config @@ -203,7 +203,7 @@ def collect_rules( config: Config, *, # out-param to capture reasons when disabling rules for debugging - debug_reasons: Optional[Dict[Type[LintRule], str]] = None, + debug_reasons: Dict[Type[LintRule], str] | None = None, ) -> Collection[LintRule]: """ Import and return rules specified by `enables` and `disables`. @@ -272,7 +272,7 @@ def collect_rules( return materialized_rules -def locate_configs(path: Path, root: Optional[Path] = None) -> List[Path]: +def locate_configs(path: Path, root: Path | None = None) -> List[Path]: """ Given a file path, locate all relevant config files in priority order. @@ -335,7 +335,7 @@ def read_configs(paths: List[Path]) -> List[RawConfig]: def get_sequence( - config: RawConfig, key: str, *, data: Optional[Dict[str, Any]] = None + config: RawConfig, key: str, *, data: Dict[str, Any] | None = None ) -> Sequence[str]: value: Sequence[str] if data: @@ -352,7 +352,7 @@ def get_sequence( def get_options( - config: RawConfig, key: str, *, data: Optional[Dict[str, Any]] = None + config: RawConfig, key: str, *, data: Dict[str, Any] | None = None ) -> RuleOptionsTable: if data: mapping = data.pop(key, {}) @@ -379,9 +379,7 @@ def get_options( return rule_configs -def parse_rule( - rule: str, root: Path, config: Optional[RawConfig] = None -) -> QualifiedRule: +def parse_rule(rule: str, root: Path, config: RawConfig | None = None) -> QualifiedRule: """ Given a raw rule string, parse and return a QualifiedRule object """ @@ -400,7 +398,7 @@ def parse_rule( def merge_configs( - path: Path, raw_configs: List[RawConfig], root: Optional[Path] = None + path: Path, raw_configs: List[RawConfig], root: Path | None = None ) -> Config: """ Given multiple raw configs, merge them in priority order. @@ -413,8 +411,8 @@ def merge_configs( enable_rules: Set[QualifiedRule] = {QualifiedRule("fixit.rules")} disable_rules: Set[QualifiedRule] = set() rule_options: RuleOptionsTable = {} - target_python_version: Optional[Version] = Version(platform.python_version()) - target_formatter: Optional[str] = None + target_python_version: Version | None = Version(platform.python_version()) + target_formatter: str | None = None output_format: OutputFormat = OutputFormat.fixit output_template: str = "" @@ -423,9 +421,9 @@ def process_subpath( *, enable: Sequence[str] = (), disable: Sequence[str] = (), - options: Optional[RuleOptionsTable] = None, + options: RuleOptionsTable | None = None, python_version: Any = None, - formatter: Optional[str] = None, + formatter: str | None = None, ) -> None: nonlocal target_python_version nonlocal target_formatter @@ -556,10 +554,10 @@ def process_subpath( def generate_config( - path: Optional[Path] = None, - root: Optional[Path] = None, + path: Path | None = None, + root: Path | None = None, *, - options: Optional[Options] = None, + options: Options | None = None, ) -> Config: """ Given a file path, walk upwards looking for and applying cascading configs diff --git a/src/fixit/engine.py b/src/fixit/engine.py index 36ec39d6..ccc8578f 100644 --- a/src/fixit/engine.py +++ b/src/fixit/engine.py @@ -59,7 +59,7 @@ def collect_violations( self, rules: Collection[LintRule], config: Config, - metrics_hook: Optional[MetricsHook] = None, + metrics_hook: MetricsHook | None = None, ) -> Generator[LintViolation, None, int]: """Run multiple `LintRule`s and yield any lint violations. diff --git a/src/fixit/ftypes.py b/src/fixit/ftypes.py index 0205cc36..1238fa09 100644 --- a/src/fixit/ftypes.py +++ b/src/fixit/ftypes.py @@ -63,9 +63,9 @@ class OutputFormat(str, Enum): @dataclass(frozen=True) class Invalid: code: str - range: Optional[CodeRange] = None - expected_message: Optional[str] = None - expected_replacement: Optional[str] = None + range: CodeRange | None = None + expected_message: str | None = None + expected_replacement: str | None = None @dataclass(frozen=True) @@ -105,8 +105,8 @@ class Valid: class QualifiedRuleRegexResult(TypedDict): module: str - name: Optional[str] - local: Optional[str] + name: str | None + local: str | None def is_sequence(value: Any) -> bool: @@ -120,9 +120,9 @@ def is_collection(value: Any) -> bool: @dataclass(frozen=True) class QualifiedRule: module: str - name: Optional[str] = None - local: Optional[str] = None - root: Optional[Path] = field(default=None, hash=False, compare=False) + name: str | None = None + local: str | None = None + root: Path | None = field(default=None, hash=False, compare=False) def __str__(self) -> str: return self.module + (f":{self.name}" if self.name else "") @@ -139,7 +139,7 @@ class Tags(Container[str]): exclude: Tuple[str, ...] = () @staticmethod - def parse(value: Optional[str]) -> "Tags": + def parse(value: str | None) -> "Tags": if not value: return Tags() @@ -185,11 +185,11 @@ class Options: Command-line options to affect runtime behavior """ - debug: Optional[bool] = None - config_file: Optional[Path] = None - tags: Optional[Tags] = None + debug: bool | None = None + config_file: Path | None = None + tags: Tags | None = None rules: Sequence[QualifiedRule] = () - output_format: Optional[OutputFormat] = None + output_format: OutputFormat | None = None output_template: str = "" print_metrics: bool = False @@ -200,8 +200,8 @@ class LSPOptions: Command-line options to affect LSP runtime behavior """ - tcp: Optional[int] - ws: Optional[int] + tcp: int | None + ws: int | None stdio: bool = True debounce_interval: float = 0.5 @@ -226,13 +226,13 @@ class Config: options: RuleOptionsTable = field(default_factory=dict) # filtering criteria - python_version: Optional[Version] = field( + python_version: Version | None = field( default_factory=lambda: Version(platform.python_version()) ) tags: Tags = field(default_factory=Tags) # post-run processing - formatter: Optional[str] = None + formatter: str | None = None # output formatting options output_format: OutputFormat = OutputFormat.fixit @@ -263,7 +263,7 @@ class LintViolation: range: CodeRange message: str node: CSTNode - replacement: Optional[NodeReplacement[CSTNode]] + replacement: NodeReplacement[CSTNode] | None diff: str = "" @property @@ -281,5 +281,5 @@ class Result: """ path: Path - violation: Optional[LintViolation] - error: Optional[Tuple[Exception, str]] = None + violation: LintViolation | None + error: Tuple[Exception, str] | None = None diff --git a/src/fixit/lsp.py b/src/fixit/lsp.py index 00f34b7c..c735a853 100644 --- a/src/fixit/lsp.py +++ b/src/fixit/lsp.py @@ -65,7 +65,7 @@ def load_config(self, path: Path) -> Config: def diagnostic_generator( self, uri: str, autofix: bool = False - ) -> Optional[Generator[Result, bool, Optional[FileContent]]]: + ) -> Generator[Result, bool, Optional[FileContent]] | None: """ LSP wrapper (provides document state from `pygls`) for `fixit_bytes`. """ @@ -125,7 +125,7 @@ def on_did_open(self, params: DidOpenTextDocumentParams) -> None: def on_did_change(self, params: DidChangeTextDocumentParams) -> None: self.validate(params.text_document.uri, params.text_document.version) - def format(self, params: DocumentFormattingParams) -> Optional[List[TextEdit]]: + def format(self, params: DocumentFormattingParams) -> List[TextEdit] | None: generator = self.diagnostic_generator(params.text_document.uri, autofix=True) if generator is None: return None @@ -164,7 +164,7 @@ class Debouncer: def __init__(self, f: Callable[..., Any], interval: float) -> None: self.f = f self.interval = interval - self._timer: Optional[threading.Timer] = None + self._timer: threading.Timer | None = None self._lock = threading.Lock() def __call__(self, *args: Any, **kwargs: Any) -> None: diff --git a/src/fixit/rule.py b/src/fixit/rule.py index 954972b1..a3f8eb40 100644 --- a/src/fixit/rule.py +++ b/src/fixit/rule.py @@ -114,7 +114,7 @@ def __init_subclass__(cls) -> None: def __str__(self) -> str: return f"{self.__class__.__module__}:{self.__class__.__name__}" - _visit_hook: Optional[VisitHook] = None + _visit_hook: VisitHook | None = None def node_comments(self, node: CSTNode) -> Generator[str, None, None]: """ @@ -125,11 +125,9 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: while not isinstance(node, Module): # trailing_whitespace can either be a property of the node itself, or in # case of blocks, be part of the block's body element - tw: Optional[TrailingWhitespace] = getattr( - node, "trailing_whitespace", None - ) + tw: TrailingWhitespace | None = getattr(node, "trailing_whitespace", None) if tw is None: - body: Optional[BaseSuite] = getattr(node, "body", None) + body: BaseSuite | None = getattr(node, "body", None) if isinstance(body, SimpleStatementSuite): tw = body.trailing_whitespace elif isinstance(body, IndentedBlock): @@ -138,20 +136,20 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: if tw and tw.comment: yield tw.comment.value - comma: Optional[Comma] = getattr(node, "comma", None) + comma: Comma | None = getattr(node, "comma", None) if isinstance(comma, Comma): tw = getattr(comma.whitespace_after, "first_line", None) if tw and tw.comment: yield tw.comment.value - rb: Optional[RightSquareBracket] = getattr(node, "rbracket", None) + rb: RightSquareBracket | None = getattr(node, "rbracket", None) if rb is not None: tw = getattr(rb.whitespace_before, "first_line", None) if tw and tw.comment: yield tw.comment.value - el: Optional[Sequence[EmptyLine]] = None - lb: Optional[LeftSquareBracket] = getattr(node, "lbracket", None) + el: Sequence[EmptyLine] | None = None + lb: LeftSquareBracket | None = getattr(node, "lbracket", None) if lb is not None: el = getattr(lb.whitespace_after, "empty_lines", None) if el is not None: @@ -165,7 +163,7 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: if line.comment: yield line.comment.value - ll: Optional[Sequence[EmptyLine]] = getattr(node, "leading_lines", None) + ll: Sequence[EmptyLine] | None = getattr(node, "leading_lines", None) if ll is not None: for line in ll: if line.comment: @@ -219,10 +217,10 @@ def ignore_lint(self, node: CSTNode) -> bool: def report( self, node: CSTNode, - message: Optional[str] = None, + message: str | None = None, *, - position: Optional[Union[CodePosition, CodeRange]] = None, - replacement: Optional[NodeReplacement[CSTNode]] = None, + position: Union[CodePosition, CodeRange] | None = None, + replacement: NodeReplacement[CSTNode] | None = None, ) -> None: """ Report a lint rule violation. diff --git a/src/fixit/rules/deprecated_abc_import.py b/src/fixit/rules/deprecated_abc_import.py index 03a3607c..adac9cc1 100644 --- a/src/fixit/rules/deprecated_abc_import.py +++ b/src/fixit/rules/deprecated_abc_import.py @@ -222,7 +222,7 @@ def visit_ImportFrom(self, node: cst.ImportFrom) -> None: def get_import_from( self, node: Union[cst.SimpleStatementLine, cst.BaseCompoundStatement] - ) -> Optional[cst.ImportFrom]: + ) -> cst.ImportFrom | None: """ Iterate over a Statement Sequence and return a Statement if it is a `cst.ImportFrom` statement. diff --git a/src/fixit/rules/no_namedtuple.py b/src/fixit/rules/no_namedtuple.py index 95854566..d1fd190f 100644 --- a/src/fixit/rules/no_namedtuple.py +++ b/src/fixit/rules/no_namedtuple.py @@ -186,7 +186,7 @@ def partition_bases( self, original_bases: Sequence[cst.Arg] ) -> Tuple[Optional[cst.Arg], List[cst.Arg]]: # Returns a tuple of NamedTuple base object if it exists, and a list of non-NamedTuple bases - namedtuple_base: Optional[cst.Arg] = None + namedtuple_base: cst.Arg | None = None new_bases: List[cst.Arg] = [] for base_class in original_bases: if QualifiedNameProvider.has_name( diff --git a/src/fixit/rules/no_static_if_condition.py b/src/fixit/rules/no_static_if_condition.py index dd7fba4c..4893bf33 100644 --- a/src/fixit/rules/no_static_if_condition.py +++ b/src/fixit/rules/no_static_if_condition.py @@ -115,7 +115,7 @@ async def some_func() -> none: ] @classmethod - def _extract_static_bool(cls, node: cst.BaseExpression) -> Optional[bool]: + def _extract_static_bool(cls, node: cst.BaseExpression) -> bool | None: if m.matches(node, m.Call()): # cannot reason about function calls return None diff --git a/src/fixit/rules/replace_union_with_optional.py b/src/fixit/rules/replace_optional_type_annotation.py similarity index 51% rename from src/fixit/rules/replace_union_with_optional.py rename to src/fixit/rules/replace_optional_type_annotation.py index d7f2c1b7..f900dc69 100644 --- a/src/fixit/rules/replace_union_with_optional.py +++ b/src/fixit/rules/replace_optional_type_annotation.py @@ -9,27 +9,26 @@ from fixit import Invalid, LintRule, Valid -class ReplaceUnionWithOptional(LintRule): +class ReplaceOptionalTypeAnnotation(LintRule): """ - Enforces the use of ``Optional[T]`` over ``Union[T, None]`` and ``Union[None, T]``. - See https://docs.python.org/3/library/typing.html#typing.Optional to learn more about Optionals. + Enforces the use of ``T | None`` over ``Optional[T]`` and ``Union[T, None]`` and ``Union[None, T]``. + See https://docs.python.org/3/library/stdtypes.html#types-union. """ MESSAGE: str = ( - "`Optional[T]` is preferred over `Union[T, None]` or `Union[None, T]`. " - + "Learn more: https://docs.python.org/3/library/typing.html#typing.Optional" + "`T | None` is preferred over `Optional[T]` or `Union[T, None]` or `Union[None, T]`. " + + "Learn more: https://docs.python.org/3/library/stdtypes.html#types-union" ) - METADATA_DEPENDENCIES = (cst.metadata.ScopeProvider,) VALID = [ Valid( """ - def func() -> Optional[str]: + def func() -> str | None: pass """ ), Valid( """ - def func() -> Optional[Dict]: + def func() -> Dict | None: pass """ ), @@ -43,43 +42,41 @@ def func() -> Union[str, int, None]: INVALID = [ Invalid( """ - def func() -> Union[str, None]: + def func() -> Optional[str]: + pass + """, + expected_replacement=""" + def func() -> str | None: pass """, ), Invalid( """ - from typing import Optional def func() -> Union[Dict[str, int], None]: pass """, expected_replacement=""" - from typing import Optional - def func() -> Optional[Dict[str, int]]: + def func() -> Dict[str, int] | None: pass """, ), Invalid( """ - from typing import Optional def func() -> Union[str, None]: pass """, expected_replacement=""" - from typing import Optional - def func() -> Optional[str]: + def func() -> str | None: pass """, ), Invalid( """ - from typing import Optional def func() -> Union[Dict, None]: pass """, expected_replacement=""" - from typing import Optional - def func() -> Optional[Dict]: + def func() -> Dict | None: pass """, ), @@ -87,24 +84,34 @@ def func() -> Optional[Dict]: def leave_Annotation(self, original_node: cst.Annotation) -> None: if self.contains_union_with_none(original_node): - scope = self.get_metadata(cst.metadata.ScopeProvider, original_node, None) nones = 0 indexes = [] replacement = None - if scope is not None and "Optional" in scope: - for s in cst.ensure_type(original_node.annotation, cst.Subscript).slice: - if m.matches(s, m.SubscriptElement(m.Index(m.Name("None")))): - nones += 1 - else: - indexes.append(s.slice) - if not (nones > 1) and len(indexes) == 1: - replacement = original_node.with_changes( - annotation=cst.Subscript( - value=cst.Name("Optional"), - slice=(cst.SubscriptElement(indexes[0]),), - ) + for s in cst.ensure_type(original_node.annotation, cst.Subscript).slice: + if m.matches(s, m.SubscriptElement(m.Index(m.Name("None")))): + nones += 1 + else: + indexes.append(s.slice) + if not (nones > 1) and len(indexes) == 1: + inner_type = cst.ensure_type(indexes[0], cst.Index).value + replacement = original_node.with_changes( + annotation=cst.BinaryOperation( + operator=cst.BitOr(), + left=inner_type, + right=cst.Name("None"), ) - # TODO(T57106602) refactor lint replacement once extract exists + ) + self.report(original_node, replacement=replacement) + elif self.contains_optional(original_node): + subscript_element = cst.ensure_type( + original_node.annotation, cst.Subscript + ).slice[0] + inner_type = cst.ensure_type(subscript_element.slice, cst.Index).value + replacement = original_node.with_changes( + annotation=cst.BinaryOperation( + operator=cst.BitOr(), left=inner_type, right=cst.Name("None") + ) + ) self.report(original_node, replacement=replacement) def contains_union_with_none(self, node: cst.Annotation) -> bool: @@ -126,3 +133,14 @@ def contains_union_with_none(self, node: cst.Annotation) -> bool: ) ), ) + + def contains_optional(self, node: cst.Annotation) -> bool: + return m.matches( + node, + m.Annotation( + m.Subscript( + value=m.Name("Optional"), + slice=[m.SubscriptElement(m.Index())], + ) + ), + ) diff --git a/src/fixit/rules/rewrite_to_comprehension.py b/src/fixit/rules/rewrite_to_comprehension.py index 68e0953c..c3a5787b 100644 --- a/src/fixit/rules/rewrite_to_comprehension.py +++ b/src/fixit/rules/rewrite_to_comprehension.py @@ -114,7 +114,7 @@ def visit_Call(self, node: cst.Call) -> None: exp = cst.ensure_type(node.args[0].value, cst.ListComp) message_formatter = UNNECESSARY_LIST_COMPREHENSION - replacement: Optional[Union[cst.Call, cst.BaseComp]] = None + replacement: Union[cst.Call, cst.BaseComp] | None = None if call_name == "list": replacement = node.deep_replace( node, cst.ListComp(elt=exp.elt, for_in=exp.for_in) diff --git a/src/fixit/rules/use_fstring.py b/src/fixit/rules/use_fstring.py index b56ebcf5..605a1dc6 100644 --- a/src/fixit/rules/use_fstring.py +++ b/src/fixit/rules/use_fstring.py @@ -144,9 +144,9 @@ class UseFstring(LintRule): ), ] - _codegen: Optional[Callable[[cst.CSTNode], str]] + _codegen: Callable[[cst.CSTNode], str] | None - def visit_Module(self, node: cst.Module) -> Optional[bool]: + def visit_Module(self, node: cst.Module) -> bool | None: self._codegen = node.code_for_node return super().visit_Module(node) diff --git a/src/fixit/tests/ftypes.py b/src/fixit/tests/ftypes.py index 292aac01..5467c65a 100644 --- a/src/fixit/tests/ftypes.py +++ b/src/fixit/tests/ftypes.py @@ -106,7 +106,7 @@ def test_tags_parser(self) -> None: def test_tags_bool(self) -> None: Tags = ftypes.Tags - tags: Optional[str] + tags: str | None for tags in ( "hello", diff --git a/src/fixit/upgrade/deprecated_testcase_keywords.py b/src/fixit/upgrade/deprecated_testcase_keywords.py index f38e4599..d82f7753 100755 --- a/src/fixit/upgrade/deprecated_testcase_keywords.py +++ b/src/fixit/upgrade/deprecated_testcase_keywords.py @@ -73,8 +73,8 @@ def visit_Call(self, node: Call) -> None: self.convert_linecol_to_range(node) def convert_linecol_to_range(self, node: Call) -> None: - line: Optional[BaseExpression] = None - col: Optional[BaseExpression] = None + line: BaseExpression | None = None + col: BaseExpression | None = None index_to_remove = [] for ind, arg in enumerate(node.args): if not arg.keyword: diff --git a/src/fixit/util.py b/src/fixit/util.py index 44ce518f..a13f6dce 100644 --- a/src/fixit/util.py +++ b/src/fixit/util.py @@ -36,7 +36,7 @@ class capture(Generic[Yield, Send, Return]): def __init__(self, generator: Generator[Yield, Send, Return]) -> None: self.generator = generator - self._send: Optional[Send] = None + self._send: Send | None = None self._result: Union[Return, object] = Sentinel def __iter__(self) -> Generator[Yield, Send, Return]: From 3b82f4d5da1c9d4c7651701132f8e4b4822d2e10 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 16:27:54 -0500 Subject: [PATCH 2/7] update docs --- docs/guide/builtins.rst | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index 6f2b1484..b5bb342c 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -33,7 +33,7 @@ Built-in Rules - :class:`NoRedundantListComprehension` - :class:`NoStaticIfCondition` - :class:`NoStringTypeAnnotation` -- :class:`ReplaceUnionWithOptional` +- :class:`ReplaceOptionalTypeAnnotation` - :class:`RewriteToComprehension` - :class:`RewriteToLiteral` - :class:`SortedAttributes` @@ -716,14 +716,14 @@ Built-in Rules async def foo() -> Class: return await Class() -.. class:: ReplaceUnionWithOptional +.. class:: ReplaceOptionalTypeAnnotation - Enforces the use of ``Optional[T]`` over ``Union[T, None]`` and ``Union[None, T]``. - See https://docs.python.org/3/library/typing.html#typing.Optional to learn more about Optionals. + Enforces the use of ``T | None`` over ``Optional[T]`` and ``Union[T, None]`` and ``Union[None, T]``. + See https://docs.python.org/3/library/stdtypes.html#types-union. .. attribute:: MESSAGE - `Optional[T]` is preferred over `Union[T, None]` or `Union[None, T]`. Learn more: https://docs.python.org/3/library/typing.html#typing.Optional + `T | None` is preferred over `Optional[T]` or `Union[T, None]` or `Union[None, T]`. Learn more: https://docs.python.org/3/library/stdtypes.html#types-union .. attribute:: AUTOFIX :type: Yes @@ -733,28 +733,31 @@ Built-in Rules .. code:: python - def func() -> Optional[str]: + def func() -> str | None: pass .. code:: python - def func() -> Optional[Dict]: + def func() -> Dict | None: pass .. attribute:: INVALID .. code:: python - def func() -> Union[str, None]: + def func() -> Optional[str]: + pass + + # suggested fix + def func() -> str | None: pass + .. code:: python - from typing import Optional def func() -> Union[Dict[str, int], None]: pass # suggested fix - from typing import Optional - def func() -> Optional[Dict[str, int]]: + def func() -> Dict[str, int] | None: pass .. class:: RewriteToComprehension From 7c84949d541dac1a9f1451262b8666401e154d74 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 16:29:47 -0500 Subject: [PATCH 3/7] restrict to 3.10 --- src/fixit/rules/replace_optional_type_annotation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fixit/rules/replace_optional_type_annotation.py b/src/fixit/rules/replace_optional_type_annotation.py index f900dc69..8f4a2fa9 100644 --- a/src/fixit/rules/replace_optional_type_annotation.py +++ b/src/fixit/rules/replace_optional_type_annotation.py @@ -15,6 +15,7 @@ class ReplaceOptionalTypeAnnotation(LintRule): See https://docs.python.org/3/library/stdtypes.html#types-union. """ + PYTHON_VERSION = ">= 3.10" MESSAGE: str = ( "`T | None` is preferred over `Optional[T]` or `Union[T, None]` or `Union[None, T]`. " + "Learn more: https://docs.python.org/3/library/stdtypes.html#types-union" From b3e7ca8dff3a73065b48f695c27974d163cb9c78 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Mon, 18 Nov 2024 16:40:51 -0500 Subject: [PATCH 4/7] revert lint fixes --- src/fixit/api.py | 18 ++++----- src/fixit/cli.py | 10 ++--- src/fixit/config.py | 30 +++++++------- src/fixit/engine.py | 2 +- src/fixit/ftypes.py | 40 +++++++++---------- src/fixit/lsp.py | 6 +-- src/fixit/rule.py | 24 ++++++----- src/fixit/rules/deprecated_abc_import.py | 6 +-- src/fixit/rules/no_namedtuple.py | 6 +-- src/fixit/rules/no_static_if_condition.py | 2 +- src/fixit/rules/rewrite_to_comprehension.py | 2 +- src/fixit/rules/use_fstring.py | 4 +- src/fixit/tests/ftypes.py | 2 +- .../upgrade/deprecated_testcase_keywords.py | 8 ++-- src/fixit/util.py | 2 +- 15 files changed, 83 insertions(+), 79 deletions(-) diff --git a/src/fixit/api.py b/src/fixit/api.py index c125c94e..32fb79b3 100644 --- a/src/fixit/api.py +++ b/src/fixit/api.py @@ -100,7 +100,7 @@ def fixit_bytes( *, config: Config, autofix: bool = False, - metrics_hook: MetricsHook | None = None, + metrics_hook: Optional[MetricsHook] = None, ) -> Generator[Result, bool, Optional[FileContent]]: """ Lint raw bytes content representing a single path, using the given configuration. @@ -154,8 +154,8 @@ def fixit_stdin( path: Path, *, autofix: bool = False, - options: Options | None = None, - metrics_hook: MetricsHook | None = None, + options: Optional[Options] = None, + metrics_hook: Optional[MetricsHook] = None, ) -> Generator[Result, bool, None]: """ Wrapper around :func:`fixit_bytes` for formatting content from STDIN. @@ -187,8 +187,8 @@ def fixit_file( path: Path, *, autofix: bool = False, - options: Options | None = None, - metrics_hook: MetricsHook | None = None, + options: Optional[Options] = None, + metrics_hook: Optional[MetricsHook] = None, ) -> Generator[Result, bool, None]: """ Lint a single file on disk, detecting and generating appropriate configuration. @@ -223,8 +223,8 @@ def _fixit_file_wrapper( path: Path, *, autofix: bool = False, - options: Options | None = None, - metrics_hook: MetricsHook | None = None, + options: Optional[Options] = None, + metrics_hook: Optional[MetricsHook] = None, ) -> List[Result]: """ Wrapper because generators can't be pickled or used directly via multiprocessing @@ -239,9 +239,9 @@ def fixit_paths( paths: Iterable[Path], *, autofix: bool = False, - options: Options | None = None, + options: Optional[Options] = None, parallel: bool = True, - metrics_hook: MetricsHook | None = None, + metrics_hook: Optional[MetricsHook] = None, ) -> Generator[Result, bool, None]: """ Lint multiple files or directories, recursively expanding each path. diff --git a/src/fixit/cli.py b/src/fixit/cli.py index 02939870..84bd344a 100644 --- a/src/fixit/cli.py +++ b/src/fixit/cli.py @@ -89,11 +89,11 @@ def f(v: int) -> str: @click.option("--print-metrics", is_flag=True, help="Print metrics of this run") def main( ctx: click.Context, - debug: bool | None, - config_file: Path | None, + debug: Optional[bool], + config_file: Optional[Path], tags: str, rules: str, - output_format: OutputFormat | None, + output_format: Optional[OutputFormat], output_template: str, print_metrics: bool, ) -> None: @@ -258,8 +258,8 @@ def fix( def lsp( ctx: click.Context, stdio: bool, - tcp: int | None, - ws: int | None, + tcp: Optional[int], + ws: Optional[int], debounce_interval: float, ) -> None: """ diff --git a/src/fixit/config.py b/src/fixit/config.py index 7be4c516..ededa89f 100644 --- a/src/fixit/config.py +++ b/src/fixit/config.py @@ -62,7 +62,7 @@ class ConfigError(ValueError): - def __init__(self, msg: str, config: RawConfig | None = None): + def __init__(self, msg: str, config: Optional[RawConfig] = None): super().__init__(msg) self.config = config @@ -203,7 +203,7 @@ def collect_rules( config: Config, *, # out-param to capture reasons when disabling rules for debugging - debug_reasons: Dict[Type[LintRule], str] | None = None, + debug_reasons: Optional[Dict[Type[LintRule], str]] = None, ) -> Collection[LintRule]: """ Import and return rules specified by `enables` and `disables`. @@ -272,7 +272,7 @@ def collect_rules( return materialized_rules -def locate_configs(path: Path, root: Path | None = None) -> List[Path]: +def locate_configs(path: Path, root: Optional[Path] = None) -> List[Path]: """ Given a file path, locate all relevant config files in priority order. @@ -335,7 +335,7 @@ def read_configs(paths: List[Path]) -> List[RawConfig]: def get_sequence( - config: RawConfig, key: str, *, data: Dict[str, Any] | None = None + config: RawConfig, key: str, *, data: Optional[Dict[str, Any]] = None ) -> Sequence[str]: value: Sequence[str] if data: @@ -352,7 +352,7 @@ def get_sequence( def get_options( - config: RawConfig, key: str, *, data: Dict[str, Any] | None = None + config: RawConfig, key: str, *, data: Optional[Dict[str, Any]] = None ) -> RuleOptionsTable: if data: mapping = data.pop(key, {}) @@ -379,7 +379,9 @@ def get_options( return rule_configs -def parse_rule(rule: str, root: Path, config: RawConfig | None = None) -> QualifiedRule: +def parse_rule( + rule: str, root: Path, config: Optional[RawConfig] = None +) -> QualifiedRule: """ Given a raw rule string, parse and return a QualifiedRule object """ @@ -398,7 +400,7 @@ def parse_rule(rule: str, root: Path, config: RawConfig | None = None) -> Qualif def merge_configs( - path: Path, raw_configs: List[RawConfig], root: Path | None = None + path: Path, raw_configs: List[RawConfig], root: Optional[Path] = None ) -> Config: """ Given multiple raw configs, merge them in priority order. @@ -411,8 +413,8 @@ def merge_configs( enable_rules: Set[QualifiedRule] = {QualifiedRule("fixit.rules")} disable_rules: Set[QualifiedRule] = set() rule_options: RuleOptionsTable = {} - target_python_version: Version | None = Version(platform.python_version()) - target_formatter: str | None = None + target_python_version: Optional[Version] = Version(platform.python_version()) + target_formatter: Optional[str] = None output_format: OutputFormat = OutputFormat.fixit output_template: str = "" @@ -421,9 +423,9 @@ def process_subpath( *, enable: Sequence[str] = (), disable: Sequence[str] = (), - options: RuleOptionsTable | None = None, + options: Optional[RuleOptionsTable] = None, python_version: Any = None, - formatter: str | None = None, + formatter: Optional[str] = None, ) -> None: nonlocal target_python_version nonlocal target_formatter @@ -554,10 +556,10 @@ def process_subpath( def generate_config( - path: Path | None = None, - root: Path | None = None, + path: Optional[Path] = None, + root: Optional[Path] = None, *, - options: Options | None = None, + options: Optional[Options] = None, ) -> Config: """ Given a file path, walk upwards looking for and applying cascading configs diff --git a/src/fixit/engine.py b/src/fixit/engine.py index ccc8578f..36ec39d6 100644 --- a/src/fixit/engine.py +++ b/src/fixit/engine.py @@ -59,7 +59,7 @@ def collect_violations( self, rules: Collection[LintRule], config: Config, - metrics_hook: MetricsHook | None = None, + metrics_hook: Optional[MetricsHook] = None, ) -> Generator[LintViolation, None, int]: """Run multiple `LintRule`s and yield any lint violations. diff --git a/src/fixit/ftypes.py b/src/fixit/ftypes.py index 1238fa09..0205cc36 100644 --- a/src/fixit/ftypes.py +++ b/src/fixit/ftypes.py @@ -63,9 +63,9 @@ class OutputFormat(str, Enum): @dataclass(frozen=True) class Invalid: code: str - range: CodeRange | None = None - expected_message: str | None = None - expected_replacement: str | None = None + range: Optional[CodeRange] = None + expected_message: Optional[str] = None + expected_replacement: Optional[str] = None @dataclass(frozen=True) @@ -105,8 +105,8 @@ class Valid: class QualifiedRuleRegexResult(TypedDict): module: str - name: str | None - local: str | None + name: Optional[str] + local: Optional[str] def is_sequence(value: Any) -> bool: @@ -120,9 +120,9 @@ def is_collection(value: Any) -> bool: @dataclass(frozen=True) class QualifiedRule: module: str - name: str | None = None - local: str | None = None - root: Path | None = field(default=None, hash=False, compare=False) + name: Optional[str] = None + local: Optional[str] = None + root: Optional[Path] = field(default=None, hash=False, compare=False) def __str__(self) -> str: return self.module + (f":{self.name}" if self.name else "") @@ -139,7 +139,7 @@ class Tags(Container[str]): exclude: Tuple[str, ...] = () @staticmethod - def parse(value: str | None) -> "Tags": + def parse(value: Optional[str]) -> "Tags": if not value: return Tags() @@ -185,11 +185,11 @@ class Options: Command-line options to affect runtime behavior """ - debug: bool | None = None - config_file: Path | None = None - tags: Tags | None = None + debug: Optional[bool] = None + config_file: Optional[Path] = None + tags: Optional[Tags] = None rules: Sequence[QualifiedRule] = () - output_format: OutputFormat | None = None + output_format: Optional[OutputFormat] = None output_template: str = "" print_metrics: bool = False @@ -200,8 +200,8 @@ class LSPOptions: Command-line options to affect LSP runtime behavior """ - tcp: int | None - ws: int | None + tcp: Optional[int] + ws: Optional[int] stdio: bool = True debounce_interval: float = 0.5 @@ -226,13 +226,13 @@ class Config: options: RuleOptionsTable = field(default_factory=dict) # filtering criteria - python_version: Version | None = field( + python_version: Optional[Version] = field( default_factory=lambda: Version(platform.python_version()) ) tags: Tags = field(default_factory=Tags) # post-run processing - formatter: str | None = None + formatter: Optional[str] = None # output formatting options output_format: OutputFormat = OutputFormat.fixit @@ -263,7 +263,7 @@ class LintViolation: range: CodeRange message: str node: CSTNode - replacement: NodeReplacement[CSTNode] | None + replacement: Optional[NodeReplacement[CSTNode]] diff: str = "" @property @@ -281,5 +281,5 @@ class Result: """ path: Path - violation: LintViolation | None - error: Tuple[Exception, str] | None = None + violation: Optional[LintViolation] + error: Optional[Tuple[Exception, str]] = None diff --git a/src/fixit/lsp.py b/src/fixit/lsp.py index c735a853..00f34b7c 100644 --- a/src/fixit/lsp.py +++ b/src/fixit/lsp.py @@ -65,7 +65,7 @@ def load_config(self, path: Path) -> Config: def diagnostic_generator( self, uri: str, autofix: bool = False - ) -> Generator[Result, bool, Optional[FileContent]] | None: + ) -> Optional[Generator[Result, bool, Optional[FileContent]]]: """ LSP wrapper (provides document state from `pygls`) for `fixit_bytes`. """ @@ -125,7 +125,7 @@ def on_did_open(self, params: DidOpenTextDocumentParams) -> None: def on_did_change(self, params: DidChangeTextDocumentParams) -> None: self.validate(params.text_document.uri, params.text_document.version) - def format(self, params: DocumentFormattingParams) -> List[TextEdit] | None: + def format(self, params: DocumentFormattingParams) -> Optional[List[TextEdit]]: generator = self.diagnostic_generator(params.text_document.uri, autofix=True) if generator is None: return None @@ -164,7 +164,7 @@ class Debouncer: def __init__(self, f: Callable[..., Any], interval: float) -> None: self.f = f self.interval = interval - self._timer: threading.Timer | None = None + self._timer: Optional[threading.Timer] = None self._lock = threading.Lock() def __call__(self, *args: Any, **kwargs: Any) -> None: diff --git a/src/fixit/rule.py b/src/fixit/rule.py index a3f8eb40..954972b1 100644 --- a/src/fixit/rule.py +++ b/src/fixit/rule.py @@ -114,7 +114,7 @@ def __init_subclass__(cls) -> None: def __str__(self) -> str: return f"{self.__class__.__module__}:{self.__class__.__name__}" - _visit_hook: VisitHook | None = None + _visit_hook: Optional[VisitHook] = None def node_comments(self, node: CSTNode) -> Generator[str, None, None]: """ @@ -125,9 +125,11 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: while not isinstance(node, Module): # trailing_whitespace can either be a property of the node itself, or in # case of blocks, be part of the block's body element - tw: TrailingWhitespace | None = getattr(node, "trailing_whitespace", None) + tw: Optional[TrailingWhitespace] = getattr( + node, "trailing_whitespace", None + ) if tw is None: - body: BaseSuite | None = getattr(node, "body", None) + body: Optional[BaseSuite] = getattr(node, "body", None) if isinstance(body, SimpleStatementSuite): tw = body.trailing_whitespace elif isinstance(body, IndentedBlock): @@ -136,20 +138,20 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: if tw and tw.comment: yield tw.comment.value - comma: Comma | None = getattr(node, "comma", None) + comma: Optional[Comma] = getattr(node, "comma", None) if isinstance(comma, Comma): tw = getattr(comma.whitespace_after, "first_line", None) if tw and tw.comment: yield tw.comment.value - rb: RightSquareBracket | None = getattr(node, "rbracket", None) + rb: Optional[RightSquareBracket] = getattr(node, "rbracket", None) if rb is not None: tw = getattr(rb.whitespace_before, "first_line", None) if tw and tw.comment: yield tw.comment.value - el: Sequence[EmptyLine] | None = None - lb: LeftSquareBracket | None = getattr(node, "lbracket", None) + el: Optional[Sequence[EmptyLine]] = None + lb: Optional[LeftSquareBracket] = getattr(node, "lbracket", None) if lb is not None: el = getattr(lb.whitespace_after, "empty_lines", None) if el is not None: @@ -163,7 +165,7 @@ def node_comments(self, node: CSTNode) -> Generator[str, None, None]: if line.comment: yield line.comment.value - ll: Sequence[EmptyLine] | None = getattr(node, "leading_lines", None) + ll: Optional[Sequence[EmptyLine]] = getattr(node, "leading_lines", None) if ll is not None: for line in ll: if line.comment: @@ -217,10 +219,10 @@ def ignore_lint(self, node: CSTNode) -> bool: def report( self, node: CSTNode, - message: str | None = None, + message: Optional[str] = None, *, - position: Union[CodePosition, CodeRange] | None = None, - replacement: NodeReplacement[CSTNode] | None = None, + position: Optional[Union[CodePosition, CodeRange]] = None, + replacement: Optional[NodeReplacement[CSTNode]] = None, ) -> None: """ Report a lint rule violation. diff --git a/src/fixit/rules/deprecated_abc_import.py b/src/fixit/rules/deprecated_abc_import.py index adac9cc1..e9917cab 100644 --- a/src/fixit/rules/deprecated_abc_import.py +++ b/src/fixit/rules/deprecated_abc_import.py @@ -8,10 +8,10 @@ import libcst as cst import libcst.matchers as m -from libcst.metadata import ParentNodeProvider - from fixit import Invalid, LintRule, Valid +from libcst.metadata import ParentNodeProvider + # The ABCs that have been moved to `collections.abc` ABCS = frozenset( @@ -222,7 +222,7 @@ def visit_ImportFrom(self, node: cst.ImportFrom) -> None: def get_import_from( self, node: Union[cst.SimpleStatementLine, cst.BaseCompoundStatement] - ) -> cst.ImportFrom | None: + ) -> Optional[cst.ImportFrom]: """ Iterate over a Statement Sequence and return a Statement if it is a `cst.ImportFrom` statement. diff --git a/src/fixit/rules/no_namedtuple.py b/src/fixit/rules/no_namedtuple.py index d1fd190f..cae8d0cd 100644 --- a/src/fixit/rules/no_namedtuple.py +++ b/src/fixit/rules/no_namedtuple.py @@ -6,10 +6,10 @@ from typing import List, Optional, Sequence, Tuple import libcst as cst -from libcst import ensure_type, MaybeSentinel, parse_expression -from libcst.metadata import QualifiedName, QualifiedNameProvider, QualifiedNameSource from fixit import Invalid, LintRule, Valid +from libcst import ensure_type, MaybeSentinel, parse_expression +from libcst.metadata import QualifiedName, QualifiedNameProvider, QualifiedNameSource class NoNamedTuple(LintRule): @@ -186,7 +186,7 @@ def partition_bases( self, original_bases: Sequence[cst.Arg] ) -> Tuple[Optional[cst.Arg], List[cst.Arg]]: # Returns a tuple of NamedTuple base object if it exists, and a list of non-NamedTuple bases - namedtuple_base: cst.Arg | None = None + namedtuple_base: Optional[cst.Arg] = None new_bases: List[cst.Arg] = [] for base_class in original_bases: if QualifiedNameProvider.has_name( diff --git a/src/fixit/rules/no_static_if_condition.py b/src/fixit/rules/no_static_if_condition.py index 4893bf33..dd7fba4c 100644 --- a/src/fixit/rules/no_static_if_condition.py +++ b/src/fixit/rules/no_static_if_condition.py @@ -115,7 +115,7 @@ async def some_func() -> none: ] @classmethod - def _extract_static_bool(cls, node: cst.BaseExpression) -> bool | None: + def _extract_static_bool(cls, node: cst.BaseExpression) -> Optional[bool]: if m.matches(node, m.Call()): # cannot reason about function calls return None diff --git a/src/fixit/rules/rewrite_to_comprehension.py b/src/fixit/rules/rewrite_to_comprehension.py index c3a5787b..68e0953c 100644 --- a/src/fixit/rules/rewrite_to_comprehension.py +++ b/src/fixit/rules/rewrite_to_comprehension.py @@ -114,7 +114,7 @@ def visit_Call(self, node: cst.Call) -> None: exp = cst.ensure_type(node.args[0].value, cst.ListComp) message_formatter = UNNECESSARY_LIST_COMPREHENSION - replacement: Union[cst.Call, cst.BaseComp] | None = None + replacement: Optional[Union[cst.Call, cst.BaseComp]] = None if call_name == "list": replacement = node.deep_replace( node, cst.ListComp(elt=exp.elt, for_in=exp.for_in) diff --git a/src/fixit/rules/use_fstring.py b/src/fixit/rules/use_fstring.py index 605a1dc6..b56ebcf5 100644 --- a/src/fixit/rules/use_fstring.py +++ b/src/fixit/rules/use_fstring.py @@ -144,9 +144,9 @@ class UseFstring(LintRule): ), ] - _codegen: Callable[[cst.CSTNode], str] | None + _codegen: Optional[Callable[[cst.CSTNode], str]] - def visit_Module(self, node: cst.Module) -> bool | None: + def visit_Module(self, node: cst.Module) -> Optional[bool]: self._codegen = node.code_for_node return super().visit_Module(node) diff --git a/src/fixit/tests/ftypes.py b/src/fixit/tests/ftypes.py index 5467c65a..292aac01 100644 --- a/src/fixit/tests/ftypes.py +++ b/src/fixit/tests/ftypes.py @@ -106,7 +106,7 @@ def test_tags_parser(self) -> None: def test_tags_bool(self) -> None: Tags = ftypes.Tags - tags: str | None + tags: Optional[str] for tags in ( "hello", diff --git a/src/fixit/upgrade/deprecated_testcase_keywords.py b/src/fixit/upgrade/deprecated_testcase_keywords.py index d82f7753..863f3a03 100755 --- a/src/fixit/upgrade/deprecated_testcase_keywords.py +++ b/src/fixit/upgrade/deprecated_testcase_keywords.py @@ -6,6 +6,8 @@ from typing import Optional +from fixit import Invalid, LintRule, Valid + from libcst import ( Arg, BaseExpression, @@ -17,8 +19,6 @@ ) from libcst.metadata import QualifiedNameProvider -from fixit import Invalid, LintRule, Valid - class FixitDeprecatedTestCaseKeywords(LintRule): """ @@ -73,8 +73,8 @@ def visit_Call(self, node: Call) -> None: self.convert_linecol_to_range(node) def convert_linecol_to_range(self, node: Call) -> None: - line: BaseExpression | None = None - col: BaseExpression | None = None + line: Optional[BaseExpression] = None + col: Optional[BaseExpression] = None index_to_remove = [] for ind, arg in enumerate(node.args): if not arg.keyword: diff --git a/src/fixit/util.py b/src/fixit/util.py index a13f6dce..44ce518f 100644 --- a/src/fixit/util.py +++ b/src/fixit/util.py @@ -36,7 +36,7 @@ class capture(Generic[Yield, Send, Return]): def __init__(self, generator: Generator[Yield, Send, Return]) -> None: self.generator = generator - self._send: Send | None = None + self._send: Optional[Send] = None self._result: Union[Return, object] = Sentinel def __iter__(self) -> Generator[Yield, Send, Return]: From 9536d24ef1e0884851a3496fb61e0ab9a6486b9d Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Tue, 19 Nov 2024 10:25:29 -0500 Subject: [PATCH 5/7] change fixit config to 3.8 --- docs/guide/builtins.rst | 2 ++ pyproject.toml | 2 +- src/fixit/rules/deprecated_abc_import.py | 4 ++-- src/fixit/rules/no_namedtuple.py | 4 ++-- src/fixit/upgrade/deprecated_testcase_keywords.py | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/guide/builtins.rst b/docs/guide/builtins.rst index b5bb342c..c588187d 100644 --- a/docs/guide/builtins.rst +++ b/docs/guide/builtins.rst @@ -728,6 +728,8 @@ Built-in Rules .. attribute:: AUTOFIX :type: Yes + .. attribute:: PYTHON_VERSION + :type: '>= 3.10' .. attribute:: VALID diff --git a/pyproject.toml b/pyproject.toml index 5eb0080c..f3689299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ target-version = ["py38"] [tool.fixit] enable = ["fixit.rules"] -python-version = "3.10" +python-version = "3.8" formatter = "ufmt" [[tool.fixit.overrides]] diff --git a/src/fixit/rules/deprecated_abc_import.py b/src/fixit/rules/deprecated_abc_import.py index e9917cab..03a3607c 100644 --- a/src/fixit/rules/deprecated_abc_import.py +++ b/src/fixit/rules/deprecated_abc_import.py @@ -8,10 +8,10 @@ import libcst as cst import libcst.matchers as m -from fixit import Invalid, LintRule, Valid - from libcst.metadata import ParentNodeProvider +from fixit import Invalid, LintRule, Valid + # The ABCs that have been moved to `collections.abc` ABCS = frozenset( diff --git a/src/fixit/rules/no_namedtuple.py b/src/fixit/rules/no_namedtuple.py index cae8d0cd..95854566 100644 --- a/src/fixit/rules/no_namedtuple.py +++ b/src/fixit/rules/no_namedtuple.py @@ -6,11 +6,11 @@ from typing import List, Optional, Sequence, Tuple import libcst as cst - -from fixit import Invalid, LintRule, Valid from libcst import ensure_type, MaybeSentinel, parse_expression from libcst.metadata import QualifiedName, QualifiedNameProvider, QualifiedNameSource +from fixit import Invalid, LintRule, Valid + class NoNamedTuple(LintRule): """ diff --git a/src/fixit/upgrade/deprecated_testcase_keywords.py b/src/fixit/upgrade/deprecated_testcase_keywords.py index 863f3a03..f38e4599 100755 --- a/src/fixit/upgrade/deprecated_testcase_keywords.py +++ b/src/fixit/upgrade/deprecated_testcase_keywords.py @@ -6,8 +6,6 @@ from typing import Optional -from fixit import Invalid, LintRule, Valid - from libcst import ( Arg, BaseExpression, @@ -19,6 +17,8 @@ ) from libcst.metadata import QualifiedNameProvider +from fixit import Invalid, LintRule, Valid + class FixitDeprecatedTestCaseKeywords(LintRule): """ From 8de94535df9079f9cef335d0f519d9096d2da28a Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Wed, 5 Nov 2025 00:43:42 -0500 Subject: [PATCH 6/7] Update src/fixit/rules/replace_optional_type_annotation.py Co-authored-by: Bowie Chen <543091+bowiechen@users.noreply.github.com> --- src/fixit/rules/replace_optional_type_annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixit/rules/replace_optional_type_annotation.py b/src/fixit/rules/replace_optional_type_annotation.py index 8f4a2fa9..248ecccc 100644 --- a/src/fixit/rules/replace_optional_type_annotation.py +++ b/src/fixit/rules/replace_optional_type_annotation.py @@ -93,7 +93,7 @@ def leave_Annotation(self, original_node: cst.Annotation) -> None: nones += 1 else: indexes.append(s.slice) - if not (nones > 1) and len(indexes) == 1: + if nones <= 1 and len(indexes) == 1: inner_type = cst.ensure_type(indexes[0], cst.Index).value replacement = original_node.with_changes( annotation=cst.BinaryOperation( From f8225e0d07a6cb31646f8bfca03a94a71c3aa829 Mon Sep 17 00:00:00 2001 From: Danny Yang Date: Wed, 5 Nov 2025 00:44:01 -0500 Subject: [PATCH 7/7] Update pyproject.toml Co-authored-by: Bowie Chen <543091+bowiechen@users.noreply.github.com> --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f3689299..53850f81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,12 @@ target-version = ["py38"] [tool.fixit] enable = ["fixit.rules"] python-version = "3.8" -formatter = "ufmt" +[tool.black] +target-version = ["py310"] + +[tool.fixit] +enable = ["fixit.rules"] +python-version = "3.10" [[tool.fixit.overrides]] path = "examples"