package imports → undefined name

Bug #1270169 reported by spaceone
10
This bug affects 2 people
Affects Status Importance Assigned to Milestone
Pyflakes
Incomplete
Undecided
Unassigned

Bug Description

import foo.bar.baz
baz.blah()

foo/bar/__init__.py:1: 'foo' imported but unused
foo/bar/__init__.py:2: undefined name 'baz'

Revision history for this message
Florent (florent.x) wrote :

hello,

your import looks weird, btw. I believe it's wrong.

IMHO it should read:

from foo.bar import baz
baz.blah()

Changed in pyflakes:
status: New → Incomplete
Revision history for this message
spaceone (spaceone) wrote :

That does not work because you are in foo/bar/__init__.py.
My example is fully valid python2 (and the only way i know to do this).

To setup a testennvironment run these command (linux):

mkdir -p foo/bar/baz;
touch foo/{,bar/,bar/baz/}__init__.py

echo "def test(): print 'TEST OK'" > foo/bar/baz/__init__.py
echo "import foo.bar.baz; baz.test()" > foo/bar/__init__.py

then run it:
$ python2
>>> import foo
>>> import foo.bar
TEST OK
>>>

There is no other way to do this.
Changing the line to import foo.bar.baz as baz ends in this error:
AttributeError: 'module' object has no attribute 'bar'

Revision history for this message
asmeurer (asmeurer) wrote :

But the problem is that if you do `import foo.bar.baz`, then the name `baz` is not defined. You have to access it as `foo.bar.baz`.

Revision history for this message
spaceone (spaceone) wrote :

No, thats wrong. You can access baz because you are in bar's context. It is appended to the local scope. (you cann see this in the example i wrote).

Revision history for this message
Florent (florent.x) wrote :

Even if your syntax is valid, I still believe it is not the recommended way to do it.
It is counter-intuitive, at least:

>>> import foo.bar
>>> 'bar' in dir()
False
>>> import foo.bar.baz
>>> 'bar' in dir()
False

The example you give in comment #2 should be changed to one of these syntaxes:

== foo/bar/__init__.py (proposition A) ==

from foo.bar import baz
baz.test()

== foo/bar/__init__.py (proposition B) ==

from . import baz
baz.test()

Both propositions A and B are valid in Python 2/3 and they are the "right way to do it"™.

Revision history for this message
spaceone (spaceone) wrote :

And you cannot use foo.bar.baz because this does not exists because the module is not yet appended to foo.bar;

$ cat foo/bar/__init__.py
import foo.bar.baz

baz.test()
foo.bar.baz.test()

$ python
>>> import foo
TEST OK
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "./foo/__init__.py", line 1, in <module>
    import foo.bar
  File "./foo/bar/__init__.py", line 4, in <module>
    foo.bar.baz.test()
AttributeError: 'module' object has no attribute 'bar'

With the first line i can access the function, the other one not!

Revision history for this message
Phil Frost (bitglue) wrote :

I can't decide if this is a legitimate thing to do, or something that just happens to work due to some bug or implementation detail in cPython.

The language specification <https://docs.python.org/2/reference/simple_stmts.html#the-import-statement> doesn't seem to be much help. It talks a lot about how the import statement finds the thing to import, but then step 2, binding it to some local variables, there's pretty much no detail. I don't see anywhere in that specification to *what* local variables anything is bound in the case of your example.

The official language tutorial <https://docs.python.org/2/tutorial/modules.html#packages>, while perhaps not normative, does give this example:

'''
Users of the package can import individual modules from the package, for example:

import sound.effects.echo

This loads the submodule sound.effects.echo. It must be referenced with its full name.

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

An alternative way of importing the submodule is:

from sound.effects import echo

This also loads the submodule echo, and makes it available without its package prefix, so it can be used as follows:

echo.echofilter(input, output, delay=0.7, atten=4)
'''

Phrased in the terms of your example, it doesn't say anything about how one might do "import foo.bar.baz" and end up with "baz" defined. But as your test environment demonstrates, and I've been able to reproduce, it does.

Can you cite any normative references that provide additional clarity?

Revision history for this message
asmeurer (asmeurer) wrote :

Dave Beazley mentioned this in his modules tutorial (https://www.youtube.com/watch?v=0oTh1CXRaQ0, see around 32:00 or so; I recommend watching the whole thing if you can find the time). The gist of it is that because echo is a sub-module/package of sound.effects, you can type sound.effects.echo. The way that Python implements this is by literally putting "echo" in the namespace of sound.effects (i.e., its __init__.py). That's why import foo.bar.baz pulls in the name "baz" **in foo/baz/__init__.py**.

Dave doesn't touch on this any more than that once in his tutorial as I recall, so you may want to look for some more references on how the import system works if it's still unclear.

Revision history for this message
Ian Cordasco (icordasc) wrote :

So this also works on 3.4 (which makes me assume it has worked on prior 3.x versions) but I haven't tested 3.5. Further if you toss a

    print(globals())

Into foo/bar/__init__.py, you'll see very clearly that `baz` is defined as the submodule.

While I don't think this is conventional python or even common to see, it makes sense to support. For what it's worth, I would expect something more akin to

    from . import baz

Inside foo/bar/__init__.py instead of

    import foo.bar.baz

That said, I know many people follow the Google Styleguide and have linters that enforce that meaning they have to use the latter.

Revision history for this message
spaceone (spaceone) wrote :

The use of
     from . import baz
instead of
    import foo.bar.baz

is not practicable/considerable:
It is not possible to use the "if __name__ == '__main__'" -feature then to call that script also directly. It breaks with:
"ValueError: Attempted relative import in non-package"

@asmeurer
yes, i watched the whole tutorial

To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.