[
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 90\nignore =\n  B3,\n  DW12,\n  W504,\n  W503,\n  E203\nexclude =\n  src/test_com2ann.py\n"
  },
  {
    "path": ".flake8-tests",
    "content": "[flake8]\nmax-line-length = 110\nignore =\n  E306,\n  E701,\n  E704,\n  F811,\n  B3,\n  DW12\nexclude =\n  src/com2ann.py\n"
  },
  {
    "path": ".github/workflows/lint_python.yml",
    "content": "name: lint_python\non: [pull_request, push]\njobs:\n  lint_python:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: 3.x\n      - run: pip install --upgrade pip wheel\n      - run: pip install -r test-requirements.txt\n      - run: bandit --recursive --skip B101 .  # B101 is assert statements\n      - run: black --check .\n      - run: codespell --ignore-words-list=\"fo\"\n      - run: flake8 . --count --max-complexity=12 --max-line-length=89 --show-source --statistics\n      - run: isort --check-only --profile black .\n      - run: |\n          pip install --editable .\n          mkdir --parents --verbose .mypy_cache\n      - run: mypy --ignore-missing-imports --install-types --non-interactive .\n      - run: pytest .\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# IPython Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# dotenv\n.env\n\n# virtualenv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n.idea\n"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "content": "- id: com2ann\r\n  name: com2ann\r\n  entry: com2ann\r\n  language: python\r\n  types: [ file ]\r\n  files: .*\\.(py|pyi|md|rst)"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017-2019 Ivan Levkivskyi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\ninclude README.md\ninclude setup.py\ninclude src/com2ann.py\ninclude src/test_com2ann.py\n"
  },
  {
    "path": "README.md",
    "content": "com2ann\n=======\n\n[![Build Status](https://travis-ci.org/ilevkivskyi/com2ann.svg)](https://travis-ci.org/ilevkivskyi/com2ann)\n[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)\n\nTool for translation of type comments to type annotations in Python.\n\nThe tool requires Python 3.8 to run. But the supported target code version\nis Python 3.4+ (can be specified with `--python-minor-version`).\n\nCurrently, the tool translates function and assignment type comments to\ntype annotations. For example this code:\n```python\nfrom typing import Optional, Final\n\nMAX_LEVEL = 80  # type: Final\n\nclass Template:\n    default = None  # type: Optional[str]\n\n    def apply(self, value, **opts):\n        # type: (str, **bool) -> str\n        ...\n```\nwill be translated to:\n```python\nfrom typing import Optional, Final\n\nMAX_LEVEL: Final = 80\n\nclass Template:\n    default: Optional[str] = None\n\n    def apply(self, value: str, **opts: bool) -> str:\n        ...\n```\n\nThe philosophy of the tool is to be minimally invasive, and preserve original\nformatting as much as possible. This is why the tool doesn't use un-parse.\n\nThe only (optional) formatting code modification is wrapping long function\nsignatures. To specify the maximal length, use `--wrap-signatures MAX_LENGTH`.\nThe signatures are wrapped one argument per line (after each comma), for example:\n```python\n    def apply(self,\n              value: str,\n              **opts: bool) -> str:\n        ...\n```\n\nFor working with stubs, there are two additional options for assignments:\n`--drop-ellipsis` and `--drop-none`. They will result in omitting the redundant\nright hand sides. For example, this:\n```python\nvar = ...  # type: List[int]\nclass Test:\n    attr = None  # type: str\n```\nwill be translated with such options to:\n```python\nvar: List[int]\nclass Test:\n    attr: str\n```\n### Usage\n$ `com2ann --help`\n```\nusage: com2ann [-h] [-o OUTFILE] [-s] [-n] [-e] [-i] [-w WRAP_SIGNATURES]\n               [-v PYTHON_MINOR_VERSION]\n               infile\n\nHelper module to translate type comments to type annotations. The key idea of\nthis module is to perform the translation while preserving the original\nformatting as much as possible. We try to be not opinionated about code\nformatting and therefore work at the source code and tokenizer level instead\nof modifying AST and using un-parse. We are especially careful about\nassignment statements, and keep the placement of additional (non-type)\ncomments. For function definitions, we might introduce some formatting\nmodifications, if the original formatting was too tricky.\n\npositional arguments:\n  infile                input file or directory for translation, must contain\n                        no syntax errors; if --outfile is not given,\n                        translation is made *in place*\n\noptional arguments:\n  -h, --help            show this help message and exit\n  -o OUTFILE, --outfile OUTFILE\n                        output file or directory, will be overwritten if\n                        exists, defaults to input file or directory\n  -s, --silent          do not print summary for line numbers of translated\n                        and rejected comments\n  -n, --drop-none       drop any None as assignment value during translation\n                        if it is annotated by a type comment\n  -e, --drop-ellipsis   drop any Ellipsis (...) as assignment value during\n                        translation if it is annotated by a type comment\n  -i, --add-future-imports\n                        add 'from __future__ import annotations' to any file\n                        where type comments were successfully translated\n  -w WRAP_SIGNATURES, --wrap-signatures WRAP_SIGNATURES\n                        wrap function headers that are longer than given\n                        length\n  -v PYTHON_MINOR_VERSION, --python-minor-version PYTHON_MINOR_VERSION\n                        Python 3 minor version to use to parse the files\n```\n"
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\ndisallow_untyped_calls = True\ndisallow_untyped_defs = True\ndisallow_incomplete_defs = True\ncheck_untyped_defs = True\ndisallow_subclassing_any = True\nwarn_no_return = True\nstrict_optional = True\nno_implicit_optional = True\ndisallow_any_generics = True\ndisallow_any_unimported = True\nwarn_redundant_casts = True\nwarn_unused_configs = True\nstrict_equality = True\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n\nimport sys\n\nfrom setuptools import setup\n\nif sys.version_info < (3, 8, 0):\n    sys.stderr.write(\"ERROR: You need Python 3.8 or later to use com2ann.\\n\")\n    exit(1)\n\nversion = \"0.3.0\"\ndescription = \"Tool to translate type comments to annotations.\"\nlong_description = \"\"\"\ncom2ann\n=======\n\nTool for translation of type comments to type annotations in Python.\n\nThis tool requires Python 3.8 to run. But the supported target code version\nis Python 3.4+ (can be specified with ``--python-minor-version``).\n\nCurrently, the tool translates function and assignment type comments to\ntype annotations.\n\nThe philosophy of of the tool is too minimally invasive, and preserve original\nformatting as much as possible. This is why the tool doesn't use un-parse.\n\"\"\".lstrip()\n\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Environment :: Console\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Topic :: Software Development\",\n]\n\nsetup(\n    name=\"com2ann\",\n    version=version,\n    description=description,\n    long_description=long_description,\n    author=\"Ivan Levkivskyi\",\n    author_email=\"levkivskyi@gmail.com\",\n    url=\"https://github.com/ilevkivskyi/com2ann\",\n    license=\"MIT\",\n    keywords=\"typing function annotations type hints \"\n    \"type comments variable annotations\",\n    python_requires=\">=3.8\",\n    package_dir={\"\": \"src\"},\n    py_modules=[\"com2ann\"],\n    entry_points={\"console_scripts\": [\"com2ann=com2ann:main\"]},\n    classifiers=classifiers,\n)\n"
  },
  {
    "path": "src/com2ann.py",
    "content": "\"\"\"Helper module to translate type comments to type annotations.\n\nThe key idea of this module is to perform the translation while preserving\nthe original formatting as much as possible. We try to be not opinionated\nabout code formatting and therefore work at the source code and tokenizer level\ninstead of modifying AST and using un-parse.\n\nWe are especially careful about assignment statements, and keep the placement\nof additional (non-type) comments. For function definitions, we might introduce\nsome formatting modifications, if the original formatting was too tricky.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport ast\nimport pathlib\nimport re\nimport sys\nimport tokenize\nfrom collections import defaultdict\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nfrom io import BytesIO\nfrom tokenize import TokenInfo\nfrom typing import Any, DefaultDict, Sequence, Union\n\n__all__ = [\"com2ann\", \"TYPE_COM\"]\n\nTYPE_COM = re.compile(r\"\\s*#\\s*type\\s*:(.*)$\", flags=re.DOTALL)\n\n# For internal use only.\n_TRAILER = re.compile(r\"\\s*$\", flags=re.DOTALL)\n_NICE_IGNORE = re.compile(r\"\\s*# type: ignore(\\[\\S+\\])?\\s*$\", flags=re.DOTALL)\n\nFUTURE_IMPORT_WHITELIST = {\"str\", \"int\", \"bool\", \"None\"}\n\nUnsupported = Union[ast.For, ast.AsyncFor, ast.With, ast.AsyncWith]\nFunction = Union[ast.FunctionDef, ast.AsyncFunctionDef]\n\n\n@dataclass\nclass Options:\n    \"\"\"Config options, see details in main().\"\"\"\n\n    drop_none: bool\n    drop_ellipsis: bool\n    silent: bool\n    add_future_imports: bool = False\n    wrap_signatures: int = 0\n    python_minor_version: int = -1\n\n\nclass RvalueKind(Enum):\n    \"\"\"Special cases for assignment r.h.s.\"\"\"\n\n    OTHER = auto()\n    TUPLE = auto()\n    NONE = auto()\n    ELLIPSIS = auto()\n\n\n@dataclass\nclass AssignData:\n    \"\"\"Location data for translating assignment type comment.\"\"\"\n\n    type_comment: str\n\n    # Position where l.h.s. ends (may not include closing paren).\n    lvalue_end_line: int\n    lvalue_end_offset: int\n\n    # Position range for the r.h.s. (may also not include parentheses\n    # if they are redundant).\n    rvalue_start_line: int\n    rvalue_start_offset: int\n    rvalue_end_line: int\n    rvalue_end_offset: int\n\n    # Is there any r.h.s. that requires special treatment.\n    rvalue_kind: RvalueKind = RvalueKind.OTHER\n\n\n@dataclass\nclass ArgComment:\n    \"\"\"Location data for insertion of an argument annotation.\"\"\"\n\n    type_comment: str\n\n    # Place where a given argument ends, insert an annotation here.\n    arg_line: int\n    arg_end_offset: int\n\n    has_default: bool = False\n\n\n@dataclass\nclass FunctionData:\n    \"\"\"Location data for translating function comment.\"\"\"\n\n    arg_types: list[ArgComment]\n    ret_type: str | None\n\n    # The line where 'def' appears.\n    header_start_line: int\n    # This doesn't include any comments or whitespace-only lines.\n    body_first_line: int\n\n\nclass FileData:\n    \"\"\"Internal class describing global data on file.\"\"\"\n\n    def __init__(\n        self, lines: list[str], tokens: list[TokenInfo], tree: ast.AST\n    ) -> None:\n        # Source code lines.\n        self.lines = lines\n        # Tokens for the source code.\n        self.tokens = tokens\n        # Parsed tree (with type_comments = True).\n        self.tree = tree\n\n        # Map line number to token numbers. For example {1: [0, 1, 2, 3], 2: [4, 5]}\n        # means that first to fourth tokens are on the first line.\n        token_tab: DefaultDict[int, list[int]] = defaultdict(list)\n        for i, tok in enumerate(tokens):\n            token_tab[tok.start[0]].append(i)\n        self.token_tab = token_tab\n\n        # Basic translation logging.\n        self.success: list[int] = (\n            []\n        )  # list of lines where type comments where processed\n        self.fail: list[int] = []  # list of lines where type comments where rejected\n\n        # Types we have inserted during translation.\n        self.seen: set[str] = set()\n\n\nclass TypeCommentCollector(ast.NodeVisitor):\n    \"\"\"Visitor to collect type comments from an AST.\n\n    This also records other necessary information such as location data for\n    various nodes and their kinds.\n    \"\"\"\n\n    def __init__(self, silent: bool) -> None:\n        super().__init__()\n        self.silent = silent\n        # Type comments we can translate.\n        self.found: list[AssignData | FunctionData] = []\n        # Type comments that are not supported yet (for reporting).\n        self.found_unsupported: list[int] = []\n\n    def visit_Assign(self, s: ast.Assign) -> None:\n        if s.type_comment:\n            if not check_target(s):\n                self.found_unsupported.append(s.lineno)\n                return\n            target = s.targets[0]\n            value = s.value\n\n            # These may require special treatment.\n            if isinstance(value, ast.Tuple):\n                rvalue_kind = RvalueKind.TUPLE\n            elif isinstance(value, ast.Constant) and value.value is None:\n                rvalue_kind = RvalueKind.NONE\n            elif isinstance(value, ast.Constant) and value.value is Ellipsis:\n                rvalue_kind = RvalueKind.ELLIPSIS\n            else:\n                rvalue_kind = RvalueKind.OTHER\n\n            assert (\n                target.end_lineno\n                and target.end_col_offset\n                and value.end_lineno\n                and value.end_col_offset\n            )\n            found = AssignData(\n                s.type_comment,\n                target.end_lineno,\n                target.end_col_offset,\n                value.lineno,\n                value.col_offset,\n                value.end_lineno,\n                value.end_col_offset,\n                rvalue_kind,\n            )\n            self.found.append(found)\n\n    def visit_For(self, o: ast.For) -> None:\n        self.visit_unsupported(o)\n\n    def visit_AsyncFor(self, o: ast.AsyncFor) -> None:\n        self.visit_unsupported(o)\n\n    def visit_With(self, o: ast.With) -> None:\n        self.visit_unsupported(o)\n\n    def visit_AsyncWith(self, o: ast.AsyncWith) -> None:\n        self.visit_unsupported(o)\n\n    def visit_unsupported(self, o: Unsupported) -> None:\n        if o.type_comment:\n            self.found_unsupported.append(o.lineno)\n        self.generic_visit(o)\n\n    def visit_FunctionDef(self, fdef: ast.FunctionDef) -> None:\n        self.visit_function_impl(fdef)\n\n    def visit_AsyncFunctionDef(self, fdef: ast.AsyncFunctionDef) -> None:\n        self.visit_function_impl(fdef)\n\n    def visit_function_impl(self, fdef: Function) -> None:\n        if (\n            fdef.type_comment\n            or any(a.type_comment for a in fdef.args.args)\n            or any(a.type_comment for a in fdef.args.kwonlyargs)\n            or fdef.args.vararg\n            and fdef.args.vararg.type_comment\n            or fdef.args.kwarg\n            and fdef.args.kwarg.type_comment\n        ):\n\n            # Number of non-default positional arguments.\n            num_non_defs = len(fdef.args.args) - len(fdef.args.defaults)\n\n            # Positions of non-default keyword-only arguments.\n            kw_non_defs = {i for i, d in enumerate(fdef.args.kw_defaults) if d is None}\n\n            args = self.process_per_arg_comments(fdef, num_non_defs, kw_non_defs)\n\n            ret: str | None\n            if fdef.type_comment:\n                res = split_function_comment(fdef.type_comment, self.silent)\n                if not res:\n                    self.found_unsupported.append(fdef.lineno)\n                    return\n                f_args, ret = res\n            else:\n                f_args, ret = [], None\n\n            if args and f_args:\n                if not self.silent:\n                    print(\n                        f'Both per-argument and function comments for \"{fdef.name}\"',\n                        file=sys.stderr,\n                    )\n                self.found_unsupported.append(fdef.lineno)\n                return\n\n            body_start = fdef.body[0].lineno\n            if isinstance(\n                fdef.body[0], (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef)\n            ):\n                # We need to compensate for decorators, because the first line of a\n                # class/function is the line where 'class' or 'def' appears.\n                if fdef.body[0].decorator_list:\n                    body_start = min(it.lineno for it in fdef.body[0].decorator_list)\n            if args:\n                self.found.append(FunctionData(args, ret, fdef.lineno, body_start))\n            elif not f_args:\n                self.found.append(FunctionData([], ret, fdef.lineno, body_start))\n            else:\n                c_args = self.process_function_comment(fdef, f_args, num_non_defs)\n                if c_args is None:\n                    # There was an error processing comment.\n                    return\n                self.found.append(FunctionData(c_args, ret, fdef.lineno, body_start))\n        self.generic_visit(fdef)\n\n    def process_per_arg_comments(\n        self, fdef: Function, num_non_defs: int, kw_non_defs: set[int]\n    ) -> list[ArgComment]:\n        \"\"\"Collect information about per-argument function comments.\n\n        These comments look like:\n\n            def func(\n                arg1,  # type: Type1\n                arg2,  # type: Type2\n            ):\n                ...\n        \"\"\"\n        args: list[ArgComment] = []\n\n        for i, a in enumerate(fdef.args.args):\n            if a.type_comment:\n                assert a.end_col_offset\n                args.append(\n                    ArgComment(\n                        a.type_comment, a.lineno, a.end_col_offset, i >= num_non_defs\n                    )\n                )\n        if fdef.args.vararg and fdef.args.vararg.type_comment:\n            vararg = fdef.args.vararg\n            assert vararg.end_col_offset\n            args.append(\n                ArgComment(\n                    fdef.args.vararg.type_comment,\n                    vararg.lineno,\n                    vararg.end_col_offset,\n                    False,\n                )\n            )\n\n        for i, a in enumerate(fdef.args.kwonlyargs):\n            if a.type_comment:\n                assert a.end_col_offset\n                args.append(\n                    ArgComment(\n                        a.type_comment, a.lineno, a.end_col_offset, i not in kw_non_defs\n                    )\n                )\n        if fdef.args.kwarg and fdef.args.kwarg.type_comment:\n            kwarg = fdef.args.kwarg\n            assert kwarg.end_col_offset\n            args.append(\n                ArgComment(\n                    fdef.args.kwarg.type_comment,\n                    kwarg.lineno,\n                    kwarg.end_col_offset,\n                    False,\n                )\n            )\n        return args\n\n    def process_function_comment(\n        self, fdef: Function, f_args: list[str], num_non_defs: int\n    ) -> list[ArgComment] | None:\n        \"\"\"Combine location data for function arguments with types from a comment.\n\n        f_args contains already split argument strings from the function type comment,\n        for example if the comment is # type: (int, str) -> None, the f_args should be\n        ['int', 'str'].\n        \"\"\"\n        args: list[ArgComment] = []\n\n        tot_args = len(fdef.args.args) + len(fdef.args.kwonlyargs)\n        if fdef.args.vararg:\n            tot_args += 1\n        if fdef.args.kwarg:\n            tot_args += 1\n\n        # One is only allowed to skip annotation for self or cls.\n        if len(f_args) not in (tot_args, tot_args - 1):\n            if not self.silent:\n                print(\n                    f'Invalid number of arguments in comment for \"{fdef.name}\"',\n                    file=sys.stderr,\n                )\n            self.found_unsupported.append(fdef.lineno)\n            return None\n\n        # The list of arguments we need to annotate.\n        if len(f_args) == tot_args - 1:\n            iter_args = fdef.args.args[1:]\n        else:\n            iter_args = fdef.args.args.copy()\n\n        # Extend the list with other possible arguments.\n        if fdef.args.vararg:\n            iter_args.append(fdef.args.vararg)\n        iter_args.extend(fdef.args.kwonlyargs)\n        if fdef.args.kwarg:\n            iter_args.append(fdef.args.kwarg)\n\n        # Combine arguments locations with corresponding comments.\n        for typ, a in zip(f_args, iter_args):\n            has_default = False\n            if a in fdef.args.args and fdef.args.args.index(a) >= num_non_defs:\n                has_default = True\n\n            kwonlyargs = fdef.args.kwonlyargs\n            if a in kwonlyargs and fdef.args.kw_defaults[kwonlyargs.index(a)]:\n                has_default = True\n\n            assert a.end_col_offset\n            args.append(ArgComment(typ, a.lineno, a.end_col_offset, has_default))\n        return args\n\n\ndef split_sub_comment(comment: str) -> tuple[str, str | None]:\n    \"\"\"Split extra comment from a type comment.\n\n    The only non-trivial thing here is to take care of literal types,\n    that can contain arbitrary chars, including '#'.\n    \"\"\"\n    rl = BytesIO(comment.encode(\"utf-8\")).readline\n    tokens = list(tokenize.tokenize(rl))\n\n    i_sub = None\n    for i, tok in enumerate(tokens):\n        if tok.exact_type == tokenize.COMMENT:\n            _, i_sub = tokens[i - 1].end\n\n    if i_sub is not None:\n        return comment[:i_sub], comment[i_sub:]\n    return comment, None\n\n\ndef split_function_comment(\n    comment: str, silent: bool = False\n) -> tuple[list[str], str] | None:\n    \"\"\"Split function type comment into argument types and return types.\n\n    This also removes any additional sub-comment. For example:\n\n        # type: (int, str) -> None  # some explanation\n\n    is transformed into: ['int', 'str'], 'None'.\n    \"\"\"\n    typ, _ = split_sub_comment(comment)\n    if typ.count(\"->\") != 1:\n        if not silent:\n            print(\"Invalid function type comment:\", comment, file=sys.stderr)\n        return None\n\n    # TODO: ()->int vs () -> int -- keep spacing (also # type:int vs # type: int).\n    arg_list, ret = typ.split(\"->\")\n\n    arg_list = arg_list.strip()\n    ret = ret.strip()\n\n    if not (arg_list[0] == \"(\" and arg_list[-1] == \")\"):\n        if not silent:\n            print(\"Invalid function type comment:\", comment, file=sys.stderr)\n        return None\n\n    arg_list = arg_list[1:-1]\n    args: list[str] = []\n\n    # TODO: use tokenizer to guard against Literal[','].\n    next_arg = \"\"\n    nested = 0\n    for c in arg_list:\n        if c in \"([{\":\n            nested += 1\n        if c in \")]}\":\n            nested -= 1\n        if c == \",\" and not nested:\n            args.append(next_arg.strip())\n            next_arg = \"\"\n        else:\n            next_arg += c\n\n    if next_arg:\n        args.append(next_arg.strip())\n\n    # Currently mypy just ignores * and ** and just gets the argument kind from the\n    # function header, so we don't need any additional checks.\n    return [a.lstrip(\"*\") for a in args if a != \"...\"], ret\n\n\ndef strip_type_comment(line: str) -> str:\n    \"\"\"Remove any type comments from this line.\n\n    We however keep # type: ignore comments, and any sub-comments.\n    This raises if there is no type comment found.\n    \"\"\"\n    match = re.search(TYPE_COM, line)\n    assert match, line\n    if match.group(1).lstrip().startswith(\"ignore\"):\n        # Keep # type: ignore[=code] comments.\n        return line\n    rest = line[: match.start()]\n\n    typ = match.group(1)\n    _, sub_comment = split_sub_comment(typ)\n    if sub_comment is None:\n        # Just keep exactly the same kind of endline.\n        trailer = re.search(_TRAILER, typ)\n        assert trailer\n        sub_comment = typ[trailer.start() :]\n\n    if rest:\n        new_line = rest + sub_comment\n    else:\n        # A type comment on line of its own.\n        new_line = line[: line.index(\"#\")] + sub_comment.lstrip(\" \\t\")\n    return new_line\n\n\ndef string_insert(line: str, extra: str, pos: int) -> str:\n    return line[:pos] + extra + line[pos:]\n\n\ndef process_assign(\n    comment: AssignData, data: FileData, drop_none: bool, drop_ellipsis: bool\n) -> None:\n    \"\"\"Process type comment in an assignment statement.\n\n    Remove the matching r.h.s. if drop_none or drop_ellipsis is True.\n    For example:\n\n        x = ...  # type: int\n\n    will be translated to\n\n        x: int\n    \"\"\"\n    lines = data.lines\n\n    # In ast module line numbers start from 1, not 0.\n    rv_end = comment.rvalue_end_line - 1\n    rv_start = comment.rvalue_start_line - 1\n\n    # We perform the tasks in order from larger line/columns to smaller ones\n    # to avoid shuffling the line column numbers in following code.\n    # First remove the type comment.\n    match = re.search(TYPE_COM, lines[rv_end])\n    if match and not match.group(1).lstrip().startswith(\"ignore\"):\n        lines[rv_end] = strip_type_comment(lines[rv_end])\n    else:\n        # Special case: type comment moved to a separate continuation line.\n        # There two ways to have continuation...\n        assert lines[rv_end].rstrip().endswith(\"\\\\\") or lines[  # ... a slash\n            rv_end + 1\n        ].lstrip().startswith(\n            \")\"\n        )  # ... inside parentheses\n\n        lines[rv_end + 1] = strip_type_comment(lines[rv_end + 1])\n        if not lines[rv_end + 1].strip():\n            del lines[rv_end + 1]\n            # Also remove the \\ symbol from the previous line, but keep\n            # the original line ending.\n            trailer = re.search(_TRAILER, lines[rv_end])\n            assert trailer\n            lines[rv_end] = lines[rv_end].rstrip()[:-1].rstrip() + trailer.group()\n\n    # Second we take care of r.h.s. special cases.\n    if comment.rvalue_kind == RvalueKind.TUPLE:\n        # TODO: take care of (1, 2), (3, 4) with matching pars.\n        if not (\n            lines[rv_start][comment.rvalue_start_offset] == \"(\"\n            and lines[rv_end][comment.rvalue_end_offset - 1] == \")\"\n        ):\n            # We need to wrap rvalue in parentheses before Python 3.8,\n            # because x: Tuple[int, ...] = 1, 2, 3 used to be a syntax error.\n            end_line = lines[rv_end]\n            lines[rv_end] = string_insert(end_line, \")\", comment.rvalue_end_offset)\n\n            start_line = lines[rv_start]\n            lines[rv_start] = string_insert(\n                start_line, \"(\", comment.rvalue_start_offset\n            )\n\n            if comment.rvalue_end_line > comment.rvalue_start_line:\n                # Add a space to fix indentation after inserting paren.\n                for i in range(comment.rvalue_end_line, comment.rvalue_start_line, -1):\n                    if lines[i - 1].strip():\n                        lines[i - 1] = \" \" + lines[i - 1]\n\n    elif (\n        comment.rvalue_kind == RvalueKind.NONE\n        and drop_none\n        or comment.rvalue_kind == RvalueKind.ELLIPSIS\n        and drop_ellipsis\n    ):\n        # TODO: more tricky (multi-line) cases.\n        if comment.lvalue_end_line == comment.rvalue_end_line:\n            line = lines[comment.lvalue_end_line - 1]\n            lines[comment.lvalue_end_line - 1] = (\n                line[: comment.lvalue_end_offset] + line[comment.rvalue_end_offset :]\n            )\n\n    # Finally we insert the annotation.\n    lvalue_line = lines[comment.lvalue_end_line - 1]\n    typ, _ = split_sub_comment(comment.type_comment)\n    data.seen.add(typ)\n\n    # Take care of '(foo) = bar  # type: baz'.\n    # TODO: this is pretty ad hoc.\n    while (\n        comment.lvalue_end_offset < len(lvalue_line)\n        and lvalue_line[comment.lvalue_end_offset] == \")\"\n    ):\n        comment.lvalue_end_offset += 1\n\n    lines[comment.lvalue_end_line - 1] = (\n        lvalue_line[: comment.lvalue_end_offset]\n        + \": \"\n        + typ\n        + lvalue_line[comment.lvalue_end_offset :]\n    )\n\n\ndef insert_arg_type(line: str, arg: ArgComment, seen: set[str]) -> str:\n    \"\"\"Insert the argument type at a given location.\n\n    Also record the type we translated.\n    \"\"\"\n    typ, _ = split_sub_comment(arg.type_comment)\n    seen.add(typ)\n\n    new_line = line[: arg.arg_end_offset] + \": \" + typ\n\n    rest = line[arg.arg_end_offset :]\n    if not arg.has_default:\n        return new_line + rest\n\n    # Here we are a bit opinionated about spacing (see PEP 8).\n    rest = rest.lstrip()\n    assert rest[0] == \"=\"\n    rest = rest[1:].lstrip()\n\n    return new_line + \" = \" + rest\n\n\ndef wrap_function_header(header: str) -> list[str]:\n    \"\"\"Wrap long function signature (header) one argument per line.\n\n    Currently only headers that are initially one-line are supported.\n    For example:\n\n        def foo(arg1: LongType1, arg2: LongType2) -> None:\n            ...\n\n    becomes\n\n        def foo(arg1: LongType1,\n                arg2: LongType2) -> None:\n            ...\n    \"\"\"\n    # TODO: use tokenizer to guard against Literal[','].\n    parts: list[str] = []\n    next_part = \"\"\n    nested = 0\n    complete = False  # Did we split all the arguments inside (...)?\n    indent: int | None = None\n\n    for i, c in enumerate(header):\n        if c in \"([{\":\n            nested += 1\n            if c == \"(\" and indent is None:\n                indent = i + 1\n        if c in \")]}\":\n            nested -= 1\n            if not nested:\n                # To avoid splitting return types that also have commas.\n                complete = True\n        if c == \",\" and nested == 1 and not complete:\n            next_part += c\n            parts.append(next_part)\n            next_part = \"\"\n        else:\n            next_part += c\n\n    parts.append(next_part)\n\n    if len(parts) == 1:\n        return parts\n\n    # Indent all the wrapped lines.\n    assert indent is not None\n    parts = [parts[0]] + [\" \" * indent + p.lstrip(\" \\t\") for p in parts[1:]]\n\n    # Add line endings like in the original header.\n    trailer = re.search(_TRAILER, header)\n    assert trailer\n    end_line = header[trailer.start() :].lstrip(\" \\t\")\n    parts = [p + end_line for p in parts[:-1]] + [parts[-1]]\n\n    # TODO: handle type ignores better.\n    ignore = re.search(_NICE_IGNORE, parts[-1])\n    if ignore:\n        # We should keep # type: ignore on the first line of the wrapped header.\n        last = parts[-1]\n        first = parts[0]\n        first_trailer = re.search(_TRAILER, first)\n        assert first_trailer\n        parts[0] = first[: first_trailer.start()] + ignore.group()\n        parts[-1] = last[: ignore.start()] + first_trailer.group()\n\n    return parts\n\n\ndef process_func_def(func_type: FunctionData, data: FileData, wrap_sig: int) -> None:\n    \"\"\"Perform translation for an (async) function definition.\n\n    This supports two main ways of adding type comments for argument:\n\n        def one(\n            arg,  # type: Type\n        ):\n            ...\n\n        def another(arg):\n            # type: (Type) -> AnotherType\n    \"\"\"\n    lines = data.lines\n\n    # Find line and column where _actual_ colon is located.\n    ret_line = func_type.body_first_line - 1\n    ret_line -= 1\n    while not lines[ret_line].split(\"#\")[0].strip():\n        ret_line -= 1\n\n    colon = None\n    for i in reversed(data.token_tab[ret_line + 1]):\n        if data.tokens[i].exact_type == tokenize.COLON:\n            _, colon = data.tokens[i].start\n            break\n    assert colon is not None\n\n    # Note that -1 offset is because line numbers starts from 1 in ast module.\n    for i in range(func_type.body_first_line - 2, func_type.header_start_line - 2, -1):\n        if re.search(TYPE_COM, lines[i]):\n            lines[i] = strip_type_comment(lines[i])\n            if not lines[i].strip():\n                if i > ret_line:\n                    del lines[i]\n                else:\n                    # Removing an empty line in argument list is unsafe, since it\n                    # can cause shuffling of following line numbers.\n                    # TODO: find a cleaner fix.\n                    lines[i] = \"\"\n\n    # Inserting return type is a bit dirty...\n    if func_type.ret_type:\n        data.seen.add(func_type.ret_type)\n        right_par = lines[ret_line][:colon].rindex(\")\")\n        lines[ret_line] = (\n            lines[ret_line][: right_par + 1]\n            + \" -> \"\n            + func_type.ret_type\n            + lines[ret_line][colon:]\n        )\n\n    # Inserting argument types is pretty straightforward.\n    for arg in reversed(func_type.arg_types):\n        lines[arg.arg_line - 1] = insert_arg_type(\n            lines[arg.arg_line - 1], arg, data.seen\n        )\n\n    # Finally wrap the translated function header if needed.\n    if ret_line == func_type.header_start_line - 1:\n        header = data.lines[ret_line]\n        if wrap_sig and len(header) > wrap_sig:\n            data.lines[ret_line : ret_line + 1] = wrap_function_header(header)\n\n\ndef com2ann_impl(\n    data: FileData,\n    drop_none: bool,\n    drop_ellipsis: bool,\n    wrap_sig: int = 0,\n    silent: bool = True,\n    add_future_imports: bool = False,\n) -> str:\n    \"\"\"Collect type annotations in AST and perform code translation.\n\n    Add the future import if necessary. Currently only type comments in\n    functions and simple assignments are supported.\n    \"\"\"\n    finder = TypeCommentCollector(silent)\n    finder.visit(data.tree)\n\n    data.fail.extend(finder.found_unsupported)\n    found = list(reversed(finder.found))\n\n    # Perform translations in reverse order to avoid shuffling line numbers.\n    for item in found:\n        if isinstance(item, AssignData):\n            process_assign(item, data, drop_none, drop_ellipsis)\n            data.success.append(item.lvalue_end_line)\n        elif isinstance(item, FunctionData):\n            process_func_def(item, data, wrap_sig)\n            data.success.append(item.header_start_line)\n\n    if add_future_imports and data.success and not data.seen <= FUTURE_IMPORT_WHITELIST:\n        # Find first non-trivial line of code.\n        i = 0\n        while not data.lines[i].split(\"#\")[0].strip():\n            i += 1\n        trailer = re.search(_TRAILER, data.lines[i])\n        assert trailer\n        data.lines.insert(i, \"from __future__ import annotations\" + trailer.group())\n\n    return \"\".join(data.lines)\n\n\ndef check_target(assign: ast.Assign) -> bool:\n    \"\"\"Check if the statement is suitable for annotation.\n\n    Type comments can placed on with and for statements, but\n    annotation can be placed only on an simple assignment with a single target.\n    \"\"\"\n    if len(assign.targets) == 1:\n        target = assign.targets[0]\n    else:\n        return False\n    if (\n        isinstance(target, ast.Name)\n        or isinstance(target, ast.Attribute)\n        or isinstance(target, ast.Subscript)\n    ):\n        return True\n    return False\n\n\ndef com2ann(\n    code: str,\n    *,\n    drop_none: bool = False,\n    drop_ellipsis: bool = False,\n    silent: bool = False,\n    add_future_imports: bool = False,\n    wrap_sig: int = 0,\n    python_minor_version: int = -1,\n) -> tuple[str, FileData] | None:\n    \"\"\"Translate type comments to type annotations in code.\n\n    Take code as string and return this string where::\n\n      variable = value  # type: annotation  # real comment\n\n    is translated to::\n\n      variable: annotation = value  # real comment\n\n    For unsupported syntax cases, the type comments are\n    left intact. If drop_None is True or if drop_Ellipsis\n    is True translate correspondingly::\n\n      variable = None  # type: annotation\n      variable = ...  # type: annotation\n\n    into::\n\n      variable: annotation\n\n    The tool tries to preserve code formatting as much as\n    possible, but an exact translation is not guaranteed.\n    A summary of translated comments id printed by default.\n    \"\"\"\n    try:\n        # We want to work only with file without syntax errors\n        tree = ast.parse(\n            code, type_comments=True, feature_version=(3, python_minor_version)\n        )\n    except SyntaxError as exc:\n        print(\"SyntaxError:\", exc, file=sys.stderr)\n        return None\n    lines = code.splitlines(keepends=True)\n    rl = BytesIO(code.encode(\"utf-8\")).readline\n    tokens = list(tokenize.tokenize(rl))\n\n    data = FileData(lines, tokens, tree)\n    new_code = com2ann_impl(\n        data, drop_none, drop_ellipsis, wrap_sig, silent, add_future_imports\n    )\n\n    if not silent:\n        if data.success:\n            print(\n                \"Comments translated for statements on lines:\",\n                \", \".join(str(lno) for lno in data.success),\n            )\n        if data.fail:\n            print(\n                \"Comments skipped for statements on lines:\",\n                \", \".join(str(lno) for lno in data.fail),\n            )\n        if not data.success and not data.fail:\n            print(\"No type comments found\")\n\n    return new_code, data\n\n\ndef translate_file(infile: str, outfile: str, options: Options) -> None:\n    try:\n        opened = tokenize.open(infile)\n    except SyntaxError:\n        print(\"Cannot open\", infile, file=sys.stderr)\n        return\n    with opened as f:\n        code = f.read()\n        enc = f.encoding\n    if not options.silent:\n        print(\"File:\", infile)\n\n    future_imports = options.add_future_imports\n    if outfile.endswith(\".pyi\"):\n        future_imports = False\n\n    try:\n        result = com2ann(\n            code,\n            drop_none=options.drop_none,\n            drop_ellipsis=options.drop_ellipsis,\n            silent=options.silent,\n            add_future_imports=future_imports,\n            wrap_sig=options.wrap_signatures,\n            python_minor_version=options.python_minor_version,\n        )\n    except Exception:\n        print(f\"INTERNAL ERROR while processing {infile}\", file=sys.stderr)\n        print(\n            \"Please report bug at https://github.com/ilevkivskyi/com2ann/issues\",\n            file=sys.stderr,\n        )\n        raise\n\n    if result is None:\n        print(\"SyntaxError in\", infile, file=sys.stderr)\n        return\n    new_code, _ = result\n    with open(outfile, \"wb\") as fo:\n        fo.write(new_code.encode(enc))\n\n\ndef parse_cli_args(args: Sequence[str] | None = None) -> dict[str, Any]:\n    parser = argparse.ArgumentParser(description=__doc__)\n    parser.add_argument(\n        \"infiles\",\n        nargs=\"*\",\n        type=pathlib.Path,\n        help=\"input files or directories for translation, must\\n\"\n        \"contain no syntax errors;\\n\"\n        \"if --outfile is not given or multiple input files are\\n\"\n        \"given, translation is made *in place*\",\n    )\n\n    parser.add_argument(\n        \"-o\",\n        \"--outfile\",\n        type=pathlib.Path,\n        help=\"output file or directory, will be overwritten if exists,\\n\"\n        \"defaults to input file or directory. Cannot be used\\n\"\n        \"when multiple input files are defined. infiles and\\n\"\n        \"outfile must be of the same type (file vs. directory)\",\n    )\n\n    parser.add_argument(\n        \"-s\",\n        \"--silent\",\n        help=\"do not print summary for line numbers of\\n\"\n        \"translated and rejected comments\",\n        action=\"store_true\",\n    )\n\n    parser.add_argument(\n        \"-n\",\n        \"--drop-none\",\n        help=\"drop any None as assignment value during\\n\"\n        \"translation if it is annotated by a type comment\",\n        action=\"store_true\",\n    )\n\n    parser.add_argument(\n        \"-e\",\n        \"--drop-ellipsis\",\n        help=\"drop any Ellipsis (...) as assignment value during\\n\"\n        \"translation if it is annotated by a type comment\",\n        action=\"store_true\",\n    )\n\n    parser.add_argument(\n        \"-i\",\n        \"--add-future-imports\",\n        help=\"add 'from __future__ import annotations' to any file\\n\"\n        \"where type comments were successfully translated\",\n        action=\"store_true\",\n    )\n\n    parser.add_argument(\n        \"-w\",\n        \"--wrap-signatures\",\n        help=\"wrap function headers that are longer than given length\",\n        type=int,\n        default=0,\n    )\n\n    parser.add_argument(\n        \"-v\",\n        \"--python-minor-version\",\n        help=\"Python 3 minor version to use to parse the files\",\n        type=int,\n        default=-1,\n    )\n\n    options = parser.parse_args(args)\n\n    infiles: list[pathlib.Path] = options.infiles\n    outfile: pathlib.Path | None = options.outfile\n\n    if not infiles:\n        parser.exit(status=0, message=\"No input file, exiting\")\n\n    missing_files = [file for file in infiles if not file.exists()]\n    if missing_files:\n        parser.error(f\"File(s) not found: {', '.join(str(e) for e in missing_files)}\")\n\n    if len(infiles) == 1:\n        if outfile and outfile.exists() and infiles[0].is_dir() != outfile.is_dir():\n            parser.error(\"Infile must be the same type as outfile (file vs. directory)\")\n    else:\n        if outfile is not None:\n            parser.error(\"Cannot use --outfile if multiple infiles are given\")\n\n    return vars(options)\n\n\ndef rebase_path(\n    path: pathlib.Path, root: pathlib.Path, new_root: pathlib.Path\n) -> pathlib.Path:\n    \"\"\"\n    Generate a path that is at the same location relative to `new_root` as `path`\n    was relative to `root`\n    \"\"\"\n    return new_root / path.relative_to(root)\n\n\ndef process_single_entry(\n    in_path: pathlib.Path, out_path: pathlib.Path, options: Options\n) -> None:\n\n    if in_path.is_file():\n        translate_file(infile=str(in_path), outfile=str(out_path), options=options)\n    else:\n        for in_file in sorted(in_path.glob(\"**/*.py*\")):\n            if in_file.suffix not in [\".py\", \".pyi\"] or in_file.is_dir():\n                continue\n\n            out_file = rebase_path(path=in_file, root=in_path, new_root=out_path)\n            if out_file != in_file:\n                out_file.parent.mkdir(parents=True, exist_ok=True)\n\n            translate_file(infile=str(in_file), outfile=str(out_file), options=options)\n\n\ndef main() -> None:\n    args = parse_cli_args()\n\n    infiles = args.pop(\"infiles\")\n    outfile = args.pop(\"outfile\")\n    options = Options(**args)\n\n    for infile in infiles:\n        cur_outfile = outfile or infile\n        process_single_entry(in_path=infile, out_path=cur_outfile, options=options)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/test_cli.py",
    "content": "from __future__ import annotations\n\nimport os\nimport pathlib\nfrom dataclasses import asdict\nfrom typing import Any, Callable, Iterable, TypedDict\n\nimport pytest\n\nimport com2ann\n\n\n@pytest.fixture\ndef test_path(tmp_path: pathlib.Path) -> Iterable[pathlib.Path]:\n    old_path = pathlib.Path.cwd()\n    os.chdir(tmp_path)\n    yield tmp_path\n    os.chdir(old_path)\n\n\nclass Exited(Exception):\n    pass\n\n\nclass ParseResult(TypedDict, total=False):\n    status: int | str | None\n    args: dict[str, Any]\n    error: bool\n    out: str\n    err: str\n\n\nParseCallable = Callable[..., ParseResult]\n\n\n@pytest.fixture\ndef parse(\n    capsys: pytest.CaptureFixture[str],\n) -> ParseCallable:\n    # parse is a pytext fixture returning a function\n    # This function will:\n    # - Ensure that SystemExit exceptions raised by ArgParse are\n    #   caught before they exit pytest\n    # - Save the result of the arg parsing if successful\n    # - Save the status code otherwise\n    # - Save and print whatever argparse printed to stdout and stderr\n\n    def _(*args: str) -> ParseResult:\n        result: ParseResult = {\"status\": 0}\n        try:\n            result.update({\"args\": com2ann.parse_cli_args(args), \"error\": False})\n        except SystemExit as exc:\n            result.update({\"status\": exc.code})\n\n        out, err = capsys.readouterr()\n        # Will only display if the test fails\n        if out:\n            print(\"stdout:\", out)\n            result[\"out\"] = out\n        if err:\n            print(\"stderr:\", err)\n            result[\"err\"] = err\n\n        return result\n\n    return _\n\n\ndef test_parse_cli_args__minimal(parse: ParseCallable, test_path: pathlib.Path) -> None:\n    (test_path / \"a.py\").touch()\n\n    assert parse(\"a.py\")[\"args\"] == {\n        \"add_future_imports\": False,\n        \"drop_ellipsis\": False,\n        \"drop_none\": False,\n        \"infiles\": [pathlib.Path(\"a.py\")],\n        \"outfile\": None,\n        \"python_minor_version\": -1,\n        \"silent\": False,\n        \"wrap_signatures\": 0,\n    }\n\n\ndef test_parse_cli_args__maximal(parse: ParseCallable, test_path: pathlib.Path) -> None:\n    (test_path / \"a.py\").touch()\n\n    assert parse(\n        \"a.py\",\n        \"--outfile=b.py\",\n        \"--add-future-imports\",\n        \"--drop-ellipsis\",\n        \"--drop-none\",\n        \"--python-minor-version=7\",\n        \"--silent\",\n        \"--wrap-signatures=88\",\n    )[\"args\"] == {\n        \"add_future_imports\": True,\n        \"drop_ellipsis\": True,\n        \"drop_none\": True,\n        \"infiles\": [pathlib.Path(\"a.py\")],\n        \"outfile\": pathlib.Path(\"b.py\"),\n        \"python_minor_version\": 7,\n        \"silent\": True,\n        \"wrap_signatures\": 88,\n    }\n\n\ndef test_parse_cli_args__no_infile(parse: ParseCallable) -> None:\n    result = parse()\n\n    assert result == {\"err\": \"No input file, exiting\", \"status\": 0}\n\n\ndef test_parse_cli_args__missing_file(parse: ParseCallable) -> None:\n\n    result = parse(\"a.py\")\n    assert result[\"status\"] == 2\n    assert \"File(s) not found: a.py\" in result[\"err\"]\n\n\n@pytest.mark.parametrize(\n    \"infile, outfile\",\n    [\n        (\"file.py\", \"dir\"),\n        (\"dir\", \"file.py\"),\n    ],\n)\ndef test_parse_cli_args__in_out_type_mismatch(\n    parse: ParseCallable, test_path: pathlib.Path, infile: str, outfile: str\n) -> None:\n    (test_path / \"file.py\").touch()\n    (test_path / \"dir\").mkdir()\n\n    result = parse(infile, \"--outfile\", outfile)\n    assert result[\"status\"] == 2\n    assert \"Infile must be the same type\" in result[\"err\"]\n\n\ndef test_parse_cli_args__outfile_doesnt_exist(\n    parse: ParseCallable, test_path: pathlib.Path\n) -> None:\n    (test_path / \"dir\").mkdir()\n\n    result = parse(\"dir\", \"--outfile\", \"other_dir\")\n    assert result[\"status\"] == 0\n\n\ndef test_parse_cli_args__multiple_inputs_and_output(\n    parse: ParseCallable, test_path: pathlib.Path\n) -> None:\n    (test_path / \"a.py\").touch()\n    (test_path / \"b.py\").touch()\n\n    result = parse(\"a.py\", \"b.py\", \"--outfile\", \"c.py\")\n    assert result[\"status\"] == 2\n    assert \"Cannot use --outfile if multiple infiles are given\" in result[\"err\"]\n\n\ndef test_rebase_path() -> None:\n    assert com2ann.rebase_path(\n        path=pathlib.Path(\"a/b/c/d\"),\n        root=pathlib.Path(\"a/b/\"),\n        new_root=pathlib.Path(\"e/f\"),\n    ) == pathlib.Path(\"e/f/c/d\")\n\n\n@pytest.fixture\ndef options() -> com2ann.Options:\n    return com2ann.Options(drop_none=True, drop_ellipsis=True, silent=False)\n\n\n@pytest.fixture\ndef translate_file(mocker: Any) -> Any:\n    return mocker.patch(\"com2ann.translate_file\", autospec=True)\n\n\ndef test_process_single_entry__file(\n    test_path: pathlib.Path, translate_file: Any, options: com2ann.Options\n) -> None:\n    in_path = test_path / \"a.py\"\n    in_path.touch()\n\n    out_path = test_path / \"b.py\"\n\n    com2ann.process_single_entry(in_path=in_path, out_path=out_path, options=options)\n\n    translate_file.assert_called_with(\n        infile=str(in_path),\n        outfile=str(out_path),\n        options=options,\n    )\n\n\ndef test_process_single_entry__dir(\n    test_path: pathlib.Path, translate_file: Any, options: com2ann.Options, mocker: Any\n) -> None:\n    in_path = test_path / \"a\"\n    (in_path / \"c/d/e\").mkdir(parents=True)\n    (in_path / \"c/d/e/f.txt\").touch()\n    (in_path / \"c/d/e/f.py\").touch()\n    (in_path / \"c/d/e/f.pyi\").touch()\n    (in_path / \"c/d/e/f.pyx\").touch()\n\n    out_path = test_path / \"b\"\n\n    com2ann.process_single_entry(in_path=in_path, out_path=out_path, options=options)\n\n    assert translate_file.mock_calls == [\n        mocker.call(\n            infile=str(in_path / \"c/d/e/f.py\"),\n            outfile=str(out_path / \"c/d/e/f.py\"),\n            options=options,\n        ),\n        mocker.call(\n            infile=str(in_path / \"c/d/e/f.pyi\"),\n            outfile=str(out_path / \"c/d/e/f.pyi\"),\n            options=options,\n        ),\n    ]\n\n\n@pytest.fixture\ndef parse_cli_args(\n    test_path: pathlib.Path,\n    mocker: Any,\n    options: com2ann.Options,\n) -> Any:\n    f1 = test_path / \"f1.py\"\n    f2 = test_path / \"f2.py\"\n\n    f1.write_text(\"f1 = True\")\n    f2.write_text(\"f2 = False\")\n\n    args = {\"infiles\": [f1, f2], \"outfile\": None, **asdict(options)}\n    return mocker.patch(\"com2ann.parse_cli_args\", return_value=args)\n\n\ndef test_process_multiple_input_files(\n    test_path: pathlib.Path,\n    translate_file: Any,\n    mocker: Any,\n    parse_cli_args: Any,\n    options: com2ann.Options,\n) -> None:\n    com2ann.main()\n\n    assert translate_file.mock_calls == [\n        mocker.call(\n            infile=str(test_path / \"f1.py\"),\n            outfile=str(test_path / \"f1.py\"),\n            options=options,\n        ),\n        mocker.call(\n            infile=str(test_path / \"f2.py\"),\n            outfile=str(test_path / \"f2.py\"),\n            options=options,\n        ),\n    ]\n    assert (test_path / \"f1.py\").read_text() == \"f1 = True\"\n    assert (test_path / \"f2.py\").read_text() == \"f2 = False\"\n"
  },
  {
    "path": "src/test_com2ann.py",
    "content": "\"\"\"Tests for the com2ann.py script in the Tools/parser directory.\"\"\"\n\nimport re\nimport unittest\nfrom textwrap import dedent\nfrom typing import List, Optional\n\nfrom com2ann import TYPE_COM, com2ann\n\n\nclass BaseTestCase(unittest.TestCase):\n\n    def check(\n        self,\n        code: str,\n        expected: Optional[str],\n        n: bool = False,\n        e: bool = False,\n        w: int = 0,\n        i: bool = False,\n    ) -> None:\n        result = com2ann(\n            dedent(code),\n            drop_none=n,\n            drop_ellipsis=e,\n            silent=True,\n            wrap_sig=w,\n            add_future_imports=i,\n        )\n        if expected is None:\n            self.assertIs(result, None)\n        else:\n            assert result is not None\n            new_code, _ = result\n            self.assertEqual(new_code, dedent(expected))\n\n\nclass AssignTestCase(BaseTestCase):\n    def test_basics(self) -> None:\n        self.check(\"z = 5\", \"z = 5\")\n        self.check(\"z: int = 5\", \"z: int = 5\")\n        self.check(\"z = 5 # type: int\", \"z: int = 5\")\n        self.check(\"z = 5 # type: int # comment\", \"z: int = 5 # comment\")\n\n    def test_type_ignore(self) -> None:\n        self.check(\n            \"foobar = foo_baz() # type: ignore\", \"foobar = foo_baz() # type: ignore\"\n        )\n        self.check(\"a = 42 #type: ignore #comment\", \"a = 42 #type: ignore #comment\")\n        self.check(\n            \"foobar = None  # type: int  # type: ignore\",\n            \"foobar: int  # type: ignore\",\n            True,\n            False,\n        )\n\n    def test_complete_tuple(self) -> None:\n        self.check(\n            \"t = 1, 2, 3 # type: Tuple[int, ...]\", \"t: Tuple[int, ...] = (1, 2, 3)\"\n        )\n        self.check(\"t = 1, # type: Tuple[int]\", \"t: Tuple[int] = (1,)\")\n        self.check(\n            \"t = (1, 2, 3) # type: Tuple[int, ...]\", \"t: Tuple[int, ...] = (1, 2, 3)\"\n        )\n\n    def test_drop_None(self) -> None:\n        self.check(\"x = None # type: int\", \"x: int\", True)\n        self.check(\"x = None # type: int # another\", \"x: int # another\", True)\n        self.check(\"x = None # type: int # None\", \"x: int # None\", True)\n\n    def test_drop_Ellipsis(self) -> None:\n        self.check(\"x = ... # type: int\", \"x: int\", False, True)\n        self.check(\"x = ... # type: int # another\", \"x: int # another\", False, True)\n        self.check(\"x = ... # type: int # ...\", \"x: int # ...\", False, True)\n\n    def test_newline(self) -> None:\n        self.check(\"z = 5 # type: int\\r\\n\", \"z: int = 5\\r\\n\")\n        self.check(\"z = 5 # type: int # comment\\x85\", \"z: int = 5 # comment\\x85\")\n\n    def test_wrong(self) -> None:\n        self.check(\"#type : str\", \"#type : str\")\n        self.check(\"x==y #type: bool\", None)  # this is syntax error\n        self.check(\"x==y ##type: bool\", \"x==y ##type: bool\")  # this is OK\n\n    def test_pattern(self) -> None:\n        for line in [\"#type: int\", \"  # type:  str[:] # com\"]:\n            self.assertTrue(re.search(TYPE_COM, line))\n        for line in [\"\", \"#\", \"# comment\", \"#type\", \"type int:\"]:\n            self.assertFalse(re.search(TYPE_COM, line))\n\n    def test_uneven_spacing(self) -> None:\n        self.check(\"x = 5   #type: int # this one is OK\", \"x: int = 5 # this one is OK\")\n\n    def test_coding_kept(self) -> None:\n        self.check(\n            \"\"\"\n            # -*- coding: utf-8 -*- # this should not be spoiled\n            '''\n            Docstring here\n            '''\n\n            import testmod\n            from typing import Optional\n\n            coding = None  # type: Optional[str]\n            \"\"\",\n            \"\"\"\n            # -*- coding: utf-8 -*- # this should not be spoiled\n            '''\n            Docstring here\n            '''\n\n            import testmod\n            from typing import Optional\n\n            coding: Optional[str] = None\n            \"\"\",\n        )\n\n    def test_multi_line_tuple_value(self) -> None:\n        self.check(\n            \"\"\"\n            ttt \\\\\n                 = \\\\\n                   1.0, \\\\\n                   2.0, \\\\\n                   3.0, #type: Tuple[float, float, float]\n            \"\"\",\n            \"\"\"\n            ttt: Tuple[float, float, float] \\\\\n                 = \\\\\n                   (1.0, \\\\\n                    2.0, \\\\\n                    3.0,)\n            \"\"\",\n        )\n\n    def test_complex_targets(self) -> None:\n        self.check(\"x = y = z = 1 # type: int\", \"x = y = z = 1 # type: int\")\n        self.check(\n            \"x, y, z = [], [], []  # type: (List[int], List[int], List[str])\",\n            \"x, y, z = [], [], []  # type: (List[int], List[int], List[str])\",\n        )\n        self.check(\n            \"self.x = None  # type: int  # type: ignore\",\n            \"self.x: int  # type: ignore\",\n            True,\n            False,\n        )\n        self.check(\n            \"self.x[0] = []  # type: int  # type: ignore\",\n            \"self.x[0]: int = []  # type: ignore\",\n        )\n\n    def test_multi_line_assign(self) -> None:\n        self.check(\n            \"\"\"\n            class C:\n\n                l[f(x\n                    =1)] = [\n\n                     g(y), # type: ignore\n                     2,\n                     ]  # type: List[int]\n            \"\"\",\n            \"\"\"\n            class C:\n\n                l[f(x\n                    =1)]: List[int] = [\n\n                     g(y), # type: ignore\n                     2,\n                     ]\n            \"\"\",\n        )\n\n    def test_parenthesized_lhs(self) -> None:\n        self.check(\n            \"\"\"\n            (C.x[1]) = \\\\\n                42 == 5# type: bool\n            \"\"\",\n            \"\"\"\n            (C.x[1]): bool = \\\\\n                42 == 5\n            \"\"\",\n        )\n\n    def test_literal_types(self) -> None:\n        self.check(\n            \"x = None  # type: Optional[Literal['#']]\",\n            \"x: Optional[Literal['#']] = None\",\n        )\n\n    def test_comment_on_separate_line(self) -> None:\n        self.check(\n            \"\"\"\n            bar = {} \\\\\n                # type: SuperLongType[WithArgs]\n            \"\"\",\n            \"\"\"\n            bar: SuperLongType[WithArgs] = {}\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            bar = {} \\\\\n                # type: SuperLongType[WithArgs]  # noqa\n            \"\"\",\n            \"\"\"\n            bar: SuperLongType[WithArgs] = {} \\\\\n                # noqa\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            bar = None \\\\\n                # type: SuperLongType[WithArgs]\n            \"\"\",\n            \"\"\"\n            bar: SuperLongType[WithArgs]\n            \"\"\",\n            n=True,\n        )\n\n    def test_continuation_using_parens(self) -> None:\n        self.check(\n            \"\"\"\n            X = (\n                {one}\n                | {other}\n            )  # type: Final  # another option\n            \"\"\",\n            \"\"\"\n            X: Final = (\n                {one}\n                | {other}\n            )  # another option\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            X = (  # type: ignore\n                {one}\n                | {other}\n            )  # type: Final\n            \"\"\",\n            \"\"\"\n            X: Final = (  # type: ignore\n                {one}\n                | {other}\n            )\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            foo = object()\n\n            bar = (\n                # Comment which explains why this ignored\n                foo.quox   # type: ignore[attribute]\n            )  # type: Mapping[str, Distribution]\n            \"\"\",\n            \"\"\"\n            foo = object()\n\n            bar: Mapping[str, Distribution] = (\n                # Comment which explains why this ignored\n                foo.quox   # type: ignore[attribute]\n            )\n            \"\"\",\n        )\n\n    def test_with_for(self) -> None:\n        self.check(\n            \"\"\"\n            for i in range(test):  # type: float\n                with open('/some/file'):\n                    def f():\n                        # type: () -> None\n                        x = []  # type: List[int]  # unused\n            \"\"\",\n            \"\"\"\n            for i in range(test):  # type: float\n                with open('/some/file'):\n                    def f() -> None:\n                        x: List[int] = []  # unused\n            \"\"\",\n        )\n\n\nclass FunctionTestCase(BaseTestCase):\n    def test_single(self) -> None:\n        self.check(\n            \"\"\"\n            def add(a, b):  # type: (int, int) -> int\n                '''# type: yes'''\n            \"\"\",\n            \"\"\"\n            def add(a: int, b: int) -> int:\n                '''# type: yes'''\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            def add(a, b):  # type: (int, int) -> int  # extra comment\n                pass\n            \"\"\",\n            \"\"\"\n            def add(a: int, b: int) -> int:  # extra comment\n                pass\n            \"\"\",\n        )\n\n    def test_async_single(self) -> None:\n        self.check(\n            \"\"\"\n            async def add(a, b):  # type: (int, int) -> int\n                '''# type: yes'''\n            \"\"\",\n            \"\"\"\n            async def add(a: int, b: int) -> int:\n                '''# type: yes'''\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            async def add(a, b):  # type: (int, int) -> int  # extra comment\n                pass\n            \"\"\",\n            \"\"\"\n            async def add(a: int, b: int) -> int:  # extra comment\n                pass\n            \"\"\",\n        )\n\n    def test_complex_kinds(self) -> None:\n        self.check(\n            \"\"\"\n            def embezzle(account, funds=MANY, *fake_receipts, stuff, other=None, **kwarg):\n                # type: (str, int, *str, Any, Optional[Any], Any) -> None  # note: vararg and kwarg\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(account: str, funds: int = MANY, *fake_receipts: str, stuff: Any, other: Optional[Any] = None, **kwarg: Any) -> None:\n                # note: vararg and kwarg\n                pass\n            \"\"\",\n        )  # noqa\n        self.check(\n            \"\"\"\n            def embezzle(account, funds=MANY, *fake_receipts, stuff, other=None, **kwarg):  # type: ignore\n                # type: (str, int, *str, Any, Optional[Any], Any) -> None\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(account: str, funds: int = MANY, *fake_receipts: str, stuff: Any, other: Optional[Any] = None, **kwarg: Any) -> None:  # type: ignore\n                pass\n            \"\"\",\n        )  # noqa\n\n    def test_self_argument(self) -> None:\n        self.check(\n            \"\"\"\n            def load_cache(self):\n                # type: () -> bool\n                pass\n            \"\"\",\n            \"\"\"\n            def load_cache(self) -> bool:\n                pass\n            \"\"\",\n        )\n\n    def test_invalid_return_type(self) -> None:\n        self.check(\n            \"\"\"\n            def load_cache(x):\n                # type: (str) -> bool -> invalid\n                pass\n            \"\"\",\n            \"\"\"\n            def load_cache(x):\n                # type: (str) -> bool -> invalid\n                pass\n            \"\"\",\n        )\n\n    def test_combined_annotations_single(self) -> None:\n        self.check(\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body):\n                # type: (...) -> bool\n                pass\n            \"\"\",\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body) -> bool:\n                pass\n            \"\"\",\n        )\n        # TODO: should we move an ignore on its own line somewhere else?\n        self.check(\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body):\n                # type: (...) -> BadType  # type: ignore\n                pass\n            \"\"\",\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body) -> BadType:\n                # type: ignore\n                pass\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body):  # type: ignore\n                # type: (...) -> bool\n                pass\n            \"\"\",\n            \"\"\"\n            def send_email(address, sender, cc, bcc, subject, body) -> bool:  # type: ignore\n                pass\n            \"\"\",\n        )\n\n    def test_combined_annotations_multi(self) -> None:\n        self.check(\n            \"\"\"\n            def send_email(address,     # type: Union[str, List[str]]\n               sender,      # type: str\n               cc,          # type: Optional[List[str]]  # this is OK\n               bcc,         # type: Optional[List[Bad]]  # type: ignore\n               subject='',\n               body=None,   # type: List[str]\n               *args        # type: ignore\n               ):\n               # type: (...) -> bool\n               pass\n            \"\"\",\n            \"\"\"\n            def send_email(address: Union[str, List[str]],\n               sender: str,\n               cc: Optional[List[str]],  # this is OK\n               bcc: Optional[List[Bad]],  # type: ignore\n               subject='',\n               body: List[str] = None,\n               *args        # type: ignore\n               ) -> bool:\n               pass\n            \"\"\",\n        )\n\n    def test_literal_type(self) -> None:\n        self.check(\n            \"\"\"\n            def force_hash(\n                arg,  # type: Literal['#']\n            ):\n                # type: (...) -> Literal['#']\n                pass\n            \"\"\",\n            \"\"\"\n            def force_hash(\n                arg: Literal['#'],\n            ) -> Literal['#']:\n                pass\n            \"\"\",\n        )\n\n    def test_wrap_lines(self) -> None:\n        self.check(\n            \"\"\"\n            def embezzle(self, account, funds=MANY, *fake_receipts):\n                # type: (str, int, *str) -> None  # some comment\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(self,\n                         account: str,\n                         funds: int = MANY,\n                         *fake_receipts: str) -> None:\n                # some comment\n                pass\n            \"\"\",\n            False,\n            False,\n            10,\n        )\n        self.check(\n            \"\"\"\n            def embezzle(self, account, funds=MANY, *fake_receipts):  # type: ignore\n                # type: (str, int, *str) -> None\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(self,  # type: ignore\n                         account: str,\n                         funds: int = MANY,\n                         *fake_receipts: str) -> None:\n                pass\n            \"\"\",\n            False,\n            False,\n            10,\n        )\n        self.check(\n            \"\"\"\n            def embezzle(self, account, funds=MANY, *fake_receipts):\n                # type: (str, int, *str) -> Dict[str, Dict[str, int]]\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(self,\n                         account: str,\n                         funds: int = MANY,\n                         *fake_receipts: str) -> Dict[str, Dict[str, int]]:\n                pass\n            \"\"\",\n            False,\n            False,\n            10,\n        )\n\n    def test_wrap_lines_error_code(self) -> None:\n        self.check(\n            \"\"\"\n            def embezzle(self, account, funds=MANY, *fake_receipts):  # type: ignore[override]\n                # type: (str, int, *str) -> None\n                pass\n            \"\"\",\n            \"\"\"\n            def embezzle(self,  # type: ignore[override]\n                         account: str,\n                         funds: int = MANY,\n                         *fake_receipts: str) -> None:\n                pass\n            \"\"\",\n            False,\n            False,\n            10,\n        )\n\n    def test_decorator_body(self) -> None:\n        self.check(\n            \"\"\"\n            def outer(self):  # a method\n                # type: () -> None\n                @deco()\n                def inner():\n                    # type: () -> None\n                    pass\n            \"\"\",\n            \"\"\"\n            def outer(self) -> None:  # a method\n                @deco()\n                def inner() -> None:\n                    pass\n            \"\"\",\n        )\n        self.check(\n            \"\"\"\n            def func(\n                x,  # type: int\n                *other,  # type: Any\n            ):\n                # type: () -> None\n                @dataclass\n                class C:\n                    x = None  # type: int\n            \"\"\",\n            \"\"\"\n            def func(\n                x: int,\n                *other: Any,\n            ) -> None:\n                @dataclass\n                class C:\n                    x: int\n            \"\"\",\n            n=True,\n        )\n\n    def test_keyword_only_args(self) -> None:\n        self.check(\n            \"\"\"\n            def func(self,\n                *,\n                account,\n                callback,  # type: Callable[[], None]\n                start=0,  # type: int\n                order,  # type: bool\n                ):\n                # type: (...) -> None\n                ...\n            \"\"\",\n            \"\"\"\n            def func(self,\n                *,\n                account,\n                callback: Callable[[], None],\n                start: int = 0,\n                order: bool,\n                ) -> None:\n                ...\n            \"\"\",\n        )\n\n    def test_next_line_comment(self) -> None:\n        self.check(\n            \"\"\"\n            def __init__(\n                self,\n                short,                # type: Short\n                long_argument,\n                # type: LongType[int, str]\n                other,                # type: Other\n            ):\n                # type: (...) -> None\n                '''\n                Some function.\n                '''\n            \"\"\",\n            \"\"\"\n            def __init__(\n                self,\n                short: Short,\n                long_argument: LongType[int, str],\n                other: Other,\n            ) -> None:\n                '''\n                Some function.\n                '''\n            \"\"\",\n        )\n\n\nclass LineReportingTestCase(BaseTestCase):\n    def compare(self, code: str, success: List[int], fail: List[int]) -> None:\n        result = com2ann(dedent(code), silent=True)\n        assert result is not None\n        _, data = result\n        self.assertEqual(data.success, success)\n        self.assertEqual(data.fail, fail)\n\n    def test_simple_assign(self) -> None:\n        self.compare(\n            \"\"\"\n            x = None  # type: Optional[str]\n            \"\"\",\n            [2],\n            [],\n        )\n\n    def test_simple_function(self) -> None:\n        self.compare(\n            \"\"\"\n            def func(arg):\n                # type: (int) -> int\n                pass\n            \"\"\",\n            [2],\n            [],\n        )\n\n    def test_unsupported_assigns(self) -> None:\n        self.compare(\n            \"\"\"\n            x, y = None, None  # type: (int, int)\n            x = None  # type: Optional[str]\n            x = y = []  # type: List[int]\n            \"\"\",\n            [3],\n            [2, 4],\n        )\n\n    def test_invalid_function_comments(self) -> None:\n        self.compare(\n            \"\"\"\n            def func(arg):\n                # type: bad\n                pass\n            def func(arg):\n                # type: bad -> bad\n                pass\n            \"\"\",\n            [],\n            [2, 5],\n        )\n\n    def test_confusing_function_comments(self) -> None:\n        self.compare(\n            \"\"\"\n            def func1(\n                arg  # type: int\n            ):\n                # type: (str) -> int\n                pass\n            def func2(arg1, arg2, arg3):\n                # type: (int) -> int\n                pass\n            \"\"\",\n            [],\n            [2, 7],\n        )\n\n    def test_unsupported_statements(self) -> None:\n        self.compare(\n            \"\"\"\n            with foo(x==1) as f: # type: str\n                print(f)\n            with foo(x==1) as f:\n                print(f)\n            x = None  # type: Optional[str]\n            for i, j in my_inter(x=1):\n                i + j\n            for i, j in my_inter(x=1): # type: (int, int)  # type: ignore\n                i + j\n            \"\"\",\n            [6],\n            [2, 9],\n        )\n\n\nclass ForAndWithTestCase(BaseTestCase):\n    def test_with(self) -> None:\n        self.check(\n            \"\"\"\n            with foo(x==1) as f: #type: str\n                print(f)\n            \"\"\",\n            \"\"\"\n            with foo(x==1) as f: #type: str\n                print(f)\n            \"\"\",\n        )\n\n    def test_for(self) -> None:\n        self.check(\n            \"\"\"\n            for i, j in my_inter(x=1): # type: (int, int)  # type: ignore\n                i + j\n            \"\"\",\n            \"\"\"\n            for i, j in my_inter(x=1): # type: (int, int)  # type: ignore\n                i + j\n            \"\"\",\n        )\n\n    def test_async_with(self) -> None:\n        self.check(\n            \"\"\"\n            async with foo(x==1) as f: #type: str\n                print(f)\n            \"\"\",\n            \"\"\"\n            async with foo(x==1) as f: #type: str\n                print(f)\n            \"\"\",\n        )\n\n    def test_async_for(self) -> None:\n        self.check(\n            \"\"\"\n            async for i, j in my_inter(x=1): # type: (int, int)  # type: ignore\n                i + j\n            \"\"\",\n            \"\"\"\n            async for i, j in my_inter(x=1): # type: (int, int)  # type: ignore\n                i + j\n            \"\"\",\n        )\n\n\nclass FutureImportTestCase(BaseTestCase):\n    def test_added_future_import(self) -> None:\n        self.check(\n            \"\"\"\n            # coding: utf-8\n\n            x = None  # type: Optional[str]\n            \"\"\",\n            \"\"\"\n            # coding: utf-8\n\n            from __future__ import annotations\n            x: Optional[str] = None\n            \"\"\",\n            i=True,\n        )\n\n    def test_not_added_future_import(self) -> None:\n        self.check(\n            \"\"\"\n            x = 1\n            \"\"\",\n            \"\"\"\n            x = 1\n            \"\"\",\n            i=True,\n        )\n        self.check(\n            \"\"\"\n            x, y = a, b  # type: Tuple[int, int]\n            \"\"\",\n            \"\"\"\n            x, y = a, b  # type: Tuple[int, int]\n            \"\"\",\n            i=True,\n        )\n        self.check(\n            \"\"\"\n            def foo(arg1, arg2):\n                # type: (int, str) -> None\n                pass\n            x = False  # type: bool\n            \"\"\",\n            \"\"\"\n            def foo(arg1: int, arg2: str) -> None:\n                pass\n            x: bool = False\n            \"\"\",\n            i=True,\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "test-requirements.txt",
    "content": "bandit\nblack\ncodespell\nflake8\nflake8-bugbear\nflake8-pyi\nisort\nmypy\npytest\npytest-cov>=2.4.0\npytest-mock>=3.7.0\npytest-xdist>=1.13\npyupgrade\nsafety\ntypes-setuptools\n"
  }
]