Pythonic way to resolve circular import statements?
Asked Answered
W

4

69

I just inherited some code which makes me uneasy: There is a testing library, full of classes corresponding to webpages on our site, and each webpage class has methods to automate the functionality on that page.

There are methods to click the link between pages, which returns the class of the linked page. Here's a simplified example:

File homePageLib.py:

class HomePage(object):
    def clickCalendarLink(self):
        # Click page2 link which navigates browswer to page2
        print "Click Calendar link"
        # Then returns the page2 object
        from calendarLib import CalendarPage
        return CalendarPage()

File calendarLib.py:

class CalendarPage(object):
    def clickHomePageLink(self):
        # Click page1 link which navigates browswer to page1
        print "Click Home Page link"
        # Then return the page2 object
        from homePageLib import HomePage
        return HomePage()

This then allows the script file to click on pages and get the object as a return value from that method, meaning the script author won't have to keep instantiating new pages as they navigate around the site. (I feel like this is an odd design, but I can't exactly put my finger on why, other than that it seems strange to have a method named 'clickSomeLink' and return an object of the resulting page.)

The following script illustrates how a script would navigate around the site: (I inserted print page to show how the page object changes)

Script File:

from homePageLib import HomePage

page = HomePage()    
print page
page = page.clickCalendarLink()
print page
page = page.clickHomePageLink()
print page

which produces the following output:

<homePageLib.HomePage object at 0x00B57570>
Click Calendar link
<calendarLib.CalendarPage object at 0x00B576F0>
Click Home Page link
<homePageLib.HomePage object at 0x00B57570>

So, the part of this that I specifically feel most uneasy about are the from ____ import ____ lines that end up all over. These strike me as bad for the following reasons:

  1. I've always made it a convention to put all import statements at the top of a file.
  2. Since there may be multiple links to a page, this results in the same from foo import bar line of code in several places in a file.

The problem is, if we put these import statements at the top of the page, we get import errors, because (as per this example), HomePage imports CalendarPage and vice versa:

File homePageLib.py

from calendarLib import CalendarPage

class HomePage(object):
    def clickCalendarLink(self):
        # Click page2 link which navigates browswer to page2
        print "Click Calendar link"
        # Then returns the page2 object

        return CalendarPage()

File calendarLib.py

from homePageLib import HomePage

class CalendarPage(object):
    def clickHomePageLink(self):
        # Click page1 link which navigates browswer to page1
        print "Click Home Page link"
        # Then return the page2 object
        return HomePage()

This results in the following error:

>>> from homePageLib import HomePage
Traceback (most recent call last):
  File "c:\temp\script.py", line 1, in ?
    #Script
  File "c:\temp\homePageLib.py", line 2, in ?
    from calendarLib import CalendarPage
  File "c:\temp\calendarLib.py", line 2, in ?
    from homePageLib import HomePage
ImportError: cannot import name HomePage

(tips on how to better format python output?)

Rather than perpetuating this style, I'd like to find a better way. Is there a Pythonic way to deal with circular dependencies like this and still keep import statements at the top of the file?

Weighbridge answered 21/4, 2011 at 19:37 Comment(1)
V
127

Resolving these constructs usually involves techniques like Dependency Injection.

It is, however, rather simple to fix this error:

In calendarLib.py:

import homePageLib

class CalendarPage(object):
    def clickHomePageLink(self):
        [...]
        return homePageLib.HomePage()

The code at module level is executed at import time. Using the from [...] import [...] syntax requires the module to be completely initialized to succeed.

A simple import [...] does not, because no symbols are accessed, thus breaking the dependency chain.

Vinasse answered 21/4, 2011 at 19:44 Comment(4)
I see. why is it that that works? homePageLib still imports calendarPageLib and vice-versa.Weighbridge
@nathan: please look at this for more information. #711051Audy
Will this work for all cyclic import issues? Cause it didn't solve mine.Catarina
but how about if clickHomePageLink has a return value type hinting? which means the return value type is outside 😂Navigation
B
11

I have a circular import because I reference a class in a type hint. This can be solved using from __future__ import annotations (tested with Python 3.9.x).

Example:

AClass.py

from BClass import BClass
class AClass():
    def __init__(self) -> None:
        self.bClass = BClass(self)

BClass.py

from __future__ import annotations  # Without this, the type hint below would not work.
import AClass  # Note that `from AClass import AClass` would not work here.`
class BClass:
    def __init__(self, aClass: AClass.AClass) -> None:
        self.aClass = aClass
Bucktooth answered 3/9, 2021 at 9:9 Comment(2)
If you add from typing import TYPE_CHECKING and then use if TYPE_CHECKING: from AClass import AClass to import the class, you can use AClass in your type hints instead of AClass.AClass. For a complete example, see https://mcmap.net/q/20029/-what-happens-when-using-mutual-or-circular-cyclic-importsThick
This worked for me. But does anyone know why this worked? I'm also in Python 3.9Lifeline
P
1

Please read Sebastian's answer for detailed explanation. This approach was proposed by David Beazley in PyCon

Try positioning the imports on the top like this

try:
    from homePageLib import HomePage
except ImportError:
    import sys
    HomePage = sys.modules[__package__ + '.HomePage']

This will try to import your HomePage and if failed, will try to load it from the cache

Potman answered 6/4, 2016 at 5:10 Comment(3)
Why this multi-line construct does not have built-in shortcut in Python standard library?Pinsky
This looks hacky, but it's actually explicit (read: following PEP 20). Very good!Calamondin
HomePage is a class in the question, but this answer treats it as a module.Byblow
E
0

Another method not yet mentioned here: put the imports below the class definitions.

# homePageLib.py:

class HomePage(object):
    def clickCalendarLink(self):
        # Click page2 link which navigates browswer to page2
        print "Click Calendar link"
        # Then returns the page2 object
        return CalendarPage()
        
from calendarLib import CalendarPage
# calendarLib.py:

class CalendarPage(object):
    def clickHomePageLink(self):
        # Click page1 link which navigates browswer to page1
        print "Click Home Page link"
        # Then return the page2 object
        return HomePage()

from homePageLib import HomePage

Works in both Python 2 and 3.

Elongation answered 13/2 at 12:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.