Repository: numba/numba

Issue:
Numba 0.49.0rc1 does not provide aliases for some use cases
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

When I use Numba 0.49.0rc1 I see many warnings like this:
```
Import of 'from_dtype' requested from: 'numba.numpy_support', please update to use 'numba.np.numpy_support' or pin to Numba version 0.48.0. This alias will not be present in Numba version 0.50.0.`
```
It is OK and aliases work fine for the most of my cases but does not work for some cases I have:
1. `from numba.targets import quicksort`
1. `from numba.special import literally` and `from numba.special import prange`
1. `from numba import postproc`
1. `import numba` and `numba.analysis.ir_extension_usedefs[Join] = join_usedefs`
1. `from numba import consts`
1. `import numba` and `lower_builtin('getiter', string_array_type)(numba.targets.arrayobj.getiter_array)`
1. `from numba.config import IS_32BITS`
1. `import numba` and `numba.datamodel.register_default(...)`

Please, give me note whether it is possible to be fixed before 0.49.0 release.

Discussion:

Patch:
diff --git a/numba/__init__.py b/numba/__init__.py
index eaf4106a7..e1fc56180 100644
--- a/numba/__init__.py
+++ b/numba/__init__.py
@@ -2,6 +2,7 @@
 Expose top-level symbols that are safe for import *
 """
 
+import importlib
 import platform
 import re
 import sys
@@ -58,6 +59,90 @@ import numba.cpython.charseq
 test = runtests.main
 
 
+# needed for compatibility until 0.50.0. Note that accessing members of
+# any of these modules, or the modules themselves, will raise DeprecationWarning
+_auto_import_submodules = {
+    'numba.numpy_support': 'numba.np.numpy_support',
+    'numba.special': 'numba.misc.special',
+    'numba.analysis': 'numba.core.analysis',
+    'numba.datamodel': 'numba.core.datamodel',
+    'numba.unsafe': 'numba.core.unsafe',
+    'numba.annotations': 'numba.core.annotations',
+    'numba.appdirs': 'numba.misc.appdirs',
+    'numba.array_analysis': 'numba.parfors.array_analysis',
+    'numba.bytecode': 'numba.core.bytecode',
+    'numba.byteflow': 'numba.core.byteflow',
+    'numba.caching': 'numba.core.caching',
+    'numba.callwrapper': 'numba.core.callwrapper',
+    'numba.cgutils': 'numba.core.cgutils',
+    'numba.charseq': 'numba.cpython.charseq',
+    'numba.compiler': 'numba.core.compiler',
+    'numba.compiler_lock': 'numba.core.compiler_lock',
+    'numba.compiler_machinery': 'numba.core.compiler_machinery',
+    'numba.consts': 'numba.core.consts',
+    'numba.controlflow': 'numba.core.controlflow',
+    'numba.dataflow': 'numba.core.dataflow',
+    'numba.debuginfo': 'numba.core.debuginfo',
+    'numba.decorators': 'numba.core.decorators',
+    'numba.dictobject': 'numba.typed.dictobject',
+    'numba.dispatcher': 'numba.core.dispatcher',
+    'numba.entrypoints': 'numba.core.entrypoints',
+    'numba.funcdesc': 'numba.core.funcdesc',
+    'numba.generators': 'numba.core.generators',
+    'numba.inline_closurecall': 'numba.core.inline_closurecall',
+    'numba.interpreter': 'numba.core.interpreter',
+    'numba.ir': 'numba.core.ir',
+    'numba.ir_utils': 'numba.core.ir_utils',
+    'numba.itanium_mangler': 'numba.core.itanium_mangler',
+    'numba.listobject': 'numba.typed.listobject',
+    'numba.lowering': 'numba.core.lowering',
+    'numba.npdatetime': 'numba.np.npdatetime',
+    'numba.object_mode_passes': 'numba.core.object_mode_passes',
+    'numba.parfor': 'numba.parfors.parfor',
+    'numba.postproc': 'numba.core.postproc',
+    'numba.pylowering': 'numba.core.pylowering',
+    'numba.pythonapi': 'numba.core.pythonapi',
+    'numba.rewrites': 'numba.core.rewrites',
+    'numba.runtime': 'numba.core.runtime',
+    'numba.serialize': 'numba.core.serialize',
+    'numba.sigutils': 'numba.core.sigutils',
+    'numba.stencilparfor': 'numba.stencils.stencilparfor',
+    'numba.tracing': 'numba.core.tracing',
+    'numba.transforms': 'numba.core.transforms',
+    'numba.typeconv': 'numba.core.typeconv',
+    'numba.withcontexts': 'numba.core.withcontexts',
+    'numba.typed_passes': 'numba.core.typed_passes',
+    'numba.untyped_passes': 'numba.core.untyped_passes',
+    'numba.utils': 'numba.core.utils',
+    'numba.unicode_support': 'numba.cpython.unicode_support',
+    'numba.unicode': 'numba.cpython.unicode',
+    'numba.typing': 'numba.core.typing',
+    'numba.typeinfer': 'numba.core.typeinfer',
+    'numba.typedobjectutils': 'numba.typed.typedobjectutils',
+}
+
+if sys.version_info < (3, 7):
+    with warnings.catch_warnings():
+        # Ignore warnings from making top-level alias.
+        warnings.simplefilter(
+            "ignore", category=errors.NumbaDeprecationWarning,
+        )
+        for _old_mod in _auto_import_submodules.keys():
+            importlib.import_module(_old_mod)
+else:
+    def __getattr__(attr):
+        submodule_name = 'numba.{}'.format(attr)
+        try:
+            new_name = _auto_import_submodules[submodule_name]
+        except KeyError:
+            raise AttributeError(
+                f"module {__name__!r} has no attribute {attr!r}",
+            ) from None
+        else:
+            errors.deprecate_moved_module(submodule_name, new_name)
+            return importlib.import_module(new_name)
+
+
 __all__ = """
     cfunc
     from_dtype
diff --git a/numba/annotations.py b/numba/annotations.py
new file mode 100644
index 000000000..758b43397
--- /dev/null
+++ b/numba/annotations.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.annotations")
diff --git a/numba/appdirs.py b/numba/appdirs.py
new file mode 100644
index 000000000..ba2ea479a
--- /dev/null
+++ b/numba/appdirs.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.misc.appdirs")
diff --git a/numba/bytecode.py b/numba/bytecode.py
new file mode 100644
index 000000000..4427ffcd8
--- /dev/null
+++ b/numba/bytecode.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.bytecode")
diff --git a/numba/byteflow.py b/numba/byteflow.py
new file mode 100644
index 000000000..ad49ef2bd
--- /dev/null
+++ b/numba/byteflow.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.byteflow")
diff --git a/numba/caching.py b/numba/caching.py
new file mode 100644
index 000000000..fad546277
--- /dev/null
+++ b/numba/caching.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.caching")
diff --git a/numba/callwrapper.py b/numba/callwrapper.py
new file mode 100644
index 000000000..8b83bd844
--- /dev/null
+++ b/numba/callwrapper.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.callwrapper")
diff --git a/numba/charseq.py b/numba/charseq.py
new file mode 100644
index 000000000..1e1a72162
--- /dev/null
+++ b/numba/charseq.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.cpython.charseq")
diff --git a/numba/compiler_lock.py b/numba/compiler_lock.py
new file mode 100644
index 000000000..e7b78f5b6
--- /dev/null
+++ b/numba/compiler_lock.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.compiler_lock")
diff --git a/numba/consts.py b/numba/consts.py
new file mode 100644
index 000000000..82b2551f6
--- /dev/null
+++ b/numba/consts.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.consts")
diff --git a/numba/controlflow.py b/numba/controlflow.py
new file mode 100644
index 000000000..8960f3530
--- /dev/null
+++ b/numba/controlflow.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.controlflow")
diff --git a/numba/core/errors.py b/numba/core/errors.py
index 2598fc757..001ec856d 100644
--- a/numba/core/errors.py
+++ b/numba/core/errors.py
@@ -445,7 +445,6 @@ class _MovedModule(ModuleType):
 
     def __getattr__(self, attr):
         """ warn users above modules moving locations """
-
         try:
             # import from the moved module
             if self.__new_module is not None:
diff --git a/numba/dataflow.py b/numba/dataflow.py
new file mode 100644
index 000000000..795c02e20
--- /dev/null
+++ b/numba/dataflow.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.dataflow")
diff --git a/numba/debuginfo.py b/numba/debuginfo.py
new file mode 100644
index 000000000..f8a282a53
--- /dev/null
+++ b/numba/debuginfo.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.debuginfo")
diff --git a/numba/entrypoints.py b/numba/entrypoints.py
new file mode 100644
index 000000000..1ddc5f6ae
--- /dev/null
+++ b/numba/entrypoints.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.entrypoints")
diff --git a/numba/funcdesc.py b/numba/funcdesc.py
new file mode 100644
index 000000000..be9ca0ecf
--- /dev/null
+++ b/numba/funcdesc.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.funcdesc")
diff --git a/numba/generators.py b/numba/generators.py
new file mode 100644
index 000000000..3e39bfac0
--- /dev/null
+++ b/numba/generators.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.generators")
diff --git a/numba/interpreter.py b/numba/interpreter.py
new file mode 100644
index 000000000..66a199385
--- /dev/null
+++ b/numba/interpreter.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.interpreter")
diff --git a/numba/itanium_mangler.py b/numba/itanium_mangler.py
new file mode 100644
index 000000000..f39a1b3f9
--- /dev/null
+++ b/numba/itanium_mangler.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.itanium_mangler")
diff --git a/numba/listobject.py b/numba/listobject.py
new file mode 100644
index 000000000..5ed0e6459
--- /dev/null
+++ b/numba/listobject.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.typed.listobject")
diff --git a/numba/object_mode_passes.py b/numba/object_mode_passes.py
new file mode 100644
index 000000000..474b380ee
--- /dev/null
+++ b/numba/object_mode_passes.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.object_mode_passes")
diff --git a/numba/postproc.py b/numba/postproc.py
new file mode 100644
index 000000000..d5c5e8b70
--- /dev/null
+++ b/numba/postproc.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.misc.postproc")
diff --git a/numba/pylowering.py b/numba/pylowering.py
new file mode 100644
index 000000000..d8f7c9cc3
--- /dev/null
+++ b/numba/pylowering.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.pylowering")
diff --git a/numba/serialize.py b/numba/serialize.py
new file mode 100644
index 000000000..cdd583704
--- /dev/null
+++ b/numba/serialize.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.serialize")
diff --git a/numba/sigutils.py b/numba/sigutils.py
new file mode 100644
index 000000000..29a058120
--- /dev/null
+++ b/numba/sigutils.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.sigutils")
diff --git a/numba/special.py b/numba/special.py
new file mode 100644
index 000000000..17aaf78ea
--- /dev/null
+++ b/numba/special.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.misc.special")
diff --git a/numba/targets/__init__.py b/numba/targets/__init__.py
index cc07b02dc..f671c2c00 100644
--- a/numba/targets/__init__.py
+++ b/numba/targets/__init__.py
@@ -1,3 +1,8 @@
 import sys
 from numba.core.errors import _MovedModule
+
+from numba.misc import quicksort
+
 sys.modules[__name__] = _MovedModule(locals(), None)
+sys.modules[__name__].quicksort = quicksort
+
diff --git a/numba/tracing.py b/numba/tracing.py
new file mode 100644
index 000000000..5c9ce23e5
--- /dev/null
+++ b/numba/tracing.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.tracing")
diff --git a/numba/transforms.py b/numba/transforms.py
new file mode 100644
index 000000000..6a228d628
--- /dev/null
+++ b/numba/transforms.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.transforms")
diff --git a/numba/unicode_support.py b/numba/unicode_support.py
new file mode 100644
index 000000000..270d5f679
--- /dev/null
+++ b/numba/unicode_support.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.cpython.unicode_support")
diff --git a/numba/withcontexts.py b/numba/withcontexts.py
new file mode 100644
index 000000000..dbec5285e
--- /dev/null
+++ b/numba/withcontexts.py
@@ -0,0 +1,3 @@
+import sys
+from numba.core.errors import _MovedModule
+sys.modules[__name__] = _MovedModule(locals(), "numba.core.withcontexts")


Test patch:
diff --git a/numba/tests/test_refactor_moves.py b/numba/tests/test_refactor_moves.py
index 953b57999..5eec57b07 100644
--- a/numba/tests/test_refactor_moves.py
+++ b/numba/tests/test_refactor_moves.py
@@ -1,10 +1,82 @@
+import sys
 import unittest
 import warnings
+import subprocess
 from importlib import import_module
 from contextlib import contextmanager
 
 from numba.core.errors import NumbaDeprecationWarning
 from numba.tests.support import TestCase
+from numba.core.utils import PYVERSION
+
+IS_PY36 = PYVERSION[:2] == (3, 6)
+NOT_PY36 = not IS_PY36
+
+# All pre0.49 top-level module re-export that will be removed.
+_pre_49_top_level_modules = [
+    'analysis',
+    'annotations',
+    'appdirs',
+    'array_analysis',
+    'bytecode',
+    'byteflow',
+    'caching',
+    'callwrapper',
+    'cgutils',
+    'charseq',
+    'compiler',
+    'compiler_lock',
+    'compiler_machinery',
+    'consts',
+    'controlflow',
+    # 'ctypes_support', # removed
+    'dataflow',
+    'datamodel',
+    'debuginfo',
+    'decorators',
+    'dictobject',
+    'dispatcher',
+    'entrypoints',
+    'funcdesc',
+    'generators',
+    'inline_closurecall',
+    'interpreter',
+    # 'io_support', # removed
+    'ir',
+    'ir_utils',
+    'itanium_mangler',
+    'listobject',
+    'lowering',
+    'npdatetime',
+    # 'npyufunc', # removed
+    'numpy_support',
+    'object_mode_passes',
+    'parfor',
+    'postproc',
+    'pylowering',
+    'pythonapi',
+    'rewrites',
+    'runtime',
+    'serialize',
+    'sigutils',
+    # 'six', # removed
+    'special',
+    'stencilparfor',
+    # 'targets', # removed
+    'tracing',
+    'transforms',
+    'typeconv',
+    'typed_passes',
+    'typedobjectutils',
+    'typeinfer',
+    'typing',
+    'unicode',
+    'unicode_support',
+    'unsafe',
+    'untyped_passes',
+    'utils',
+    'withcontexts',
+]
 
 
 class TestAPIMoves_Q1_2020(TestCase):
@@ -23,7 +95,7 @@ class TestAPIMoves_Q1_2020(TestCase):
             import_module(to_mod)
 
         @contextmanager
-        def checker(fn=None):
+        def checker(fn=None, skip=False):
             """
             If fn is not None then a check will be made to ensure the module
             level `__getattr__`
@@ -31,6 +103,8 @@ class TestAPIMoves_Q1_2020(TestCase):
             with warnings.catch_warnings(record=True) as w:
                 warnings.simplefilter("always", NumbaDeprecationWarning)
                 yield
+            if skip:
+                return
             self.assertTrue(len(w) > 0)
             _require = "requested from a module that has moved location"
             for x in w:
@@ -74,7 +148,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_utils(self):
         checker = self.check_warning("numba.utils", "numba.core.utils")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.utils
 
         for fn in ("pysignature", "OPERATORS_TO_BUILTINS"):
@@ -85,7 +159,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.untyped_passes", "numba.core.untyped_passes"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.untyped_passes
 
         fn = "InlineClosureLikes"
@@ -96,7 +170,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.config", "numba.core.config"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.config
 
         fn = "DISABLE_JIT"
@@ -113,11 +187,13 @@ class TestAPIMoves_Q1_2020(TestCase):
             import numba.unsafe
 
             getattr(numba.unsafe, fn)
-        for x in w:
-            if "No direct replacement for 'numba.unsafe'" in str(x.message):
-                break
-        else:
-            raise ValueError("Could not find expected warning message")
+
+        if NOT_PY36:
+            for x in w:
+                if "No direct replacement for 'numba.unsafe'" in str(x.message):
+                    break
+            else:
+                self.fail("Could not find expected warning message")
 
     def test_numba_unsafe_ndarray(self):
         checker = self.check_warning(
@@ -132,7 +208,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_unicode(self):
         checker = self.check_warning("numba.unicode", "numba.cpython.unicode")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.unicode
 
         for fn in (
@@ -154,7 +230,8 @@ class TestAPIMoves_Q1_2020(TestCase):
         # once and the `TestAPIMoves_Q1_2020` hits numba.typing.* so this needs
         # to run first
         checker = self.check_warning("numba.typing", "numba.core.typing")
-        with checker():
+
+        with checker(skip=IS_PY36):
             import numba.typing
 
         for fn in (
@@ -273,7 +350,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_typeinfer(self):
         checker = self.check_warning("numba.typeinfer", "numba.core.typeinfer")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.typeinfer
 
         fn = "IntrinsicCallConstraint"
@@ -284,7 +361,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.typedobjectutils", "numba.typed.typedobjectutils"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.typedobjectutils
 
         fn = "_cast"
@@ -295,7 +372,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.typed_passes", "numba.core.typed_passes"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.typed_passes
 
         for fn in ("type_inference_stage", "AnnotateTypes"):
@@ -314,7 +391,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_typeconv(self):
         checker = self.check_warning("numba.typeconv", "numba.core.typeconv")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.typeconv
 
         fn = "Conversion"
@@ -567,7 +644,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.stencilparfor", "numba.stencils.stencilparfor"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.stencilparfor
 
         fn = "_compute_last_ind"
@@ -596,7 +673,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_runtime(self):
         checker = self.check_warning("numba.runtime", "numba.core.runtime")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.runtime
 
         fn = "nrt"
@@ -605,11 +682,11 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_rewrites(self):
         checker = self.check_warning("numba.rewrites", "numba.core.rewrites")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.rewrites
 
         fn = "rewrite_registry"
-        with checker(fn):
+        with checker(skip=IS_PY36):
             getattr(numba.rewrites, fn)
 
         # want to check this works, not that it warns
@@ -621,7 +698,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_pythonapi(self):
         checker = self.check_warning("numba.pythonapi", "numba.core.pythonapi")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.pythonapi
 
         for fn in (
@@ -633,7 +710,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_parfor(self):
         checker = self.check_warning("numba.parfor", "numba.parfors.parfor")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.parfor
 
         for fn in (
@@ -657,9 +734,10 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.numpy_support", "numba.np.numpy_support"
         )
-        with checker():
-            import numba.numpy_support
-
+        if NOT_PY36:
+            with checker():
+                import numba.numpy_support
+        import numba
         for fn in ("map_layout", "from_dtype", "as_dtype"):
             with checker(fn):
                 getattr(numba.numpy_support, fn)
@@ -682,7 +760,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.npdatetime", "numba.np.npdatetime_helpers"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.npdatetime
 
         for fn in ("DATETIME_UNITS",):
@@ -691,12 +769,12 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_lowering(self):
         checker = self.check_warning("numba.lowering", "numba.core.lowering")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.lowering  # noqa: F401
 
     def test_numba_ir_utils(self):
         checker = self.check_warning("numba.ir_utils", "numba.core.ir_utils")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.ir_utils
 
         for fn in (
@@ -724,7 +802,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_ir(self):
         checker = self.check_warning("numba.ir", "numba.core.ir")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.ir
 
         for fn in ("Assign", "Const", "Expr", "Var"):
@@ -735,7 +813,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.inline_closurecall", "numba.core.inline_closurecall"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.inline_closurecall
 
         for fn in (
@@ -749,7 +827,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_errors(self):
         checker = self.check_warning("numba.errors", "numba.core.errors")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.errors
 
         for fn in (
@@ -765,7 +843,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.dispatcher", "numba.core.dispatcher"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.dispatcher
 
         for fn in (
@@ -779,7 +857,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.dictobject", "numba.typed.dictobject"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.dictobject
 
         fn = "DictModel"
@@ -788,7 +866,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_datamodel(self):
         checker = self.check_warning("numba.datamodel", "numba.core.datamodel")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.datamodel
 
         for fn in (
@@ -819,12 +897,12 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.compiler_machinery", "numba.core.compiler_machinery"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.compiler_machinery  # noqa: F401
 
     def test_numba_compiler(self):
         checker = self.check_warning("numba.compiler", "numba.core.compiler")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.compiler
 
         for fn in (
@@ -847,7 +925,7 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_cgutils(self):
         checker = self.check_warning("numba.cgutils", "numba.core.cgutils")
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.cgutils
         for fn in (
             "unpack_tuple",
@@ -867,7 +945,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.array_analysis", "numba.parfors.array_analysis"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.array_analysis
 
         for fn in ("ArrayAnalysis", "array_analysis_extensions"):
@@ -885,8 +963,10 @@ class TestAPIMoves_Q1_2020(TestCase):
 
     def test_numba_analysis(self):
         checker = self.check_warning("numba.analysis", "numba.core.analysis")
-        with checker():
-            import numba.analysis
+        if NOT_PY36:
+            with checker():
+                import numba.analysis
+        import numba
         for fn in (
             "ir_extension_usedefs",
             "compute_use_defs",
@@ -900,7 +980,7 @@ class TestAPIMoves_Q1_2020(TestCase):
         checker = self.check_warning(
             "numba.decorators", "numba.core.decorators"
         )
-        with checker():
+        with checker(skip=IS_PY36):
             import numba.decorators
         for fn in (
             "njit",
@@ -931,6 +1011,120 @@ class TestAPIMoves_Q1_2020(TestCase):
         else:
             raise ValueError("Expected warning not found")
 
+    def run_in_fresh_python(self, code):
+        return subprocess.check_output(
+            [sys.executable, '-c', code],
+            stderr=subprocess.STDOUT,
+        ).decode()
+
+    def test_top_level_export_as_attr(self):
+        for mod in _pre_49_top_level_modules:
+            mod = f'numba.{mod}'
+            with self.subTest(mod):
+                out = self.run_in_fresh_python(f'import numba; {mod}')
+                if NOT_PY36:
+                    self.assertIn("NumbaDeprecationWarning", out)
+                    self.assertIn(f"Import requested from: '{mod}'", out)
+
+    def test_top_level_export_as_import(self):
+        for mod in _pre_49_top_level_modules:
+            if mod in {'unsafe'}:
+                # SKIP.
+                # different behavior and tested elsewhere.
+                continue
+            mod = f'numba.{mod}'
+            with self.subTest(mod):
+                out = self.run_in_fresh_python(f'import {mod}')
+                if NOT_PY36:
+                    self.assertIn("NumbaDeprecationWarning", out)
+                    self.assertIn(f"Import requested from: '{mod}'", out)
+
+    def test_top_level_export_attr_matches_import(self):
+        for mod in _pre_49_top_level_modules:
+            mod = f'numba.{mod}'
+            with self.subTest(mod):
+                code = [
+                    'import numba',
+                    f'import {mod} as ref',
+                    f'assert {mod} is ref',
+                ]
+                self.run_in_fresh_python('; '.join(code))
+
+    def test_issue_5528(self):
+        # Check specific issues from the ticket.
+        code = """
+import numba
+numba.numpy_support.from_dtype
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+import numba.numpy_support
+numba.numpy_support.from_dtype
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba.numpy_support import from_dtype
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba.targets import quicksort
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+import numba
+numba.special
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba.special import literally
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba.special import prange
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba import postproc
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+import numba
+numba.analysis.ir_extension_usedefs
+
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba import consts
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+import numba
+numba.extending.lower_builtin
+
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+from numba.config import IS_32BITS
+"""
+        self.run_in_fresh_python(code)
+
+        code = """
+import numba
+numba.datamodel.register_default
+"""
+        self.run_in_fresh_python(code)
+
 
 if __name__ == "__main__":
     unittest.main()