Skip to content

Commit c568fbe

Browse files
authored
Merge pull request #2492 from jsiirola/relocate-module
Add `deprecation.relocated_module()` utility
2 parents 312b0f8 + 9f04390 commit c568fbe

File tree

4 files changed

+147
-34
lines changed

4 files changed

+147
-34
lines changed

pyomo/common/deprecation.py

+100-34
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
deprecated
1717
deprecation_warning
18+
relocated_module
1819
relocated_module_attribute
1920
RenamedClass
2021
"""
@@ -322,6 +323,71 @@ def __getattr__(self, name):
322323
raise AttributeError("module '%s' has no attribute '%s'"
323324
% (self.__name__, name))
324325

326+
def relocated_module(new_name, msg=None, logger=None,
327+
version=None, remove_in=None):
328+
"""Provide a deprecation path for moved / renamed modules
329+
330+
Upon import, the old module (that called `relocated_module()`) will
331+
be replaced in `sys.modules` by an alias that points directly to the
332+
new module. As a result, the old module should have only two lines
333+
of executable Python code (the import of `relocated_module` and the
334+
call to it).
335+
336+
Parameters
337+
----------
338+
new_name: str
339+
The new (fully-qualified) module name
340+
341+
msg: str
342+
A custom deprecation message.
343+
344+
logger: str
345+
The logger to use for emitting the warning (default: the calling
346+
pyomo package, or "pyomo")
347+
348+
version: str [required]
349+
The version in which the module was renamed or moved.
350+
General practice is to set version to '' or 'TBD' during
351+
development and update it to the actual release as part of the
352+
release process.
353+
354+
remove_in: str
355+
The version in which the module will be removed from the code.
356+
357+
Example
358+
-------
359+
>>> from pyomo.common.deprecation import relocated_module
360+
>>> relocated_module('pyomo.common.deprecation', version='1.2.3')
361+
WARNING: DEPRECATED: The '...' module has been moved to
362+
'pyomo.common.deprecation'. Please update your import.
363+
(deprecated in 1.2.3) ...
364+
365+
"""
366+
from importlib import import_module
367+
new_module = import_module(new_name)
368+
369+
# The relevant module (the one being deprecated) is the one that
370+
# holds the function/method that called deprecated_module(). The
371+
# relevant calling frame for the deprecation warning is the first
372+
# frame in the stack that doesn't look like the importer (i.e., the
373+
# thing that imported the deprecated module).
374+
cf = _find_calling_frame(1)
375+
old_name = cf.f_globals.get('__name__', '<stdin>')
376+
cf = cf.f_back
377+
if cf is not None:
378+
importer = cf.f_back.f_globals['__name__'].split('.')[0]
379+
while cf is not None and \
380+
cf.f_globals['__name__'].split('.')[0] == importer:
381+
cf = cf.f_back
382+
if cf is None:
383+
cf = _find_calling_frame(1)
384+
385+
sys.modules[old_name] = new_module
386+
if msg is None:
387+
msg = f"The '{old_name}' module has been moved to '{new_name}'. " \
388+
'Please update your import.'
389+
deprecation_warning(msg, logger, version, remove_in, cf)
390+
325391
def relocated_module_attribute(local, target, version, remove_in=None):
326392
"""Provide a deprecation path for moved / renamed module attributes
327393
@@ -386,40 +452,40 @@ class RenamedClass(type):
386452
This metaclass provides a mechanism for renaming old classes while
387453
still preserving isinstance / issubclass relationships.
388454
389-
Example
390-
-------
391-
>>> from pyomo.common.deprecation import RenamedClass
392-
>>> class NewClass(object):
393-
... pass
394-
>>> class OldClass(metaclass=RenamedClass):
395-
... __renamed__new_class__ = NewClass
396-
... __renamed__version__ = '6.0'
397-
398-
Deriving from the old class generates a warning:
399-
400-
>>> class DerivedOldClass(OldClass):
401-
... pass
402-
WARNING: DEPRECATED: Declaring class 'DerivedOldClass' derived from
403-
'OldClass'. The class 'OldClass' has been renamed to 'NewClass'.
404-
(deprecated in 6.0) ...
405-
406-
As does instantiating the old class:
407-
408-
>>> old = OldClass()
409-
WARNING: DEPRECATED: Instantiating class 'OldClass'. The class
410-
'OldClass' has been renamed to 'NewClass'. (deprecated in 6.0) ...
411-
412-
Finally, isinstance and issubclass still work, for example:
413-
414-
>>> isinstance(old, NewClass)
415-
True
416-
>>> class NewSubclass(NewClass):
417-
... pass
418-
>>> new = NewSubclass()
419-
>>> isinstance(new, OldClass)
420-
WARNING: DEPRECATED: Checking type relative to 'OldClass'. The class
421-
'OldClass' has been renamed to 'NewClass'. (deprecated in 6.0) ...
422-
True
455+
Examples
456+
--------
457+
>>> from pyomo.common.deprecation import RenamedClass
458+
>>> class NewClass(object):
459+
... pass
460+
>>> class OldClass(metaclass=RenamedClass):
461+
... __renamed__new_class__ = NewClass
462+
... __renamed__version__ = '6.0'
463+
464+
Deriving from the old class generates a warning:
465+
466+
>>> class DerivedOldClass(OldClass):
467+
... pass
468+
WARNING: DEPRECATED: Declaring class 'DerivedOldClass' derived from
469+
'OldClass'. The class 'OldClass' has been renamed to 'NewClass'.
470+
(deprecated in 6.0) ...
471+
472+
As does instantiating the old class:
473+
474+
>>> old = OldClass()
475+
WARNING: DEPRECATED: Instantiating class 'OldClass'. The class
476+
'OldClass' has been renamed to 'NewClass'. (deprecated in 6.0) ...
477+
478+
Finally, `isinstance` and `issubclass` still work, for example:
479+
480+
>>> isinstance(old, NewClass)
481+
True
482+
>>> class NewSubclass(NewClass):
483+
... pass
484+
>>> new = NewSubclass()
485+
>>> isinstance(new, OldClass)
486+
WARNING: DEPRECATED: Checking type relative to 'OldClass'. The class
487+
'OldClass' has been renamed to 'NewClass'. (deprecated in 6.0) ...
488+
True
423489
424490
"""
425491
def __new__(cls, name, bases, classdict, *args, **kwargs):

pyomo/common/tests/relo_mod.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2022
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from pyomo.common.deprecation import relocated_module
13+
14+
relocated_module('pyomo.common.tests.relo_mod_new', version='1.2')

pyomo/common/tests/relo_mod_new.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2022
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
RELO_ATTR = 42
13+
14+
class ReloClass(object): pass

pyomo/common/tests/test_deprecated.py

+19
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,25 @@ def test_relocated_message(self):
425425
"DEPRECATED: the 'oldName' class has been moved to "
426426
"'pyomo.common.tests.test_deprecated.TestRelocated'")
427427

428+
def test_relocated_module(self):
429+
with LoggingIntercept() as LOG:
430+
# Can import attributes defined only in the new module
431+
from pyomo.common.tests.relo_mod import ReloClass
432+
self.assertRegex(
433+
LOG.getvalue().replace('\n', ' '),
434+
r"DEPRECATED: The 'pyomo\.common\.tests\.relo_mod' module has "
435+
r"been moved to 'pyomo\.common\.tests\.relo_mod_new'. Please "
436+
r"update your import. \(deprecated in 1\.2\) \(called from "
437+
r".*test_deprecated\.py")
438+
with LoggingIntercept() as LOG:
439+
# Second import: no warning
440+
import pyomo.common.tests.relo_mod as relo
441+
self.assertEqual(LOG.getvalue(), '')
442+
import pyomo.common.tests.relo_mod_new as relo_new
443+
self.assertIs(relo, relo_new)
444+
self.assertEqual(relo.RELO_ATTR, 42)
445+
self.assertIs(ReloClass, relo_new.ReloClass)
446+
428447

429448
class TestRenamedClass(unittest.TestCase):
430449
def test_renamed(self):

0 commit comments

Comments
 (0)