diff --git a/src/reader/_storage.py b/src/reader/_storage.py index afb7f9c5..7669dc19 100644 --- a/src/reader/_storage.py +++ b/src/reader/_storage.py @@ -5,7 +5,6 @@ from datetime import timedelta from functools import partial from typing import Any -from typing import cast from typing import Dict from typing import Iterable from typing import Mapping @@ -984,7 +983,7 @@ def _add_or_update_entries( except sqlite3.IntegrityError as e: e_msg = str(e).lower() - feed_url, entry_id = intent.entry.object_id + feed_url, entry_id = intent.entry.resource_id log.debug( "add_entry %r of feed %r: got IntegrityError", @@ -1269,7 +1268,7 @@ def delete_tag(self, object_id: ResourceId, key: str) -> None: with self.get_db() as db: cursor = db.execute(query, params) - rowcount_exactly_one(cursor, lambda: TagNotFoundError(key, cast(str, None))) + rowcount_exactly_one(cursor, lambda: TagNotFoundError(key, object_id)) def make_get_feeds_query( diff --git a/src/reader/_types.py b/src/reader/_types.py index a0d9277d..0e53b34e 100644 --- a/src/reader/_types.py +++ b/src/reader/_types.py @@ -16,6 +16,7 @@ from typing import Union from ._hash_utils import get_hash +from ._utils import deprecated from .types import _entry_argument from .types import _feed_argument from .types import _namedtuple_compat @@ -64,7 +65,12 @@ def as_feed(self, **kwargs: object) -> Feed: return Feed(**attrs) @property - def object_id(self) -> str: + def resource_id(self) -> Tuple[str]: + return (self.url,) + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> str: # pragma: no cover return self.url _hash_exclude_ = frozenset({'url', 'updated'}) @@ -121,7 +127,12 @@ def as_entry(self, **kwargs: object) -> Entry: return Entry(**attrs) @property - def object_id(self) -> Tuple[str, str]: + def resource_id(self) -> Tuple[str, str]: + return self.feed_url, self.id + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> Tuple[str, str]: # pragma: no cover return self.feed_url, self.id _hash_exclude_ = frozenset({'feed_url', 'id', 'updated'}) diff --git a/src/reader/_utils.py b/src/reader/_utils.py index b1448dd8..af4d46b4 100644 --- a/src/reader/_utils.py +++ b/src/reader/_utils.py @@ -20,9 +20,6 @@ from typing import TypeVar from typing import Union -from .types import MISSING -from .types import MissingType - FuncType = Callable[..., Any] F = TypeVar('F', bound=FuncType) @@ -31,6 +28,15 @@ _U = TypeVar('_U') +class MissingType: + def __repr__(self) -> str: + return "no value" + + +#: Sentinel object used to detect if the `default` argument was provided.""" +MISSING = MissingType() + + def zero_or_one( it: Iterable[_U], make_exc: Callable[[], Exception], @@ -197,6 +203,33 @@ def process(self, msg: str, kwargs: Any) -> Tuple[str, Any]: # pragma: no cover return ': '.join(tuple(self._escape(p) for p in self.prefixes) + (msg,)), kwargs +_DEPRECATED_FUNC_WARNING = """\ +{old_name}() is deprecated and will be removed in reader {removed_in}. \ +Use {new_name}() instead.\ +""" +_DEPRECATED_FUNC_DOCSTRING = """\ +Deprecated alias for :meth:`{new_name}`. +{doc} +.. deprecated:: {deprecated_in} + This method will be removed in *reader* {removed_in}. + Use :meth:`{new_name}` instead. + +""" + +_DEPRECATED_PROP_WARNING = """\ +{old_name} is deprecated and will be removed in reader {removed_in}. \ +Use {new_name} instead.\ +""" +_DEPRECATED_PROP_DOCSTRING = """\ +Deprecated variant of :attr:`{new_name}`. +{doc} +.. deprecated:: {deprecated_in} + This property will be removed in *reader* {removed_in}. + Use :attr:`{new_name}` instead. + +""" + + def _deprecated_wrapper( old_name: str, new_name: str, @@ -204,26 +237,18 @@ def _deprecated_wrapper( deprecated_in: str, removed_in: str, doc: str = '', + warning_template: str = _DEPRECATED_FUNC_WARNING, + docstring_template: str = _DEPRECATED_FUNC_DOCSTRING, ) -> F: + format_kwargs = dict(locals()) + @wraps(func) def old_func(*args, **kwargs): # type: ignore - warnings.warn( - f"{old_name}() is deprecated " - f"and will be removed in reader {removed_in}. " - f"Use {new_name}() instead.", - DeprecationWarning, - ) + warnings.warn(warning_template.format_map(format_kwargs), DeprecationWarning) return func(*args, **kwargs) old_func.__name__ = old_name - old_func.__doc__ = ( - f"Deprecated alias for :meth:`{new_name}`.\n" - f"{doc}\n" - f".. deprecated:: {deprecated_in}\n" - f" This method will be removed in *reader* {removed_in}.\n" - f" Use :meth:`{new_name}` instead.\n\n" - ) - + old_func.__doc__ = docstring_template.format_map(format_kwargs) return cast(F, old_func) @@ -233,18 +258,23 @@ def deprecated_wrapper( return _deprecated_wrapper(old_name, func.__name__, func, deprecated_in, removed_in) -def deprecated(new_name: str, deprecated_in: str, removed_in: str) -> Callable[[F], F]: +def deprecated( + new_name: str, deprecated_in: str, removed_in: str, property: bool = False +) -> Callable[[F], F]: + if not property: + kwargs = {} + else: + kwargs = dict( + warning_template=_DEPRECATED_PROP_WARNING, + docstring_template=_DEPRECATED_PROP_DOCSTRING, + ) + def decorator(func: F) -> F: doc = inspect.getdoc(func) or '' if doc: # pragma: no cover doc = '\n' + doc + '\n' return _deprecated_wrapper( - func.__name__, - new_name, - func, - deprecated_in, - removed_in, - doc=doc, + func.__name__, new_name, func, deprecated_in, removed_in, doc=doc, **kwargs ) return decorator diff --git a/src/reader/core.py b/src/reader/core.py index f35cc261..a404d6d4 100644 --- a/src/reader/core.py +++ b/src/reader/core.py @@ -363,7 +363,7 @@ def __init__( #: #: The only `entry` attributes guaranteed to be present are #: :attr:`~Entry.feed_url`, :attr:`~Entry.id`, - #: and :attr:`~Entry.object_id`; + #: and :attr:`~Entry.resource_id`; #: all other attributes may be missing #: (accessing them may raise :exc:`AttributeError`). #: @@ -2054,10 +2054,9 @@ def get_tag( """ resource_id = _resource_argument(resource) - object_id: Any = resource_id if len(resource_id) != 1 else resource_id[0] # type: ignore return zero_or_one( (v for _, v in self._storage.get_tags(resource_id, key)), - lambda: TagNotFoundError(key, object_id), + lambda: TagNotFoundError(key, resource_id), default, ) @@ -2131,12 +2130,10 @@ def delete_tag( """ resource_id = _resource_argument(resource) - object_id: Any = resource_id if len(resource_id) != 1 else resource_id[0] # type: ignore try: self._storage.delete_tag(resource_id, key) - except TagNotFoundError as e: + except TagNotFoundError: if not missing_ok: - e.object_id = object_id raise def make_reader_reserved_name(self, key: str) -> str: diff --git a/src/reader/exceptions.py b/src/reader/exceptions.py index b654f0aa..54be46be 100644 --- a/src/reader/exceptions.py +++ b/src/reader/exceptions.py @@ -3,6 +3,8 @@ from typing import Tuple from typing import Union +from ._utils import deprecated + class _FancyExceptionBase(Exception): @@ -72,7 +74,10 @@ class ResourceNotFoundError(ReaderError): """ - # TODO: object_id: tuple[str, ...] (but FeedError must become tuple[str]!) + @property + def resource_id(self) -> Tuple[str, ...]: # pragma: no cover + """The `resource_id` of the resource.""" + raise NotImplementedError class FeedError(ReaderError): @@ -89,7 +94,17 @@ def _str(self) -> str: return repr(self.url) @property - def object_id(self) -> str: + def resource_id(self) -> Tuple[str]: + """Alias for (:attr:`~url`,). + + .. versionadded:: 2.17 + + """ + return (self.url,) + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> str: # pragma: no cover """Alias for :attr:`~FeedError.url`. .. versionadded:: 1.12 @@ -149,7 +164,17 @@ def _str(self) -> str: return repr((self.feed_url, self.id)) @property - def object_id(self) -> Tuple[str, str]: + def resource_id(self) -> Tuple[str, str]: + """Alias for (:attr:`~feed_url`, :attr:`~id`). + + .. versionadded:: 2.17 + + """ + return self.feed_url, self.id + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> Tuple[str, str]: # pragma: no cover """Alias for (:attr:`~EntryError.feed_url`, :attr:`~EntryError.id`). .. versionadded:: 1.12 @@ -286,27 +311,39 @@ class TagError(ReaderError): .. versionadded:: 2.8 + .. versionchanged:: 2.17 + Signature changed from ``TagError(key, object_id, message='')`` + to ``TagError(key, resource_id, message='')``. + """ def __init__( - self, - key: str, - object_id: Union[Tuple[()], str, Tuple[str, str]], - message: str = '', + self, key: str, resource_id: Tuple[str, ...], message: str = '' ) -> None: super().__init__(message) #: The tag key. self.key = key - # TODO: tuple[str, ...], once FeedError.object_id becomes tuple[str] - - #: The `object_id` of the resource. - self.object_id = object_id + #: The `resource_id` of the resource. + self.resource_id = resource_id @property def _str(self) -> str: - return f"{self.object_id!r}: {self.key!r}" + parts = self.resource_id + (self.key,) + return ': '.join(repr(part) for part in parts) + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> Union[Tuple[()], str, Tuple[str, str]]: # pragma: no cover + """The `object_id` of the resource.""" + if len(self.resource_id) == 0: + return () + if len(self.resource_id) == 1: + return self.resource_id[0] + if len(self.resource_id) == 2: + return self.resource_id[0], self.resource_id[1] + assert False, "shouldn't happen" # noqa: B011 class TagNotFoundError(TagError): diff --git a/src/reader/plugins/entry_dedupe.py b/src/reader/plugins/entry_dedupe.py index 996026e5..c63036e5 100644 --- a/src/reader/plugins/entry_dedupe.py +++ b/src/reader/plugins/entry_dedupe.py @@ -270,7 +270,7 @@ def _get_same_group_entries(reader, entry): # https://github.com/lemon24/reader/issues/202 for other in reader.get_entries(feed=entry.feed_url, read=None): - if entry.object_id == other.object_id: + if entry.resource_id == other.resource_id: continue if _normalize(entry.title) != _normalize(other.title): continue @@ -320,7 +320,7 @@ def by_title(e): return _normalize(e.title) # this reads all the feed's entries in memory; - # better would be to get all the (e.title, e.object_id), + # better would be to get all the (e.title, e.resource_id), # sort them, and then get_entry() each entry in order; # even better would be to have get_entries(sort='title'); # https://github.com/lemon24/reader/issues/202 @@ -455,7 +455,7 @@ def _get_tags(reader, entry, duplicates): else: # pragma: no cover # TODO: custom exception raise RuntimeError( - f"could not find key for entry {entry.object_id} and tag {key}" + f"could not find key for entry {entry.resource_id} and tag {key}" ) @@ -471,14 +471,14 @@ def _make_actions(reader, entry, duplicates): for key, value in _get_tags(reader, entry, duplicates): yield partial(reader.set_tag, entry, key, value) - duplicate_ids = [d.object_id for d in duplicates] + duplicate_ids = [d.resource_id for d in duplicates] yield partial(reader._storage.delete_entries, duplicate_ids) def _dedupe_entries(reader, entry, duplicates, *, dry_run): log.info( "entry_dedupe: %r (title: %r) duplicates: %r", - entry.object_id, + entry.resource_id, entry.title, [e.id for e in duplicates], ) diff --git a/src/reader/types.py b/src/reader/types.py index 090210d1..a2e371d5 100644 --- a/src/reader/types.py +++ b/src/reader/types.py @@ -24,8 +24,14 @@ from typing import TypeVar from typing import Union +from reader._utils import deprecated from reader.exceptions import ReaderError +# noreorder +# can't be defined here because of circular imports +from reader._utils import MISSING as MISSING # noqa: F401 +from reader._utils import MissingType as MissingType # noqa: F401 + _T = TypeVar('_T') @@ -144,7 +150,17 @@ class Feed(_namedtuple_compat): updates_enabled: bool = True @property - def object_id(self) -> str: + def resource_id(self) -> Tuple[str]: + """Alias for (:attr:`~url`,). + + .. versionadded:: 2.17 + + """ + return (self.url,) + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> str: # pragma: no cover """Alias for :attr:`~Feed.url`. .. versionadded:: 1.12 @@ -308,7 +324,17 @@ def feed_url(self) -> str: feed: Feed = cast(Feed, None) @property - def object_id(self) -> Tuple[str, str]: + def resource_id(self) -> Tuple[str, str]: + """Alias for (:attr:`~feed_url`, :attr:`~id`). + + .. versionadded:: 2.17 + + """ + return self.feed_url, self.id + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> Tuple[str, str]: # pragma: no cover """Alias for (:attr:`~Entry.feed_url`, :attr:`~Entry.id`). .. versionadded:: 1.12 @@ -611,7 +637,17 @@ class EntrySearchResult(_namedtuple_compat): # TODO: entry: Optional[Entry]; model it through typing if possible @property - def object_id(self) -> Tuple[str, str]: + def resource_id(self) -> Tuple[str, str]: + """Alias for (:attr:`~feed_url`, :attr:`~id`). + + .. versionadded:: 2.17 + + """ + return self.feed_url, self.id + + @property # type: ignore + @deprecated('resource_id', '2.17', '3.0', property=True) + def object_id(self) -> Tuple[str, str]: # pragma: no cover """Alias for (:attr:`~EntrySearchResult.feed_url`, :attr:`~EntrySearchResult.id`). .. versionadded:: 1.12 @@ -762,15 +798,6 @@ def _resource_argument(resource: ResourceInput) -> ResourceId: ] -class MissingType: - def __repr__(self) -> str: - return "no value" - - -#: Sentinel object used to detect if the `default` argument was provided.""" -MISSING = MissingType() - - @dataclass(frozen=True) class FeedCounts(_namedtuple_compat): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 03f435c3..d3883056 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -85,5 +85,5 @@ def test_metadata_error_str(exc_type): @pytest.mark.parametrize('exc_type', all_classes(TagError)) def test_tag_error_str(exc_type): - exc = exc_type('key', 'object') + exc = exc_type('key', ('object',)) assert "'object': 'key'" in str(exc) diff --git a/tests/test_reader.py b/tests/test_reader.py index 5f17db3f..19010d2e 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -2996,7 +2996,7 @@ def test_pagination_basic(reader, pre_stuff, call_method, sort_kwargs, chunk_siz pre_stuff(reader) def get_ids(**kwargs): - return [o.object_id for o in call_method(reader, **sort_kwargs, **kwargs)] + return [o.resource_id for o in call_method(reader, **sort_kwargs, **kwargs)] ids = get_ids() @@ -3030,9 +3030,9 @@ def test_pagination_random(reader, pre_stuff, call_method, chunk_size): pre_stuff(reader) def get_ids(**kwargs): - return [o.object_id for o in call_method(reader, sort='random', **kwargs)] + return [o.resource_id for o in call_method(reader, sort='random', **kwargs)] - ids = [o.object_id for o in call_method(reader)] + ids = [o.resource_id for o in call_method(reader)] assert len(get_ids(limit=1)) == min(1, chunk_size or 1, len(ids)) assert len(get_ids(limit=2)) == min(2, chunk_size or 2, len(ids)) @@ -3052,7 +3052,7 @@ def get_ids(**kwargs): } NOT_FOUND_STARTING_AFTER = { - get_feeds: '0', + get_feeds: ('0',), get_entries: ('1', '1, 0'), search_entries: ('1', '1, 0'), } @@ -3067,7 +3067,7 @@ def test_starting_after_errors(reader, pre_stuff, call_method, sort_kwargs): with pytest.raises(error_cls) as excinfo: list(call_method(reader, **sort_kwargs, starting_after=starting_after)) - assert excinfo.value.object_id == starting_after + assert excinfo.value.resource_id == starting_after @with_call_paginated_method @@ -3075,7 +3075,7 @@ def test_limit_errors(reader, pre_stuff, call_method, sort_kwargs): pre_stuff(reader) def get_ids(**kwargs): - return [o.object_id for o in call_method(reader, **sort_kwargs, **kwargs)] + return [o.resource_id for o in call_method(reader, **sort_kwargs, **kwargs)] with pytest.raises(ValueError): get_ids(limit=object()) @@ -3299,7 +3299,7 @@ def test_add_entry(reader): with pytest.raises(FeedNotFoundError) as excinfo: reader.add_entry(dict(feed_url='1', id='1, 1')) - assert excinfo.value.object_id == '1' + assert excinfo.value.resource_id == ('1',) # add it by user (from dict) @@ -3322,7 +3322,7 @@ def test_add_entry(reader): with pytest.raises(EntryExistsError) as excinfo: reader.add_entry(expected_entry) - assert excinfo.value.object_id == ('1', '1, 1') + assert excinfo.value.resource_id == ('1', '1, 1') # add it by user (from object) @@ -3358,7 +3358,7 @@ def test_delete_entry(reader): with pytest.raises(EntryNotFoundError) as excinfo: reader.delete_entry(('1', '1, 1')) - assert excinfo.value.object_id == ('1', '1, 1') + assert excinfo.value.resource_id == ('1', '1, 1') # no exception reader.delete_entry(('1', '1, 1'), True) @@ -3379,7 +3379,7 @@ def test_delete_entry(reader): with pytest.raises(EntryError) as excinfo: reader.delete_entry(('1', '1, 2')) - assert excinfo.value.object_id == ('1', '1, 2') + assert excinfo.value.resource_id == ('1', '1, 2') assert excinfo.value.message == "entry must be added by 'user', got 'feed'" assert {(e.id, e.added_by) for e in reader.get_entries()} == { diff --git a/tests/test_reader_private.py b/tests/test_reader_private.py index 4957a3f1..f3ff2bf5 100644 --- a/tests/test_reader_private.py +++ b/tests/test_reader_private.py @@ -128,8 +128,8 @@ def get_entry_ids(): return [e.id for e in reader.get_entries()] with pytest.raises(EntryNotFoundError) as excinfo: - reader._storage.delete_entries([entry.object_id]) - assert (excinfo.value.feed_url, excinfo.value.id) == entry.object_id + reader._storage.delete_entries([entry.resource_id]) + assert (excinfo.value.feed_url, excinfo.value.id) == entry.resource_id assert 'no such entry' in excinfo.value.message assert get_entry_ids() == [] @@ -137,11 +137,11 @@ def get_entry_ids(): reader.update_feeds() assert get_entry_ids() == ['1, 1'] - reader._storage.delete_entries([entry.object_id]) + reader._storage.delete_entries([entry.resource_id]) assert get_entry_ids() == [] with pytest.raises(EntryNotFoundError) as excinfo: - reader._storage.delete_entries([entry.object_id]) + reader._storage.delete_entries([entry.resource_id]) del parser.entries[1][1] reader.update_feeds() diff --git a/tests/test_reader_search.py b/tests/test_reader_search.py index ba641774..43456c7a 100644 --- a/tests/test_reader_search.py +++ b/tests/test_reader_search.py @@ -1091,11 +1091,11 @@ def test_add_entry_basic(reader): reader.update_search() (result,) = reader.search_entries('entry') - assert result.object_id == ('1', '1') + assert result.resource_id == ('1', '1') assert result.metadata['.title'].apply('*', '*') == 'my *entry*' assert result.content['.summary'].apply('*', '*') == 'I am a summary' (result,) = reader.search_entries('summary') - assert result.object_id == ('1', '1') + assert result.resource_id == ('1', '1') assert result.metadata['.title'].apply('*', '*') == 'my entry' assert result.content['.summary'].apply('*', '*') == 'I am a *summary*' diff --git a/tests/test_tags.py b/tests/test_tags.py index 7894e34e..ec49f89e 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -13,10 +13,12 @@ @contextmanager -def raises_TagNotFoundError(object_id, key): +def raises_TagNotFoundError(resource_id, key): + if isinstance(resource_id, str): + resource_id = (resource_id,) with pytest.raises(TagNotFoundError) as excinfo: yield - assert excinfo.value.object_id == object_id + assert excinfo.value.resource_id == resource_id assert excinfo.value.key == key assert 'no such tag' in excinfo.value.message @@ -24,7 +26,7 @@ def raises_TagNotFoundError(object_id, key): @parametrize_dict( 'resource, not_found_exc', { - 'feed': ('1', FeedNotFoundError), + 'feed': (('1',), FeedNotFoundError), 'entry': (('1', '1, 1'), EntryNotFoundError), # no global, the global namespace always exists }, @@ -41,7 +43,7 @@ def test_inexistent_resource(reader, subtests, resource, not_found_exc): with subtests.test("set tag"): with pytest.raises(not_found_exc) as excinfo: reader.set_tag(resource, 'one', 'value') - assert excinfo.value.object_id == resource + assert excinfo.value.resource_id == resource assert 'no such' in excinfo.value.message assert 'no such tag' not in excinfo.value.message @@ -329,10 +331,10 @@ def test_set_no_value(reader, resource, value): { 'global': lambda *_: (), 'feed': lambda f, _: f, - 'feed_id': lambda f, _: f.object_id, + 'feed_id': lambda f, _: f.resource_id, 'feed_tuple': lambda f, _: (f.url,), 'entry': lambda _, e: e, - 'entry_id': lambda _, e: e.object_id, + 'entry_id': lambda _, e: e.resource_id, }, ) def test_resource_argument(reader, make_resource_arg): diff --git a/tests/test_types.py b/tests/test_types.py index 55941da8..c7c3c6cc 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -105,14 +105,14 @@ def test__resource_argument_valueerror(resource): _resource_argument(resource) -def test_object_id(): - assert Feed('url').object_id == 'url' - assert Entry('entry', 'updated', feed=Feed('url')).object_id == ('url', 'entry') - assert EntrySearchResult('url', 'entry').object_id == ('url', 'entry') - assert FeedData('url').object_id == 'url' - assert EntryData('url', 'entry', 'updated').object_id == ('url', 'entry') - assert FeedError('url').object_id == 'url' - assert EntryError('url', 'entry').object_id == ('url', 'entry') +def test_resource_id(): + assert Feed('url').resource_id == ('url',) + assert Entry('entry', 'updated', feed=Feed('url')).resource_id == ('url', 'entry') + assert EntrySearchResult('url', 'entry').resource_id == ('url', 'entry') + assert FeedData('url').resource_id == ('url',) + assert EntryData('url', 'entry', 'updated').resource_id == ('url', 'entry') + assert FeedError('url').resource_id == ('url',) + assert EntryError('url', 'entry').resource_id == ('url', 'entry') @pytest.mark.parametrize( diff --git a/tests/test_utils.py b/tests/test_utils.py index 296a14c7..15de8278 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -27,6 +27,35 @@ def old(arg): _check_deprecated(old) +def test_deprecated_property(): + class Class: + @property + @deprecated('new', '1.0', '2.0', property=True) + def old(self): + "docstring" + raise ValueError() + + with pytest.raises(ValueError), pytest.deprecated_call() as warnings: + Class().old + + assert Class.old.fget.__name__ == 'old' + assert Class.old.fget.__doc__ == ( + 'Deprecated variant of :attr:`new`.\n\n' + 'docstring\n' + '\n' + '.. deprecated:: 1.0\n' + ' This property will be removed in *reader* 2.0.\n' + ' Use :attr:`new` instead.\n\n' + ) + + warning = warnings.pop() + + assert ( + str(warning.message) + == 'old is deprecated and will be removed in reader 2.0. Use new instead.' + ) + + def _check_deprecated(old): with pytest.raises(ValueError) as excinfo, pytest.deprecated_call() as warnings: old('whatever')