Implementing deprecation warning for submodule that is now it's own package

Say I had a package foo that contained a submodule bar. I want to remove bar and make it it’s own package.

I removed bar and make it it’s own package but to help with the deprecation cycle, I override the __init__.py for foo with a custom ModuleType subclass that overrides __getattr__. The goal of overriding __getattr__ is to warm the user of foo, who is trying to access bar that bar is now it’s own package and have __getattr__ return the new standalone bar package.

Is that possible? I’m not sure if submodules are attributes of a module.

So essentially the before

foo package with bar as submodule.

bar is now own package and removed.

In __init__.py of foo.

import sys
import warning
from types import ModuleType

class FooModule(ModuleType):

    def __getattr__(self, attr):
         if attr == "bar":
             warnings.warn("bar is now own package. Import directly. This will raise attribute error in future.", DeprecationWarning)
             import bar
             return bar
         else:
               raise AttributeError()

sys.modules[__name__].__class__ = FooModule

I apologize for poor indentation, I’m on mobile and tried my best.

I would like to do it like this, or some similar approach because at work we are in the process of breaking up a monorepo and having a list of submodules that have been split out and caught like this would be the easiest solution in my mind currently.

To elaborate a bit more, I’m most concerned with catching and providing a warning for from foo import bar

Love to get some thoughts on this.

So what I ended up doing was creating a _deprecated.py file at top level of old package that imports it into where the old modules are and dispatches to the new package.

Assumption is that the submodules for both old and new packages both contain the same structure overall.

foo (old package that has baz and bah being moved into new package bar)
— baz
— bah
— bad
— _deprecated.py

bar (new package with baz and bah code)
— baz
— bah

import sys
import warnings
from importlib import import_module
from types import ModuleType

def declare_warning(module_name: str) -> None:
    warnings.warn(f"{module_name} is now part of bar package. {module_name} will be removed from foo 14-OCT-2022")

def delegate_to_bar(current_module_name: str) -> None:
    declare_warning(current_module_name)
    new_module_name = current_module_name.replace("foo", "bar")
    new_module = import_module(new_module_name)
    
    def __getattr__(self, name):
        attr = getattr(new_module, name, None)
        if attr is None:
            raise AttributeError(f"{new_module} has no attribute {name}")
        else:
            return attr

    def __repr__(selfe):
        return repr(new_module)
 
    namespace = {"__getattr__": __getattr__, "__repr__": __repr__}
    DelegatorModule = type(new_module_name.split(".")[-1], (ModuleType,), namespace)
    sys.modules[current_module_name].__class__ = DelegatorModule

Then within both baz and bah of foo I do

from .._deprecation import delegate_to_bar

delegate_to_bar(__name__)

Seems to be working out just fine, it was a lot of working because I did it for every .py file in every submodule that got moved. Would love to get the thoughts of anyone on this.