
from sys import version_info

from pyflakes import messages as m
from pyflakes.test.harness import TestCase, skip, skipIf


class Test(TestCase):

    def test_unusedImport(self):
        self.flakes('import fu, bar', m.UnusedImport, m.UnusedImport)
        self.flakes('from baz import fu, bar', m.UnusedImport, m.UnusedImport)

    def test_aliasedImport(self):
        self.flakes('import fu as FU, bar as FU',
                    m.RedefinedWhileUnused, m.UnusedImport)
        self.flakes('from moo import fu as FU, bar as FU',
                    m.RedefinedWhileUnused, m.UnusedImport)

    def test_usedImport(self):
        self.flakes('import fu; print(fu)')
        self.flakes('from baz import fu; print(fu)')
        self.flakes('import fu; del fu')

    def test_redefinedWhileUnused(self):
        self.flakes('import fu; fu = 3', m.RedefinedWhileUnused)
        self.flakes('import fu; fu, bar = 3', m.RedefinedWhileUnused)
        self.flakes('import fu; [fu, bar] = 3', m.RedefinedWhileUnused)

    def test_redefinedIf(self):
        """
        Test that importing a module twice within an if
        block does raise a warning.
        """
        self.flakes('''
        i = 2
        if i==1:
            import os
            import os
        os.path''', m.RedefinedWhileUnused)

    def test_redefinedIfElse(self):
        """
        Test that importing a module twice in if
        and else blocks does not raise a warning.
        """
        self.flakes('''
        i = 2
        if i==1:
            import os
        else:
            import os
        os.path''')

    def test_redefinedTry(self):
        """
        Test that importing a module twice in an try block
        does raise a warning.
        """
        self.flakes('''
        try:
            import os
            import os
        except:
            pass
        os.path''', m.RedefinedWhileUnused)

    def test_redefinedTryExcept(self):
        """
        Test that importing a module twice in an try
        and except block does not raise a warning.
        """
        self.flakes('''
        try:
            import os
        except:
            import os
        os.path''')

    def test_redefinedTryNested(self):
        """
        Test that importing a module twice using a nested
        try/except and if blocks does not issue a warning.
        """
        self.flakes('''
        try:
            if True:
                if True:
                    import os
        except:
            import os
        os.path''')

    def test_redefinedTryExceptMulti(self):
        self.flakes("""
        try:
            from aa import mixer
        except AttributeError:
            from bb import mixer
        except RuntimeError:
            from cc import mixer
        except:
            from dd import mixer
        mixer(123)
        """)

    def test_redefinedTryElse(self):
        self.flakes("""
        try:
            from aa import mixer
        except ImportError:
            pass
        else:
            from bb import mixer
        mixer(123)
        """, m.RedefinedWhileUnused)

    def test_redefinedTryExceptElse(self):
        self.flakes("""
        try:
            import funca
        except ImportError:
            from bb import funca
            from bb import funcb
        else:
            from bbb import funcb
        print(funca, funcb)
        """)

    def test_redefinedTryExceptFinally(self):
        self.flakes("""
        try:
            from aa import a
        except ImportError:
            from bb import a
        finally:
            a = 42
        print(a)
        """)

    def test_redefinedTryExceptElseFinally(self):
        self.flakes("""
        try:
            import b
        except ImportError:
            b = Ellipsis
            from bb import a
        else:
            from aa import a
        finally:
            a = 42
        print(a, b)
        """)

    def test_redefinedByFunction(self):
        self.flakes('''
        import fu
        def fu():
            pass
        ''', m.RedefinedWhileUnused)

    def test_redefinedInNestedFunction(self):
        """
        Test that shadowing a global name with a nested function definition
        generates a warning.
        """
        self.flakes('''
        import fu
        def bar():
            def baz():
                def fu():
                    pass
        ''', m.RedefinedWhileUnused, m.UnusedImport)

    def test_redefinedInNestedFunctionTwice(self):
        """
        Test that shadowing a global name with a nested function definition
        generates a warning.
        """
        self.flakes('''
        import fu
        def bar():
            import fu
            def baz():
                def fu():
                    pass
        ''',
                    m.RedefinedWhileUnused, m.RedefinedWhileUnused,
                    m.UnusedImport, m.UnusedImport)

    def test_redefinedButUsedLater(self):
        """
        Test that a global import which is redefined locally,
        but used later in another scope does not generate a warning.
        """
        self.flakes('''
        import unittest, transport

        class GetTransportTestCase(unittest.TestCase):
            def test_get_transport(self):
                transport = 'transport'
                self.assertIsNotNone(transport)

        class TestTransportMethodArgs(unittest.TestCase):
            def test_send_defaults(self):
                transport.Transport()
        ''')

    def test_redefinedByClass(self):
        self.flakes('''
        import fu
        class fu:
            pass
        ''', m.RedefinedWhileUnused)

    def test_redefinedBySubclass(self):
        """
        If an imported name is redefined by a class statement which also uses
        that name in the bases list, no warning is emitted.
        """
        self.flakes('''
        from fu import bar
        class bar(bar):
            pass
        ''')

    def test_redefinedInClass(self):
        """
        Test that shadowing a global with a class attribute does not produce a
        warning.
        """
        self.flakes('''
        import fu
        class bar:
            fu = 1
        print(fu)
        ''')

    def test_importInClass(self):
        """
        Test that import within class is a locally scoped attribute.
        """
        self.flakes('''
        class bar:
            import fu
        ''')

        self.flakes('''
        class bar:
            import fu

        fu
        ''', m.UndefinedName)

    def test_usedInFunction(self):
        self.flakes('''
        import fu
        def fun():
            print(fu)
        ''')

    def test_shadowedByParameter(self):
        self.flakes('''
        import fu
        def fun(fu):
            print(fu)
        ''', m.UnusedImport, m.RedefinedWhileUnused)

        self.flakes('''
        import fu
        def fun(fu):
            print(fu)
        print(fu)
        ''')

    def test_newAssignment(self):
        self.flakes('fu = None')

    def test_usedInGetattr(self):
        self.flakes('import fu; fu.bar.baz')
        self.flakes('import fu; "bar".fu.baz', m.UnusedImport)

    def test_usedInSlice(self):
        self.flakes('import fu; print(fu.bar[1:])')

    def test_usedInIfBody(self):
        self.flakes('''
        import fu
        if True: print(fu)
        ''')

    def test_usedInIfConditional(self):
        self.flakes('''
        import fu
        if fu: pass
        ''')

    def test_usedInElifConditional(self):
        self.flakes('''
        import fu
        if False: pass
        elif fu: pass
        ''')

    def test_usedInElse(self):
        self.flakes('''
        import fu
        if False: pass
        else: print(fu)
        ''')

    def test_usedInCall(self):
        self.flakes('import fu; fu.bar()')

    def test_usedInClass(self):
        self.flakes('''
        import fu
        class bar:
            bar = fu
        ''')

    def test_usedInClassBase(self):
        self.flakes('''
        import fu
        class bar(object, fu.baz):
            pass
        ''')

    def test_notUsedInNestedScope(self):
        self.flakes('''
        import fu
        def bleh():
            pass
        print(fu)
        ''')

    def test_usedInFor(self):
        self.flakes('''
        import fu
        for bar in range(9):
            print(fu)
        ''')

    def test_usedInForElse(self):
        self.flakes('''
        import fu
        for bar in range(10):
            pass
        else:
            print(fu)
        ''')

    def test_redefinedByFor(self):
        self.flakes('''
        import fu
        for fu in range(2):
            pass
        ''', m.ImportShadowedByLoopVar)

    def test_shadowedByFor(self):
        """
        Test that shadowing a global name with a for loop variable generates a
        warning.
        """
        self.flakes('''
        import fu
        fu.bar()
        for fu in ():
            pass
        ''', m.ImportShadowedByLoopVar)

    def test_shadowedByForDeep(self):
        """
        Test that shadowing a global name with a for loop variable nested in a
        tuple unpack generates a warning.
        """
        self.flakes('''
        import fu
        fu.bar()
        for (x, y, z, (a, b, c, (fu,))) in ():
            pass
        ''', m.ImportShadowedByLoopVar)
        # Same with a list instead of a tuple
        self.flakes('''
        import fu
        fu.bar()
        for [x, y, z, (a, b, c, (fu,))] in ():
            pass
        ''', m.ImportShadowedByLoopVar)

    def test_usedInReturn(self):
        self.flakes('''
        import fu
        def fun():
            return fu
        ''')

    def test_usedInOperators(self):
        self.flakes('import fu; 3 + fu.bar')
        self.flakes('import fu; 3 % fu.bar')
        self.flakes('import fu; 3 - fu.bar')
        self.flakes('import fu; 3 * fu.bar')
        self.flakes('import fu; 3 ** fu.bar')
        self.flakes('import fu; 3 / fu.bar')
        self.flakes('import fu; 3 // fu.bar')
        self.flakes('import fu; -fu.bar')
        self.flakes('import fu; ~fu.bar')
        self.flakes('import fu; 1 == fu.bar')
        self.flakes('import fu; 1 | fu.bar')
        self.flakes('import fu; 1 & fu.bar')
        self.flakes('import fu; 1 ^ fu.bar')
        self.flakes('import fu; 1 >> fu.bar')
        self.flakes('import fu; 1 << fu.bar')

    def test_usedInAssert(self):
        self.flakes('import fu; assert fu.bar')

    def test_usedInSubscript(self):
        self.flakes('import fu; fu.bar[1]')

    def test_usedInLogic(self):
        self.flakes('import fu; fu and False')
        self.flakes('import fu; fu or False')
        self.flakes('import fu; not fu.bar')

    def test_usedInList(self):
        self.flakes('import fu; [fu]')

    def test_usedInTuple(self):
        self.flakes('import fu; (fu,)')

    def test_usedInTry(self):
        self.flakes('''
        import fu
        try: fu
        except: pass
        ''')

    def test_usedInExcept(self):
        self.flakes('''
        import fu
        try: fu
        except: pass
        ''')

    def test_redefinedByExcept(self):
        as_exc = ', ' if version_info < (2, 6) else ' as '
        self.flakes('''
        import fu
        try: pass
        except Exception%sfu: pass
        ''' % as_exc, m.RedefinedWhileUnused)

    def test_usedInRaise(self):
        self.flakes('''
        import fu
        raise fu.bar
        ''')

    def test_usedInYield(self):
        self.flakes('''
        import fu
        def gen():
            yield fu
        ''')

    def test_usedInDict(self):
        self.flakes('import fu; {fu:None}')
        self.flakes('import fu; {1:fu}')

    def test_usedInParameterDefault(self):
        self.flakes('''
        import fu
        def f(bar=fu):
            pass
        ''')

    def test_usedInAttributeAssign(self):
        self.flakes('import fu; fu.bar = 1')

    def test_usedInKeywordArg(self):
        self.flakes('import fu; fu.bar(stuff=fu)')

    def test_usedInAssignment(self):
        self.flakes('import fu; bar=fu')
        self.flakes('import fu; n=0; n+=fu')

    def test_usedInListComp(self):
        self.flakes('import fu; [fu for _ in range(1)]')
        self.flakes('import fu; [1 for _ in range(1) if fu]')

    @skipIf(version_info >= (3,),
            'in Python 3 list comprehensions execute in a separate scope')
    def test_redefinedByListComp(self):
        self.flakes('import fu; [1 for fu in range(1)]',
                    m.RedefinedInListComp)

    def test_usedInTryFinally(self):
        self.flakes('''
        import fu
        try: pass
        finally: fu
        ''')

        self.flakes('''
        import fu
        try: fu
        finally: pass
        ''')

    def test_usedInWhile(self):
        self.flakes('''
        import fu
        while 0:
            fu
        ''')

        self.flakes('''
        import fu
        while fu: pass
        ''')

    def test_usedInGlobal(self):
        """
        A 'global' statement shadowing an unused import should not prevent it
        from being reported.
        """
        self.flakes('''
        import fu
        def f(): global fu
        ''', m.UnusedImport)

    def test_usedAndGlobal(self):
        """
        A 'global' statement shadowing a used import should not cause it to be
        reported as unused.
        """
        self.flakes('''
            import foo
            def f(): global foo
            def g(): foo.is_used()
        ''')

    def test_assignedToGlobal(self):
        """
        Binding an import to a declared global should not cause it to be
        reported as unused.
        """
        self.flakes('''
            def f(): global foo; import foo
            def g(): foo.is_used()
        ''')

    @skipIf(version_info >= (3,), 'deprecated syntax')
    def test_usedInBackquote(self):
        self.flakes('import fu; `fu`')

    def test_usedInExec(self):
        if version_info < (3,):
            exec_stmt = 'exec "print 1" in fu.bar'
        else:
            exec_stmt = 'exec("print(1)", fu.bar)'
        self.flakes('import fu; %s' % exec_stmt)

    def test_usedInLambda(self):
        self.flakes('import fu; lambda: fu')

    def test_shadowedByLambda(self):
        self.flakes('import fu; lambda fu: fu',
                    m.UnusedImport, m.RedefinedWhileUnused)
        self.flakes('import fu; lambda fu: fu\nfu()')

    def test_usedInSliceObj(self):
        self.flakes('import fu; "meow"[::fu]')

    def test_unusedInNestedScope(self):
        self.flakes('''
        def bar():
            import fu
        fu
        ''', m.UnusedImport, m.UndefinedName)

    def test_methodsDontUseClassScope(self):
        self.flakes('''
        class bar:
            import fu
            def fun(self):
                fu
        ''', m.UndefinedName)

    def test_nestedFunctionsNestScope(self):
        self.flakes('''
        def a():
            def b():
                fu
            import fu
        ''')

    def test_nestedClassAndFunctionScope(self):
        self.flakes('''
        def a():
            import fu
            class b:
                def c(self):
                    print(fu)
        ''')

    def test_importStar(self):
        """Use of import * at module level is reported."""
        self.flakes('from fu import *', m.ImportStarUsed, m.UnusedImport)
        self.flakes('''
        try:
            from fu import *
        except:
            pass
        ''', m.ImportStarUsed, m.UnusedImport)

    @skipIf(version_info < (3,),
            'import * below module level is a warning on Python 2')
    def test_localImportStar(self):
        """import * is only allowed at module level."""
        self.flakes('''
        def a():
            from fu import *
        ''', m.ImportStarNotPermitted)
        self.flakes('''
        class a:
            from fu import *
        ''', m.ImportStarNotPermitted)

    @skipIf(version_info > (3,),
            'import * below module level is an error on Python 3')
    def test_importStarNested(self):
        """All star imports are marked as used by an undefined variable."""
        self.flakes('''
        from fu import *
        def f():
            from bar import *
            x
        ''', m.ImportStarUsed, m.ImportStarUsed, m.ImportStarUsage)

    def test_packageImport(self):
        """
        If a dotted name is imported and used, no warning is reported.
        """
        self.flakes('''
        import fu.bar
        fu.bar
        ''')

    def test_unusedPackageImport(self):
        """
        If a dotted name is imported and not used, an unused import warning is
        reported.
        """
        self.flakes('import fu.bar', m.UnusedImport)

    def test_duplicateSubmoduleImport(self):
        """
        If a submodule of a package is imported twice, an unused import warning
        and a redefined while unused warning are reported.
        """
        self.flakes('''
        import fu.bar, fu.bar
        fu.bar
        ''', m.RedefinedWhileUnused)
        self.flakes('''
        import fu.bar
        import fu.bar
        fu.bar
        ''', m.RedefinedWhileUnused)

    def test_differentSubmoduleImport(self):
        """
        If two different submodules of a package are imported, no duplicate
        import warning is reported for the package.
        """
        self.flakes('''
        import fu.bar, fu.baz
        fu.bar, fu.baz
        ''')
        self.flakes('''
        import fu.bar
        import fu.baz
        fu.bar, fu.baz
        ''')

    def test_assignRHSFirst(self):
        self.flakes('import fu; fu = fu')
        self.flakes('import fu; fu, bar = fu')
        self.flakes('import fu; [fu, bar] = fu')
        self.flakes('import fu; fu += fu')

    def test_tryingMultipleImports(self):
        self.flakes('''
        try:
            import fu
        except ImportError:
            import bar as fu
        fu
        ''')

    def test_nonGlobalDoesNotRedefine(self):
        self.flakes('''
        import fu
        def a():
            fu = 3
            return fu
        fu
        ''')

    def test_functionsRunLater(self):
        self.flakes('''
        def a():
            fu
        import fu
        ''')

    def test_functionNamesAreBoundNow(self):
        self.flakes('''
        import fu
        def fu():
            fu
        fu
        ''', m.RedefinedWhileUnused)

    def test_ignoreNonImportRedefinitions(self):
        self.flakes('a = 1; a = 2')

    @skip("todo")
    def test_importingForImportError(self):
        self.flakes('''
        try:
            import fu
        except ImportError:
            pass
        ''')

    def test_importedInClass(self):
        """Imports in class scope can be used through self."""
        self.flakes('''
        class c:
            import i
            def __init__(self):
                self.i
        ''')

    def test_importUsedInMethodDefinition(self):
        """
        Method named 'foo' with default args referring to module named 'foo'.
        """
        self.flakes('''
        import foo

        class Thing(object):
            def foo(self, parser=foo.parse_foo):
                pass
        ''')

    def test_futureImport(self):
        """__future__ is special."""
        self.flakes('from __future__ import division')
        self.flakes('''
        "docstring is allowed before future import"
        from __future__ import division
        ''')

    def test_futureImportFirst(self):
        """
        __future__ imports must come before anything else.
        """
        self.flakes('''
        x = 5
        from __future__ import division
        ''', m.LateFutureImport)
        self.flakes('''
        from foo import bar
        from __future__ import division
        bar
        ''', m.LateFutureImport)

    def test_futureImportUsed(self):
        """__future__ is special, but names are injected in the namespace."""
        self.flakes('''
        from __future__ import division
        from __future__ import print_function

        assert print_function is not division
        ''')

    def test_futureImportUndefined(self):
        """Importing undefined names from __future__ fails."""
        self.flakes('''
        from __future__ import print_statement
        ''', m.FutureFeatureNotDefined)

    def test_futureImportStar(self):
        """Importing '*' from __future__ fails."""
        self.flakes('''
        from __future__ import *
        ''', m.FutureFeatureNotDefined)


