2012-01-25 31 views
34

私はライブラリの一部であるpythonクラスの名前を変更しました。私は以前の名前をしばらく使用する可能性を残しておきたいが、それが非難され、将来削除されることをユーザーに警告したい。クラス(名前)の廃止についての警告方法

私はそのようにエイリアスを使用するのに十分だろう、下位互換性を提供することを考える:

class NewClsName: 
    pass 

OldClsName = NewClsName 

私はエレガントな方法で非推奨とOldClsNameをマークする方法は考えています。たぶん私はOldClsName(ログに)警告を発し、NewClsNameオブジェクトをそのパラメータ(*args**kvargsを使用)から構築する関数を作ることができるかもしれませんが、それは十分にエレガントではありませんか?

しかし、私はPython標準ライブラリの非推奨警告の仕組みを知らない。私は、非推奨を扱う素晴らしいマジックがあるかもしれないと想像しています。いくつかのインタプリタのコマンドラインオプションに応じてエラーとして処理するか、サイレンシングすることができます。

質問:古くなったクラスエイリアス(または廃止されたクラス)の使用についてユーザーに警告する方法。

EDIT:クラスはOldClsNameを関数として定義されたときに呼び出すことはできませんいくつかのクラスメソッド(ファクトリメソッド)を持っているので、関数アプローチは(私はすでにそれを試して与えた)私のために動作しません。 。次のコードは動作しません。

class NewClsName(object): 
    @classmethod 
    def CreateVariant1(cls, ...): 
     pass 

    @classmethod 
    def CreateVariant2(cls, ...): 
     pass 

def OldClsName(*args, **kwargs): 
    warnings.warn("The 'OldClsName' class was renamed [...]", 
        DeprecationWarning) 
    return NewClsName(*args, **kwargs) 

OldClsName.CreateVariant1(...) 

ための:

AttributeError: 'function' object has no attribute 'CreateVariant1' 

が私の唯一のオプションを継承ますか?正直言って、それは私にはきれいに見えません。それは、不要な派生を導入することによってクラス階層に影響します。さらに、OldClsName is not NewClsNameはほとんどの場合問題ではありませんが、ライブラリを使用するコードが不適切な場合には問題が発生する可能性があります。

ダミーの無関係なOldClsNameクラスを作成して、コンストラクタとその中のすべてのクラスメソッドのラッパーを実装することもできますが、私の意見ではさらに悪い解決策です。

答えて

26

たぶん私は( ログへの)警告を発し、NewClsNameオブジェクトを作成OldClsName機能を作ることができますそのパラメータから( * argsと** kvargsを使用して)、それは十分にエレガントに見えません(あるいは多分?)。

うん、私はそれはかなり標準的だと思う:

def OldClsName(*args, **kwargs): 
    from warnings import warn 
    warn("get with the program!") 
    return NewClsName(*args, **kwargs) 

だけトリッキーな事はあなたがOldClsNameからサブクラスのものを持っている場合である - そして、私たちは巧妙な取得する必要があります。あなただけのクラスメソッドへのアクセスを維持する必要がある場合、これはそれを行う必要があります。

class DeprecationHelper(object): 
    def __init__(self, new_target): 
     self.new_target = new_target 

    def _warn(self): 
     from warnings import warn 
     warn("Get with the program!") 

    def __call__(self, *args, **kwargs): 
     self._warn() 
     return self.new_target(*args, **kwargs) 

    def __getattr__(self, attr): 
     self._warn() 
     return getattr(self.new_target, attr) 

OldClsName = DeprecationHelper(NewClsName) 

私はそれをテストしていませんが、それはあなたのアイデアを与える必要があります - __call__は通常instantationルートを処理する、__getattr__をキャプチャしますクラスメソッドへのアクセス&はクラス階層を乱さずに警告を生成します。

+1

簡単同様である必要があり、継承をサポート - ちょうど 'クラスOldClsName(NewClsName書きます):#and overload __new__'。 – delnan

+1

