prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  [etymology] HTML entities not decoded before output
  The `etymology` module does not decode HTML entities in the snippet before sending it to the channel. This results in printing snippets like this:
  
  ````
  <Sopel> "Old English w&aelig;ter, from Proto-Germanic *watar (source also
          of Old Saxon watar, Old Frisian wetir, Dutch water, Old High German
          wazzar, German Wasser, Old Norse vatn, Gothic wato 'water'), from
          PIE *wod-or, from root *wed- (1) 'water, wet' (source also of
          Hittite [...]" - http://etymonline.com/?term=water
  ````
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `sopel/modules/etymology.py`
  Content:
  ```
  1 # coding=utf-8
  2 """
  3 etymology.py - Sopel Etymology Module
  4 Copyright 2007-9, Sean B. Palmer, inamidst.com
  5 Licensed under the Eiffel Forum License 2.
  6 
  7 http://sopel.chat
  8 """
  9 from __future__ import unicode_literals, absolute_import, print_function, division
  10 
  11 import re
  12 from sopel import web
  13 from sopel.module import commands, example, NOLIMIT
  14 
  15 etyuri = 'http://etymonline.com/?term=%s'
  16 etysearch = 'http://etymonline.com/?search=%s'
  17 
  18 r_definition = re.compile(r'(?ims)<dd[^>]*>.*?</dd>')
  19 r_tag = re.compile(r'<(?!!)[^>]+>')
  20 r_whitespace = re.compile(r'[\t\r\n ]+')
  21 
  22 abbrs = [
  23     'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp',
  24     'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk',
  25     '19c', '18c', '17c', '16c', 'St', 'Capt', 'obs', 'Jan', 'Feb', 'Mar',
  26     'Apr', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'c', 'tr', 'e', 'g'
  27 ]
  28 t_sentence = r'^.*?(?<!%s)(?:\.(?= [A-Z0-9]|\Z)|\Z)'
  29 r_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))
  30 
  31 
  32 def unescape(s):
  33     s = s.replace('&gt;', '>')
  34     s = s.replace('&lt;', '<')
  35     s = s.replace('&amp;', '&')
  36     return s
  37 
  38 
  39 def text(html):
  40     html = r_tag.sub('', html)
  41     html = r_whitespace.sub(' ', html)
  42     return unescape(html).strip()
  43 
  44 
  45 def etymology(word):
  46     # @@ <nsh> sbp, would it be possible to have a flag for .ety to get 2nd/etc
  47     # entries? - http://swhack.com/logs/2006-07-19#T15-05-29
  48 
  49     if len(word) > 25:
  50         raise ValueError("Word too long: %s[...]" % word[:10])
  51     word = {'axe': 'ax/axe'}.get(word, word)
  52 
  53     bytes = web.get(etyuri % word)
  54     definitions = r_definition.findall(bytes)
  55 
  56     if not definitions:
  57         return None
  58 
  59     defn = text(definitions[0])
  60     m = r_sentence.match(defn)
  61     if not m:
  62         return None
  63     sentence = m.group(0)
  64 
  65     maxlength = 275
  66     if len(sentence) > maxlength:
  67         sentence = sentence[:maxlength]
  68         words = sentence[:-5].split(' ')
  69         words.pop()
  70         sentence = ' '.join(words) + ' [...]'
  71 
  72     sentence = '"' + sentence.replace('"', "'") + '"'
  73     return sentence + ' - ' + (etyuri % word)
  74 
  75 
  76 @commands('ety')
  77 @example('.ety word')
  78 def f_etymology(bot, trigger):
  79     """Look up the etymology of a word"""
  80     word = trigger.group(2)
  81 
  82     try:
  83         result = etymology(word)
  84     except IOError:
  85         msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
  86         bot.msg(trigger.sender, msg)
  87         return NOLIMIT
  88     except (AttributeError, TypeError):
  89         result = None
  90 
  91     if result is not None:
  92         bot.msg(trigger.sender, result)
  93     else:
  94         uri = etysearch % word
  95         msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
  96         bot.msg(trigger.sender, msg)
  97         return NOLIMIT
  98
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/sopel/modules/etymology.py b/sopel/modules/etymology.py
  --- a/sopel/modules/etymology.py
  +++ b/sopel/modules/etymology.py
  @@ -8,6 +8,14 @@
   """
   from __future__ import unicode_literals, absolute_import, print_function, division
   
  +try:
  +    from html import unescape
  +except ImportError:
  +    from HTMLParser import HTMLParser
  +
  +    # pep8 dictates a blank line here...
  +    def unescape(s):
  +        return HTMLParser.unescape.__func__(HTMLParser, s)
   import re
   from sopel import web
   from sopel.module import commands, example, NOLIMIT
  @@ -29,13 +37,6 @@
   r_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))
   
   
  -def unescape(s):
  -    s = s.replace('&gt;', '>')
  -    s = s.replace('&lt;', '<')
  -    s = s.replace('&amp;', '&')
  -    return s
  -
  -
   def text(html):
       html = r_tag.sub('', html)
       html = r_whitespace.sub(' ', html)
  
verification_info: {"golden_diff": "diff --git a/sopel/modules/etymology.py b/sopel/modules/etymology.py\n--- a/sopel/modules/etymology.py\n+++ b/sopel/modules/etymology.py\n@@ -8,6 +8,14 @@\n \"\"\"\n from __future__ import unicode_literals, absolute_import, print_function, division\n \n+try:\n+    from html import unescape\n+except ImportError:\n+    from HTMLParser import HTMLParser\n+\n+    # pep8 dictates a blank line here...\n+    def unescape(s):\n+        return HTMLParser.unescape.__func__(HTMLParser, s)\n import re\n from sopel import web\n from sopel.module import commands, example, NOLIMIT\n@@ -29,13 +37,6 @@\n r_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))\n \n \n-def unescape(s):\n-    s = s.replace('&gt;', '>')\n-    s = s.replace('&lt;', '<')\n-    s = s.replace('&amp;', '&')\n-    return s\n-\n-\n def text(html):\n     html = r_tag.sub('', html)\n     html = r_whitespace.sub(' ', html)\n", "issue": "[etymology] HTML entities not decoded before output\nThe `etymology` module does not decode HTML entities in the snippet before sending it to the channel. This results in printing snippets like this:\r\n\r\n````\r\n<Sopel> \"Old English w&aelig;ter, from Proto-Germanic *watar (source also\r\n        of Old Saxon watar, Old Frisian wetir, Dutch water, Old High German\r\n        wazzar, German Wasser, Old Norse vatn, Gothic wato 'water'), from\r\n        PIE *wod-or, from root *wed- (1) 'water, wet' (source also of\r\n        Hittite [...]\" -\u00a0http://etymonline.com/?term=water\r\n````\n", "before_files": [{"content": "# coding=utf-8\n\"\"\"\netymology.py - Sopel Etymology Module\nCopyright 2007-9, Sean B. Palmer, inamidst.com\nLicensed under the Eiffel Forum License 2.\n\nhttp://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nfrom sopel import web\nfrom sopel.module import commands, example, NOLIMIT\n\netyuri = 'http://etymonline.com/?term=%s'\netysearch = 'http://etymonline.com/?search=%s'\n\nr_definition = re.compile(r'(?ims)<dd[^>]*>.*?</dd>')\nr_tag = re.compile(r'<(?!!)[^>]+>')\nr_whitespace = re.compile(r'[\\t\\r\\n ]+')\n\nabbrs = [\n    'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp',\n    'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk',\n    '19c', '18c', '17c', '16c', 'St', 'Capt', 'obs', 'Jan', 'Feb', 'Mar',\n    'Apr', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'c', 'tr', 'e', 'g'\n]\nt_sentence = r'^.*?(?<!%s)(?:\\.(?= [A-Z0-9]|\\Z)|\\Z)'\nr_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))\n\n\ndef unescape(s):\n    s = s.replace('&gt;', '>')\n    s = s.replace('&lt;', '<')\n    s = s.replace('&amp;', '&')\n    return s\n\n\ndef text(html):\n    html = r_tag.sub('', html)\n    html = r_whitespace.sub(' ', html)\n    return unescape(html).strip()\n\n\ndef etymology(word):\n    # @@ <nsh> sbp, would it be possible to have a flag for .ety to get 2nd/etc\n    # entries? - http://swhack.com/logs/2006-07-19#T15-05-29\n\n    if len(word) > 25:\n        raise ValueError(\"Word too long: %s[...]\" % word[:10])\n    word = {'axe': 'ax/axe'}.get(word, word)\n\n    bytes = web.get(etyuri % word)\n    definitions = r_definition.findall(bytes)\n\n    if not definitions:\n        return None\n\n    defn = text(definitions[0])\n    m = r_sentence.match(defn)\n    if not m:\n        return None\n    sentence = m.group(0)\n\n    maxlength = 275\n    if len(sentence) > maxlength:\n        sentence = sentence[:maxlength]\n        words = sentence[:-5].split(' ')\n        words.pop()\n        sentence = ' '.join(words) + ' [...]'\n\n    sentence = '\"' + sentence.replace('\"', \"'\") + '\"'\n    return sentence + ' - ' + (etyuri % word)\n\n\n@commands('ety')\n@example('.ety word')\ndef f_etymology(bot, trigger):\n    \"\"\"Look up the etymology of a word\"\"\"\n    word = trigger.group(2)\n\n    try:\n        result = etymology(word)\n    except IOError:\n        msg = \"Can't connect to etymonline.com (%s)\" % (etyuri % word)\n        bot.msg(trigger.sender, msg)\n        return NOLIMIT\n    except (AttributeError, TypeError):\n        result = None\n\n    if result is not None:\n        bot.msg(trigger.sender, result)\n    else:\n        uri = etysearch % word\n        msg = 'Can\\'t find the etymology for \"%s\". Try %s' % (word, uri)\n        bot.msg(trigger.sender, msg)\n        return NOLIMIT\n", "path": "sopel/modules/etymology.py"}], "after_files": [{"content": "# coding=utf-8\n\"\"\"\netymology.py - Sopel Etymology Module\nCopyright 2007-9, Sean B. Palmer, inamidst.com\nLicensed under the Eiffel Forum License 2.\n\nhttp://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\ntry:\n    from html import unescape\nexcept ImportError:\n    from HTMLParser import HTMLParser\n\n    # pep8 dictates a blank line here...\n    def unescape(s):\n        return HTMLParser.unescape.__func__(HTMLParser, s)\nimport re\nfrom sopel import web\nfrom sopel.module import commands, example, NOLIMIT\n\netyuri = 'http://etymonline.com/?term=%s'\netysearch = 'http://etymonline.com/?search=%s'\n\nr_definition = re.compile(r'(?ims)<dd[^>]*>.*?</dd>')\nr_tag = re.compile(r'<(?!!)[^>]+>')\nr_whitespace = re.compile(r'[\\t\\r\\n ]+')\n\nabbrs = [\n    'cf', 'lit', 'etc', 'Ger', 'Du', 'Skt', 'Rus', 'Eng', 'Amer.Eng', 'Sp',\n    'Fr', 'N', 'E', 'S', 'W', 'L', 'Gen', 'J.C', 'dial', 'Gk',\n    '19c', '18c', '17c', '16c', 'St', 'Capt', 'obs', 'Jan', 'Feb', 'Mar',\n    'Apr', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'c', 'tr', 'e', 'g'\n]\nt_sentence = r'^.*?(?<!%s)(?:\\.(?= [A-Z0-9]|\\Z)|\\Z)'\nr_sentence = re.compile(t_sentence % ')(?<!'.join(abbrs))\n\n\ndef text(html):\n    html = r_tag.sub('', html)\n    html = r_whitespace.sub(' ', html)\n    return unescape(html).strip()\n\n\ndef etymology(word):\n    # @@ <nsh> sbp, would it be possible to have a flag for .ety to get 2nd/etc\n    # entries? - http://swhack.com/logs/2006-07-19#T15-05-29\n\n    if len(word) > 25:\n        raise ValueError(\"Word too long: %s[...]\" % word[:10])\n    word = {'axe': 'ax/axe'}.get(word, word)\n\n    bytes = web.get(etyuri % word)\n    definitions = r_definition.findall(bytes)\n\n    if not definitions:\n        return None\n\n    defn = text(definitions[0])\n    m = r_sentence.match(defn)\n    if not m:\n        return None\n    sentence = m.group(0)\n\n    maxlength = 275\n    if len(sentence) > maxlength:\n        sentence = sentence[:maxlength]\n        words = sentence[:-5].split(' ')\n        words.pop()\n        sentence = ' '.join(words) + ' [...]'\n\n    sentence = '\"' + sentence.replace('\"', \"'\") + '\"'\n    return sentence + ' - ' + (etyuri % word)\n\n\n@commands('ety')\n@example('.ety word')\ndef f_etymology(bot, trigger):\n    \"\"\"Look up the etymology of a word\"\"\"\n    word = trigger.group(2)\n\n    try:\n        result = etymology(word)\n    except IOError:\n        msg = \"Can't connect to etymonline.com (%s)\" % (etyuri % word)\n        bot.msg(trigger.sender, msg)\n        return NOLIMIT\n    except (AttributeError, TypeError):\n        result = None\n\n    if result is not None:\n        bot.msg(trigger.sender, result)\n    else:\n        uri = etysearch % word\n        msg = 'Can\\'t find the etymology for \"%s\". Try %s' % (word, uri)\n        bot.msg(trigger.sender, msg)\n        return NOLIMIT\n", "path": "sopel/modules/etymology.py"}]}