diff --git a/setup.py b/setup.py index f22093f..e33628d 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( name='redis-info-provider', - version='0.11.0', + version='0.12.0', python_requires='>=2.7', package_dir={'': 'src'}, packages=find_packages('src'), diff --git a/src/redis_info_provider/info_servicer.py b/src/redis_info_provider/info_servicer.py index 34cafb7..e01d652 100644 --- a/src/redis_info_provider/info_servicer.py +++ b/src/redis_info_provider/info_servicer.py @@ -1,5 +1,7 @@ from __future__ import print_function import time +import functools +import warnings from .shard_pub import ShardPublisher from .redis_shard import InfoType, RedisShard import logging @@ -9,6 +11,37 @@ logger = logging.getLogger(__name__) +def deprecated_alias(**aliases): + """ + Decorator that renames deprecated parameter names of a function to their new names and then calls the function with + the new parameter names. + :param aliases: Dict[str,str] - mapping of deprecated parameter names to their new names. + """ + def deco(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + rename_kwargs(f.__name__, kwargs, aliases) + return f(*args, **kwargs) + return wrapper + return deco + + +def rename_kwargs(func_name, kwargs, aliases): + """ + Rename deprecated parameters of kwargs to their new name as as it appears in aliases dict. + :param func_name: str (name of function that the decorator is applied to) + :param kwargs: Dict[str, str] of arguments the given function was called with + :param aliases: Dict[str, str] that maps deprecated parameters to new parameter names + :raises TypeError if both an argument and its deprecated alias were received + """ + for alias, new in aliases.items(): + if alias in kwargs: + if new in kwargs: + raise TypeError('{} received both {} and {}'.format(func_name, alias, new)) + warnings.warn('{} is deprecated; use {}'.format(alias, new), DeprecationWarning) + kwargs[new] = kwargs.pop(alias) + + class InfoProviderServicer(object): """Implements the InfoProvider RPC interface.""" @@ -58,12 +91,15 @@ def _get_shard_with_info(shard_id): return shard + @deprecated_alias(key_patterns='keys') def GetInfos(self, shard_ids=(), keys=(), allow_partial=False, max_age=0.0): # type: (Sequence[str], Sequence[str], bool, float) -> List[InfoType] """ Returns a list of info dicts according to the shard-ids and keys specified in the query selector. + Note: if deprecated parameter "key_patterns" is passed, we use it as "keys" for backwards compatibility. + However, filtering by glob-like patterns is no longer supported. :param shard_ids: List of shard identifiers to query. If empty, all live shards will be returned. :param keys: List of exact-match keys to filter for. If not empty, only diff --git a/tests/test_infoServicer.py b/tests/test_infoServicer.py index d1b1ef0..0c71775 100644 --- a/tests/test_infoServicer.py +++ b/tests/test_infoServicer.py @@ -151,6 +151,18 @@ def test_allow_partial_unknown_shard(self): msg='Expected info_age for shard-2 to be very large') six.assertRegex(self, response_dict['shard-2']['meta']['error'], 'shard .* not found') + def test_backwards_compatibility(self): + ShardPublisher.add_shard(self.make_shard('shard-1', info={'dummy1': 'dummy1', 'dummy2': 'dummy2'})) + ShardPublisher.add_shard(self.make_shard('shard-2', info={'dummy1': 'dummy1'})) + + response = self.servicer.GetInfos(shard_ids=['shard-1', 'shard-2'], key_patterns=['dummy1']) + response_dict = {info['meta']['shard_identifier']: info for info in response} + self.assertIn('dummy1', response_dict['shard-1']) + self.assertIn('dummy1', response_dict['shard-2']) + + with self.assertRaises(TypeError): + self.servicer.GetInfos(shard_ids=['shard-1'], key_patterns=['dummy1'], keys=['dummy1']) + @patch('redis_info_provider.info_servicer.time.time') def test_max_age(self, time_mock): now = 1545240843.4637716