+1この解決策が使用されていることを確認し、 'warnings'モジュールに言及してください。残念ながら、関数の解決策は私のためには機能しません(編集された質問を参照)。多分あなたは他の清潔な解決策を持っていますか? :D –

+0

新しいクラスへの呼び出しと属性の両方のアクセスをプロキシできるラッパーオブジェクトを使用する例で更新されました。 – AdamKG

12

warnings.warnをご覧ください。

あなたがわかるように、ドキュメントの例では、非推奨の警告です:

def deprecation(message): 
    warnings.warn(message, DeprecationWarning, stacklevel=2) 
5

なぜサブクラスではないのですか?こうすることで、ユーザーコードを壊さないようにすることができます。 OldClassのためのプレースホルダを追加する

class OldClsName(NewClsName): 
    def __init__(self, *args, **kwargs): 
     warnings.warn("The 'OldClsName' class was renamed [...]", 
         DeprecationWarning) 
     NewClsName.__init__(*args, **kwargs) 
+1

'isinstance()' checks ... – dAnjou

+0

'isinstance'はサブクラスもチェックしています。 –

+0

'isinstance'チェックは本当に失敗します。 'isinstance(obj、OldClsName)'と 'obj = NewClsName()'をインスタンス化し、isinstance(obj、OldClsName)== False'をコード化し、コードが壊れてしまうレガシーコードがあると考えてください。 – DylanYoung

0

使用inspectモジュール、そしてOldClsName is NewClsNameチェックが合格し、pylintのようなリンターはこれをエラーとして通知します。

deprecate.py

import inspect 
import warnings 
from functools import wraps 

def renamed(old_name): 
    """Return decorator for renamed callable. 

    Args: 
     old_name (str): This name will still accessible, 
      but call it will result a warn. 

    Returns: 
     decorator: this will do the setting about `old_name` 
      in the caller's module namespace. 
    """ 

    def _wrap(obj): 
     assert callable(obj) 

     def _warn(): 
      warnings.warn('Renamed: {} -> {}' 
         .format(old_name, obj.__name__), 
         DeprecationWarning, stacklevel=3) 

     def _wrap_with_warn(func, is_inspect): 
      @wraps(func) 
      def _func(*args, **kwargs): 
       if is_inspect: 
        # XXX: If use another name to call, 
        # you will not get the warning. 
        frame = inspect.currentframe().f_back 
        code = inspect.getframeinfo(frame).code_context 
        if [line for line in code 
          if old_name in line]: 
         _warn() 
       else: 
        _warn() 
       return func(*args, **kwargs) 
      return _func 

     # Make old name available. 
     frame = inspect.currentframe().f_back 
     assert old_name not in frame.f_globals, (
      'Name already in use.', old_name) 

     if inspect.isclass(obj): 
      obj.__init__ = _wrap_with_warn(obj.__init__, True) 
      placeholder = obj 
     else: 
      placeholder = _wrap_with_warn(obj, False) 

     frame.f_globals[old_name] = placeholder 

     return obj 

    return _wrap 

test.py

from __future__ import print_function 

from deprecate import renamed 


@renamed('test1_old') 
def test1(): 
    return 'test1' 


@renamed('Test2_old') 
class Test2(object): 
    pass 

    def __init__(self): 
     self.data = 'test2_data' 

    def method(self): 
     return self.data 

# pylint: disable=undefined-variable 
# If not use this inline pylint option, 
# there will be E0602 for each old name. 
assert(test1() == test1_old()) 
assert(Test2_old is Test2) 
print('# Call new name') 
print(Test2()) 
print('# Call old name') 
print(Test2_old()) 

その後、python -W all test.pyを実行します。

test.py:22: DeprecationWarning: Renamed: test1_old -> test1 
assert(test1() == test1_old()) 
# Call new name 
<__main__.Test2 object at 0x0000000007A147B8> 
# Call old name 
test.py:27: DeprecationWarning: Renamed: Test2_old -> Test2 
print(Test2_old()) 
<__main__.Test2 object at 0x0000000007A147B8> 
関連する問題