class TestSpecialAll(TestCase):
    """
    Tests for suppression of unused import warnings by C{__all__}.
    """
    def test_ignoredInFunction(self):
        """
        An C{__all__} definition does not suppress unused import warnings in a
        function scope.
        """
        self.flakes('''
        def foo():
            import bar
            __all__ = ["bar"]
        ''', m.UnusedImport, m.UnusedVariable)

    def test_ignoredInClass(self):
        """
        An C{__all__} definition in a class does not suppress unused import warnings.
        """
        self.flakes('''
        import bar
        class foo:
            __all__ = ["bar"]
        ''', m.UnusedImport)

    def test_warningSuppressed(self):
        """
        If a name is imported and unused but is named in C{__all__}, no warning
        is reported.
        """
        self.flakes('''
        import foo
        __all__ = ["foo"]
        ''')
        self.flakes('''
        import foo
        __all__ = ("foo",)
        ''')

    def test_augmentedAssignment(self):
        """
        The C{__all__} variable is defined incrementally.
        """
        self.flakes('''
        import a
        import c
        __all__ = ['a']
        __all__ += ['b']
        if 1 < 3:
            __all__ += ['c', 'd']
        ''', m.UndefinedExport, m.UndefinedExport)

    def test_unrecognizable(self):
        """
        If C{__all__} is defined in a way that can't be recognized statically,
        it is ignored.
        """
        self.flakes('''
        import foo
        __all__ = ["f" + "oo"]
        ''', m.UnusedImport)
        self.flakes('''
        import foo
        __all__ = [] + ["foo"]
        ''', m.UnusedImport)

    def test_unboundExported(self):
        """
        If C{__all__} includes a name which is not bound, a warning is emitted.
        """
        self.flakes('''
        __all__ = ["foo"]
        ''', m.UndefinedExport)

        # Skip this in __init__.py though, since the rules there are a little
        # different.
        for filename in ["foo/__init__.py", "__init__.py"]:
            self.flakes('''
            __all__ = ["foo"]
            ''', filename=filename)

    def test_importStarExported(self):
        """
        Do not report undefined if import * is used
        """
        self.flakes('''
        from foolib import *
        __all__ = ["foo"]
        ''', m.ImportStarUsed)

    def test_importStarNotExported(self):
        """Report unused import when not needed to satisfy __all__."""
        self.flakes('''
        from foolib import *
        a = 1
        __all__ = ['a']
        ''', m.ImportStarUsed, m.UnusedImport)

    def test_usedInGenExp(self):
        """
        Using a global in a generator expression results in no warnings.
        """
        self.flakes('import fu; (fu for _ in range(1))')
        self.flakes('import fu; (1 for _ in range(1) if fu)')

    def test_redefinedByGenExp(self):
        """
        Re-using a global name as the loop variable for a generator
        expression results in a redefinition warning.
        """
        self.flakes('import fu; (1 for fu in range(1))',
                    m.RedefinedWhileUnused, m.UnusedImport)

    def test_usedAsDecorator(self):
        """
        Using a global name in a decorator statement results in no warnings,
        but using an undefined name in a decorator statement results in an
        undefined name warning.
        """
        self.flakes('''
        from interior import decorate
        @decorate
        def f():
            return "hello"
        ''')

        self.flakes('''
        from interior import decorate
        @decorate('value')
        def f():
            return "hello"
        ''')

        self.flakes('''
        @decorate
        def f():
            return "hello"
        ''', m.UndefinedName)


class Python26Tests(TestCase):
    """
    Tests for checking of syntax which is valid in PYthon 2.6 and newer.
    """

    @skipIf(version_info < (2, 6), "Python >= 2.6 only")
    def test_usedAsClassDecorator(self):
        """
        Using an imported name as a class decorator results in no warnings,
        but using an undefined name as a class decorator results in an
        undefined name warning.
        """
        self.flakes('''
        from interior import decorate
        @decorate
        class foo:
            pass
        ''')

        self.flakes('''
        from interior import decorate
        @decorate("foo")
        class bar:
            pass
        ''')

        self.flakes('''
        @decorate
        class foo:
            pass
        ''', m.UndefinedName)
