We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
search: Unicode URLs get mojibake'd under Python 2
Behold, the curse of bad Unicode handling:

````
23:33:27 <~dgw>  ;g grandorder.wiki chulainn alter
23:33:28 <&Kaede>  dgw: https://grandorder.wiki/CÃº_Chulainn_(Alter)
````

Versus the glory of Python 3:

````
01:00:10 <~dgw>  .g grandorder.wiki chulainn alter
01:00:11 <Sopel>  dgw: https://grandorder.wiki/Cú_Chulainn_(Alter)
````

Thanks to @prahulkgp for triggering this error, so I could find it. I've already added a test case using this exact query locally, both to verify the fix and to see that it doesn't break later. (Here's hoping it doesn't start bugging out Travis builds like the existing Bing tests sometimes do.)

--- 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/search.py`
Content:
```
1 # coding=utf-8
2 # Copyright 2008-9, Sean B. Palmer, inamidst.com
3 # Copyright 2012, Elsie Powell, embolalia.com
4 # Licensed under the Eiffel Forum License 2.
5 from __future__ import unicode_literals, absolute_import, print_function, division
6 
7 import re
8 from sopel import web
9 from sopel.module import commands, example
10 import requests
11 import xmltodict
12 import sys
13 
14 if sys.version_info.major < 3:
15     from urllib import quote_plus, unquote
16 else:
17     from urllib.parse import quote_plus, unquote
18 
19 
20 def formatnumber(n):
21     """Format a number with beautiful commas."""
22     parts = list(str(n))
23     for i in range((len(parts) - 3), 0, -3):
24         parts.insert(i, ',')
25     return ''.join(parts)
26 
27 
28 r_bing = re.compile(r'<h2(?: class=" b_topTitle")?><a href="([^"]+)"')
29 
30 
31 def bing_search(query, lang='en-US'):
32     base = 'https://www.bing.com/search?mkt=%s&q=' % lang
33     bytes = requests.get(base + query).text
34     m = r_bing.search(bytes)
35     if m:
36         return m.group(1)
37 
38 
39 r_duck = re.compile(r'nofollow" class="[^"]+" href="(?!(?:https?:\/\/r\.search\.yahoo)|(?:https?:\/\/duckduckgo\.com\/y\.js)(?:\/l\/\?kh=-1&amp;uddg=))(.*?)">')
40 
41 
42 def duck_search(query):
43     query = query.replace('!', '')
44     uri = 'https://duckduckgo.com/html/?q=%s&kl=us-en' % query
45     bytes = requests.get(uri, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'}).text
46     if 'web-result' in bytes:  # filter out the adds on top of the page
47         bytes = bytes.split('web-result')[1]
48     m = r_duck.search(bytes)
49     if m:
50         unquoted_m = unquote(m.group(1))
51         return web.decode(unquoted_m)
52 
53 
54 # Alias google_search to duck_search
55 google_search = duck_search
56 
57 
58 def duck_api(query):
59     if '!bang' in query.lower():
60         return 'https://duckduckgo.com/bang.html'
61 
62     # This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)
63     # It seems that duckduckgo api redirects to its Instant answer API html page
64     # if the query constains special charactares that aren't urlencoded.
65     # So in order to always get a JSON response back the query is urlencoded
66     query = quote_plus(query)
67     uri = 'https://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query
68     try:
69         results = requests.get(uri).json()
70     except ValueError:
71         return None
72     if results['Redirect']:
73         return results['Redirect']
74     else:
75         return None
76 
77 
78 @commands('duck', 'ddg', 'g')
79 @example('.duck sopel irc bot', r'https?:\/\/sopel\.chat\/?', re=True)
80 def duck(bot, trigger):
81     """Queries Duck Duck Go for the specified input."""
82     query = trigger.group(2)
83     if not query:
84         return bot.reply('.ddg what?')
85 
86     # If the API gives us something, say it and stop
87     result = duck_api(query)
88     if result:
89         bot.reply(result)
90         return
91 
92     # Otherwise, look it up on the HTMl version
93     uri = duck_search(query)
94 
95     if uri:
96         bot.reply(uri)
97         if 'last_seen_url' in bot.memory:
98             bot.memory['last_seen_url'][trigger.sender] = uri
99     else:
100         bot.reply("No results found for '%s'." % query)
101 
102 
103 @commands('bing')
104 @example('.bing sopel irc bot', r'https?:\/\/sopel\.chat\/?', re=True)
105 def bing(bot, trigger):
106     """Queries Bing for the specified input."""
107     if not trigger.group(2):
108         return bot.reply('.bing what?')
109     query = trigger.group(2)
110     result = bing_search(query)
111     if result:
112         bot.say(result)
113     else:
114         bot.reply("No results found for '%s'." % query)
115 
116 
117 @commands('search')
118 @example('.search sopel irc bot', r'(https?:\/\/sopel\.chat\/? \(b, d\)|https?:\/\/sopel\.chat\/? \(b\), https?:\/\/sopel\.chat\/? \(d\))', re=True)
119 def search(bot, trigger):
120     """Searches Bing and Duck Duck Go."""
121     if not trigger.group(2):
122         return bot.reply('.search for what?')
123     query = trigger.group(2)
124     bu = bing_search(query) or '-'
125     du = duck_search(query) or '-'
126 
127     if bu == du:
128         result = '%s (b, d)' % bu
129     else:
130         if len(bu) > 150:
131             bu = '(extremely long link)'
132         if len(du) > 150:
133             du = '(extremely long link)'
134         result = '%s (b), %s (d)' % (bu, du)
135 
136     bot.reply(result)
137 
138 
139 @commands('suggest')
140 @example('.suggest wikip', 'wikipedia')
141 @example('.suggest ', 'No query term.')
142 @example('.suggest lkashdfiauwgeaef', 'Sorry, no result.')
143 def suggest(bot, trigger):
144     """Suggest terms starting with given input"""
145     if not trigger.group(2):
146         return bot.reply("No query term.")
147     query = trigger.group(2)
148     # Using Google isn't necessarily ideal, but at most they'll be able to build
149     # a composite profile of all users on a given instance, not a profile of any
150     # single user. This can be switched out as soon as someone finds (or builds)
151     # an alternative suggestion API.
152     uri = 'https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q='
153     answer = xmltodict.parse(requests.get(uri + query.replace('+', '%2B')).text)['toplevel']
154     try:
155         answer = answer['CompleteSuggestion'][0]['suggestion']['@data']
156     except TypeError:
157         answer = None
158     if answer:
159         bot.say(answer)
160     else:
161         bot.reply('Sorry, no result.')
162
```
--- 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/search.py b/sopel/modules/search.py
--- a/sopel/modules/search.py
+++ b/sopel/modules/search.py
@@ -12,7 +12,8 @@
 import sys
 
 if sys.version_info.major < 3:
-    from urllib import quote_plus, unquote
+    from urllib import quote_plus, unquote as _unquote
+    unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')
 else:
     from urllib.parse import quote_plus, unquote
 
@@ -76,6 +77,9 @@
 
 
 @commands('duck', 'ddg', 'g')
+# test for bad Unicode handling in py2
+@example('.duck grandorder.wiki chulainn alter', 'https://grandorder.wiki/Cú_Chulainn_(Alter)')
+# the last example is what .help displays
 @example('.duck sopel irc bot', r'https?:\/\/sopel\.chat\/?', re=True)
 def duck(bot, trigger):
     """Queries Duck Duck Go for the specified input."""

{"golden_diff": "diff --git a/sopel/modules/search.py b/sopel/modules/search.py\n--- a/sopel/modules/search.py\n+++ b/sopel/modules/search.py\n@@ -12,7 +12,8 @@\n import sys\n \n if sys.version_info.major < 3:\n-    from urllib import quote_plus, unquote\n+    from urllib import quote_plus, unquote as _unquote\n+    unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')\n else:\n     from urllib.parse import quote_plus, unquote\n \n@@ -76,6 +77,9 @@\n \n \n @commands('duck', 'ddg', 'g')\n+# test for bad Unicode handling in py2\n+@example('.duck grandorder.wiki chulainn alter', 'https://grandorder.wiki/C\u00fa_Chulainn_(Alter)')\n+# the last example is what .help displays\n @example('.duck sopel irc bot', r'https?:\\/\\/sopel\\.chat\\/?', re=True)\n def duck(bot, trigger):\n     \"\"\"Queries Duck Duck Go for the specified input.\"\"\"\n", "issue": "search: Unicode URLs get mojibake'd under Python 2\nBehold, the curse of bad Unicode handling:\r\n\r\n````\r\n23:33:27 <~dgw>  ;g grandorder.wiki chulainn alter\r\n23:33:28 <&Kaede>  dgw: https://grandorder.wiki/C\u00c3\u00ba_Chulainn_(Alter)\r\n````\r\n\r\nVersus the glory of Python 3:\r\n\r\n````\r\n01:00:10 <~dgw>  .g grandorder.wiki chulainn alter\r\n01:00:11 <Sopel>  dgw: https://grandorder.wiki/C\u00fa_Chulainn_(Alter)\r\n````\r\n\r\nThanks to @prahulkgp for triggering this error, so I could find it. I've already added a test case using this exact query locally, both to verify the fix and to see that it doesn't break later. (Here's hoping it doesn't start bugging out Travis builds like the existing Bing tests sometimes do.)\n", "before_files": [{"content": "# coding=utf-8\n# Copyright 2008-9, Sean B. Palmer, inamidst.com\n# Copyright 2012, Elsie Powell, embolalia.com\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nfrom sopel import web\nfrom sopel.module import commands, example\nimport requests\nimport xmltodict\nimport sys\n\nif sys.version_info.major < 3:\n    from urllib import quote_plus, unquote\nelse:\n    from urllib.parse import quote_plus, unquote\n\n\ndef formatnumber(n):\n    \"\"\"Format a number with beautiful commas.\"\"\"\n    parts = list(str(n))\n    for i in range((len(parts) - 3), 0, -3):\n        parts.insert(i, ',')\n    return ''.join(parts)\n\n\nr_bing = re.compile(r'<h2(?: class=\" b_topTitle\")?><a href=\"([^\"]+)\"')\n\n\ndef bing_search(query, lang='en-US'):\n    base = 'https://www.bing.com/search?mkt=%s&q=' % lang\n    bytes = requests.get(base + query).text\n    m = r_bing.search(bytes)\n    if m:\n        return m.group(1)\n\n\nr_duck = re.compile(r'nofollow\" class=\"[^\"]+\" href=\"(?!(?:https?:\\/\\/r\\.search\\.yahoo)|(?:https?:\\/\\/duckduckgo\\.com\\/y\\.js)(?:\\/l\\/\\?kh=-1&amp;uddg=))(.*?)\">')\n\n\ndef duck_search(query):\n    query = query.replace('!', '')\n    uri = 'https://duckduckgo.com/html/?q=%s&kl=us-en' % query\n    bytes = requests.get(uri, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'}).text\n    if 'web-result' in bytes:  # filter out the adds on top of the page\n        bytes = bytes.split('web-result')[1]\n    m = r_duck.search(bytes)\n    if m:\n        unquoted_m = unquote(m.group(1))\n        return web.decode(unquoted_m)\n\n\n# Alias google_search to duck_search\ngoogle_search = duck_search\n\n\ndef duck_api(query):\n    if '!bang' in query.lower():\n        return 'https://duckduckgo.com/bang.html'\n\n    # This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)\n    # It seems that duckduckgo api redirects to its Instant answer API html page\n    # if the query constains special charactares that aren't urlencoded.\n    # So in order to always get a JSON response back the query is urlencoded\n    query = quote_plus(query)\n    uri = 'https://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query\n    try:\n        results = requests.get(uri).json()\n    except ValueError:\n        return None\n    if results['Redirect']:\n        return results['Redirect']\n    else:\n        return None\n\n\n@commands('duck', 'ddg', 'g')\n@example('.duck sopel irc bot', r'https?:\\/\\/sopel\\.chat\\/?', re=True)\ndef duck(bot, trigger):\n    \"\"\"Queries Duck Duck Go for the specified input.\"\"\"\n    query = trigger.group(2)\n    if not query:\n        return bot.reply('.ddg what?')\n\n    # If the API gives us something, say it and stop\n    result = duck_api(query)\n    if result:\n        bot.reply(result)\n        return\n\n    # Otherwise, look it up on the HTMl version\n    uri = duck_search(query)\n\n    if uri:\n        bot.reply(uri)\n        if 'last_seen_url' in bot.memory:\n            bot.memory['last_seen_url'][trigger.sender] = uri\n    else:\n        bot.reply(\"No results found for '%s'.\" % query)\n\n\n@commands('bing')\n@example('.bing sopel irc bot', r'https?:\\/\\/sopel\\.chat\\/?', re=True)\ndef bing(bot, trigger):\n    \"\"\"Queries Bing for the specified input.\"\"\"\n    if not trigger.group(2):\n        return bot.reply('.bing what?')\n    query = trigger.group(2)\n    result = bing_search(query)\n    if result:\n        bot.say(result)\n    else:\n        bot.reply(\"No results found for '%s'.\" % query)\n\n\n@commands('search')\n@example('.search sopel irc bot', r'(https?:\\/\\/sopel\\.chat\\/? \\(b, d\\)|https?:\\/\\/sopel\\.chat\\/? \\(b\\), https?:\\/\\/sopel\\.chat\\/? \\(d\\))', re=True)\ndef search(bot, trigger):\n    \"\"\"Searches Bing and Duck Duck Go.\"\"\"\n    if not trigger.group(2):\n        return bot.reply('.search for what?')\n    query = trigger.group(2)\n    bu = bing_search(query) or '-'\n    du = duck_search(query) or '-'\n\n    if bu == du:\n        result = '%s (b, d)' % bu\n    else:\n        if len(bu) > 150:\n            bu = '(extremely long link)'\n        if len(du) > 150:\n            du = '(extremely long link)'\n        result = '%s (b), %s (d)' % (bu, du)\n\n    bot.reply(result)\n\n\n@commands('suggest')\n@example('.suggest wikip', 'wikipedia')\n@example('.suggest ', 'No query term.')\n@example('.suggest lkashdfiauwgeaef', 'Sorry, no result.')\ndef suggest(bot, trigger):\n    \"\"\"Suggest terms starting with given input\"\"\"\n    if not trigger.group(2):\n        return bot.reply(\"No query term.\")\n    query = trigger.group(2)\n    # Using Google isn't necessarily ideal, but at most they'll be able to build\n    # a composite profile of all users on a given instance, not a profile of any\n    # single user. This can be switched out as soon as someone finds (or builds)\n    # an alternative suggestion API.\n    uri = 'https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q='\n    answer = xmltodict.parse(requests.get(uri + query.replace('+', '%2B')).text)['toplevel']\n    try:\n        answer = answer['CompleteSuggestion'][0]['suggestion']['@data']\n    except TypeError:\n        answer = None\n    if answer:\n        bot.say(answer)\n    else:\n        bot.reply('Sorry, no result.')\n", "path": "sopel/modules/search.py"}], "after_files": [{"content": "# coding=utf-8\n# Copyright 2008-9, Sean B. Palmer, inamidst.com\n# Copyright 2012, Elsie Powell, embolalia.com\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport re\nfrom sopel import web\nfrom sopel.module import commands, example\nimport requests\nimport xmltodict\nimport sys\n\nif sys.version_info.major < 3:\n    from urllib import quote_plus, unquote as _unquote\n    unquote = lambda s: _unquote(s.encode('utf-8')).decode('utf-8')\nelse:\n    from urllib.parse import quote_plus, unquote\n\n\ndef formatnumber(n):\n    \"\"\"Format a number with beautiful commas.\"\"\"\n    parts = list(str(n))\n    for i in range((len(parts) - 3), 0, -3):\n        parts.insert(i, ',')\n    return ''.join(parts)\n\n\nr_bing = re.compile(r'<h2(?: class=\" b_topTitle\")?><a href=\"([^\"]+)\"')\n\n\ndef bing_search(query, lang='en-US'):\n    base = 'https://www.bing.com/search?mkt=%s&q=' % lang\n    bytes = requests.get(base + query).text\n    m = r_bing.search(bytes)\n    if m:\n        return m.group(1)\n\n\nr_duck = re.compile(r'nofollow\" class=\"[^\"]+\" href=\"(?!(?:https?:\\/\\/r\\.search\\.yahoo)|(?:https?:\\/\\/duckduckgo\\.com\\/y\\.js)(?:\\/l\\/\\?kh=-1&amp;uddg=))(.*?)\">')\n\n\ndef duck_search(query):\n    query = query.replace('!', '')\n    uri = 'https://duckduckgo.com/html/?q=%s&kl=us-en' % query\n    bytes = requests.get(uri, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'}).text\n    if 'web-result' in bytes:  # filter out the adds on top of the page\n        bytes = bytes.split('web-result')[1]\n    m = r_duck.search(bytes)\n    if m:\n        unquoted_m = unquote(m.group(1))\n        return web.decode(unquoted_m)\n\n\n# Alias google_search to duck_search\ngoogle_search = duck_search\n\n\ndef duck_api(query):\n    if '!bang' in query.lower():\n        return 'https://duckduckgo.com/bang.html'\n\n    # This fixes issue #885 (https://github.com/sopel-irc/sopel/issues/885)\n    # It seems that duckduckgo api redirects to its Instant answer API html page\n    # if the query constains special charactares that aren't urlencoded.\n    # So in order to always get a JSON response back the query is urlencoded\n    query = quote_plus(query)\n    uri = 'https://api.duckduckgo.com/?q=%s&format=json&no_html=1&no_redirect=1' % query\n    try:\n        results = requests.get(uri).json()\n    except ValueError:\n        return None\n    if results['Redirect']:\n        return results['Redirect']\n    else:\n        return None\n\n\n@commands('duck', 'ddg', 'g')\n# test for bad Unicode handling in py2\n@example('.duck grandorder.wiki chulainn alter', 'https://grandorder.wiki/C\u00fa_Chulainn_(Alter)')\n# the last example is what .help displays\n@example('.duck sopel irc bot', r'https?:\\/\\/sopel\\.chat\\/?', re=True)\ndef duck(bot, trigger):\n    \"\"\"Queries Duck Duck Go for the specified input.\"\"\"\n    query = trigger.group(2)\n    if not query:\n        return bot.reply('.ddg what?')\n\n    # If the API gives us something, say it and stop\n    result = duck_api(query)\n    if result:\n        bot.reply(result)\n        return\n\n    # Otherwise, look it up on the HTMl version\n    uri = duck_search(query)\n\n    if uri:\n        bot.reply(uri)\n        if 'last_seen_url' in bot.memory:\n            bot.memory['last_seen_url'][trigger.sender] = uri\n    else:\n        bot.reply(\"No results found for '%s'.\" % query)\n\n\n@commands('bing')\n@example('.bing sopel irc bot', r'https?:\\/\\/sopel\\.chat\\/?', re=True)\ndef bing(bot, trigger):\n    \"\"\"Queries Bing for the specified input.\"\"\"\n    if not trigger.group(2):\n        return bot.reply('.bing what?')\n    query = trigger.group(2)\n    result = bing_search(query)\n    if result:\n        bot.say(result)\n    else:\n        bot.reply(\"No results found for '%s'.\" % query)\n\n\n@commands('search')\n@example('.search sopel irc bot', r'(https?:\\/\\/sopel\\.chat\\/? \\(b, d\\)|https?:\\/\\/sopel\\.chat\\/? \\(b\\), https?:\\/\\/sopel\\.chat\\/? \\(d\\))', re=True)\ndef search(bot, trigger):\n    \"\"\"Searches Bing and Duck Duck Go.\"\"\"\n    if not trigger.group(2):\n        return bot.reply('.search for what?')\n    query = trigger.group(2)\n    bu = bing_search(query) or '-'\n    du = duck_search(query) or '-'\n\n    if bu == du:\n        result = '%s (b, d)' % bu\n    else:\n        if len(bu) > 150:\n            bu = '(extremely long link)'\n        if len(du) > 150:\n            du = '(extremely long link)'\n        result = '%s (b), %s (d)' % (bu, du)\n\n    bot.reply(result)\n\n\n@commands('suggest')\n@example('.suggest wikip', 'wikipedia')\n@example('.suggest ', 'No query term.')\n@example('.suggest lkashdfiauwgeaef', 'Sorry, no result.')\ndef suggest(bot, trigger):\n    \"\"\"Suggest terms starting with given input\"\"\"\n    if not trigger.group(2):\n        return bot.reply(\"No query term.\")\n    query = trigger.group(2)\n    # Using Google isn't necessarily ideal, but at most they'll be able to build\n    # a composite profile of all users on a given instance, not a profile of any\n    # single user. This can be switched out as soon as someone finds (or builds)\n    # an alternative suggestion API.\n    uri = 'https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q='\n    answer = xmltodict.parse(requests.get(uri + query.replace('+', '%2B')).text)['toplevel']\n    try:\n        answer = answer['CompleteSuggestion'][0]['suggestion']['@data']\n    except TypeError:\n        answer = None\n    if answer:\n        bot.say(answer)\n    else:\n        bot.reply('Sorry, no result.')\n", "path": "sopel/modules/search.py"}]}