pyflakes icon indicating copy to clipboard operation
pyflakes copied to clipboard

Detect unused functions and classes in closures

Open adamchainz opened this issue 6 years ago • 10 comments

Fixes #392.

adamchainz avatar Oct 25 '19 07:10 adamchainz

Nice. It looks good to me too. Apparently the redefinition warnings already work with functions and classes, so this adds consistency there.

asmeurer avatar Oct 25 '19 18:10 asmeurer

hmmm I'm not sure about the latest commit -- if someone is doing global decorator magic I think that should be flagged as suspicious

asottile avatar Oct 26 '19 14:10 asottile

Decorators don't always count as global or magic. For example in some tests on a project I work on we have a function that returns a flask app, something like:

def make_app():
    app = Flask('test app')
    
    @app.route('/')
    def home():
        return 'Home'
    
    return app

I feel like this is pretty standard Pythonic behaviour. The suspiciousness of whether the variable is used depends vastly on the decorators used. Also one can always course rewrite @decorator to func = decorator(func) and Flake8 will not detect that as suspicious since it's a legitimate variable use.

adamchainz avatar Oct 27 '19 08:10 adamchainz

that decorator is purely side-effects -- in my book, magic

asottile avatar Oct 31 '19 22:10 asottile

I disagree, it would be a block in Ruby or an anonymous function in JavaScript. Python just requires us to name our >1 line functions so you end up with side-effect decorators like this.

Anyway, can we merge this as-is and make a separate issue for detecting such cases? I feel like it would want to be a separate check so it can be enabled/disabled separately.

adamchainz avatar Nov 01 '19 10:11 adamchainz

Anyway, can we merge this as-is and make a separate issue for detecting such cases? I feel like it would want to be a separate check so it can be enabled/disabled separately.

if anything we should merge this without that commit and then discuss separately that controversial portion

asottile avatar Nov 01 '19 12:11 asottile

I'd be ok with merging this without 8d3dbb8 if all side effecting functions are also forbidden.

requests.post(url, data) # side effecting magic

graingert avatar Nov 02 '19 19:11 graingert

@asottile can you merge this one way or another?

I still believe ignoring decorated functions/classes makes sense.

I ran against Django master, with and without the "ignore decorated items" commit.

Including decorated items, there are 138 errors:

$ python -m pyflakes ~/Documents/Projects/django 2>/dev/null | grep -E 'local (function|class)' | wc -l
     138

Without decorated items, there are 118:

$ git checkout HEAD~1
$ python -m pyflakes ~/Documents/Projects/django 2>/dev/null | grep -E 'local (function|class)' | wc -l
     118

Of these 118, only 1 is an unused local function, that is placed into a Django view's locals to check that its stack-inspecting error logic doesn't accidentally call it:

# django/tests/view_tests/views.py
def raises(request):
    # Make sure that a callable that raises an exception in the stack frame's
    # local vars won't hijack the technical 500 response (#15025).
    def callable():
        raise Exception
    try:
        raise Exception
    except Exception:
        return technical_500_response(request, *sys.exc_info())

The other 117 seem to all be checking for exceptions during class definition (Django models, forms, etc.). Due to meta classes, defining a class is not side-effect-free - just like a decorator - and its these side effects those tests are checking for.

The 20 extra errors seen when including decorated functions/classes seem to all be based on various "registry patterns," similar to my original Flask example, for example with a Django signal:

def test_receiver_single_signal(self):
    @receiver(a_signal)
    def f(val, **kwargs):
        self.state = val
    # ...

Such code can always be rewritten like:

def test_receiver_single_signal(self):
    def f(val, **kwargs):
        self.state = val
    receiver(a_signal)(f)
    # ...

Which pyflakes would count as a legitimate variable use.

I also ran against CPython master. There were also many unused classes, all of which seem to be in tests defined for their side effects, but a number of accidentally unused functions which I fixed in https://github.com/python/cpython/pull/17189 . All unused functions with decorators were using a similar "registry" pattern again.

adamchainz avatar Nov 16 '19 10:11 adamchainz

there's a bit of a weird sign off model here where we need two approvals to merge (and I'd prefer without the decorator patch myself) -- but other than that the change looks good to me 👍

asottile avatar Nov 16 '19 14:11 asottile

Note that the decorator discussion is happening in #488 where the answer from maintainers is pretty unanimously "no"

asottile avatar Nov 16 '19 15:11 asottile