Repository: Pyomo/pyomo

Issue:
pyomo.common.deprecation failure with 6.4.3 if installed from PyPI
## Summary
This issue emerged as part of the IDAES release earlier today (a test in IDAES uses an import path that has been deprecated in pyomo 6.4.3, which is used below to reproduce the error), but it doesn't seem to be IDAES-specific.

It appears that the same version of Pyomo 6.4.3 results in different behaviors depending on whether it was installed from PyPI or from a source-like URL. 

If pyomo 6.4.3 is installed from source, e.g. using `pip install "pyomo @ https://github.com/Pyomo/pyomo/archive/6.4.3.zip"`, then the deprecation infrastructure seems to work as expected:

```sh
$ python -c "from pyomo.core.expr.logical_expr import EqualityExpression"
WARNING: DEPRECATED: the 'EqualityExpression' class has been moved to
    'pyomo.core.expr.relational_expr.EqualityExpression'.  Please update your
    import.  (deprecated in 6.4.3)
```

However, if the same version of Pyomo is installed from PyPI with `pip install pyomo==6.4.3`, the exact same import fails with `ImportError`:

```sh
$ python -c "from pyomo.core.expr.logical_expr import EqualityExpression"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: cannot import name 'EqualityExpression' from 'pyomo.core.expr.logical_expr' (/opt/conda/envs/test-pyomo-6.4.3/lib/python3.9/site-packages/pyomo/core/expr/logical_expr.cpython-39-x86_64-linux-gnu.so)
```

### Steps to reproduce the issue

```sh
conda create --name test-pyomo-6.4.3-pypi --yes python=3.9 && conda activate test-pyomo-6.4.3-pypi
pip install pyomo==6.4.3
python -c "from pyomo.core.expr.logical_expr import EqualityExpression"  # ImportError
conda deactivate
conda create --name test-pyomo-6.4.3-gh --yes python=3.9 && conda activate test-pyomo-6.4.3-gh
pip install "pyomo @ https://github.com/Pyomo/pyomo/archive/6.4.3.zip"
python -c "from pyomo.core.expr.logical_expr import EqualityExpression"  # deprecation warning
```

### Error Message

```log
ImportError: cannot import name 'EqualityExpression' from 'pyomo.core.expr.logical_expr' (/opt/conda/envs/test-pyomo-6.4.3/lib/python3.9/site-packages/pyomo/core/expr/logical_expr.cpython-39-x86_64-linux-gnu.so)
```

### Information on your system

Pyomo version: 6.4.3
Python version: 3.9
Operating system: Ubuntu 20.04.5
How Pyomo was installed (PyPI, conda, source): See above
Solver (if applicable): N/A



Discussion:

Resolving patch:
diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py
--- a/pyomo/common/deprecation.py
+++ b/pyomo/common/deprecation.py
@@ -272,62 +272,6 @@ def _import_object(name, target, version, remove_in, msg):
     deprecation_warning(msg, version=version, remove_in=remove_in)
     return _object
 
-class _ModuleGetattrBackport_27(object):
-    """Backport for support of module.__getattr__
-
-
-    Beginning in Python 3.7, modules support the declaration of a
-    module-scoped __getattr__ and __dir__ to allow for the dynamic
-    resolution of module attributes.  This class wraps the module class
-    and implements `__getattr__`.  As it declares no local
-    attributes, all module attribute accesses incur a slight runtime
-    penalty (one extra function call).
-
-    """
-    def __init__(self, module):
-        # Wrapped module needs to be a local attribute.  Everything else
-        # is delegated to the inner module type
-        super(_ModuleGetattrBackport_27, self).__setattr__(
-            '_wrapped_module', module)
-
-    def __getattr__(self, name):
-        try:
-            return getattr(self._wrapped_module, name)
-        except AttributeError:
-            info = self._wrapped_module.__relocated_attrs__.get(name, None)
-            if info is not None:
-                target_obj = _import_object(name, *info)
-                setattr(self, name, target_obj)
-                return target_obj
-            raise
-
-    def __dir__(self):
-        return dir(self._wrapped_module)
-
-    def __setattr__(self, name, val):
-        setattr(self._wrapped_module, name, val)
-
-class _ModuleGetattrBackport_35(types.ModuleType):
-    """Backport for support of module.__getattr__
-
-    Beginning in Python 3.7, modules support the declaration of a
-    module-scoped __getattr__ and __dir__ to allow for the dynamic
-    resolution of module attributes.  This class derives from
-    types.ModuleType and implements `__getattr__`.  As it is a direct
-    replacement for types.ModuleType (i.e., we can reassign the already
-    loaded module to this type, it is more efficient that the
-    ModuleGetattrBackport_27 class which must wrap the already loaded
-    module.
-
-    """
-    def __getattr__(self, name):
-        info = self.__relocated_attrs__.get(name, None)
-        if info is not None:
-            target_obj = _import_object(name, *info)
-            setattr(self, name, target_obj)
-            return target_obj
-        raise AttributeError("module '%s' has no attribute '%s'"
-                             % (self.__name__, name))
 
 def relocated_module(new_name, msg=None, logger=None,
                      version=None, remove_in=None):
