Created on 2013-04-05 01:15 by brett.cannon, last changed 2020-02-04 20:36 by dino.viehland. This issue is now closed.
To help with circular imports, IMPORT_FROM should be modified to check sys.modules if a getattr() check fails.
Like I mentioned on python-dev, it worries me a bit that this could be considered "unexpected", i.e. when there is a conflict between a legitimage attribute member of a module, and a submodule of the same name.
Also, I wonder if this isn't a bigger change to the import mechanism, than simply:
| Another would be
| to always require an 'as' clause in this case, so that you would have
| to write'
| import .foo as foo
which would possibly only require a change to the syntax.
It won't conflict with attributes. Only if the attribute does not exist on the module already will it fall back to sys.modules. If the import finished then any attribute created from the import will already be there and thus not be an issue.
But to make sure this isn't a problem we can make sure there is a test in this instance.
While I'm happy that this is being ackowledged as a problem, I'm not sure changing the "import x from y" semantics is necessarily a good idea. I mean, it is obvious to me that it means "import x, then getattr(x, "y")". I presume that is the meaning most people associate with it. Certainly, "y" can be any old module attribute.
To change it to actually fall back to a submodule, well. I suppose if you could explain it roughly like "y = getattr(x, 'y', x.y)" then it would be ok..
I'm basically saying that it is possible that the fallback to submodule will occur, where the successful getattr would occur later and return something different than the submodule. Possible. But perhaps very much an edge case :)
By declaring what the semantics will be to make the case even possible we are not breaking anything but making something possible. IOW it isn't even an edge case to me since there is no working case to compare against. =)
On Fri, Apr 5, 2013 at 9:07 AM, Kristján Valur Jónsson
<report@bugs.python.org> wrote:
>..
Actually, after a little reflection, I can see that there are more
complex conditions to analyze, if 'b' doesn't import 'b.util', but
some other module imports b and sets b.util. But that's just freaking
insane and whoever does that probably deserves whatever they get,
especially since a later 'import b.util' would blow away the modified
attribute.
So, this note is just to satisfy myself that the change doesn't
introduce any *new* weirdness unless you are in a case where the
parent imports and replaces the child, and the child is involved in an
import cycle. If the parent doesn't import the child but just assigns
an attribute, it's already broken the minute somebody else imports the
child, and the same thing applies if something else sets the attribute
without importing the child first, and if it imports the child first,
then the previous analysis (mostly) applies, but either way that code
is broken and will have plenty of other ordering dependencies to debug
on its own. ;-)
(No wonder Nick said nobody wanted to touch this... the analysis
alone is a killer. ;-) Luckily, it seems we're good to go unless
somebody can find a case I missed.)
...and I thought of *one* more way to trigger the changed behavior,
which looks like:
#b.py
from .b import util
import .a
util = util.util
#b/util.py
def util(): pass
(with the other files the same as before).
I'm including it only for completeness' sake, because my original
enumeration of cases ignored the possibility that 'a' could be doing
the import *after* b.util is loaded and bound, but *before* it's
rebound. However, it doesn't produce any new or problematic effects:
it's essentially the same as if 'a' were imported from 'b.util'. Once
again, regardless of the order in which imports happen, 'a' ends up
with 'b.util' the moment the circularity is introduced, and it stays
that way.
It's also hard to argue that a case written this way isn't getting
exactly what it *says* it wants. In fact, it looks like code that was
deliberately written to *force* a to end up with the original b.util
instead of the replaced one. ;-)
Final (hopefully) conclusion: this change replaces the FAQ of "Don't
use 'from-import' for circular imports" with the hopefully-less-FA'd Q
of "when from-import is part of an import cycle, it works *exactly*
like regular import, so you're going to get a submodule rather than an
attribute. If you need the attribute instead, move the import so that
it happens after the attribute is set up." (Which is exactly the same
advice that would apply in a cycle with any other unitialized
attribute, whether you were using from-import or not.)
Wow, Good analysis Phillip.
So, we agree that the fallback still is a sensible fallback? Then everything is fine and I'll put my official +1 stamp of approval on this.
Now... should we consider the current behavious to be a bug? 2.7 sure could do with some fixing :)
I don't care much one way or the other about it being considered a bug
in 2.x, but it might be worth considering a bug in 3.x.
Either way, though, the language reference for "from import" should
reflect the change, and "alternative implementations" should be
informed of the update.
It is not a bug but a change in semantics to accommodate a use-case so this will only be going into Python 3.4.
Thanks for working through that Phillip - falling back to sys.modules when the expected attribute was missing is actually something I suggested as a possibility years ago, and Guido's response at the time was "If it was that easy, someone would have done it already".
Your analysis is one of the pieces that was missing, along with Brett's insight that the code that needs the fallback is the IMPORT_FROM bytecode rather than the import implementation.
I'm going to close the original circular import bug as being superseded by this one.. :-(
I..
Don't be distracted when trying to write tests is the lesson learned. Fixed basic and rebinding and just deleted indirect.
One question.
when doing "Programmatic" import, i.e. using the __import__ function, how do we do the same?
#module foo.B
A = __import__(".", fromlist=["A"]).A
#module foo.A
B = __import__(".", fromlist=["B"]).B
Clearly the above won't work. Can we extend __import__ to allow a full path, including relative? The objection about which name to bind to is no longer valid, since the binding is explicit.
So, could we do:
#module foo.B
A = __import__(".A")
#module foo.A
B = __import__(".B")
?
sorry, the last example really needs to be:
#module foo.B
A = __import__(".A", fromlist=["dummy"])
to invoke the "return final module" behaviour.
Hm, maybe this simply works... I didn't test....
Nope, I get
ValueError: Empty module name (in 2.7)
You use importlib.import_module to do programmatic imports (and FYI dots never go into the first argument of __import__).
Interesting.
And I can report that it works, in 2.7, with code such as
b = importlib.import_module("..b", __name__)
and
a = importlib.import_module("..a", __name__)
Still, it seems odd that a whole "importlib" is requried ot resolve the relative import, and that it doesn"t work with __import__, given how "supposedly" the import statement is supposed to translate to a __import__ call internally. One would think that __import__ had access to the relative path machinery somehow.
If).
With.
Unfortunately,.
This is Brett's tests patch updated against default branch.
And this is a full patch.
(full patch is actually the latest upload: circrelimports.patch. Sorry for the glitch)
By the way, is there any documentation that would need to be updated for this?
The.
By the way, my patch is currently using the package's __name__ attribute to build the fully-qualified name. Should it use the package's __package__ attribute instead?
If I understand your question correctly, then yes, __package__ should be
used when converting explicit relative imports to absolute ones.
To write a test, run a submodule that needs the new fallback feature via
the -m switch (it will fail if using __name__)
Le 10/10/2014 00:11, Nick Coghlan a écrit :
>
> Nick Coghlan added the comment:
>
> If I understand your question correctly, then yes, __package__ should be
> used when converting explicit relative imports to absolute ones.
>
> To write a test, run a submodule that needs the new fallback feature via
> the -m switch (it will fail if using __name__)
I'm not sure it would. __package__ is not looked up on the module (since
precisely we failed importing it) but on the parent package (which is
passed to IMPORT_FROM).
Actually, looking up __package__ would be wrong.
Say I have: from pack.module import foo
and "foo" doesn't exist in pack.module but exists in pack.
Since pack.module.__package__ == "pack", using __package__ would wrongly find the "foo" in pack.
New changeset fded07a2d616 by Antoine Pitrou in branch 'default':
Issue #17636: Circular imports involving relative imports are now supported.
New changeset 3a35638bce66 by Ned Deily in branch 'default':
Issue #17636: Install new test directories.