Skip to content

Commit

Permalink
Deprecate .object_id in favor of .resource_id.
Browse files Browse the repository at this point in the history
For #286 / #266.
  • Loading branch information
lemon24 committed Jul 19, 2022
1 parent d0b1412 commit e6bb220
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 95 deletions.
5 changes: 2 additions & 3 deletions src/reader/_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 13 additions & 2 deletions src/reader/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'})
Expand Down Expand Up @@ -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'})
Expand Down
78 changes: 54 additions & 24 deletions src/reader/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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],
Expand Down Expand Up @@ -197,33 +203,52 @@ 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,
func: F,
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)


Expand All @@ -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
Expand Down
9 changes: 3 additions & 6 deletions src/reader/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
#:
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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:
Expand Down
61 changes: 49 additions & 12 deletions src/reader/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from typing import Tuple
from typing import Union

from ._utils import deprecated


class _FancyExceptionBase(Exception):

Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
10 changes: 5 additions & 5 deletions src/reader/plugins/entry_dedupe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}"
)


Expand All @@ -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],
)
Expand Down
Loading

0 comments on commit e6bb220

Please sign in to comment.