We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Search plugins storing BeautifulSoup objects in Entry cause excessive recursion in Entry::take_snapshot
### Expected behaviour:

Flexget doesn't crash.
### Actual behaviour:

Flexget crashes due to excessive recursion depth while taking the snapshot of an entry produced by the TorrentLeech search plugin.
### Steps to reproduce:

Use the TorrentLeech search plugin in a task.
#### Config (snippet):

```
tasks:
  tv-discover:
    interval: 12 hours
    discover:
      what:
        - next_series_episodes: yes
      from:
        - torrentleech:
            rss_key: "{{ secrets.torrentleech.rsskey }}"
            username: "{{ secrets.torrentleech.username }}"
            password: "{{ secrets.torrentleech.password }}"
            category: Episodes HD
    transmission: yes
```
### Additional information:
- Flexget Version: 2.2.1
- Python Version: 2.7.12
- Installation method: pip
- OS and version: Fedora 24 x86_64
- Link to crash log: https://gist.github.com/danielkza/d6e8c98b31037de731c5b5ac76733b39
### Debugging information

I'm not familiar with the Flexget codebase, but some quick debugging shows the problem is caused by the TL plugin inserting some text directly from the HTML tree produced by BeautifulSoup. That text is actually a `bs4.element.NavigableString` that contains references to the whole tree, which Flexget attempts to serialize when saving a snapshot of the entry.

