Tool for translation type comments to type annotations in Python

Overview

com2ann

Build Status Checked with mypy

Tool for translation of type comments to type annotations in Python.

The tool requires Python 3.8 to run. But the supported target code version is Python 3.4+ (can be specified with --python-minor-version).

Currently, the tool translates function and assignment type comments to type annotations. For example this code:

from typing import Optional, Final

MAX_LEVEL = 80  # type: Final

class Template:
    default = None  # type: Optional[str]

    def apply(self, value, **opts):
        # type (str, **bool) -> str
        ...

will be translated to:

from typing import Optional, Final

MAX_LEVEL: Final = 80

class Template:
    default: Optional[str] = None

    def apply(self, value: str, **opts: str) -> str:
        ...

The philosophy of the tool is to be minimally invasive, and preserve original formatting as much as possible. This is why the tool doesn't use un-parse.

The only (optional) formatting code modification is wrapping long function signatures. To specify the maximal length, use --wrap-signatures MAX_LENGTH. The signatures are wrapped one argument per line (after each comma), for example:

    def apply(self,
              value: str,
              **opts: str) -> str:
        ...

For working with stubs, there are two additional options for assignments: --drop-ellipsis and --drop-none. They will result in omitting the redundant right hand sides. For example, this:

var = ...  # type: List[int]
class Test:
    attr = None  # type: str

will be translated with such options to:

var: List[int]
class Test:
    attr: str
