Let’s say I have the following structure:

my_module/
  __init__.py
  utilities.py

and __init__.py contains

from .utilities import SomeUtilityFunction

Is there a way to prevent or alert developers when they do

from my_module.utilities import SomeUtilityFunction

instead of

from my_module import SomeUtilityFunction

The problem arose when a few modules started using a function that was imported inside a module in which it wasn’t used, while also being available on the module’s __init__.py, so after linting the file and removing the unused import my tests started failing.

any other advice for situations like this?

  • @Chais@sh.itjust.works
    link
    fedilink
    English
    3
    edit-2
    7 months ago

    You could guard it.
    __init__.py:

    _GUARD_SOME_UTILITY_FUNCTION = True
    from .utilities import SomeUtilityFunction
    

    utilities.py:

    def SomeUtilityFunction():
        if not _GUARD_SOME_UTILITY_FUNCTION:
            raise SomeException("Helpful error message")
    

    Take this with a grain of salt, as I’m typing this on my phone and haven’t actually tried it.

    Alternatively there’s the import-guard package on PyPI. No idea if it’s any good, though. Just something a quick search brought up.

    Edit:
    Ok, I tried my suggestion and it doesn’t work.

  • Ben Clifford
    link
    fedilink
    17 months ago

    @fixmycode mypy type checking can report this error in your code:

    iox3.py:3: error: Module “iox2” does not explicitly export attribute “y” [attr-defined]

    which I think is roughly the problem you are encountering: an attribute in an imported module that wasn’t explicitly defined in that module, but instead came from somewhere else.

    • @fixmycodeOP
      link
      17 months ago

      I think this is the more sensitive approach, I’ll take a look at putting mypy in my pipeline

    • @Chais@sh.itjust.works
      link
      fedilink
      17 months ago

      That’s not correct. __all__ is not a whitelist. It is only the list used for

      from module import *
      

      If you have a module with submodules foo, bar and baz and __all__ = ["foo", "bar"] it will not prevent you from importing baz manually. It just won’t do it automatically.

      • @sebsch@discuss.tchncs.de
        link
        fedilink
        0
        edit-2
        7 months ago

        It works exactly like one. You get a warning if you try to import something not defined in it. The docs are just very confusing here ;)

        • @Chais@sh.itjust.works
          link
          fedilink
          English
          0
          edit-2
          7 months ago

          Bullshit!

          module/__init__.py:

          __all__ = ["foo", "bar"]
          

          module/foo.py:

          def foo():
              print("foo")
          

          module/bar.py:

          def bar():
              print("bar")
          

          module/baz.py:

          def baz():
              print("baz")
          

          main.py:

          from module import *
          from module import baz
          
          if __name__ == "__main__":
              print("main")
              foo.foo()
              bar.bar()
              baz.baz()
          

          Output:

          $ python main.py 
          main
          foo
          bar
          baz
          

          No errors, warnings or anything.