
from _ast import PyCF_ONLY_AST
from sys import version_info

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


class Test(TestCase):
    def test_undefined(self):
        self.flakes('bar', m.UndefinedName)

    def test_definedInListComp(self):
        self.flakes('[a for a in range(10) if a]')

    @skipIf(version_info < (3,),
            'in Python 2 list comprehensions execute in the same scope')
    def test_undefinedInListComp(self):
        self.flakes('''
        [a for a in range(10)]
        a
        ''',
                    m.UndefinedName)

    def test_functionsNeedGlobalScope(self):
        self.flakes('''
        class a:
            def b():
                fu
        fu = 1
        ''')

    def test_builtins(self):
        self.flakes('range(10)')

    def test_builtinWindowsError(self):
        """
        C{WindowsError} is sometimes a builtin name, so no warning is emitted
        for using it.
        """
        self.flakes('WindowsError')

    def test_magicGlobalsFile(self):
        """
        Use of the C{__file__} magic global should not emit an undefined name
        warning.
        """
        self.flakes('__file__')

    def test_magicGlobalsBuiltins(self):
        """
        Use of the C{__builtins__} magic global should not emit an undefined
        name warning.
        """
        self.flakes('__builtins__')

    def test_magicGlobalsName(self):
        """
        Use of the C{__name__} magic global should not emit an undefined name
        warning.
        """
        self.flakes('__name__')

    def test_magicGlobalsPath(self):
        """
        Use of the C{__path__} magic global should not emit an undefined name
        warning, if you refer to it from a file called __init__.py.
        """
        self.flakes('__path__', m.UndefinedName)
        self.flakes('__path__', filename='package/__init__.py')

    def test_globalImportStar(self):
        """Can't find undefined names with import *."""
        self.flakes('from fu import *; bar',
                    m.ImportStarUsed, m.ImportStarUsage)

    @skipIf(version_info >= (3,), 'obsolete syntax')
    def test_localImportStar(self):
        """
        A local import * still allows undefined names to be found
        in upper scopes.
        """
        self.flakes('''
        def a():
            from fu import *
        bar
        ''', m.ImportStarUsed, m.UndefinedName, m.UnusedImport)

    @skipIf(version_info >= (3,), 'obsolete syntax')
    def test_unpackedParameter(self):
        """Unpacked function parameters create bindings."""
        self.flakes('''
        def a((bar, baz)):
            bar; baz
        ''')

    def test_definedByGlobal(self):
        """
        "global" can make an otherwise undefined name in another function
        defined.
        """
        self.flakes('''
        def a(): global fu; fu = 1
        def b(): fu
        ''')
        self.flakes('''
        def c(): bar
        def b(): global bar; bar = 1
        ''')

    def test_definedByGlobalMultipleNames(self):
        """
        "global" can accept multiple names.
        """
        self.flakes('''
        def a(): global fu, bar; fu = 1; bar = 2
        def b(): fu; bar
        ''')

    def test_globalInGlobalScope(self):
        """
        A global statement in the global scope is ignored.
        """
        self.flakes('''
        global x
        def foo():
            print(x)
        ''', m.UndefinedName)

    def test_global_reset_name_only(self):
        """A global statement does not prevent other names being undefined."""
        # Only different undefined names are reported.
        # See following test that fails where the same name is used.
        self.flakes('''
        def f1():
            s

        def f2():
            global m
        ''', m.UndefinedName)

    @skip("todo")
    def test_unused_global(self):
        """An unused global statement does not define the name."""
        self.flakes('''
        def f1():
            m

        def f2():
            global m
        ''', m.UndefinedName)

    def test_del(self):
        """Del deletes bindings."""
        self.flakes('a = 1; del a; a', m.UndefinedName)

    def test_delGlobal(self):
        """Del a global binding from a function."""
        self.flakes('''
        a = 1
        def f():
            global a
            del a
        a
        ''')

    def test_delUndefined(self):
        """Del an undefined name."""
        self.flakes('del a', m.UndefinedName)

    def test_delConditional(self):
        """
        Ignores conditional bindings deletion.
        """
        self.flakes('''
        context = None
        test = True
        if False:
            del(test)
        assert(test)
        ''')

    def test_delConditionalNested(self):
        """
        Ignored conditional bindings deletion even if they are nested in other
        blocks.
        """
        self.flakes('''
        context = None
        test = True
        if False:
            with context():
                del(test)
        assert(test)
        ''')

    def test_delWhile(self):
        """
        Ignore bindings deletion if called inside the body of a while
        statement.
        """
        self.flakes('''
        def test():
            foo = 'bar'
            while False:
                del foo
            assert(foo)
        ''')

    def test_delWhileTestUsage(self):
        """
        Ignore bindings deletion if called inside the body of a while
        statement and name is used inside while's test part.
        """
        self.flakes('''
        def _worker():
            o = True
            while o is not True:
                del o
                o = False
        ''')

    def test_delWhileNested(self):
        """
        Ignore bindings deletions if node is part of while's test, even when
        del is in a nested block.
        """
        self.flakes('''
        context = None
        def _worker():
            o = True
            while o is not True:
                while True:
                    with context():
                        del o
                o = False
        ''')

    def test_globalFromNestedScope(self):
        """Global names are available from nested scopes."""
        self.flakes('''
        a = 1
        def b():
            def c():
                a
        ''')

    def test_laterRedefinedGlobalFromNestedScope(self):
        """
        Test that referencing a local name that shadows a global, before it is
        defined, generates a warning.
        """
        self.flakes('''
        a = 1
        def fun():
            a
            a = 2
            return a
        ''', m.UndefinedLocal)

    def test_laterRedefinedGlobalFromNestedScope2(self):
        """
        Test that referencing a local name in a nested scope that shadows a
        global declared in an enclosing scope, before it is defined, generates
        a warning.
        """
        self.flakes('''
            a = 1
            def fun():
                global a
                def fun2():
                    a
                    a = 2
                    return a
        ''', m.UndefinedLocal)

    def test_intermediateClassScopeIgnored(self):
        """
        If a name defined in an enclosing scope is shadowed by a local variable
        and the name is used locally before it is bound, an unbound local
        warning is emitted, even if there is a class scope between the enclosing
        scope and the local scope.
        """
        self.flakes('''
        def f():
            x = 1
            class g:
                def h(self):
                    a = x
                    x = None
                    print(x, a)
            print(x)
        ''', m.UndefinedLocal)

    def test_doubleNestingReportsClosestName(self):
        """
        Test that referencing a local name in a nested scope that shadows a
        variable declared in two different outer scopes before it is defined
        in the innermost scope generates an UnboundLocal warning which
        refers to the nearest shadowed name.
        """
        exc = self.flakes('''
            def a():
                x = 1
                def b():
                    x = 2 # line 5
                    def c():
                        x
                        x = 3
                        return x
                    return x
                return x
        ''', m.UndefinedLocal).messages[0]

        # _DoctestMixin.flakes adds two lines preceding the code above.
        expected_line_num = 7 if self.withDoctest else 5

        self.assertEqual(exc.message_args, ('x', expected_line_num))

    def test_laterRedefinedGlobalFromNestedScope3(self):
        """
        Test that referencing a local name in a nested scope that shadows a
        global, before it is defined, generates a warning.
        """
        self.flakes('''
            def fun():
                a = 1
                def fun2():
                    a
                    a = 1
                    return a
                return a
        ''', m.UndefinedLocal)

    def test_undefinedAugmentedAssignment(self):
        self.flakes(
            '''
            def f(seq):
                a = 0
                seq[a] += 1
                seq[b] /= 2
                c[0] *= 2
                a -= 3
                d += 4
                e[any] = 5
            ''',
            m.UndefinedName,    # b
            m.UndefinedName,    # c
            m.UndefinedName, m.UnusedVariable,  # d
            m.UndefinedName,    # e
        )

    def test_nestedClass(self):
        """Nested classes can access enclosing scope."""
        self.flakes('''
        def f(foo):
            class C:
                bar = foo
                def f(self):
                    return foo
            return C()

        f(123).f()
        ''')

    def test_badNestedClass(self):
        """Free variables in nested classes must bind at class creation."""
        self.flakes('''
        def f():
            class C:
                bar = foo
            foo = 456
            return foo
        f()
        ''', m.UndefinedName)

    def test_definedAsStarArgs(self):
        """Star and double-star arg names are defined."""
        self.flakes('''
        def f(a, *b, **c):
            print(a, b, c)
        ''')

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_definedAsStarUnpack(self):
        """Star names in unpack are defined."""
        self.flakes('''
        a, *b = range(10)
        print(a, b)
        ''')
        self.flakes('''
        *a, b = range(10)
        print(a, b)
        ''')
        self.flakes('''
        a, *b, c = range(10)
        print(a, b, c)
        ''')

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_usedAsStarUnpack(self):
        """
        Star names in unpack are used if RHS is not a tuple/list literal.
        """
        self.flakes('''
        def f():
            a, *b = range(10)
        ''')
        self.flakes('''
        def f():
            (*a, b) = range(10)
        ''')
        self.flakes('''
        def f():
            [a, *b, c] = range(10)
        ''')

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_unusedAsStarUnpack(self):
        """
        Star names in unpack are unused if RHS is a tuple/list literal.
        """
        self.flakes('''
        def f():
            a, *b = any, all, 4, 2, 'un'
        ''', m.UnusedVariable, m.UnusedVariable)
        self.flakes('''
        def f():
            (*a, b) = [bool, int, float, complex]
        ''', m.UnusedVariable, m.UnusedVariable)
        self.flakes('''
        def f():
            [a, *b, c] = 9, 8, 7, 6, 5, 4
        ''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable)

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_keywordOnlyArgs(self):
        """Keyword-only arg names are defined."""
        self.flakes('''
        def f(*, a, b=None):
            print(a, b)
        ''')

        self.flakes('''
        import default_b
        def f(*, a, b=default_b):
            print(a, b)
        ''')

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_keywordOnlyArgsUndefined(self):
        """Typo in kwonly name."""
        self.flakes('''
        def f(*, a, b=default_c):
            print(a, b)
        ''', m.UndefinedName)

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_annotationUndefined(self):
        """Undefined annotations."""
        self.flakes('''
        from abc import note1, note2, note3, note4, note5
        def func(a: note1, *args: note2,
                 b: note3=12, **kw: note4) -> note5: pass
        ''')

        self.flakes('''
        def func():
            d = e = 42
            def func(a: {1, d}) -> (lambda c: e): pass
        ''')

    @skipIf(version_info < (3,), 'new in Python 3')
    def test_metaClassUndefined(self):
        self.flakes('''
        from abc import ABCMeta
        class A(metaclass=ABCMeta): pass
        ''')

    def test_definedInGenExp(self):
        """
        Using the loop variable of a generator expression results in no
        warnings.
        """
        self.flakes('(a for a in [1, 2, 3] if a)')

        self.flakes('(b for b in (a for a in [1, 2, 3] if a) if b)')

    def test_undefinedInGenExpNested(self):
        """
        The loop variables of generator expressions nested together are
        not defined in the other generator.
        """
        self.flakes('(b for b in (a for a in [1, 2, 3] if b) if b)',
                    m.UndefinedName)

        self.flakes('(b for b in (a for a in [1, 2, 3] if a) if a)',
                    m.UndefinedName)

    def test_undefinedWithErrorHandler(self):
        """
        Some compatibility code checks explicitly for NameError.
        It should not trigger warnings.
        """
        self.flakes('''
        try:
            socket_map
        except NameError:
            socket_map = {}
        ''')
        self.flakes('''
        try:
            _memoryview.contiguous
        except (NameError, AttributeError):
            raise RuntimeError("Python >= 3.3 is required")
        ''')
        # If NameError is not explicitly handled, generate a warning
        self.flakes('''
        try:
            socket_map
        except:
            socket_map = {}
        ''', m.UndefinedName)
        self.flakes('''
        try:
            socket_map
        except Exception:
            socket_map = {}
        ''', m.UndefinedName)

    def test_definedInClass(self):
        """
        Defined name for generator expressions and dict/set comprehension.
        """
        self.flakes('''
        class A:
            T = range(10)

            Z = (x for x in T)
            L = [x for x in T]
            B = dict((i, str(i)) for i in T)
        ''')

        if version_info >= (2, 7):
            self.flakes('''
            class A:
                T = range(10)

                X = {x for x in T}
                Y = {x:x for x in T}
            ''')

    def test_definedInClassNested(self):
        """Defined name for nested generator expressions in a class."""
        self.flakes('''
        class A:
            T = range(10)

            Z = (x for x in (a for a in T))
        ''')

    def test_undefinedInLoop(self):
        """
        The loop variable is defined after the expression is computed.
        """
        self.flakes('''
        for i in range(i):
            print(i)
        ''', m.UndefinedName)
        self.flakes('''
        [42 for i in range(i)]
        ''', m.UndefinedName)
        self.flakes('''
        (42 for i in range(i))
        ''', m.UndefinedName)

    @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist')
    def test_definedFromLambdaInDictionaryComprehension(self):
        """
        Defined name referenced from a lambda function within a dict/set
        comprehension.
        """
        self.flakes('''
        {lambda: id(x) for x in range(10)}
        ''')

    def test_definedFromLambdaInGenerator(self):
        """
        Defined name referenced from a lambda function within a generator
        expression.
        """
        self.flakes('''
        any(lambda: id(x) for x in range(10))
        ''')

    @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist')
    def test_undefinedFromLambdaInDictionaryComprehension(self):
        """
        Undefined name referenced from a lambda function within a dict/set
        comprehension.
        """
        self.flakes('''
        {lambda: id(y) for x in range(10)}
        ''', m.UndefinedName)

    def test_undefinedFromLambdaInComprehension(self):
        """
        Undefined name referenced from a lambda function within a generator
        expression.
        """
        self.flakes('''
        any(lambda: id(y) for x in range(10))
        ''', m.UndefinedName)


class NameTests(TestCase):
    """
    Tests for some extra cases of name handling.
    """
    def test_impossibleContext(self):
        """
        A Name node with an unrecognized context results in a RuntimeError being
        raised.
        """
        tree = compile("x = 10", "<test>", "exec", PyCF_ONLY_AST)
        # Make it into something unrecognizable.
        tree.body[0].targets[0].ctx = object()
        self.assertRaises(RuntimeError, checker.Checker, tree)
