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()

```

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:

{"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"}]}