We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
/openlibrary/openlibrary/templates/lists/widget.html: error in processing template: TypeError: Object of type Nothing is not JSON serializable (falling back to default template)
Patron is reporting the following error:
`/openlibrary/openlibrary/templates/lists/widget.html: error in processing template: TypeError: Object of type Nothing is not JSON serializable (falling back to default template)`

### Evidence / Screenshot (if possible)

### Relevant url?
https://openlibrary.org/works/OL24171550W/Kelebihan_Amalan_Bulan_Rejab_Sya%E2%80%99ban_Ramadhan

### Steps to Reproduce
<!-- What steps caused you to find the bug? -->
1. Go to ... https://openlibrary.org/works/OL24171550W/Kelebihan_Amalan_Bulan_Rejab_Sya%E2%80%99ban_Ramadhan
2. Do ... view error.

<!-- What actually happened after these steps? What did you expect to happen? -->
* Actual: 
* Expected: 

### Details

- **Logged in (Y/N)?** 
- **Browser type/version?** 
- **Operating system?** 
- **Environment (prod/dev/local)?** prod
<!-- If not sure, put prod -->

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->

### Related files
<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->

### Stakeholders
<!-- @ tag stakeholders of this bug -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `openlibrary/core/helpers.py`
Content:
```
1 """Generic helper functions to use in the templates and the webapp.
2 """
3 import web
4 from datetime import datetime
5 import re
6 
7 import six
8 from six.moves.urllib.parse import urlsplit
9 
10 if six.PY2:  # See #4525 json.dump(indent) MUST be an int on PY2
11     import simplejson as json
12 else:
13     import json
14 
15 import babel
16 import babel.core
17 import babel.dates
18 import babel.numbers
19 
20 try:
21     import genshi
22     import genshi.filters
23 except ImportError:
24     genshi = None
25 
26 try:
27     from bs4 import BeautifulSoup
28 except ImportError:
29     BeautifulSoup = None
30 
31 from infogami import config
32 
33 # handy utility to parse ISO date strings
34 from infogami.infobase.utils import parse_datetime
35 from infogami.utils.view import safeint
36 
37 # TODO: i18n should be moved to core or infogami
38 from openlibrary.i18n import gettext as _  # noqa: F401
39 
40 __all__ = [
41     "sanitize",
42     "json_encode",
43     "safesort",
44     "days_since", "datestr", "format_date",
45     "sprintf", "cond", "commify", "truncate", "datetimestr_utc",
46     "urlsafe", "texsafe",
47     "percentage", "affiliate_id", "bookreader_host",
48     "private_collections", "private_collection_in",
49 
50     # functions imported from elsewhere
51     "parse_datetime", "safeint"
52 ]
53 __docformat__ = "restructuredtext en"
54 
55 def sanitize(html, encoding='utf8'):
56     """Removes unsafe tags and attributes from html and adds
57     ``rel="nofollow"`` attribute to all external links.
58     Using encoding=None if passing unicode strings e.g. for Python 3.
59     encoding="utf8" matches default format for earlier versions of Genshi
60     https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version
61     """
62 
63     # Can't sanitize unless genshi module is available
64     if genshi is None:
65         return html
66 
67     def get_nofollow(name, event):
68         attrs = event[1][1]
69         href = attrs.get('href', '')
70 
71         if href:
72             # add rel=nofollow to all absolute links
73             _, host, _, _, _ = urlsplit(href)
74             if host:
75                 return 'nofollow'
76 
77     try:
78         html = genshi.HTML(html, encoding=encoding)
79 
80     # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:
81     # don't catch Unicode errors so we can tell if we're getting bytes
82     except genshi.ParseError:
83         if BeautifulSoup:
84             # Bad html. Tidy it up using BeautifulSoup
85             html = str(BeautifulSoup(html, "lxml"))
86             try:
87                 html = genshi.HTML(html)
88             except Exception:
89                 # Failed to sanitize.
90                 # We can't do any better than returning the original HTML, without sanitizing.
91                 return html
92         else:
93             raise
94 
95     stream = html \
96         | genshi.filters.HTMLSanitizer() \
97         | genshi.filters.Transformer("//a").attr("rel", get_nofollow)
98     return stream.render()
99 
100 
101 def json_encode(d, **kw):
102     """Same as json.dumps.
103     """
104     return json.dumps(d, **kw)
105 
106 
107 def safesort(iterable, key=None, reverse=False):
108     """Sorts heterogeneous of objects without raising errors.
109 
110     Sorting heterogeneous objects sometimes causes error. For example,
111     datetime and Nones don't go well together. This function takes special
112     care to make that work.
113     """
114     key = key or (lambda x: x)
115     def safekey(x):
116         k = key(x)
117         return (k.__class__.__name__, k)
118     return sorted(iterable, key=safekey, reverse=reverse)
119 
120 
121 def days_since(then, now=None):
122     delta = then - (now or datetime.now())
123     return abs(delta.days)
124 
125 
126 def datestr(then, now=None, lang=None, relative=True):
127     """Internationalized version of web.datestr."""
128     lang = lang or web.ctx.get('lang') or "en"
129     if relative:
130         if now is None:
131             now = datetime.now()
132         delta = then - now
133         if abs(delta.days) < 4: # Threshold from web.py
134             return babel.dates.format_timedelta(delta,
135                                                 add_direction=True,
136                                                 locale=_get_babel_locale(lang))
137     return format_date(then, lang=lang)
138 
139 
140 def datetimestr_utc(then):
141     return then.strftime("%Y-%m-%dT%H:%M:%SZ")
142 
143 def format_date(date, lang=None):
144     lang = lang or web.ctx.get('lang') or "en"
145     locale = _get_babel_locale(lang)
146     return babel.dates.format_date(date, format="long", locale=locale)
147 
148 def _get_babel_locale(lang):
149     try:
150         return babel.Locale(lang)
151     except babel.core.UnknownLocaleError:
152         return babel.Locale("en")
153 
154 
155 def sprintf(s, *a, **kw):
156     """Handy utility for string replacements.
157 
158         >>> sprintf('hello %s', 'python')
159         'hello python'
160         >>> sprintf('hello %(name)s', name='python')
161         'hello python'
162     """
163     args = kw or a
164     if args:
165         return s % args
166     else:
167         return s
168 
169 
170 def cond(pred, true_value, false_value=""):
171     """Lisp style cond function.
172 
173     Hanly to use instead of if-else expression.
174     """
175     if pred:
176         return true_value
177     else:
178         return false_value
179 
180 
181 def commify(number, lang=None):
182     """localized version of web.commify"""
183     try:
184         lang = lang or web.ctx.get("lang") or "en"
185         return babel.numbers.format_number(int(number), lang)
186     except:
187         return six.text_type(number)
188 
189 
190 def truncate(text, limit):
191     """Truncate text and add ellipses if it longer than specified limit."""
192     if not text:
193         return ''
194     if len(text) <= limit:
195         return text
196     return text[:limit] + "..."
197 
198 
199 def urlsafe(path):
200     """Replaces the unsafe chars from path with underscores.
201     """
202     return _get_safepath_re().sub('_', path).strip('_')[:100]
203 
204 @web.memoize
205 def _get_safepath_re():
206     """Make regular expression that matches all unsafe chars."""
207     # unsafe chars according to RFC 2396
208     reserved = ";/?:@&=+$,"
209     delims = '<>#%"'
210     unwise = "{}|\\^[]`"
211     space = ' \n\r'
212 
213     unsafe = reserved + delims + unwise + space
214     pattern = '[%s]+' % "".join(re.escape(c) for c in unsafe)
215     return re.compile(pattern)
216 
217 
218 def get_coverstore_url():
219     """Returns the base url of coverstore by looking at the config."""
220     return config.get('coverstore_url', 'https://covers.openlibrary.org').rstrip('/')
221 
222 
223 _texsafe_map = {
224     '"': r'\textquotedbl{}',
225     '#': r'\#',
226     '$': r'\$',
227     '%': r'\%',
228     '&': r'\&',
229     '<': r'\textless{}',
230     '>': r'\textgreater{}',
231     '\\': r'\textbackslash{}',
232     '^': r'\^{}',
233     '_': r'\_{}',
234     '{': r'\{',
235     '}': r'\}',
236     '|': r'\textbar{}',
237     '~': r'\~{}',
238 }
239 
240 _texsafe_re = None
241 
242 def texsafe(text):
243     """Escapes the special characters in the given text for using it in tex type setting.
244 
245     Tex (or Latex) uses some characters in the ascii character range for
246     special notations. These characters must be escaped when occur in the
247     regular text. This function escapes those special characters.
248 
249     The list of special characters and the latex command to typeset them can
250     be found in `The Comprehensive LaTeX Symbol List`_.
251 
252     .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf
253     """
254     global _texsafe_re
255     if _texsafe_re is None:
256         pattern = "[%s]" % re.escape("".join(list(_texsafe_map)))
257         _texsafe_re = re.compile(pattern)
258 
259     return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)
260 
261 def percentage(value, total):
262     """Computes percentage.
263 
264         >>> percentage(1, 10)
265         10.0
266         >>> percentage(0, 0)
267         0.0
268     """
269     return (value * 100.0) / total if total else 0.0
270 
271 def uniq(values, key=None):
272     """Returns the unique entries from the given values in the original order.
273 
274     The value of the optional `key` parameter should be a function that takes
275     a single argument and returns a key to test the uniqueness.
276     """
277     key = key or (lambda x: x)
278     s = set()
279     result = []
280     for v in values:
281         k = key(v)
282         if k not in s:
283             s.add(k)
284             result.append(v)
285     return result
286 
287 def affiliate_id(affiliate):
288     return config.get('affiliate_ids', {}).get(affiliate, '')
289 
290 def bookreader_host():
291     return config.get('bookreader_host', '')
292 
293 def private_collections():
294     """Collections which are lendable but should not be linked from OL
295     TODO: Remove when we can handle institutional books"""
296     return ['georgetown-university-law-library-rr']
297 
298 def private_collection_in(collections):
299     return any(x in private_collections() for x in collections)
300 
301 def _get_helpers():
302     _globals = globals()
303     return web.storage((k, _globals[k]) for k in __all__)
304 
305 
306 ## This must be at the end of this module
307 helpers = _get_helpers()
308
```
--- 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/openlibrary/core/helpers.py b/openlibrary/core/helpers.py
--- a/openlibrary/core/helpers.py
+++ b/openlibrary/core/helpers.py
@@ -101,7 +101,7 @@
 def json_encode(d, **kw):
     """Same as json.dumps.
     """
-    return json.dumps(d, **kw)
+    return json.dumps(d or {}, **kw)
 
 
 def safesort(iterable, key=None, reverse=False):

{"golden_diff": "diff --git a/openlibrary/core/helpers.py b/openlibrary/core/helpers.py\n--- a/openlibrary/core/helpers.py\n+++ b/openlibrary/core/helpers.py\n@@ -101,7 +101,7 @@\n def json_encode(d, **kw):\n     \"\"\"Same as json.dumps.\n     \"\"\"\n-    return json.dumps(d, **kw)\n+    return json.dumps(d or {}, **kw)\n \n \n def safesort(iterable, key=None, reverse=False):\n", "issue": "/openlibrary/openlibrary/templates/lists/widget.html: error in processing template: TypeError: Object of type Nothing is not JSON serializable (falling back to default template)\nPatron is reporting the following error:\r\n`/openlibrary/openlibrary/templates/lists/widget.html: error in processing template: TypeError: Object of type Nothing is not JSON serializable (falling back to default template)`\r\n\r\n### Evidence / Screenshot (if possible)\r\n\r\n### Relevant url?\r\nhttps://openlibrary.org/works/OL24171550W/Kelebihan_Amalan_Bulan_Rejab_Sya%E2%80%99ban_Ramadhan\r\n\r\n### Steps to Reproduce\r\n<!-- What steps caused you to find the bug? -->\r\n1. Go to ... https://openlibrary.org/works/OL24171550W/Kelebihan_Amalan_Bulan_Rejab_Sya%E2%80%99ban_Ramadhan\r\n2. Do ... view error.\r\n\r\n<!-- What actually happened after these steps? What did you expect to happen? -->\r\n* Actual: \r\n* Expected: \r\n\r\n### Details\r\n\r\n- **Logged in (Y/N)?** \r\n- **Browser type/version?** \r\n- **Operating system?** \r\n- **Environment (prod/dev/local)?** prod\r\n<!-- If not sure, put prod -->\r\n\r\n### Proposal & Constraints\r\n<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->\r\n\r\n### Related files\r\n<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->\r\n\r\n### Stakeholders\r\n<!-- @ tag stakeholders of this bug -->\r\n\n", "before_files": [{"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\nimport web\nfrom datetime import datetime\nimport re\n\nimport six\nfrom six.moves.urllib.parse import urlsplit\n\nif six.PY2:  # See #4525 json.dump(indent) MUST be an int on PY2\n    import simplejson as json\nelse:\n    import json\n\nimport babel\nimport babel.core\nimport babel.dates\nimport babel.numbers\n\ntry:\n    import genshi\n    import genshi.filters\nexcept ImportError:\n    genshi = None\n\ntry:\n    from bs4 import BeautifulSoup\nexcept ImportError:\n    BeautifulSoup = None\n\nfrom infogami import config\n\n# handy utility to parse ISO date strings\nfrom infogami.infobase.utils import parse_datetime\nfrom infogami.utils.view import safeint\n\n# TODO: i18n should be moved to core or infogami\nfrom openlibrary.i18n import gettext as _  # noqa: F401\n\n__all__ = [\n    \"sanitize\",\n    \"json_encode\",\n    \"safesort\",\n    \"days_since\", \"datestr\", \"format_date\",\n    \"sprintf\", \"cond\", \"commify\", \"truncate\", \"datetimestr_utc\",\n    \"urlsafe\", \"texsafe\",\n    \"percentage\", \"affiliate_id\", \"bookreader_host\",\n    \"private_collections\", \"private_collection_in\",\n\n    # functions imported from elsewhere\n    \"parse_datetime\", \"safeint\"\n]\n__docformat__ = \"restructuredtext en\"\n\ndef sanitize(html, encoding='utf8'):\n    \"\"\"Removes unsafe tags and attributes from html and adds\n    ``rel=\"nofollow\"`` attribute to all external links.\n    Using encoding=None if passing unicode strings e.g. for Python 3.\n    encoding=\"utf8\" matches default format for earlier versions of Genshi\n    https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version\n    \"\"\"\n\n    # Can't sanitize unless genshi module is available\n    if genshi is None:\n        return html\n\n    def get_nofollow(name, event):\n        attrs = event[1][1]\n        href = attrs.get('href', '')\n\n        if href:\n            # add rel=nofollow to all absolute links\n            _, host, _, _, _ = urlsplit(href)\n            if host:\n                return 'nofollow'\n\n    try:\n        html = genshi.HTML(html, encoding=encoding)\n\n    # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:\n    # don't catch Unicode errors so we can tell if we're getting bytes\n    except genshi.ParseError:\n        if BeautifulSoup:\n            # Bad html. Tidy it up using BeautifulSoup\n            html = str(BeautifulSoup(html, \"lxml\"))\n            try:\n                html = genshi.HTML(html)\n            except Exception:\n                # Failed to sanitize.\n                # We can't do any better than returning the original HTML, without sanitizing.\n                return html\n        else:\n            raise\n\n    stream = html \\\n        | genshi.filters.HTMLSanitizer() \\\n        | genshi.filters.Transformer(\"//a\").attr(\"rel\", get_nofollow)\n    return stream.render()\n\n\ndef json_encode(d, **kw):\n    \"\"\"Same as json.dumps.\n    \"\"\"\n    return json.dumps(d, **kw)\n\n\ndef safesort(iterable, key=None, reverse=False):\n    \"\"\"Sorts heterogeneous of objects without raising errors.\n\n    Sorting heterogeneous objects sometimes causes error. For example,\n    datetime and Nones don't go well together. This function takes special\n    care to make that work.\n    \"\"\"\n    key = key or (lambda x: x)\n    def safekey(x):\n        k = key(x)\n        return (k.__class__.__name__, k)\n    return sorted(iterable, key=safekey, reverse=reverse)\n\n\ndef days_since(then, now=None):\n    delta = then - (now or datetime.now())\n    return abs(delta.days)\n\n\ndef datestr(then, now=None, lang=None, relative=True):\n    \"\"\"Internationalized version of web.datestr.\"\"\"\n    lang = lang or web.ctx.get('lang') or \"en\"\n    if relative:\n        if now is None:\n            now = datetime.now()\n        delta = then - now\n        if abs(delta.days) < 4: # Threshold from web.py\n            return babel.dates.format_timedelta(delta,\n                                                add_direction=True,\n                                                locale=_get_babel_locale(lang))\n    return format_date(then, lang=lang)\n\n\ndef datetimestr_utc(then):\n    return then.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\ndef format_date(date, lang=None):\n    lang = lang or web.ctx.get('lang') or \"en\"\n    locale = _get_babel_locale(lang)\n    return babel.dates.format_date(date, format=\"long\", locale=locale)\n\ndef _get_babel_locale(lang):\n    try:\n        return babel.Locale(lang)\n    except babel.core.UnknownLocaleError:\n        return babel.Locale(\"en\")\n\n\ndef sprintf(s, *a, **kw):\n    \"\"\"Handy utility for string replacements.\n\n        >>> sprintf('hello %s', 'python')\n        'hello python'\n        >>> sprintf('hello %(name)s', name='python')\n        'hello python'\n    \"\"\"\n    args = kw or a\n    if args:\n        return s % args\n    else:\n        return s\n\n\ndef cond(pred, true_value, false_value=\"\"):\n    \"\"\"Lisp style cond function.\n\n    Hanly to use instead of if-else expression.\n    \"\"\"\n    if pred:\n        return true_value\n    else:\n        return false_value\n\n\ndef commify(number, lang=None):\n    \"\"\"localized version of web.commify\"\"\"\n    try:\n        lang = lang or web.ctx.get(\"lang\") or \"en\"\n        return babel.numbers.format_number(int(number), lang)\n    except:\n        return six.text_type(number)\n\n\ndef truncate(text, limit):\n    \"\"\"Truncate text and add ellipses if it longer than specified limit.\"\"\"\n    if not text:\n        return ''\n    if len(text) <= limit:\n        return text\n    return text[:limit] + \"...\"\n\n\ndef urlsafe(path):\n    \"\"\"Replaces the unsafe chars from path with underscores.\n    \"\"\"\n    return _get_safepath_re().sub('_', path).strip('_')[:100]\n\n@web.memoize\ndef _get_safepath_re():\n    \"\"\"Make regular expression that matches all unsafe chars.\"\"\"\n    # unsafe chars according to RFC 2396\n    reserved = \";/?:@&=+$,\"\n    delims = '<>#%\"'\n    unwise = \"{}|\\\\^[]`\"\n    space = ' \\n\\r'\n\n    unsafe = reserved + delims + unwise + space\n    pattern = '[%s]+' % \"\".join(re.escape(c) for c in unsafe)\n    return re.compile(pattern)\n\n\ndef get_coverstore_url():\n    \"\"\"Returns the base url of coverstore by looking at the config.\"\"\"\n    return config.get('coverstore_url', 'https://covers.openlibrary.org').rstrip('/')\n\n\n_texsafe_map = {\n    '\"': r'\\textquotedbl{}',\n    '#': r'\\#',\n    '$': r'\\$',\n    '%': r'\\%',\n    '&': r'\\&',\n    '<': r'\\textless{}',\n    '>': r'\\textgreater{}',\n    '\\\\': r'\\textbackslash{}',\n    '^': r'\\^{}',\n    '_': r'\\_{}',\n    '{': r'\\{',\n    '}': r'\\}',\n    '|': r'\\textbar{}',\n    '~': r'\\~{}',\n}\n\n_texsafe_re = None\n\ndef texsafe(text):\n    \"\"\"Escapes the special characters in the given text for using it in tex type setting.\n\n    Tex (or Latex) uses some characters in the ascii character range for\n    special notations. These characters must be escaped when occur in the\n    regular text. This function escapes those special characters.\n\n    The list of special characters and the latex command to typeset them can\n    be found in `The Comprehensive LaTeX Symbol List`_.\n\n    .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf\n    \"\"\"\n    global _texsafe_re\n    if _texsafe_re is None:\n        pattern = \"[%s]\" % re.escape(\"\".join(list(_texsafe_map)))\n        _texsafe_re = re.compile(pattern)\n\n    return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)\n\ndef percentage(value, total):\n    \"\"\"Computes percentage.\n\n        >>> percentage(1, 10)\n        10.0\n        >>> percentage(0, 0)\n        0.0\n    \"\"\"\n    return (value * 100.0) / total if total else 0.0\n\ndef uniq(values, key=None):\n    \"\"\"Returns the unique entries from the given values in the original order.\n\n    The value of the optional `key` parameter should be a function that takes\n    a single argument and returns a key to test the uniqueness.\n    \"\"\"\n    key = key or (lambda x: x)\n    s = set()\n    result = []\n    for v in values:\n        k = key(v)\n        if k not in s:\n            s.add(k)\n            result.append(v)\n    return result\n\ndef affiliate_id(affiliate):\n    return config.get('affiliate_ids', {}).get(affiliate, '')\n\ndef bookreader_host():\n    return config.get('bookreader_host', '')\n\ndef private_collections():\n    \"\"\"Collections which are lendable but should not be linked from OL\n    TODO: Remove when we can handle institutional books\"\"\"\n    return ['georgetown-university-law-library-rr']\n\ndef private_collection_in(collections):\n    return any(x in private_collections() for x in collections)\n\ndef _get_helpers():\n    _globals = globals()\n    return web.storage((k, _globals[k]) for k in __all__)\n\n\n## This must be at the end of this module\nhelpers = _get_helpers()\n", "path": "openlibrary/core/helpers.py"}], "after_files": [{"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\nimport web\nfrom datetime import datetime\nimport re\n\nimport six\nfrom six.moves.urllib.parse import urlsplit\n\nif six.PY2:  # See #4525 json.dump(indent) MUST be an int on PY2\n    import simplejson as json\nelse:\n    import json\n\nimport babel\nimport babel.core\nimport babel.dates\nimport babel.numbers\n\ntry:\n    import genshi\n    import genshi.filters\nexcept ImportError:\n    genshi = None\n\ntry:\n    from bs4 import BeautifulSoup\nexcept ImportError:\n    BeautifulSoup = None\n\nfrom infogami import config\n\n# handy utility to parse ISO date strings\nfrom infogami.infobase.utils import parse_datetime\nfrom infogami.utils.view import safeint\n\n# TODO: i18n should be moved to core or infogami\nfrom openlibrary.i18n import gettext as _  # noqa: F401\n\n__all__ = [\n    \"sanitize\",\n    \"json_encode\",\n    \"safesort\",\n    \"days_since\", \"datestr\", \"format_date\",\n    \"sprintf\", \"cond\", \"commify\", \"truncate\", \"datetimestr_utc\",\n    \"urlsafe\", \"texsafe\",\n    \"percentage\", \"affiliate_id\", \"bookreader_host\",\n    \"private_collections\", \"private_collection_in\",\n\n    # functions imported from elsewhere\n    \"parse_datetime\", \"safeint\"\n]\n__docformat__ = \"restructuredtext en\"\n\ndef sanitize(html, encoding='utf8'):\n    \"\"\"Removes unsafe tags and attributes from html and adds\n    ``rel=\"nofollow\"`` attribute to all external links.\n    Using encoding=None if passing unicode strings e.g. for Python 3.\n    encoding=\"utf8\" matches default format for earlier versions of Genshi\n    https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version\n    \"\"\"\n\n    # Can't sanitize unless genshi module is available\n    if genshi is None:\n        return html\n\n    def get_nofollow(name, event):\n        attrs = event[1][1]\n        href = attrs.get('href', '')\n\n        if href:\n            # add rel=nofollow to all absolute links\n            _, host, _, _, _ = urlsplit(href)\n            if host:\n                return 'nofollow'\n\n    try:\n        html = genshi.HTML(html, encoding=encoding)\n\n    # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:\n    # don't catch Unicode errors so we can tell if we're getting bytes\n    except genshi.ParseError:\n        if BeautifulSoup:\n            # Bad html. Tidy it up using BeautifulSoup\n            html = str(BeautifulSoup(html, \"lxml\"))\n            try:\n                html = genshi.HTML(html)\n            except Exception:\n                # Failed to sanitize.\n                # We can't do any better than returning the original HTML, without sanitizing.\n                return html\n        else:\n            raise\n\n    stream = html \\\n        | genshi.filters.HTMLSanitizer() \\\n        | genshi.filters.Transformer(\"//a\").attr(\"rel\", get_nofollow)\n    return stream.render()\n\n\ndef json_encode(d, **kw):\n    \"\"\"Same as json.dumps.\n    \"\"\"\n    return json.dumps(d or {}, **kw)\n\n\ndef safesort(iterable, key=None, reverse=False):\n    \"\"\"Sorts heterogeneous of objects without raising errors.\n\n    Sorting heterogeneous objects sometimes causes error. For example,\n    datetime and Nones don't go well together. This function takes special\n    care to make that work.\n    \"\"\"\n    key = key or (lambda x: x)\n    def safekey(x):\n        k = key(x)\n        return (k.__class__.__name__, k)\n    return sorted(iterable, key=safekey, reverse=reverse)\n\n\ndef days_since(then, now=None):\n    delta = then - (now or datetime.now())\n    return abs(delta.days)\n\n\ndef datestr(then, now=None, lang=None, relative=True):\n    \"\"\"Internationalized version of web.datestr.\"\"\"\n    lang = lang or web.ctx.get('lang') or \"en\"\n    if relative:\n        if now is None:\n            now = datetime.now()\n        delta = then - now\n        if abs(delta.days) < 4: # Threshold from web.py\n            return babel.dates.format_timedelta(delta,\n                                                add_direction=True,\n                                                locale=_get_babel_locale(lang))\n    return format_date(then, lang=lang)\n\n\ndef datetimestr_utc(then):\n    return then.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\ndef format_date(date, lang=None):\n    lang = lang or web.ctx.get('lang') or \"en\"\n    locale = _get_babel_locale(lang)\n    return babel.dates.format_date(date, format=\"long\", locale=locale)\n\ndef _get_babel_locale(lang):\n    try:\n        return babel.Locale(lang)\n    except babel.core.UnknownLocaleError:\n        return babel.Locale(\"en\")\n\n\ndef sprintf(s, *a, **kw):\n    \"\"\"Handy utility for string replacements.\n\n        >>> sprintf('hello %s', 'python')\n        'hello python'\n        >>> sprintf('hello %(name)s', name='python')\n        'hello python'\n    \"\"\"\n    args = kw or a\n    if args:\n        return s % args\n    else:\n        return s\n\n\ndef cond(pred, true_value, false_value=\"\"):\n    \"\"\"Lisp style cond function.\n\n    Hanly to use instead of if-else expression.\n    \"\"\"\n    if pred:\n        return true_value\n    else:\n        return false_value\n\n\ndef commify(number, lang=None):\n    \"\"\"localized version of web.commify\"\"\"\n    try:\n        lang = lang or web.ctx.get(\"lang\") or \"en\"\n        return babel.numbers.format_number(int(number), lang)\n    except:\n        return six.text_type(number)\n\n\ndef truncate(text, limit):\n    \"\"\"Truncate text and add ellipses if it longer than specified limit.\"\"\"\n    if not text:\n        return ''\n    if len(text) <= limit:\n        return text\n    return text[:limit] + \"...\"\n\n\ndef urlsafe(path):\n    \"\"\"Replaces the unsafe chars from path with underscores.\n    \"\"\"\n    return _get_safepath_re().sub('_', path).strip('_')[:100]\n\n@web.memoize\ndef _get_safepath_re():\n    \"\"\"Make regular expression that matches all unsafe chars.\"\"\"\n    # unsafe chars according to RFC 2396\n    reserved = \";/?:@&=+$,\"\n    delims = '<>#%\"'\n    unwise = \"{}|\\\\^[]`\"\n    space = ' \\n\\r'\n\n    unsafe = reserved + delims + unwise + space\n    pattern = '[%s]+' % \"\".join(re.escape(c) for c in unsafe)\n    return re.compile(pattern)\n\n\ndef get_coverstore_url():\n    \"\"\"Returns the base url of coverstore by looking at the config.\"\"\"\n    return config.get('coverstore_url', 'https://covers.openlibrary.org').rstrip('/')\n\n\n_texsafe_map = {\n    '\"': r'\\textquotedbl{}',\n    '#': r'\\#',\n    '$': r'\\$',\n    '%': r'\\%',\n    '&': r'\\&',\n    '<': r'\\textless{}',\n    '>': r'\\textgreater{}',\n    '\\\\': r'\\textbackslash{}',\n    '^': r'\\^{}',\n    '_': r'\\_{}',\n    '{': r'\\{',\n    '}': r'\\}',\n    '|': r'\\textbar{}',\n    '~': r'\\~{}',\n}\n\n_texsafe_re = None\n\ndef texsafe(text):\n    \"\"\"Escapes the special characters in the given text for using it in tex type setting.\n\n    Tex (or Latex) uses some characters in the ascii character range for\n    special notations. These characters must be escaped when occur in the\n    regular text. This function escapes those special characters.\n\n    The list of special characters and the latex command to typeset them can\n    be found in `The Comprehensive LaTeX Symbol List`_.\n\n    .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf\n    \"\"\"\n    global _texsafe_re\n    if _texsafe_re is None:\n        pattern = \"[%s]\" % re.escape(\"\".join(list(_texsafe_map)))\n        _texsafe_re = re.compile(pattern)\n\n    return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)\n\ndef percentage(value, total):\n    \"\"\"Computes percentage.\n\n        >>> percentage(1, 10)\n        10.0\n        >>> percentage(0, 0)\n        0.0\n    \"\"\"\n    return (value * 100.0) / total if total else 0.0\n\ndef uniq(values, key=None):\n    \"\"\"Returns the unique entries from the given values in the original order.\n\n    The value of the optional `key` parameter should be a function that takes\n    a single argument and returns a key to test the uniqueness.\n    \"\"\"\n    key = key or (lambda x: x)\n    s = set()\n    result = []\n    for v in values:\n        k = key(v)\n        if k not in s:\n            s.add(k)\n            result.append(v)\n    return result\n\ndef affiliate_id(affiliate):\n    return config.get('affiliate_ids', {}).get(affiliate, '')\n\ndef bookreader_host():\n    return config.get('bookreader_host', '')\n\ndef private_collections():\n    \"\"\"Collections which are lendable but should not be linked from OL\n    TODO: Remove when we can handle institutional books\"\"\"\n    return ['georgetown-university-law-library-rr']\n\ndef private_collection_in(collections):\n    return any(x in private_collections() for x in collections)\n\ndef _get_helpers():\n    _globals = globals()\n    return web.storage((k, _globals[k]) for k in __all__)\n\n\n## This must be at the end of this module\nhelpers = _get_helpers()\n", "path": "openlibrary/core/helpers.py"}]}