prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  [wikipedia] Queries with e.g. & do not work properly
  ````
  <dgw> ;w Miraculous Ladybug
  <Kaede> KeyError: u'extract' (file "/usr/local/lib/python2.7/dist-packages/sopel/modules/wikipedia.py",
          line 87, in mw_snippet)
  […]
  <dgw> https://en.wikipedia.org/wiki/Miraculous:_Tales_of_Ladybug_%26_Cat_Noir
  <Kaede> KeyError: u'extract' (file "/usr/local/lib/python2.7/dist-packages/sopel/modules/wikipedia.py",
          line 87, in mw_snippet)
  ````
  
  Basically, if a search returns a page title that contains `&`, e.g. "Miraculous: Tales of Ladybug & Cat Noir", the `wikipedia` module winds up trying to retrieve a snippet for, e.g., "Miraculous: Tales of Ladybug " and failing to do so because the page doesn't exist.
  
  I have a fix for this in testing, but wanted to open the issue so anyone searching for the problem would find out that it is already on the radar.
  Small fix for URLs with "+" in them
  
  
  --- 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/wikipedia.py`
  Content:
  ```
  1 # coding=utf-8
  2 # Copyright 2013 Elsie Powell - embolalia.com
  3 # Licensed under the Eiffel Forum License 2.
  4 from __future__ import unicode_literals, absolute_import, print_function, division
  5 from sopel import web, tools
  6 from sopel.config.types import StaticSection, ValidatedAttribute
  7 from sopel.module import NOLIMIT, commands, example, rule
  8 import json
  9 import re
  10 
  11 import sys
  12 if sys.version_info.major < 3:
  13     from urlparse import unquote as _unquote
  14     unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')
  15 else:
  16     from urllib.parse import unquote
  17 
  18 REDIRECT = re.compile(r'^REDIRECT (.*)')
  19 
  20 
  21 class WikipediaSection(StaticSection):
  22     default_lang = ValidatedAttribute('default_lang', default='en')
  23     """The default language to find articles from."""
  24     lang_per_channel = ValidatedAttribute('lang_per_channel')
  25 
  26 
  27 def setup(bot):
  28     bot.config.define_section('wikipedia', WikipediaSection)
  29 
  30     regex = re.compile('([a-z]+).(wikipedia.org/wiki/)([^ ]+)')
  31     if not bot.memory.contains('url_callbacks'):
  32         bot.memory['url_callbacks'] = tools.SopelMemory()
  33     bot.memory['url_callbacks'][regex] = mw_info
  34 
  35 
  36 def configure(config):
  37     config.define_section('wikipedia', WikipediaSection)
  38     config.wikipedia.configure_setting(
  39         'default_lang',
  40         "Enter the default language to find articles from."
  41     )
  42 
  43 
  44 def mw_search(server, query, num):
  45     """
  46     Searches the specified MediaWiki server for the given query, and returns
  47     the specified number of results.
  48     """
  49     search_url = ('http://%s/w/api.php?format=json&action=query'
  50                   '&list=search&srlimit=%d&srprop=timestamp&srwhat=text'
  51                   '&srsearch=') % (server, num)
  52     search_url += query
  53     query = json.loads(web.get(search_url))
  54     if 'query' in query:
  55         query = query['query']['search']
  56         return [r['title'] for r in query]
  57     else:
  58         return None
  59 
  60 
  61 def say_snippet(bot, server, query, show_url=True):
  62     page_name = query.replace('_', ' ')
  63     query = query.replace(' ', '_')
  64     try:
  65         snippet = mw_snippet(server, query)
  66     except KeyError:
  67         if show_url:
  68             bot.say("[WIKIPEDIA] Error fetching snippet for \"{}\".".format(page_name))
  69         return
  70     msg = '[WIKIPEDIA] {} | "{}"'.format(page_name, snippet)
  71     if show_url:
  72         msg = msg + ' | https://{}/wiki/{}'.format(server, query)
  73     bot.say(msg)
  74 
  75 
  76 def mw_snippet(server, query):
  77     """
  78     Retrives a snippet of the specified length from the given page on the given
  79     server.
  80     """
  81     snippet_url = ('https://' + server + '/w/api.php?format=json'
  82                    '&action=query&prop=extracts&exintro&explaintext'
  83                    '&exchars=300&redirects&titles=')
  84     snippet_url += query
  85     snippet = json.loads(web.get(snippet_url))
  86     snippet = snippet['query']['pages']
  87 
  88     # For some reason, the API gives the page *number* as the key, so we just
  89     # grab the first page number in the results.
  90     snippet = snippet[list(snippet.keys())[0]]
  91 
  92     return snippet['extract']
  93 
  94 
  95 @rule('.*\/([a-z]+\.wikipedia.org)\/wiki\/((?!File\:)[^ ]+).*')
  96 def mw_info(bot, trigger, found_match=None):
  97     """
  98     Retrives a snippet of the specified length from the given page on the given
  99     server.
  100     """
  101     match = found_match or trigger
  102     say_snippet(bot, match.group(1), unquote(match.group(2)), show_url=False)
  103 
  104 
  105 @commands('w', 'wiki', 'wik')
  106 @example('.w San Francisco')
  107 def wikipedia(bot, trigger):
  108     lang = bot.config.wikipedia.default_lang
  109 
  110     # change lang if channel has custom language set
  111     if (trigger.sender and not trigger.sender.is_nick() and
  112             bot.config.wikipedia.lang_per_channel):
  113         customlang = re.search('(' + trigger.sender + '):(\w+)',
  114                                bot.config.wikipedia.lang_per_channel)
  115         if customlang is not None:
  116             lang = customlang.group(2)
  117 
  118     if trigger.group(2) is None:
  119         bot.reply("What do you want me to look up?")
  120         return NOLIMIT
  121 
  122     query = trigger.group(2)
  123     args = re.search(r'^-([a-z]{2,12})\s(.*)', query)
  124     if args is not None:
  125         lang = args.group(1)
  126         query = args.group(2)
  127 
  128     if not query:
  129         bot.reply('What do you want me to look up?')
  130         return NOLIMIT
  131     server = lang + '.wikipedia.org'
  132     query = mw_search(server, query, 1)
  133     if not query:
  134         bot.reply("I can't find any results for that.")
  135         return NOLIMIT
  136     else:
  137         query = query[0]
  138     say_snippet(bot, server, query)
  139
  ```
  --- 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/wikipedia.py b/sopel/modules/wikipedia.py
  --- a/sopel/modules/wikipedia.py
  +++ b/sopel/modules/wikipedia.py
  @@ -10,10 +10,12 @@
   
   import sys
   if sys.version_info.major < 3:
  +    from urllib import quote as _quote
       from urlparse import unquote as _unquote
  +    quote = lambda s: _quote(s.encode('utf-8')).decode('utf-8')
       unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')
   else:
  -    from urllib.parse import unquote
  +    from urllib.parse import quote, unquote
   
   REDIRECT = re.compile(r'^REDIRECT (.*)')
   
  @@ -60,7 +62,7 @@
   
   def say_snippet(bot, server, query, show_url=True):
       page_name = query.replace('_', ' ')
  -    query = query.replace(' ', '_')
  +    query = quote(query.replace(' ', '_'))
       try:
           snippet = mw_snippet(server, query)
       except KeyError:
  
verification_info: {"golden_diff": "diff --git a/sopel/modules/wikipedia.py b/sopel/modules/wikipedia.py\n--- a/sopel/modules/wikipedia.py\n+++ b/sopel/modules/wikipedia.py\n@@ -10,10 +10,12 @@\n \n import sys\n if sys.version_info.major < 3:\n+    from urllib import quote as _quote\n     from urlparse import unquote as _unquote\n+    quote = lambda s: _quote(s.encode('utf-8')).decode('utf-8')\n     unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')\n else:\n-    from urllib.parse import unquote\n+    from urllib.parse import quote, unquote\n \n REDIRECT = re.compile(r'^REDIRECT (.*)')\n \n@@ -60,7 +62,7 @@\n \n def say_snippet(bot, server, query, show_url=True):\n     page_name = query.replace('_', ' ')\n-    query = query.replace(' ', '_')\n+    query = quote(query.replace(' ', '_'))\n     try:\n         snippet = mw_snippet(server, query)\n     except KeyError:\n", "issue": "[wikipedia] Queries with e.g. & do not work properly\n````\r\n<dgw> ;w Miraculous Ladybug\r\n<Kaede> KeyError: u'extract' (file \"/usr/local/lib/python2.7/dist-packages/sopel/modules/wikipedia.py\",\r\n        line 87, in mw_snippet)\r\n[\u2026]\r\n<dgw> https://en.wikipedia.org/wiki/Miraculous:_Tales_of_Ladybug_%26_Cat_Noir\r\n<Kaede> KeyError: u'extract' (file \"/usr/local/lib/python2.7/dist-packages/sopel/modules/wikipedia.py\",\r\n        line 87, in mw_snippet)\r\n````\r\n\r\nBasically, if a search returns a page title that contains `&`, e.g. \"Miraculous: Tales of Ladybug & Cat Noir\", the `wikipedia` module winds up trying to retrieve a snippet for, e.g., \"Miraculous: Tales of Ladybug \" and failing to do so because the page doesn't exist.\r\n\r\nI have a fix for this in testing, but wanted to open the issue so anyone searching for the problem would find out that it is already on the radar.\nSmall fix for URLs with \"+\" in them\n\n", "before_files": [{"content": "# coding=utf-8\n# Copyright 2013 Elsie Powell - embolalia.com\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\nfrom sopel import web, tools\nfrom sopel.config.types import StaticSection, ValidatedAttribute\nfrom sopel.module import NOLIMIT, commands, example, rule\nimport json\nimport re\n\nimport sys\nif sys.version_info.major < 3:\n    from urlparse import unquote as _unquote\n    unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')\nelse:\n    from urllib.parse import unquote\n\nREDIRECT = re.compile(r'^REDIRECT (.*)')\n\n\nclass WikipediaSection(StaticSection):\n    default_lang = ValidatedAttribute('default_lang', default='en')\n    \"\"\"The default language to find articles from.\"\"\"\n    lang_per_channel = ValidatedAttribute('lang_per_channel')\n\n\ndef setup(bot):\n    bot.config.define_section('wikipedia', WikipediaSection)\n\n    regex = re.compile('([a-z]+).(wikipedia.org/wiki/)([^ ]+)')\n    if not bot.memory.contains('url_callbacks'):\n        bot.memory['url_callbacks'] = tools.SopelMemory()\n    bot.memory['url_callbacks'][regex] = mw_info\n\n\ndef configure(config):\n    config.define_section('wikipedia', WikipediaSection)\n    config.wikipedia.configure_setting(\n        'default_lang',\n        \"Enter the default language to find articles from.\"\n    )\n\n\ndef mw_search(server, query, num):\n    \"\"\"\n    Searches the specified MediaWiki server for the given query, and returns\n    the specified number of results.\n    \"\"\"\n    search_url = ('http://%s/w/api.php?format=json&action=query'\n                  '&list=search&srlimit=%d&srprop=timestamp&srwhat=text'\n                  '&srsearch=') % (server, num)\n    search_url += query\n    query = json.loads(web.get(search_url))\n    if 'query' in query:\n        query = query['query']['search']\n        return [r['title'] for r in query]\n    else:\n        return None\n\n\ndef say_snippet(bot, server, query, show_url=True):\n    page_name = query.replace('_', ' ')\n    query = query.replace(' ', '_')\n    try:\n        snippet = mw_snippet(server, query)\n    except KeyError:\n        if show_url:\n            bot.say(\"[WIKIPEDIA] Error fetching snippet for \\\"{}\\\".\".format(page_name))\n        return\n    msg = '[WIKIPEDIA] {} | \"{}\"'.format(page_name, snippet)\n    if show_url:\n        msg = msg + ' | https://{}/wiki/{}'.format(server, query)\n    bot.say(msg)\n\n\ndef mw_snippet(server, query):\n    \"\"\"\n    Retrives a snippet of the specified length from the given page on the given\n    server.\n    \"\"\"\n    snippet_url = ('https://' + server + '/w/api.php?format=json'\n                   '&action=query&prop=extracts&exintro&explaintext'\n                   '&exchars=300&redirects&titles=')\n    snippet_url += query\n    snippet = json.loads(web.get(snippet_url))\n    snippet = snippet['query']['pages']\n\n    # For some reason, the API gives the page *number* as the key, so we just\n    # grab the first page number in the results.\n    snippet = snippet[list(snippet.keys())[0]]\n\n    return snippet['extract']\n\n\n@rule('.*\\/([a-z]+\\.wikipedia.org)\\/wiki\\/((?!File\\:)[^ ]+).*')\ndef mw_info(bot, trigger, found_match=None):\n    \"\"\"\n    Retrives a snippet of the specified length from the given page on the given\n    server.\n    \"\"\"\n    match = found_match or trigger\n    say_snippet(bot, match.group(1), unquote(match.group(2)), show_url=False)\n\n\n@commands('w', 'wiki', 'wik')\n@example('.w San Francisco')\ndef wikipedia(bot, trigger):\n    lang = bot.config.wikipedia.default_lang\n\n    # change lang if channel has custom language set\n    if (trigger.sender and not trigger.sender.is_nick() and\n            bot.config.wikipedia.lang_per_channel):\n        customlang = re.search('(' + trigger.sender + '):(\\w+)',\n                               bot.config.wikipedia.lang_per_channel)\n        if customlang is not None:\n            lang = customlang.group(2)\n\n    if trigger.group(2) is None:\n        bot.reply(\"What do you want me to look up?\")\n        return NOLIMIT\n\n    query = trigger.group(2)\n    args = re.search(r'^-([a-z]{2,12})\\s(.*)', query)\n    if args is not None:\n        lang = args.group(1)\n        query = args.group(2)\n\n    if not query:\n        bot.reply('What do you want me to look up?')\n        return NOLIMIT\n    server = lang + '.wikipedia.org'\n    query = mw_search(server, query, 1)\n    if not query:\n        bot.reply(\"I can't find any results for that.\")\n        return NOLIMIT\n    else:\n        query = query[0]\n    say_snippet(bot, server, query)\n", "path": "sopel/modules/wikipedia.py"}], "after_files": [{"content": "# coding=utf-8\n# Copyright 2013 Elsie Powell - embolalia.com\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\nfrom sopel import web, tools\nfrom sopel.config.types import StaticSection, ValidatedAttribute\nfrom sopel.module import NOLIMIT, commands, example, rule\nimport json\nimport re\n\nimport sys\nif sys.version_info.major < 3:\n    from urllib import quote as _quote\n    from urlparse import unquote as _unquote\n    quote = lambda s: _quote(s.encode('utf-8')).decode('utf-8')\n    unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')\nelse:\n    from urllib.parse import quote, unquote\n\nREDIRECT = re.compile(r'^REDIRECT (.*)')\n\n\nclass WikipediaSection(StaticSection):\n    default_lang = ValidatedAttribute('default_lang', default='en')\n    \"\"\"The default language to find articles from.\"\"\"\n    lang_per_channel = ValidatedAttribute('lang_per_channel')\n\n\ndef setup(bot):\n    bot.config.define_section('wikipedia', WikipediaSection)\n\n    regex = re.compile('([a-z]+).(wikipedia.org/wiki/)([^ ]+)')\n    if not bot.memory.contains('url_callbacks'):\n        bot.memory['url_callbacks'] = tools.SopelMemory()\n    bot.memory['url_callbacks'][regex] = mw_info\n\n\ndef configure(config):\n    config.define_section('wikipedia', WikipediaSection)\n    config.wikipedia.configure_setting(\n        'default_lang',\n        \"Enter the default language to find articles from.\"\n    )\n\n\ndef mw_search(server, query, num):\n    \"\"\"\n    Searches the specified MediaWiki server for the given query, and returns\n    the specified number of results.\n    \"\"\"\n    search_url = ('http://%s/w/api.php?format=json&action=query'\n                  '&list=search&srlimit=%d&srprop=timestamp&srwhat=text'\n                  '&srsearch=') % (server, num)\n    search_url += query\n    query = json.loads(web.get(search_url))\n    if 'query' in query:\n        query = query['query']['search']\n        return [r['title'] for r in query]\n    else:\n        return None\n\n\ndef say_snippet(bot, server, query, show_url=True):\n    page_name = query.replace('_', ' ')\n    query = quote(query.replace(' ', '_'))\n    try:\n        snippet = mw_snippet(server, query)\n    except KeyError:\n        if show_url:\n            bot.say(\"[WIKIPEDIA] Error fetching snippet for \\\"{}\\\".\".format(page_name))\n        return\n    msg = '[WIKIPEDIA] {} | \"{}\"'.format(page_name, snippet)\n    if show_url:\n        msg = msg + ' | https://{}/wiki/{}'.format(server, query)\n    bot.say(msg)\n\n\ndef mw_snippet(server, query):\n    \"\"\"\n    Retrives a snippet of the specified length from the given page on the given\n    server.\n    \"\"\"\n    snippet_url = ('https://' + server + '/w/api.php?format=json'\n                   '&action=query&prop=extracts&exintro&explaintext'\n                   '&exchars=300&redirects&titles=')\n    snippet_url += query\n    snippet = json.loads(web.get(snippet_url))\n    snippet = snippet['query']['pages']\n\n    # For some reason, the API gives the page *number* as the key, so we just\n    # grab the first page number in the results.\n    snippet = snippet[list(snippet.keys())[0]]\n\n    return snippet['extract']\n\n\n@rule('.*\\/([a-z]+\\.wikipedia.org)\\/wiki\\/((?!File\\:)[^ ]+).*')\ndef mw_info(bot, trigger, found_match=None):\n    \"\"\"\n    Retrives a snippet of the specified length from the given page on the given\n    server.\n    \"\"\"\n    match = found_match or trigger\n    say_snippet(bot, match.group(1), unquote(match.group(2)), show_url=False)\n\n\n@commands('w', 'wiki', 'wik')\n@example('.w San Francisco')\ndef wikipedia(bot, trigger):\n    lang = bot.config.wikipedia.default_lang\n\n    # change lang if channel has custom language set\n    if (trigger.sender and not trigger.sender.is_nick() and\n            bot.config.wikipedia.lang_per_channel):\n        customlang = re.search('(' + trigger.sender + '):(\\w+)',\n                               bot.config.wikipedia.lang_per_channel)\n        if customlang is not None:\n            lang = customlang.group(2)\n\n    if trigger.group(2) is None:\n        bot.reply(\"What do you want me to look up?\")\n        return NOLIMIT\n\n    query = trigger.group(2)\n    args = re.search(r'^-([a-z]{2,12})\\s(.*)', query)\n    if args is not None:\n        lang = args.group(1)\n        query = args.group(2)\n\n    if not query:\n        bot.reply('What do you want me to look up?')\n        return NOLIMIT\n    server = lang + '.wikipedia.org'\n    query = mw_search(server, query, 1)\n    if not query:\n        bot.reply(\"I can't find any results for that.\")\n        return NOLIMIT\n    else:\n        query = query[0]\n    say_snippet(bot, server, query)\n", "path": "sopel/modules/wikipedia.py"}]}