The plugin should convert any such usages to `unicode` before storing it in the entry. It is possible other search plugins have the same issue, so a generic solution might be desirable. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `flexget/entry.py`
Content:
```
1 from __future__ import unicode_literals, division, absolute_import
2 from builtins import *  # pylint: disable=unused-import, redefined-builtin
3 from future.utils import PY2, native_str
4 
5 import copy
6 import functools
7 import logging
8 
9 from flexget.plugin import PluginError
10 from flexget.utils.lazy_dict import LazyDict, LazyLookup
11 from flexget.utils.template import render_from_entry
12 
13 log = logging.getLogger('entry')
14 
15 
16 class EntryUnicodeError(Exception):
17     """This exception is thrown when trying to set non-unicode compatible field value to entry."""
18 
19     def __init__(self, key, value):
20         self.key = key
21         self.value = value
22 
23     def __str__(self):
24         return 'Entry strings must be unicode: %s (%r)' % (self.key, self.value)
25 
26 
27 class Entry(LazyDict):
28     """
29     Represents one item in task. Must have `url` and *title* fields.
30 
31     Stores automatically *original_url* key, which is necessary because
32     plugins (eg. urlrewriters) may change *url* into something else
33     and otherwise that information would be lost.
34 
35     Entry will also transparently convert all ascii strings into unicode
36     and raises :class:`EntryUnicodeError` if conversion fails on any value
37     being set. Such failures are caught by :class:`~flexget.task.Task`
38     and trigger :meth:`~flexget.task.Task.abort`.
39     """
40 
41     def __init__(self, *args, **kwargs):
42         super(Entry, self).__init__()
43         self.traces = []
44         self.snapshots = {}
45         self._state = 'undecided'
46         self._hooks = {'accept': [], 'reject': [], 'fail': [], 'complete': []}
47         self.task = None
48 
49         if len(args) == 2:
50             kwargs['title'] = args[0]
51             kwargs['url'] = args[1]
52             args = []
53 
54         # Make sure constructor does not escape our __setitem__ enforcement
55         self.update(*args, **kwargs)
56 
57     def trace(self, message, operation=None, plugin=None):
58         """
59         Adds trace message to the entry which should contain useful information about why
60         plugin did not operate on entry. Accept and Reject messages are added to trace automatically.
61 
62         :param string message: Message to add into entry trace.
63         :param string operation: None, reject, accept or fail
64         :param plugin: Uses task.current_plugin by default, pass value to override
65         """
66         if operation not in (None, 'accept', 'reject', 'fail'):
67             raise ValueError('Unknown operation %s' % operation)
68         item = (plugin, operation, message)
69         if item not in self.traces:
70             self.traces.append(item)
71 
72     def run_hooks(self, action, **kwargs):
73         """
74         Run hooks that have been registered for given ``action``.
75 
76         :param action: Name of action to run hooks for
77         :param kwargs: Keyword arguments that should be passed to the registered functions
78         """
79         for func in self._hooks[action]:
80             func(self, **kwargs)
81 
82     def add_hook(self, action, func, **kwargs):
83         """
84         Add a hook for ``action`` to this entry.
85 
86         :param string action: One of: 'accept', 'reject', 'fail', 'complete'
87         :param func: Function to execute when event occurs
88         :param kwargs: Keyword arguments that should be passed to ``func``
89         :raises: ValueError when given an invalid ``action``
90         """
91         try:
92             self._hooks[action].append(functools.partial(func, **kwargs))
93         except KeyError:
94             raise ValueError('`%s` is not a valid entry action' % action)
95 
96     def on_accept(self, func, **kwargs):
97         """
98         Register a function to be called when this entry is accepted.
99 
100         :param func: The function to call
101         :param kwargs: Keyword arguments that should be passed to the registered function
102         """
103         self.add_hook('accept', func, **kwargs)
104 
105     def on_reject(self, func, **kwargs):
106         """
107         Register a function to be called when this entry is rejected.
108 
109         :param func: The function to call
110         :param kwargs: Keyword arguments that should be passed to the registered function
111         """
112         self.add_hook('reject', func, **kwargs)
113 
114     def on_fail(self, func, **kwargs):
115         """
116         Register a function to be called when this entry is failed.
117 
118         :param func: The function to call
119         :param kwargs: Keyword arguments that should be passed to the registered function
120         """
121         self.add_hook('fail', func, **kwargs)
122 
123     def on_complete(self, func, **kwargs):
124         """
125         Register a function to be called when a :class:`Task` has finished processing this entry.
126 
127         :param func: The function to call
128         :param kwargs: Keyword arguments that should be passed to the registered function
129         """
130         self.add_hook('complete', func, **kwargs)
131 
132     def accept(self, reason=None, **kwargs):
133         if self.rejected:
134             log.debug('tried to accept rejected %r' % self)
135         elif not self.accepted:
136             self._state = 'accepted'
137             self.trace(reason, operation='accept')
138             # Run entry on_accept hooks
139             self.run_hooks('accept', reason=reason, **kwargs)
140 
141     def reject(self, reason=None, **kwargs):
142         # ignore rejections on immortal entries
143         if self.get('immortal'):
144             reason_str = '(%s)' % reason if reason else ''
145             log.info('Tried to reject immortal %s %s' % (self['title'], reason_str))
146             self.trace('Tried to reject immortal %s' % reason_str)
147             return
148         if not self.rejected:
149             self._state = 'rejected'
150             self.trace(reason, operation='reject')
151             # Run entry on_reject hooks
152             self.run_hooks('reject', reason=reason, **kwargs)
153 
154     def fail(self, reason=None, **kwargs):
155         log.debug('Marking entry \'%s\' as failed' % self['title'])
156         if not self.failed:
157             self._state = 'failed'
158             self.trace(reason, operation='fail')
159             log.error('Failed %s (%s)' % (self['title'], reason))
160             # Run entry on_fail hooks
161             self.run_hooks('fail', reason=reason, **kwargs)
162 
163     def complete(self, **kwargs):
164         # Run entry on_complete hooks
165         self.run_hooks('complete', **kwargs)
166 
167     @property
168     def state(self):
169         return self._state
170 
171     @property
172     def accepted(self):
173         return self._state == 'accepted'
174 
175     @property
176     def rejected(self):
177         return self._state == 'rejected'
178 
179     @property
180     def failed(self):
181         return self._state == 'failed'
182 
183     @property
184     def undecided(self):
185         return self._state == 'undecided'
186 
187     def __setitem__(self, key, value):
188         # Enforce unicode compatibility.
189         if PY2 and isinstance(value, native_str):
190             # Allow Python 2's implicit string decoding, but fail now instead of when entry fields are used.
191             # If encoding is anything but ascii, it should be decoded it to text before setting an entry field
192             try:
193                 value = value.decode('ascii')
194             except UnicodeDecodeError:
195                 raise EntryUnicodeError(key, value)
196         elif isinstance(value, bytes):
197             raise EntryUnicodeError(key, value)
198 
199         # url and original_url handling
200         if key == 'url':
201             if not isinstance(value, (str, LazyLookup)):
202                 raise PluginError('Tried to set %r url to %r' % (self.get('title'), value))
203             self.setdefault('original_url', value)
204 
205         # title handling
206         if key == 'title':
207             if not isinstance(value, (str, LazyLookup)):
208                 raise PluginError('Tried to set title to %r' % value)
209 
210         try:
211             log.trace('ENTRY SET: %s = %r' % (key, value))
212         except Exception as e:
213             log.debug('trying to debug key `%s` value threw exception: %s' % (key, e))
214 
215         super(Entry, self).__setitem__(key, value)
216 
217     def safe_str(self):
218         return '%s | %s' % (self['title'], self['url'])
219 
220     # TODO: this is too manual, maybe we should somehow check this internally and throw some exception if
221     # application is trying to operate on invalid entry
222     def isvalid(self):
223         """
224         :return: True if entry is valid. Return False if this cannot be used.
225         :rtype: bool
226         """
227         if 'title' not in self:
228             return False
229         if 'url' not in self:
230             return False
231         if not isinstance(self['url'], str):
232             return False
233         if not isinstance(self['title'], str):
234             return False
235         return True
236 
237     def take_snapshot(self, name):
238         """
239         Takes a snapshot of the entry under *name*. Snapshots can be accessed via :attr:`.snapshots`.
240         :param string name: Snapshot name
241         """
242         snapshot = {}
243         for field, value in self.items():
244             try:
245                 snapshot[field] = copy.deepcopy(value)
246             except TypeError:
247                 log.warning('Unable to take `%s` snapshot for field `%s` in `%s`' % (name, field, self['title']))
248         if snapshot:
249             if name in self.snapshots:
250                 log.warning('Snapshot `%s` is being overwritten for `%s`' % (name, self['title']))
251             self.snapshots[name] = snapshot
252 
253     def update_using_map(self, field_map, source_item, ignore_none=False):
254         """
255         Populates entry fields from a source object using a dictionary that maps from entry field names to
256         attributes (or keys) in the source object.
257 
258         :param dict field_map:
259           A dictionary mapping entry field names to the attribute in source_item (or keys,
260           if source_item is a dict)(nested attributes/dicts are also supported, separated by a dot,)
261           or a function that takes source_item as an argument
262         :param source_item:
263           Source of information to be used by the map
264         :param ignore_none:
265           Ignore any None values, do not record it to the Entry
266         """
267         func = dict.get if isinstance(source_item, dict) else getattr
268         for field, value in field_map.items():
269             if isinstance(value, str):
270                 v = functools.reduce(func, value.split('.'), source_item)
271             else:
272                 v = value(source_item)
273             if ignore_none and v is None:
274                 continue
275             self[field] = v
276 
277     def render(self, template):
278         """
279         Renders a template string based on fields in the entry.
280 
281         :param string template: A template string that uses jinja2 or python string replacement format.
282         :return: The result of the rendering.
283         :rtype: string
284         :raises RenderError: If there is a problem.
285         """
286         if not isinstance(template, str):
287             raise ValueError('Trying to render non string template, got %s' % repr(template))
288         log.trace('rendering: %s' % template)
289         return render_from_entry(template, self)
290 
291     def __eq__(self, other):
292         return self.get('title') == other.get('title') and self.get('original_url') == other.get('original_url')
293 
294     def __hash__(self):
295         return hash(self.get('title', '') + self.get('original_url', ''))
296 
297     def __repr__(self):
298         return '<Entry(title=%s,state=%s)>' % (self['title'], self._state)
299
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/flexget/entry.py b/flexget/entry.py
--- a/flexget/entry.py
+++ b/flexget/entry.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals, division, absolute_import
 from builtins import *  # pylint: disable=unused-import, redefined-builtin
-from future.utils import PY2, native_str
+from future.utils import PY2, native_str, text_type
 
 import copy
 import functools
@@ -195,6 +195,10 @@
                 raise EntryUnicodeError(key, value)
         elif isinstance(value, bytes):
             raise EntryUnicodeError(key, value)
+        # Coerce any enriched strings (such as those returned by BeautifulSoup) to plain strings to avoid serialization
+        # troubles.
+        elif isinstance(value, text_type) and type(value) != text_type:  # pylint: disable=unidiomatic-typecheck
+            value = text_type(value)
 
         # url and original_url handling
         if key == 'url':

{"golden_diff": "diff --git a/flexget/entry.py b/flexget/entry.py\n--- a/flexget/entry.py\n+++ b/flexget/entry.py\n@@ -1,6 +1,6 @@\n from __future__ import unicode_literals, division, absolute_import\n from builtins import *  # pylint: disable=unused-import, redefined-builtin\n-from future.utils import PY2, native_str\n+from future.utils import PY2, native_str, text_type\n \n import copy\n import functools\n@@ -195,6 +195,10 @@\n                 raise EntryUnicodeError(key, value)\n         elif isinstance(value, bytes):\n             raise EntryUnicodeError(key, value)\n+        # Coerce any enriched strings (such as those returned by BeautifulSoup) to plain strings to avoid serialization\n+        # troubles.\n+        elif isinstance(value, text_type) and type(value) != text_type:  # pylint: disable=unidiomatic-typecheck\n+            value = text_type(value)\n \n         # url and original_url handling\n         if key == 'url':\n", "issue": "Search plugins storing BeautifulSoup objects in Entry cause excessive recursion in Entry::take_snapshot\n### Expected behaviour:\n\nFlexget doesn't crash.\n### Actual behaviour:\n\nFlexget crashes due to excessive recursion depth while taking the snapshot of an entry produced by the TorrentLeech search plugin.\n### Steps to reproduce:\n\nUse the TorrentLeech search plugin in a task.\n#### Config (snippet):\n\n```\ntasks:\n  tv-discover:\n    interval: 12 hours\n    discover:\n      what:\n        - next_series_episodes: yes\n      from:\n        - torrentleech:\n            rss_key: \"{{ secrets.torrentleech.rsskey }}\"\n            username: \"{{ secrets.torrentleech.username }}\"\n            password: \"{{ secrets.torrentleech.password }}\"\n            category: Episodes HD\n    transmission: yes\n```\n### Additional information:\n- Flexget Version: 2.2.1\n- Python Version: 2.7.12\n- Installation method: pip\n- OS and version: Fedora 24 x86_64\n- Link to crash log: https://gist.github.com/danielkza/d6e8c98b31037de731c5b5ac76733b39\n### Debugging information\n\nI'm not familiar with the Flexget codebase, but some quick debugging shows the problem is caused by the TL plugin inserting some text directly from the HTML tree produced by BeautifulSoup. That text is actually a `bs4.element.NavigableString` that contains references to the whole tree, which Flexget attempts to serialize when saving a snapshot of the entry.\n\nThe plugin should convert any such usages to `unicode` before storing it in the entry. It is possible other search plugins have the same issue, so a generic solution might be desirable. \n\n", "before_files": [{"content": "from __future__ import unicode_literals, division, absolute_import\nfrom builtins import *  # pylint: disable=unused-import, redefined-builtin\nfrom future.utils import PY2, native_str\n\nimport copy\nimport functools\nimport logging\n\nfrom flexget.plugin import PluginError\nfrom flexget.utils.lazy_dict import LazyDict, LazyLookup\nfrom flexget.utils.template import render_from_entry\n\nlog = logging.getLogger('entry')\n\n\nclass EntryUnicodeError(Exception):\n    \"\"\"This exception is thrown when trying to set non-unicode compatible field value to entry.\"\"\"\n\n    def __init__(self, key, value):\n        self.key = key\n        self.value = value\n\n    def __str__(self):\n        return 'Entry strings must be unicode: %s (%r)' % (self.key, self.value)\n\n\nclass Entry(LazyDict):\n    \"\"\"\n    Represents one item in task. Must have `url` and *title* fields.\n\n    Stores automatically *original_url* key, which is necessary because\n    plugins (eg. urlrewriters) may change *url* into something else\n    and otherwise that information would be lost.\n\n    Entry will also transparently convert all ascii strings into unicode\n    and raises :class:`EntryUnicodeError` if conversion fails on any value\n    being set. Such failures are caught by :class:`~flexget.task.Task`\n    and trigger :meth:`~flexget.task.Task.abort`.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(Entry, self).__init__()\n        self.traces = []\n        self.snapshots = {}\n        self._state = 'undecided'\n        self._hooks = {'accept': [], 'reject': [], 'fail': [], 'complete': []}\n        self.task = None\n\n        if len(args) == 2:\n            kwargs['title'] = args[0]\n            kwargs['url'] = args[1]\n            args = []\n\n        # Make sure constructor does not escape our __setitem__ enforcement\n        self.update(*args, **kwargs)\n\n    def trace(self, message, operation=None, plugin=None):\n        \"\"\"\n        Adds trace message to the entry which should contain useful information about why\n        plugin did not operate on entry. Accept and Reject messages are added to trace automatically.\n\n        :param string message: Message to add into entry trace.\n        :param string operation: None, reject, accept or fail\n        :param plugin: Uses task.current_plugin by default, pass value to override\n        \"\"\"\n        if operation not in (None, 'accept', 'reject', 'fail'):\n            raise ValueError('Unknown operation %s' % operation)\n        item = (plugin, operation, message)\n        if item not in self.traces:\n            self.traces.append(item)\n\n    def run_hooks(self, action, **kwargs):\n        \"\"\"\n        Run hooks that have been registered for given ``action``.\n\n        :param action: Name of action to run hooks for\n        :param kwargs: Keyword arguments that should be passed to the registered functions\n        \"\"\"\n        for func in self._hooks[action]:\n            func(self, **kwargs)\n\n    def add_hook(self, action, func, **kwargs):\n        \"\"\"\n        Add a hook for ``action`` to this entry.\n\n        :param string action: One of: 'accept', 'reject', 'fail', 'complete'\n        :param func: Function to execute when event occurs\n        :param kwargs: Keyword arguments that should be passed to ``func``\n        :raises: ValueError when given an invalid ``action``\n        \"\"\"\n        try:\n            self._hooks[action].append(functools.partial(func, **kwargs))\n        except KeyError:\n            raise ValueError('`%s` is not a valid entry action' % action)\n\n    def on_accept(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is accepted.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('accept', func, **kwargs)\n\n    def on_reject(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is rejected.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('reject', func, **kwargs)\n\n    def on_fail(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is failed.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('fail', func, **kwargs)\n\n    def on_complete(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when a :class:`Task` has finished processing this entry.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('complete', func, **kwargs)\n\n    def accept(self, reason=None, **kwargs):\n        if self.rejected:\n            log.debug('tried to accept rejected %r' % self)\n        elif not self.accepted:\n            self._state = 'accepted'\n            self.trace(reason, operation='accept')\n            # Run entry on_accept hooks\n            self.run_hooks('accept', reason=reason, **kwargs)\n\n    def reject(self, reason=None, **kwargs):\n        # ignore rejections on immortal entries\n        if self.get('immortal'):\n            reason_str = '(%s)' % reason if reason else ''\n            log.info('Tried to reject immortal %s %s' % (self['title'], reason_str))\n            self.trace('Tried to reject immortal %s' % reason_str)\n            return\n        if not self.rejected:\n            self._state = 'rejected'\n            self.trace(reason, operation='reject')\n            # Run entry on_reject hooks\n            self.run_hooks('reject', reason=reason, **kwargs)\n\n    def fail(self, reason=None, **kwargs):\n        log.debug('Marking entry \\'%s\\' as failed' % self['title'])\n        if not self.failed:\n            self._state = 'failed'\n            self.trace(reason, operation='fail')\n            log.error('Failed %s (%s)' % (self['title'], reason))\n            # Run entry on_fail hooks\n            self.run_hooks('fail', reason=reason, **kwargs)\n\n    def complete(self, **kwargs):\n        # Run entry on_complete hooks\n        self.run_hooks('complete', **kwargs)\n\n    @property\n    def state(self):\n        return self._state\n\n    @property\n    def accepted(self):\n        return self._state == 'accepted'\n\n    @property\n    def rejected(self):\n        return self._state == 'rejected'\n\n    @property\n    def failed(self):\n        return self._state == 'failed'\n\n    @property\n    def undecided(self):\n        return self._state == 'undecided'\n\n    def __setitem__(self, key, value):\n        # Enforce unicode compatibility.\n        if PY2 and isinstance(value, native_str):\n            # Allow Python 2's implicit string decoding, but fail now instead of when entry fields are used.\n            # If encoding is anything but ascii, it should be decoded it to text before setting an entry field\n            try:\n                value = value.decode('ascii')\n            except UnicodeDecodeError:\n                raise EntryUnicodeError(key, value)\n        elif isinstance(value, bytes):\n            raise EntryUnicodeError(key, value)\n\n        # url and original_url handling\n        if key == 'url':\n            if not isinstance(value, (str, LazyLookup)):\n                raise PluginError('Tried to set %r url to %r' % (self.get('title'), value))\n            self.setdefault('original_url', value)\n\n        # title handling\n        if key == 'title':\n            if not isinstance(value, (str, LazyLookup)):\n                raise PluginError('Tried to set title to %r' % value)\n\n        try:\n            log.trace('ENTRY SET: %s = %r' % (key, value))\n        except Exception as e:\n            log.debug('trying to debug key `%s` value threw exception: %s' % (key, e))\n\n        super(Entry, self).__setitem__(key, value)\n\n    def safe_str(self):\n        return '%s | %s' % (self['title'], self['url'])\n\n    # TODO: this is too manual, maybe we should somehow check this internally and throw some exception if\n    # application is trying to operate on invalid entry\n    def isvalid(self):\n        \"\"\"\n        :return: True if entry is valid. Return False if this cannot be used.\n        :rtype: bool\n        \"\"\"\n        if 'title' not in self:\n            return False\n        if 'url' not in self:\n            return False\n        if not isinstance(self['url'], str):\n            return False\n        if not isinstance(self['title'], str):\n            return False\n        return True\n\n    def take_snapshot(self, name):\n        \"\"\"\n        Takes a snapshot of the entry under *name*. Snapshots can be accessed via :attr:`.snapshots`.\n        :param string name: Snapshot name\n        \"\"\"\n        snapshot = {}\n        for field, value in self.items():\n            try:\n                snapshot[field] = copy.deepcopy(value)\n            except TypeError:\n                log.warning('Unable to take `%s` snapshot for field `%s` in `%s`' % (name, field, self['title']))\n        if snapshot:\n            if name in self.snapshots:\n                log.warning('Snapshot `%s` is being overwritten for `%s`' % (name, self['title']))\n            self.snapshots[name] = snapshot\n\n    def update_using_map(self, field_map, source_item, ignore_none=False):\n        \"\"\"\n        Populates entry fields from a source object using a dictionary that maps from entry field names to\n        attributes (or keys) in the source object.\n\n        :param dict field_map:\n          A dictionary mapping entry field names to the attribute in source_item (or keys,\n          if source_item is a dict)(nested attributes/dicts are also supported, separated by a dot,)\n          or a function that takes source_item as an argument\n        :param source_item:\n          Source of information to be used by the map\n        :param ignore_none:\n          Ignore any None values, do not record it to the Entry\n        \"\"\"\n        func = dict.get if isinstance(source_item, dict) else getattr\n        for field, value in field_map.items():\n            if isinstance(value, str):\n                v = functools.reduce(func, value.split('.'), source_item)\n            else:\n                v = value(source_item)\n            if ignore_none and v is None:\n                continue\n            self[field] = v\n\n    def render(self, template):\n        \"\"\"\n        Renders a template string based on fields in the entry.\n\n        :param string template: A template string that uses jinja2 or python string replacement format.\n        :return: The result of the rendering.\n        :rtype: string\n        :raises RenderError: If there is a problem.\n        \"\"\"\n        if not isinstance(template, str):\n            raise ValueError('Trying to render non string template, got %s' % repr(template))\n        log.trace('rendering: %s' % template)\n        return render_from_entry(template, self)\n\n    def __eq__(self, other):\n        return self.get('title') == other.get('title') and self.get('original_url') == other.get('original_url')\n\n    def __hash__(self):\n        return hash(self.get('title', '') + self.get('original_url', ''))\n\n    def __repr__(self):\n        return '<Entry(title=%s,state=%s)>' % (self['title'], self._state)\n", "path": "flexget/entry.py"}], "after_files": [{"content": "from __future__ import unicode_literals, division, absolute_import\nfrom builtins import *  # pylint: disable=unused-import, redefined-builtin\nfrom future.utils import PY2, native_str, text_type\n\nimport copy\nimport functools\nimport logging\n\nfrom flexget.plugin import PluginError\nfrom flexget.utils.lazy_dict import LazyDict, LazyLookup\nfrom flexget.utils.template import render_from_entry\n\nlog = logging.getLogger('entry')\n\n\nclass EntryUnicodeError(Exception):\n    \"\"\"This exception is thrown when trying to set non-unicode compatible field value to entry.\"\"\"\n\n    def __init__(self, key, value):\n        self.key = key\n        self.value = value\n\n    def __str__(self):\n        return 'Entry strings must be unicode: %s (%r)' % (self.key, self.value)\n\n\nclass Entry(LazyDict):\n    \"\"\"\n    Represents one item in task. Must have `url` and *title* fields.\n\n    Stores automatically *original_url* key, which is necessary because\n    plugins (eg. urlrewriters) may change *url* into something else\n    and otherwise that information would be lost.\n\n    Entry will also transparently convert all ascii strings into unicode\n    and raises :class:`EntryUnicodeError` if conversion fails on any value\n    being set. Such failures are caught by :class:`~flexget.task.Task`\n    and trigger :meth:`~flexget.task.Task.abort`.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super(Entry, self).__init__()\n        self.traces = []\n        self.snapshots = {}\n        self._state = 'undecided'\n        self._hooks = {'accept': [], 'reject': [], 'fail': [], 'complete': []}\n        self.task = None\n\n        if len(args) == 2:\n            kwargs['title'] = args[0]\n            kwargs['url'] = args[1]\n            args = []\n\n        # Make sure constructor does not escape our __setitem__ enforcement\n        self.update(*args, **kwargs)\n\n    def trace(self, message, operation=None, plugin=None):\n        \"\"\"\n        Adds trace message to the entry which should contain useful information about why\n        plugin did not operate on entry. Accept and Reject messages are added to trace automatically.\n\n        :param string message: Message to add into entry trace.\n        :param string operation: None, reject, accept or fail\n        :param plugin: Uses task.current_plugin by default, pass value to override\n        \"\"\"\n        if operation not in (None, 'accept', 'reject', 'fail'):\n            raise ValueError('Unknown operation %s' % operation)\n        item = (plugin, operation, message)\n        if item not in self.traces:\n            self.traces.append(item)\n\n    def run_hooks(self, action, **kwargs):\n        \"\"\"\n        Run hooks that have been registered for given ``action``.\n\n        :param action: Name of action to run hooks for\n        :param kwargs: Keyword arguments that should be passed to the registered functions\n        \"\"\"\n        for func in self._hooks[action]:\n            func(self, **kwargs)\n\n    def add_hook(self, action, func, **kwargs):\n        \"\"\"\n        Add a hook for ``action`` to this entry.\n\n        :param string action: One of: 'accept', 'reject', 'fail', 'complete'\n        :param func: Function to execute when event occurs\n        :param kwargs: Keyword arguments that should be passed to ``func``\n        :raises: ValueError when given an invalid ``action``\n        \"\"\"\n        try:\n            self._hooks[action].append(functools.partial(func, **kwargs))\n        except KeyError:\n            raise ValueError('`%s` is not a valid entry action' % action)\n\n    def on_accept(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is accepted.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('accept', func, **kwargs)\n\n    def on_reject(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is rejected.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('reject', func, **kwargs)\n\n    def on_fail(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when this entry is failed.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('fail', func, **kwargs)\n\n    def on_complete(self, func, **kwargs):\n        \"\"\"\n        Register a function to be called when a :class:`Task` has finished processing this entry.\n\n        :param func: The function to call\n        :param kwargs: Keyword arguments that should be passed to the registered function\n        \"\"\"\n        self.add_hook('complete', func, **kwargs)\n\n    def accept(self, reason=None, **kwargs):\n        if self.rejected:\n            log.debug('tried to accept rejected %r' % self)\n        elif not self.accepted:\n            self._state = 'accepted'\n            self.trace(reason, operation='accept')\n            # Run entry on_accept hooks\n            self.run_hooks('accept', reason=reason, **kwargs)\n\n    def reject(self, reason=None, **kwargs):\n        # ignore rejections on immortal entries\n        if self.get('immortal'):\n            reason_str = '(%s)' % reason if reason else ''\n            log.info('Tried to reject immortal %s %s' % (self['title'], reason_str))\n            self.trace('Tried to reject immortal %s' % reason_str)\n            return\n        if not self.rejected:\n            self._state = 'rejected'\n            self.trace(reason, operation='reject')\n            # Run entry on_reject hooks\n            self.run_hooks('reject', reason=reason, **kwargs)\n\n    def fail(self, reason=None, **kwargs):\n        log.debug('Marking entry \\'%s\\' as failed' % self['title'])\n        if not self.failed:\n            self._state = 'failed'\n            self.trace(reason, operation='fail')\n            log.error('Failed %s (%s)' % (self['title'], reason))\n            # Run entry on_fail hooks\n            self.run_hooks('fail', reason=reason, **kwargs)\n\n    def complete(self, **kwargs):\n        # Run entry on_complete hooks\n        self.run_hooks('complete', **kwargs)\n\n    @property\n    def state(self):\n        return self._state\n\n    @property\n    def accepted(self):\n        return self._state == 'accepted'\n\n    @property\n    def rejected(self):\n        return self._state == 'rejected'\n\n    @property\n    def failed(self):\n        return self._state == 'failed'\n\n    @property\n    def undecided(self):\n        return self._state == 'undecided'\n\n    def __setitem__(self, key, value):\n        # Enforce unicode compatibility.\n        if PY2 and isinstance(value, native_str):\n            # Allow Python 2's implicit string decoding, but fail now instead of when entry fields are used.\n            # If encoding is anything but ascii, it should be decoded it to text before setting an entry field\n            try:\n                value = value.decode('ascii')\n            except UnicodeDecodeError:\n                raise EntryUnicodeError(key, value)\n        elif isinstance(value, bytes):\n            raise EntryUnicodeError(key, value)\n        # Coerce any enriched strings (such as those returned by BeautifulSoup) to plain strings to avoid serialization\n        # troubles.\n        elif isinstance(value, text_type) and type(value) != text_type:  # pylint: disable=unidiomatic-typecheck\n            value = text_type(value)\n\n        # url and original_url handling\n        if key == 'url':\n            if not isinstance(value, (str, LazyLookup)):\n                raise PluginError('Tried to set %r url to %r' % (self.get('title'), value))\n            self.setdefault('original_url', value)\n\n        # title handling\n        if key == 'title':\n            if not isinstance(value, (str, LazyLookup)):\n                raise PluginError('Tried to set title to %r' % value)\n\n        try:\n            log.trace('ENTRY SET: %s = %r' % (key, value))\n        except Exception as e:\n            log.debug('trying to debug key `%s` value threw exception: %s' % (key, e))\n\n        super(Entry, self).__setitem__(key, value)\n\n    def safe_str(self):\n        return '%s | %s' % (self['title'], self['url'])\n\n    # TODO: this is too manual, maybe we should somehow check this internally and throw some exception if\n    # application is trying to operate on invalid entry\n    def isvalid(self):\n        \"\"\"\n        :return: True if entry is valid. Return False if this cannot be used.\n        :rtype: bool\n        \"\"\"\n        if 'title' not in self:\n            return False\n        if 'url' not in self:\n            return False\n        if not isinstance(self['url'], str):\n            return False\n        if not isinstance(self['title'], str):\n            return False\n        return True\n\n    def take_snapshot(self, name):\n        \"\"\"\n        Takes a snapshot of the entry under *name*. Snapshots can be accessed via :attr:`.snapshots`.\n        :param string name: Snapshot name\n        \"\"\"\n        snapshot = {}\n        for field, value in self.items():\n            try:\n                snapshot[field] = copy.deepcopy(value)\n            except TypeError:\n                log.warning('Unable to take `%s` snapshot for field `%s` in `%s`' % (name, field, self['title']))\n        if snapshot:\n            if name in self.snapshots:\n                log.warning('Snapshot `%s` is being overwritten for `%s`' % (name, self['title']))\n            self.snapshots[name] = snapshot\n\n    def update_using_map(self, field_map, source_item, ignore_none=False):\n        \"\"\"\n        Populates entry fields from a source object using a dictionary that maps from entry field names to\n        attributes (or keys) in the source object.\n\n        :param dict field_map:\n          A dictionary mapping entry field names to the attribute in source_item (or keys,\n          if source_item is a dict)(nested attributes/dicts are also supported, separated by a dot,)\n          or a function that takes source_item as an argument\n        :param source_item:\n          Source of information to be used by the map\n        :param ignore_none:\n          Ignore any None values, do not record it to the Entry\n        \"\"\"\n        func = dict.get if isinstance(source_item, dict) else getattr\n        for field, value in field_map.items():\n            if isinstance(value, str):\n                v = functools.reduce(func, value.split('.'), source_item)\n            else:\n                v = value(source_item)\n            if ignore_none and v is None:\n                continue\n            self[field] = v\n\n    def render(self, template):\n        \"\"\"\n        Renders a template string based on fields in the entry.\n\n        :param string template: A template string that uses jinja2 or python string replacement format.\n        :return: The result of the rendering.\n        :rtype: string\n        :raises RenderError: If there is a problem.\n        \"\"\"\n        if not isinstance(template, str):\n            raise ValueError('Trying to render non string template, got %s' % repr(template))\n        log.trace('rendering: %s' % template)\n        return render_from_entry(template, self)\n\n    def __eq__(self, other):\n        return self.get('title') == other.get('title') and self.get('original_url') == other.get('original_url')\n\n    def __hash__(self):\n        return hash(self.get('title', '') + self.get('original_url', ''))\n\n    def __repr__(self):\n        return '<Entry(title=%s,state=%s)>' % (self['title'], self._state)\n", "path": "flexget/entry.py"}]}