dflook / python-minifier

Transform Python source code into its most compact representation
MIT License
553 stars 40 forks source link

wish: remove attribute annotations not generating code #95

Open wrohdewald opened 10 months ago

wrohdewald commented 10 months ago

var: str

is a legal annotation but it creates no executable code. Would be nice to remove these.

I am using python-minifier for asserting that a commit only changes annotations but not real code. Very helpful for annotating old source code. Or would there be a better tool for this?

dflook commented 10 months ago

Hi @wrohdewald, that's an interesting use of python-minifier!

A statement like var: str is an assignment without a value, but it puts the namevar in the local scope the same way var: str = 'foo' would do. You're right it has no effect when executed, but it's presence can affect the behaviour of a program.

Consider this program:

var = 'hello'
def func():
    var: str
    print(var)

When func() is executed it should raise UnboundLocalError because var is in the function namespace but has no value. If we removed the var: str statement it would print 'hello' instead.

What python-minifier should be doing is replacing the annotation value with '0', e.g

var = 'hello'
def func():
    var: 0
    print(var)

which should preserve the behaviour but make the annotation shorter.

wrohdewald commented 10 months ago

OK, I see why this cannot be safely removed - at least not without a special option. Shortening is not much of a help - the diff output I have to read does not get shorter.

OTOH - what about code under if TYPE_CHECKING - if and only if TYPE_CHECKING is the original typing.TYPE_CHECKING - I see no reason why this could not be safely removed.

Like in

from typing import TYPE_CHECKING

b = cast(int, 1)
reveal_type(b)

if TYPE_CHECKING:
        a = 5

assert 'b' in globals()
assert 'a' not in globals()

expecting

b=1
assert'b'in globals()
assert'a'not in globals()

typing.cast() would be another candidate. reveal_type() too - it is only recognized by mypy.

dflook commented 10 months ago

Hi @wrohdewald, that's a good idea

wrohdewald commented 10 months ago

This works for me, but I do not know enough about ast to finish this. Like when somebody renames or redefines TYPE_CHECKING or cast()

class RemoveAnnotations(SuiteTransformer):
   ...
    def visit_If(self, node):
        if hasattr(node.test, 'id') and node.test.id == 'TYPE_CHECKING':
            return ast.Pass()
        return node

    def visit_ImportFrom(self, node):
        if node.module == 'typing':
            return ast.Pass()
        return node

   def visit_Call(self, node):
        if hasattr(node, 'func'):
            if hasattr(node.func, 'id'):
                if node.func.id == 'cast' and len(node.args) == 2:
                    return node.args[1]
        return node