@@ -394,7 +338,7 @@ def relocated_module(new_name, msg=None, logger=None,
               'Please update your import.'
     deprecation_warning(msg, logger, version, remove_in, cf)
 
-def relocated_module_attribute(local, target, version, remove_in=None, msg=None):
+def relocated_module_attribute(local, target, version, remove_in=None, msg=None, f_globals=None):
     """Provide a deprecation path for moved / renamed module attributes
 
     This function declares that a local module attribute has been moved
@@ -429,36 +373,31 @@ def relocated_module_attribute(local, target, version, remove_in=None, msg=None)
         location.
 
     """
-    _module = sys.modules[inspect.currentframe().f_back.f_globals['__name__']]
-    if not hasattr(_module, '__relocated_attrs__'):
-        _module.__relocated_attrs__ = {}
-        if sys.version_info >= (3,7):
-            _relocated = _module.__relocated_attrs__
-            _mod_getattr = getattr(_module, '__getattr__', None)
-            def __getattr__(name):
-                info = _relocated.get(name, None)
-                if info is not None:
-                    target_obj = _import_object(name, *info)
-                    setattr(_module, name, target_obj)
-                    return target_obj
-                elif _mod_getattr is not None:
-                    return _mod_getattr(name)
-                raise AttributeError("module '%s' has no attribute '%s'"
-                                     % (_module.__name__, name))
-            _module.__getattr__ = __getattr__
-        elif sys.version_info >= (3,5):
-            # If you run across a case where this assertion fails
-            # (because someone else has messed with the module type), we
-            # could add logic to use the _ModuleGetattrBackport_27 class
-            # to wrap the module.  However, as I believe that this will
-            # never happen in Pyomo, it is not worth adding unused
-            # functionality at this point
-            assert _module.__class__ is types.ModuleType
-            _module.__class__ = _ModuleGetattrBackport_35
-        else: # sys.version_info >= (2,7):
-            _module = sys.modules[_module.__name__] \
-                      = _ModuleGetattrBackport_27(_module)
-    _module.__relocated_attrs__[local] = (target, version, remove_in, msg)
+    # Historical note: This method only works for Python >= 3.7.  There
+    # were backports to previous Python interpreters, but were removed
+    # after SHA 4e04819aaeefc2c08b7718460918885e12343451
+    if f_globals is None:
+        f_globals = inspect.currentframe().f_back.f_globals
+        if f_globals['__name__'].startswith('importlib.'):
+            raise DeveloperError(
+                "relocated_module_attribute() called from a cythonized "
+                "module without passing f_globals")
+    _relocated = f_globals.get('__relocated_attrs__', None)
+    if _relocated is None:
+        f_globals['__relocated_attrs__'] = _relocated = {}
+        _mod_getattr = f_globals.get('__getattr__', None)
+        def __getattr__(name):
+            info = _relocated.get(name, None)
+            if info is not None:
+                target_obj = _import_object(name, *info)
+                f_globals[name] = target_obj
+                return target_obj
+            elif _mod_getattr is not None:
+                return _mod_getattr(name)
+            raise AttributeError("module '%s' has no attribute '%s'"
+                                 % (f_globals['__name__'], name))
+        f_globals['__getattr__'] = __getattr__
+    _relocated[local] = (target, version, remove_in, msg)
 
 
 class RenamedClass(type):
diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py
--- a/pyomo/core/expr/logical_expr.py
+++ b/pyomo/core/expr/logical_expr.py
@@ -46,19 +46,19 @@
 relocated_module_attribute(
     'EqualityExpression',
     'pyomo.core.expr.relational_expr.EqualityExpression',
-    version='6.4.3')
+    version='6.4.3', f_globals=globals())
 relocated_module_attribute(
     'InequalityExpression',
     'pyomo.core.expr.relational_expr.InequalityExpression',
-    version='6.4.3')
+    version='6.4.3', f_globals=globals())
 relocated_module_attribute(
     'RangedExpression',
     'pyomo.core.expr.relational_expr.RangedExpression',
-    version='6.4.3')
+    version='6.4.3', f_globals=globals())
 relocated_module_attribute(
     'inequality',
     'pyomo.core.expr.relational_expr.inequality',
-    version='6.4.3')
+    version='6.4.3', f_globals=globals())
 
 
 def _generate_logical_proposition(etype, lhs, rhs):


Test patch: