Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyupgrade for openSUSE:Factory checked in at 2021-05-02 18:36:06 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyupgrade (Old) and /work/SRC/openSUSE:Factory/.python-pyupgrade.new.1947 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyupgrade" Sun May 2 18:36:06 2021 rev:8 rq:889751 version:2.13.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyupgrade/python-pyupgrade.changes 2021-04-01 14:18:32.080094240 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyupgrade.new.1947/python-pyupgrade.changes 2021-05-02 18:39:14.108150348 +0200 @@ -1,0 +2,12 @@ +Sat Apr 24 18:16:48 UTC 2021 - Sebastian Wagner <sebix+novell....@sebix.at> + +- Update to version 2.13.0: + - move pep563 rewrite to py311 +- Update to version 2.12.0: + - document annotation unquote + - use dict comprehension + - rewrite typeddict even with total= option + - Update azure-pipelines template repositories + - _to_fstring: Use original token stream instead of unparsed AST + +------------------------------------------------------------------- Old: ---- python-pyupgrade-2.11.0.tar.gz New: ---- python-pyupgrade-2.13.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyupgrade.spec ++++++ --- /var/tmp/diff_new_pack.6RqbS3/_old 2021-05-02 18:39:14.592148285 +0200 +++ /var/tmp/diff_new_pack.6RqbS3/_new 2021-05-02 18:39:14.596148268 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-pyupgrade -Version: 2.11.0 +Version: 2.13.0 Release: 0 Summary: A tool to automatically upgrade syntax for newer versions License: MIT ++++++ python-pyupgrade-2.11.0.tar.gz -> python-pyupgrade-2.13.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/.pre-commit-config.yaml new/pyupgrade-2.13.0/.pre-commit-config.yaml --- old/pyupgrade-2.11.0/.pre-commit-config.yaml 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/.pre-commit-config.yaml 2021-04-20 23:21:45.000000000 +0200 @@ -14,13 +14,13 @@ rev: v1.17.0 hooks: - id: setup-cfg-fmt -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 +- repo: https://github.com/PyCQA/flake8 + rev: 3.9.1 hooks: - id: flake8 additional_dependencies: [flake8-typing-imports==1.7.0] - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.5 + rev: v1.5.6 hooks: - id: autopep8 - repo: https://github.com/asottile/reorder_python_imports @@ -34,7 +34,7 @@ - id: add-trailing-comma args: [--py36-plus] - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.13.0 hooks: - id: pyupgrade args: [--py36-plus] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/README.md new/pyupgrade-2.13.0/README.md --- old/pyupgrade-2.11.0/README.md 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/README.md 2021-04-20 23:21:45.000000000 +0200 @@ -20,7 +20,7 @@ ```yaml - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.13.0 hooks: - id: pyupgrade ``` @@ -505,6 +505,7 @@ ... ``` + ### pep 604 typing rewrites Availability: @@ -523,3 +524,15 @@ +def f() -> int | str: ... ``` + + +### remove quoted annotations + +Availability: +- File imports `from __future__ import annotations` +- `--py311-plus` is passed on the commandline. + +```diff +-def f(x: 'queue.Queue[int]') -> C: ++def f(x: queue.Queue[int]) -> C: +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/azure-pipelines.yml new/pyupgrade-2.13.0/azure-pipelines.yml --- old/pyupgrade-2.11.0/azure-pipelines.yml 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/azure-pipelines.yml 2021-04-20 23:21:45.000000000 +0200 @@ -10,7 +10,7 @@ type: github endpoint: github name: asottile/azure-pipeline-templates - ref: refs/tags/v2.0.0 + ref: refs/tags/v2.1.0 jobs: - template: job--python-tox.yml@asottile diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/pyupgrade/_main.py new/pyupgrade-2.13.0/pyupgrade/_main.py --- old/pyupgrade-2.11.0/pyupgrade/_main.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/pyupgrade/_main.py 2021-04-20 23:21:45.000000000 +0200 @@ -35,8 +35,8 @@ from pyupgrade._token_helpers import CLOSING from pyupgrade._token_helpers import KEYWORDS from pyupgrade._token_helpers import OPENING +from pyupgrade._token_helpers import parse_call_args from pyupgrade._token_helpers import remove_brace -from pyupgrade._token_helpers import victims DotFormatPart = Tuple[str, Optional[str], Optional[str], Optional[str]] @@ -510,26 +510,12 @@ return tokens_to_src(tokens).lstrip() -def _simple_arg(arg: ast.expr) -> bool: - return ( - isinstance(arg, ast.Name) or - (isinstance(arg, ast.Attribute) and _simple_arg(arg.value)) or - ( - isinstance(arg, ast.Call) and - _simple_arg(arg.func) and - not arg.args and not arg.keywords - ) - ) - - -def _format_params(call: ast.Call) -> Dict[str, str]: - params = {} - for i, arg in enumerate(call.args): - params[str(i)] = _unparse(arg) +def _format_params(call: ast.Call) -> Set[str]: + params = {str(i) for i, arg in enumerate(call.args)} for kwd in call.keywords: # kwd.arg can't be None here because we exclude starargs assert kwd.arg is not None - params[kwd.arg] = _unparse(kwd.value) + params.add(kwd.arg) return params @@ -568,8 +554,6 @@ isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Str) and node.func.attr == 'format' and - all(_simple_arg(arg) for arg in node.args) and - all(_simple_arg(k.value) for k in node.keywords) and not has_starargs(node) ): return None @@ -646,7 +630,11 @@ 'TypedDict', ) and len(node.value.args) == 1 and - len(node.value.keywords) > 0 + len(node.value.keywords) > 0 and + not any( + keyword.arg == 'total' + for keyword in node.value.keywords + ) ): self.kw_typed_dicts[ast_to_offset(node)] = node.value elif ( @@ -656,7 +644,17 @@ 'TypedDict', ) and len(node.value.args) == 2 and - not node.value.keywords and + ( + not node.value.keywords or + ( + len(node.value.keywords) == 1 and + node.value.keywords[0].arg == 'total' and + isinstance( + node.value.keywords[0].value, + (ast.Constant, ast.NameConstant), + ) + ) + ) and isinstance(node.value.args[1], ast.Dict) and node.value.args[1].keys and all( @@ -676,8 +674,6 @@ return node.id elif isinstance(node, ast.Attribute): return ''.join((_unparse(node.value), '.', node.attr)) - elif isinstance(node, ast.Call): - return '{}()'.format(_unparse(node.func)) elif isinstance(node, ast.Subscript): if sys.version_info >= (3, 9): # pragma: no cover (py39+) node_slice: ast.expr = node.slice @@ -692,7 +688,7 @@ slice_s = ', '.join(_unparse(elt) for elt in node_slice.elts) else: slice_s = _unparse(node_slice) - return '{}[{}]'.format(_unparse(node.value), slice_s) + return f'{_unparse(node.value)}[{slice_s}]' elif isinstance(node, ast.Str): return repr(node.s) elif isinstance(node, ast.Ellipsis): @@ -705,8 +701,28 @@ raise NotImplementedError(ast.dump(node)) -def _to_fstring(src: str, call: ast.Call) -> str: - params = _format_params(call) +def _skip_unimportant_ws(tokens: List[Token], i: int) -> int: + while tokens[i].name == 'UNIMPORTANT_WS': + i += 1 + return i + + +def _to_fstring( + src: str, tokens: List[Token], args: List[Tuple[int, int]], +) -> str: + params = {} + i = 0 + for start, end in args: + start = _skip_unimportant_ws(tokens, start) + if tokens[start].name == 'NAME': + after = _skip_unimportant_ws(tokens, start + 1) + if tokens[after].src == '=': # keyword argument + params[tokens[start].src] = tokens_to_src( + tokens[after + 1:end], + ).strip() + continue + params[str(i)] = tokens_to_src(tokens[start:end]).strip() + i += 1 parts = [] i = 0 @@ -720,12 +736,12 @@ return unparse_parsed_string(parts) -def _replace_typed_class( +def _typed_class_replacement( tokens: List[Token], i: int, call: ast.Call, types: Dict[str, ast.expr], -) -> None: +) -> Tuple[int, str]: if i > 0 and tokens[i - 1].name in {'INDENT', UNIMPORTANT_WS}: indent = f'{tokens[i - 1].src}{" " * 4}' else: @@ -738,8 +754,7 @@ end += 1 attrs = '\n'.join(f'{indent}{k}: {_unparse(v)}' for k, v in types.items()) - src = f'class {tokens[i].src}({_unparse(call.func)}):\n{attrs}' - tokens[i:end] = [Token('CODE', src)] + return end, attrs def _fix_py36_plus(contents_text: str) -> str: @@ -765,8 +780,6 @@ return contents_text for i, token in reversed_enumerate(tokens): if token.offset in visitor.fstrings: - node = visitor.fstrings[token.offset] - # TODO: handle \N escape sequences if r'\N' in token.src: continue @@ -775,29 +788,37 @@ if tokens_to_src(tokens[i + 1:paren + 1]) != '.format(': continue - # we don't actually care about arg position, so we pass `node` - fmt_victims = victims(tokens, paren, node, gen=False) - end = fmt_victims.ends[-1] + args, end = parse_call_args(tokens, paren) # if it spans more than one line, bail - if tokens[end].line != token.line: + if tokens[end - 1].line != token.line: + continue + + args_src = tokens_to_src(tokens[paren:end]) + if '\\' in args_src or '"' in args_src or "'" in args_src: continue - tokens[i] = token._replace(src=_to_fstring(token.src, node)) - del tokens[i + 1:end + 1] + tokens[i] = token._replace( + src=_to_fstring(token.src, tokens, args), + ) + del tokens[i + 1:end] elif token.offset in visitor.named_tuples and token.name == 'NAME': call = visitor.named_tuples[token.offset] types: Dict[str, ast.expr] = { tup.elts[0].s: tup.elts[1] # type: ignore # (checked above) for tup in call.args[1].elts # type: ignore # (checked above) } - _replace_typed_class(tokens, i, call, types) + end, attrs = _typed_class_replacement(tokens, i, call, types) + src = f'class {tokens[i].src}({_unparse(call.func)}):\n{attrs}' + tokens[i:end] = [Token('CODE', src)] elif token.offset in visitor.kw_typed_dicts and token.name == 'NAME': call = visitor.kw_typed_dicts[token.offset] types = { arg.arg: arg.value # type: ignore # (checked above) for arg in call.keywords } - _replace_typed_class(tokens, i, call, types) + end, attrs = _typed_class_replacement(tokens, i, call, types) + src = f'class {tokens[i].src}({_unparse(call.func)}):\n{attrs}' + tokens[i:end] = [Token('CODE', src)] elif token.offset in visitor.dict_typed_dicts and token.name == 'NAME': call = visitor.dict_typed_dicts[token.offset] types = { @@ -807,7 +828,20 @@ call.args[1].values, # type: ignore # (checked above) ) } - _replace_typed_class(tokens, i, call, types) + if call.keywords: + total = call.keywords[0].value.value # type: ignore # (checked above) # noqa: E501 + end, attrs = _typed_class_replacement(tokens, i, call, types) + src = ( + f'class {tokens[i].src}(' + f'{_unparse(call.func)}, total={total}' + f'):\n' + f'{attrs}' + ) + tokens[i:end] = [Token('CODE', src)] + else: + end, attrs = _typed_class_replacement(tokens, i, call, types) + src = f'class {tokens[i].src}({_unparse(call.func)}):\n{attrs}' + tokens[i:end] = [Token('CODE', src)] return tokens_to_src(tokens) @@ -882,6 +916,10 @@ '--py310-plus', action='store_const', dest='min_version', const=(3, 10), ) + parser.add_argument( + '--py311-plus', + action='store_const', dest='min_version', const=(3, 11), + ) args = parser.parse_args(argv) ret = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/pyupgrade/_plugins/oserror_aliases.py new/pyupgrade-2.13.0/pyupgrade/_plugins/oserror_aliases.py --- old/pyupgrade-2.11.0/pyupgrade/_plugins/oserror_aliases.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/pyupgrade/_plugins/oserror_aliases.py 2021-04-20 23:21:45.000000000 +0200 @@ -62,7 +62,7 @@ if len(unique_args) > 1: joined = '({})'.format(', '.join(unique_args)) elif tokens[start - 1].name != 'UNIMPORTANT_WS': - joined = ' {}'.format(unique_args[0]) + joined = f' {unique_args[0]}' else: joined = unique_args[0] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/pyupgrade/_plugins/typing_pep563.py new/pyupgrade-2.13.0/pyupgrade/_plugins/typing_pep563.py --- old/pyupgrade-2.11.0/pyupgrade/_plugins/typing_pep563.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/pyupgrade/_plugins/typing_pep563.py 2021-04-20 23:21:45.000000000 +0200 @@ -18,7 +18,7 @@ def _supported_version(state: State) -> bool: return ( - state.settings.min_version >= (3, 10) or + state.settings.min_version >= (3, 11) or 'annotations' in state.from_imports['__future__'] ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/setup.cfg new/pyupgrade-2.13.0/setup.cfg --- old/pyupgrade-2.11.0/setup.cfg 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/setup.cfg 2021-04-20 23:21:45.000000000 +0200 @@ -1,6 +1,6 @@ [metadata] name = pyupgrade -version = 2.11.0 +version = 2.13.0 description = A tool to automatically upgrade syntax for newer versions. long_description = file: README.md long_description_content_type = text/markdown diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/tests/features/fstrings_test.py new/pyupgrade-2.13.0/tests/features/fstrings_test.py --- old/pyupgrade-2.11.0/tests/features/fstrings_test.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/tests/features/fstrings_test.py 2021-04-20 23:21:45.000000000 +0200 @@ -30,6 +30,10 @@ r'"\N{snowman} {}".format(a)', # not enough placeholders / placeholders missing '"{}{}".format(a)', '"{a}{b}".format(a=a)', + # backslashes and quotes cannot nest + r'''"{}".format(a['\\'])''', + '"{}".format(a["b"])', + "'{}'.format(a['b'])", ), ) def test_fix_fstrings_noop(s): @@ -50,6 +54,7 @@ ('"hello {}!".format(name)', 'f"hello {name}!"'), ('"{}{{}}{}".format(escaped, y)', 'f"{escaped}{{}}{y}"'), ('"{}{b}{}".format(a, c, b=b)', 'f"{a}{b}{c}"'), + ('"{}".format(0x0)', 'f"{0x0}"'), # TODO: poor man's f-strings? # '"{foo}".format(**locals())' ), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/tests/features/typing_pep563_test.py new/pyupgrade-2.13.0/tests/features/typing_pep563_test.py --- old/pyupgrade-2.11.0/tests/features/typing_pep563_test.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/tests/features/typing_pep563_test.py 2021-04-20 23:21:45.000000000 +0200 @@ -13,7 +13,7 @@ 'from typing import Literal\n' 'x: "str"\n', (2, 7), - id='not python 3.10+', + id='not python 3.11+', ), pytest.param( 'from __future__ import annotations\n' @@ -346,6 +346,11 @@ assert ret == expected +def test_replaced_for_minimum_version(): + ret = _fix_plugins('x: "int"', settings=Settings(min_version=(3, 11))) + assert ret == 'x: int' + + @pytest.mark.xfail( sys.version_info < (3, 8), reason='posonly args not available in Python3.7', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyupgrade-2.11.0/tests/features/typing_typed_dict_test.py new/pyupgrade-2.13.0/tests/features/typing_typed_dict_test.py --- old/pyupgrade-2.11.0/tests/features/typing_typed_dict_test.py 2021-03-20 22:13:59.000000000 +0100 +++ new/pyupgrade-2.13.0/tests/features/typing_typed_dict_test.py 2021-04-20 23:21:45.000000000 +0200 @@ -39,6 +39,10 @@ 'D = typing.TypedDict("D", **types)', id='starstarkwargs', ), + pytest.param( + 'D = typing.TypedDict("D", x=int, total=False)', + id='kw_typed_dict with total', + ), ), ) def test_typing_typed_dict_noop(s): @@ -79,6 +83,16 @@ id='TypedDict from dict literal', ), pytest.param( + 'import typing\n' + 'D = typing.TypedDict("D", {"a": int}, total=False)\n', + + 'import typing\n' + 'class D(typing.TypedDict, total=False):\n' + ' a: int\n', + + id='TypedDict from dict literal with total', + ), + pytest.param( 'from typing_extensions import TypedDict\n' 'D = TypedDict("D", a=int)\n', @@ -99,6 +113,16 @@ id='keyword TypedDict from typing_extensions', ), pytest.param( + 'import typing_extensions\n' + 'D = typing_extensions.TypedDict("D", {"a": int}, total=True)\n', + + 'import typing_extensions\n' + 'class D(typing_extensions.TypedDict, total=True):\n' + ' a: int\n', + + id='keyword TypedDict from typing_extensions, with total', + ), + pytest.param( 'from typing import List\n' 'from typing_extensions import TypedDict\n' 'Foo = TypedDict("Foo", {"lsts": List[List[int]]})',