Comments
  • Accept multiple input files

    Accept multiple input files

    Closes #45

    That part of the code seems not to be tested, so I did some manual testing but nothing automated

    $ tail {a,b}.py
    ==> a.py <==
    def a(i):
        # type: (int) -> None
        pass
    
    ==> b.py <==
    def b(i):
        # type: (int) -> None
        pass
    
    
    $ com2ann a.py b.py
    File: a.py
    Comments translated for statements on lines: 1
    File: b.py
    Comments translated for statements on lines: 1
    
    
    $ tail {a,b}.py
    ==> a.py <==
    def a(i: int) -> None:
        pass
    
    ==> b.py <==
    def b(i: int) -> None:
        pass
    
    opened by ewjoachim 10
  • GitHub Action to lint Python code

    GitHub Action to lint Python code

    Because Travis CI seems to be on (permanent?) vacation.

    • https://travis-ci.org/github/ilevkivskyi/com2ann/builds

    Results: https://github.com/cclauss/com2ann/actions

    opened by cclauss 5
  • Transformation of typed class factories breaks the code

    Transformation of typed class factories breaks the code

    Given...

    from typing import Optional
    from typing import List
    
    class Pizza:
        def __init__(self, ingredients=None):
            # type: (Optional[List[str]]) -> None
            if ingredients is None:
                self.ingredients = []
            else:
                self.ingredients = ingredients
    
        def __repr__(self):
            # type: () -> str
            return "This is a Pizza with %s on it" % " ".join(self.ingredients)
    
        @classmethod
        def pizza_salami(cls):
            # type: () -> Pizza
            return cls(ingredients=["Salami", "Cheese", "Onions"])
    

    ... using com2ann factory.py, I receive the following code ...

    from typing import Optional
    from typing import List
    
    class Pizza:
        def __init__(self, ingredients: Optional[List[str]] = None) -> None:
            if ingredients is None:
                self.ingredients = []
            else:
                self.ingredients = ingredients
    
        def __repr__(self) -> str:
            return "This is a Pizza with %s on it" % " ".join(self.ingredients)
    
        @classmethod
        def pizza_salami(cls) -> Pizza:
            return cls(ingredients=["Salami", "Cheese", "Onions"])
    

    ... which is no valid Python code any more as Pizza is not yet defined.

    Here, the fix is to add quotes around the Pizza return type in the factory method.

    As I am brand new to Python typing, maybe there are some other ways to fix this problem.

    Maybe this discussion is any help: https://github.com/python/typing/issues/58

    opened by jugmac00 3
  • Do you mind if I delete the utility from the Python repo?

    Do you mind if I delete the utility from the Python repo?

    I expect that the copy of this utility in the core Python repo is very soon going to be out of date and forgotten. I propose to delete it from there so we don't have to deal with well-meaning core devs making uncoordinated changes there.

    opened by gvanrossum 3
  • Release to PyPI?

    Release to PyPI?

    Hi @ilevkivskyi - I was wondering if you could release a new version on PyPI? It would be lovely to have the python-requires metadata reflect actual compatibility, and the annotated-type-ignore support would also be great.

    opened by Zac-HD 2
  • How to handle all cases...

    How to handle all cases...

    I propose that you handle all type comments in the target program. That means with and for statements and all degrees of complexity in assignments. Comments that are ill-formed should be reported and left alone. Otherwise the comments will be stripped and the appropriate annotations inserted to replace them. I have a package scopetools which can provide lots of help. It is still under development. I'd like to work with you in enhancing scopetools and integrating it into com2ann, so please let me know if you are interested. It can find all the type comments and their containing scopes, and the owning scopes for global and nonlocal names. It can split complex target assignments, or type comments, into simpler items (names, attributes, and subscripts).

    Here's the basic strategy:

    • Function defs and args are fine. However, if there is an annotation for one of the arguments, the type from the function comment is added to the arg, resulting in two annotations, a SyntaxError. Other than that...

    Every statement has a target (or multiple targets for an assignment) and a type. With multiple targets, the same type applies to each target. The type is the type comment, parsed as an expression. Annotating the target with the type is the same process as assigning the type to the target. That is, if the target is a packing (a tuple or list of subtargets), the subtargets are annotated with subtypes derived from iterating on the type, taking into account a possible starred subtarget. If the number of subtypes is wrong, this is a malformed type comment, just like an assignment statement. This is done recursively. The result is that you have a set of (sub)targets and (sub)types, where each target is an ast.Name, ast.Subscript, or ast.Attribute. Attribute and Subscript annotations can just be inserted before the original statement. Name annotations are more complicated:

    • If the name is declared nonlocal or global (but not at the module level), then a bare annotation (that is, without an assigned value) is placed in the owning scope (the module or some enclosing function) just before the scope definition which contains the original statement. This way, if the scope definition is unreachable, then a type checker will ignore the new annotation.
    • If the statement is a simple assignment with one target and no subtargets, then the annotation is inserted after the name.
    • Otherwise, a bare annotation is inserted before the statement.

    The target for a statement is as follows:

    • Assignment: target [ = target ... ] = value # type: typeexpr Each target is a target and annotated with the entiretypeexpr. Examples: w, (x.a, (y[0], z)) = value # type: t1, (t2, (t3, t4)). This annotates w, x.a, y[0], and z with t1, t2, t3, and t4, resp. t = w, (x.a, (y[0], z)) = value # type: t1, (t2, (t3, t4)). Same and annotates t with (t1, (t2, (t3, t4))).

    • For: for target in iterable: # type: typeexpr target is the target.

    • With: with context [ as target ] [ , context [as target] ... ]: # type: typeexpr

      • With no as target clause, this statement is ignored.
      • With exactly one as target clause, target is the target.
      • With more than one, then target is (target, target [ , target ... ]). The tuple contains only the targets from all the as target clauses that are present.

      Examples: with c1 as t1, c2, c3 as t3.x: # type: type1, type3:. This annotates t1 with type1 and t3.x with type3. with c1 as t1, c2, c3: # type: type1:. This annotates t1 with type1. with c1 as t1: # type: type1:. This annotates t1 with type1. with c1 as t1: # type: type1, type2:. This annotates t1 with (type1, type2). The tuple is not unpacked.

    opened by mrolle45 1
  • Is the --python-minor-version used correctly ?

    Is the --python-minor-version used correctly ?

    I could be wrong, but following the code, whatever is put in --python-minor-version, as an int, is passed to ast.parse(feature_version=<>).

    The doc of ast.parse says:

    Also, setting feature_version to a tuple (major, minor) will attempt to parse using that Python version’s grammar. Currently major must equal to 3. For example, setting feature_version=(3, 4) will allow the use of async and await as variable names. The lowest supported version is (3, 4); the highest is sys.version_info[0:2].

    I think we should be doing ast.parse(feature_version=(3, python_minor_version)).

    Can you confirm ? Thank you !

    opened by ewjoachim 1
  • Type comment left in place when expression is type ignored

    Type comment left in place when expression is type ignored

    Thanks for this tool, it's really useful :)

    If you have code like this:

        foo = object()
    
        bar = (
            # Comment which explains why this ignored
            foo.quox   # type: ignore[attribute]
        )  # type: Mapping[str, Distribution]
    

    Then after running com2ann you end up with:

        foo = object()
    
        bar: Mapping[str, int] = (
            # Comment which explains why this ignored
            foo.quox   # type: ignore[attribute]
        )  # type: Mapping[str, int]
    

    which mypy then complains about due to the double signature.

    The intermediate (explanatory) comment doesn't seem to be related, though the type: ignore comment is.

    opened by PeterJCLaw 1
  • Require Python 3.8+ to install

    Require Python 3.8+ to install

    Without the python_requires metadata, the version check does nothing at all when installed from a bdist from e.g. PyPI. After shipping this new version, you may want to "yank" older releases so that naive install commands on older Pythons get an explicit error rather than an incompatible package.

    Prompted by my debugging this exact issue when using com2ann on Hypothesis, then planning to build it into shed --refactor and realising that I couldn't rely on importable == usable.

    opened by Zac-HD 1
  • ERROR: Could not find a version that satisfies the requirement com2amm (from versions: none)

    ERROR: Could not find a version that satisfies the requirement com2amm (from versions: none)

    pip install -v  com2amm                        
    Using pip 20.2.1 from /home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip (python 3.8)
    Non-user install because user site-packages disabled
    Created temporary directory: /tmp/pip-ephem-wheel-cache-yrrlga4n
    Created temporary directory: /tmp/pip-req-tracker-p88wxv0i
    Initialized build tracking at /tmp/pip-req-tracker-p88wxv0i
    Created build tracker: /tmp/pip-req-tracker-p88wxv0i
    Entered build tracker: /tmp/pip-req-tracker-p88wxv0i
    Created temporary directory: /tmp/pip-install-2skuyzet
    1 location(s) to search for versions of com2amm:
    * https://pypi.org/simple/com2amm/
    Fetching project page and analyzing links: https://pypi.org/simple/com2amm/
    Getting page https://pypi.org/simple/com2amm/
    Found index url https://pypi.org/simple
    Looking up "https://pypi.org/simple/com2amm/" in the cache
    Request header has "max_age" as 0, cache bypassed
    Starting new HTTPS connection (1): pypi.org:443
    https://pypi.org:443 "GET /simple/com2amm/ HTTP/1.1" 404 13
    Status code 404 not in (200, 203, 300, 301)
    Could not fetch URL https://pypi.org/simple/com2amm/: 404 Client Error: Not Found for url: https://pypi.org/simple/com2amm/ - skipping
    Given no hashes to check 0 links for project 'com2amm': discarding no candidates
    ERROR: Could not find a version that satisfies the requirement com2amm (from versions: none)
    ERROR: No matching distribution found for com2amm
    Exception information:
    Traceback (most recent call last):
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/cli/base_command.py", line 216, in _main
        status = self.run(options, args)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/cli/req_command.py", line 182, in wrapper
        return func(self, options, args)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/commands/install.py", line 324, in run
        requirement_set = resolver.resolve(
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/resolution/legacy/resolver.py", line 183, in resolve
        discovered_reqs.extend(self._resolve_one(requirement_set, req))
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/resolution/legacy/resolver.py", line 388, in _resolve_one
        abstract_dist = self._get_abstract_dist_for(req_to_install)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/resolution/legacy/resolver.py", line 339, in _get_abstract_dist_for
        self._populate_link(req)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/resolution/legacy/resolver.py", line 305, in _populate_link
        req.link = self._find_requirement_link(req)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/resolution/legacy/resolver.py", line 270, in _find_requirement_link
        best_candidate = self.finder.find_requirement(req, upgrade)
      File "/home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip/_internal/index/package_finder.py", line 926, in find_requirement
        raise DistributionNotFound(
    pip._internal.exceptions.DistributionNotFound: No matching distribution found for com2amm
    Removed build tracker: '/tmp/pip-req-tracker-p88wxv0i'
    
    
    $ pip --version
    pip 20.2.1 from /home/sebastian/Repos/ceph/src/pybind/mgr/venv38/lib/python3.8/site-packages/pip (python 3.8)
    

    Looks like pip needs some new mandatory information? Might be related to https://pip.pypa.io/en/stable/user_guide/#changes-to-the-pip-dependency-resolver-in-20-2-2020

    opened by sebastian-philipp 1
  • Merge type annotation from typeshed

    Merge type annotation from typeshed

    typeshed already carries type annotations for many "legacy" Python modules. As they evolve it becomes harder and harder to keep them synchronized. This becomes a non-issue if the type annotation is already included with the upstream source code and thus the typeshed is not needed at all. It would be cool if com2ann had a mode to merge the annotations from typeshed into the source code of the module itself.

    opened by pmhahn 1
  • SyntaxError with function type comment and annotated argument

    SyntaxError with function type comment and annotated argument

    Bug Report:

    Example program:

    def f(x: bool
    	) -> foo:
    	# type: (int) -> str
    	pass
    

    Result of com2ann:

    def f(x: bool: int
            ) -> str:
            pass
    

    The x: bool: int is a SyntaxError. In this situation, there are various ways to handle it:

    • Reject the function type comment. Preferably, output an explanation for the rejection.
    • If the function comment and the parameter annotation are the same, leave the parameter as is. In any event, you will have to examine the syntax tree for the function def and find the parameter annotations. If the function comment uses an ellipsis, then this does not apply; only the return type annotation is added.
    bug 
    opened by mrolle45 1
  • SyntaxError from assignment to nonlocal or global variable.

    SyntaxError from assignment to nonlocal or global variable.

    Bug Report:

    When assigning to a nonlocal or global variable, the translation contains syntax errors, because these variables cannot be annotated. Example

    class C:
    	global x
    	x = 2 # type: int
    

    This translates to

    class C:
            global x
            x: int = 2
    

    Running this results in

      File "t.py", line 3
        x: int = 2
        ^
    SyntaxError: annotated name 'x' can't be global
    

    Similar result if x is nonlocal rather than global.

    What to do?

    First of all, you need to find the global or nonlocal statement for the variable in its enclosing scope. Then you will know that the variable cannot be annotated at that point. Next, decide how you want to handle it, and then implement it. Some possibilities are:

    • Output an error message, with the line number, variable name, and whether it is nonlocal or global. And/or just put it in the "comments skipped" message.
    • Remove the type comment and annotate x in the scope which owns x.
      For a global, this is at the module level. For a nonlocal, you have to search enclosing scopes to find the one where x is bound.
      In either case,
      • if x is assigned in the scope, just add the annotation to it.
      • Otherwise, add a bare annotation somewhere in the scope. It should probably be in the same control flow block, so that if the code is unreachable, then a type checker won't use the annotation either.

    Some code that might be useful.

    I am writing a package which performs analysis tasks related to scopes and namespaces in a python program. It is still a work in progress. However, there is already enough functionality that you could find useful, not only in fixing the present bug but for other tasks as well. You can find it at mrolle45/scopetools. Please let me know if you find it interesting, and I'd like to work with you in incorporating my code into your code. scopetools will create a tree of Scope objects representing the scopes found in the ast.Module tree. The Scope will give you the status of a variable name including which Scope actually owns it. It also points to the ast object for that scope, so you can traverse it. I plan to provide a method to traverse the ast omitting any nested scopes. That way, you can discover all the type comments in the module and the scopes that contain them.

    opened by mrolle45 1
  • Support tuple types comments

    Support tuple types comments

    We should support translating type comments like this:

    x, y = ...  # type: Tuple[int, int]
    

    Note this may be not totally trivial in r.h.s. is a single expression (not a tuple). In this case we may need to add annotations before assignment, like this

    x: int
    y: int
    x, y = v
    
    priority-high 
    opened by ilevkivskyi 2
  • Syntax error when docstring proceeds comment with type annotation

    Syntax error when docstring proceeds comment with type annotation

    When a function in source file contains both type annotation comment and docstring, but in wrong order, com2ann fails with SyntaxError.

    Expected behaviour:

    • When the type comment is placed after docstring, it should be considered to be a basic inline comment and skipped.
    • Com2ann should skip the function and process the rest of the file.
    • The comment should be left in place so users can fix it themselves
    • Ideally there should an warning in the output informing user there was a function that was skipped due to misplaced type comment.

    How to reproduce: Create file test.py:

    class Klass:
        def function(self, parameter):
            """Comment"""
            # type: (str) -> str
            return parameter
    

    Call com2ann: com2ann test.py

    Output:

    File: test.py
    SyntaxError in test.py
    
    opened by JakubTesarek 3
  • Support translation for for-loops and with-statements

    Support translation for for-loops and with-statements

    Potentially, we can translate this:

    for i, j in foo(bar):  # type: (int, int)
        ...
    

    to something like this

    i: int
    j: int
    for i, j in foo(bar):
        ...
    
    enhancement 
    opened by ilevkivskyi 0
Owner
Ivan Levkivskyi
Ivan Levkivskyi
Re-apply type annotations from .pyi stubs to your codebase.

retype Re-apply type annotations from .pyi stubs to your codebase. Usage Usage: retype [OPTIONS] [SRC]... Re-apply type annotations from .pyi stubs

Łukasz Langa 131 Nov 17, 2022
Auto-generate PEP-484 annotations

PyAnnotate: Auto-generate PEP-484 annotations Insert annotations into your source code based on call arguments and return types observed at runtime. F

Dropbox 1.4k Dec 26, 2022
AST based refactoring tool for Python.

breakfast AST based refactoring tool. (Very early days, not usable yet.) Why 'breakfast'? I don't know about the most important, but it's a good meal.

eric casteleijn 0 Feb 22, 2022
A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language.

pyupgrade A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language. Installation pip install pyupgrade As a pre

Anthony Sottile 2.4k Jan 8, 2023
A tool (and pre-commit hook) to automatically add trailing commas to calls and literals.

add-trailing-comma A tool (and pre-commit hook) to automatically add trailing commas to calls and literals. Installation pip install add-trailing-comm

Anthony Sottile 264 Dec 20, 2022
A simple Python bytecode framework in pure Python

A simple Python bytecode framework in pure Python

null 3 Jan 23, 2022
Awesome autocompletion, static analysis and refactoring library for python

Jedi - an awesome autocompletion, static analysis and refactoring library for Python Jedi is a static analysis tool for Python that is typically used

Dave Halter 5.3k Dec 29, 2022
a python refactoring library

rope, a python refactoring library ... Overview Rope is a python refactoring library. Notes Nick Smith <[email protected]> takes over maintaining rope

null 1.5k Dec 30, 2022
Find dead Python code

Vulture - Find dead code Vulture finds unused code in Python programs. This is useful for cleaning up and finding errors in large code bases. If you r

Jendrik Seipp 2.4k Dec 27, 2022
Safe code refactoring for modern Python.

Safe code refactoring for modern Python projects. Overview Bowler is a refactoring tool for manipulating Python at the syntax tree level. It enables s

Facebook Incubator 1.4k Jan 4, 2023
Programmatically edit text files with Python. Useful for source to source transformations.

massedit formerly known as Python Mass Editor Implements a python mass editor to process text files using Python code. The modification(s) is (are) sh

null 106 Dec 17, 2022
Bottom-up approach to refactoring in python

Introduction RedBaron is a python library and tool powerful enough to be used into IPython solely that intent to make the process of writing code that

Python Code Quality Authority 653 Dec 30, 2022
Removes commented-out code from Python files

eradicate eradicate removes commented-out code from Python files. Introduction With modern revision control available, there is no reason to save comm

Steven Myint 146 Dec 13, 2022
A library that modifies python source code to conform to pep8.

Pep8ify: Clean your code with ease Pep8ify is a library that modifies python source code to conform to pep8. Installation This library currently works

Steve Pulec 117 Jan 3, 2023
Code generation and code search for Python and Javascript.

Codeon Code generation and code search for Python and Javascript. Similar to GitHub Copilot with one major difference: Code search is leveraged to mak

null 51 Dec 8, 2022
Refactoring Python Applications for Simplicity

Python Refactoring Refactoring Python Applications for Simplicity. You can open and read project files or use this summary ?? Concatenate String my_st

Mohammad Dori 3 Jul 15, 2022
Leap is an experimental package written to enable the utilization of C-like goto statements in Python functions

Leap is an experimental package written to enable the utilization of C-like goto statements in Python functions

null 6 Dec 26, 2022
Turn your C++/Java code into a Python-like format for extra style points and to make everyone hates you

Turn your C++/Java code into a Python-like format for extra style points and to make everyone hates you

Tô Đức (Watson) 4 Feb 7, 2022
A music comments dataset, containing 39,051 comments for 27,384 songs.

Music Comments Dataset A music comments dataset, containing 39,051 comments for 27,384 songs. For academic research use only. Introduction This datase

Zhang Yixiao 2 Jan 10, 2022
A system for Python that generates static type annotations by collecting runtime types

MonkeyType MonkeyType collects runtime types of function arguments and return values, and can automatically generate stub files or even add draft type

Instagram 4.1k Jan 2, 2023