Repository: cython/cython

Issue:
cgi.escape was removed in Python 3.8
I just stumbled across this grepping for `cgi.escape` . I am not sure Tempita is public and documented for usage but found a usage causing `AttributeError`. `cgi.escape` was removed in Python 3.8 with https://github.com/python/cpython/pull/7662 . Usage of `html.escape` is recommended.

https://github.com/cython/cython/blob/6e72d84f43fc8f8da4ddc7695ac9f5b434788ef5/Cython/Tempita/_tempita.py#L444

```
Python 3.8.0 (default, Nov  6 2019, 21:49:08) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import Cython.Tempita
>>> Cython.Tempita.HTMLTemplate("a")._repr(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "Cython/Tempita/_tempita.py", line 489, in Cython.Tempita._tempita.HTMLTemplate._repr
  File "Cython/Tempita/_tempita.py", line 446, in Cython.Tempita._tempita.html_quote
AttributeError: module 'cgi' has no attribute 'escape'
```

Thanks for Cython!


Discussion:
I want to contribute on this
That part isn't really intended for use, more of a left-over from the original Tempita code base.
Still, PR welcome to fix this.
Note that this issue became a bit more urgent since the whole `cgi` module is now (Python 3.11) deprecated, i.e. simply importing the `Tempita` module will lead to a `DeprecationWarning`. Some CI's might turn these warnings into errors.
I'd be happy to open a simple PR that removes the `cgi` module and replaces it by `html`, but it feels a bit odd since apparently the code in `html_quote` is not used in the first place (given that it cannot be run since Python 3.8 and no one complained...).
> I'd be happy to open a simple PR that removes the `cgi` module and replaces it by `html`

A PR would definitely be welcome. But I'd propose just removing it rather than replacing it since it looks unused.
I am a bit afraid of breaking something, since none of this is covered in the test suite and I have no idea what the original use case for this code is/was. If I remove the `html_quote` function that uses `cgi.escape`, this would not only mean removing `html_quote`, but also `attr`, `HTMLTemplate`, and `sub_html`, since they use `html_quote` directly or indirectly. The latter two (`HTMLTemplate` and `sub_html`) are included in `__all__`, so technically part of the public API. 
As mentioned before, the `html_quote` code is broken since Python 3.8, so it is unlikely that this hurts anyone, but technically the code could still be used somewhere with `force=False` and an object that has an `__html__` attribute...
https://github.com/cython/cython/blob/6e72d84f43fc8f8da4ddc7695ac9f5b434788ef5/Cython/Tempita/_tempita.py#L436-L451


The background is that Tempita was originally a generic templating tool. It was incorporated into Cython to use for templating Cython's utility code, but obviously contains quote a bit of stale code from its original general use.

My view would be to just remove `html_quote` and the stuff that depends on it. We'll quite rapidly find out if it breaks any of Cython's utility code generation (its primary use) since that is covered by the the test-suite.

A few projects (Pandas, Scipy) do use Tempita from Cython to do some code-generation of Cython code. This is also a use-case that we're keen to support better, so possibly we possibly shouldn't be too brutal removing everything that's unused. I still think it's reasonable to remove these html bits that have been broken for years.

------------------------------

With that said, I'm happy to do the removal myself if you'd rather someone else gets the blame for it!

+1 for removing the obviously broken HTML parts. If users employ Tempita on their side, it's for source code generation, not HTML. There are much better tools for the latter.

Resolving patch:
diff --git a/Cython/Tempita/_tempita.py b/Cython/Tempita/_tempita.py
--- a/Cython/Tempita/_tempita.py
+++ b/Cython/Tempita/_tempita.py
@@ -35,11 +35,6 @@ def foo(bar):
 
 import re
 import sys
-import cgi
-try:
-    from urllib import quote as url_quote
-except ImportError:  # Py3
-    from urllib.parse import quote as url_quote
 import os
 import tokenize
 from io import StringIO
@@ -47,8 +42,7 @@ def foo(bar):
 from ._looper import looper
 from .compat3 import bytes, unicode_, basestring_, next, is_unicode, coerce_text
 
-__all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate',
-           'sub_html', 'html', 'bunch']
+__all__ = ['TemplateError', 'Template', 'sub', 'bunch']
 
 in_re = re.compile(r'\s+in\s+')
 var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I)
@@ -412,91 +406,6 @@ def __repr__(self):
             self.__class__.__name__,
             ' '.join(['%s=%r' % (k, v) for k, v in sorted(self.items())]))
 
-############################################################
-## HTML Templating
-############################################################
-
-
-class html(object):
-
-    def __init__(self, value):
-        self.value = value
-
-    def __str__(self):
-        return self.value
-
-    def __html__(self):
-        return self.value
-
-    def __repr__(self):
-        return '<%s %r>' % (
-            self.__class__.__name__, self.value)
-
-
-def html_quote(value, force=True):
-    if not force and hasattr(value, '__html__'):
-        return value.__html__()
-    if value is None:
-        return ''
-    if not isinstance(value, basestring_):
-        value = coerce_text(value)
-    if sys.version >= "3" and isinstance(value, bytes):
-        value = cgi.escape(value.decode('latin1'), 1)
-        value = value.encode('latin1')
-    else:
-        value = cgi.escape(value, 1)
-    if sys.version < "3":
-        if is_unicode(value):
-            value = value.encode('ascii', 'xmlcharrefreplace')
-    return value
-
-
-def url(v):
-    v = coerce_text(v)
-    if is_unicode(v):
-        v = v.encode('utf8')
-    return url_quote(v)
-
-
-def attr(**kw):
-    parts = []
-    for name, value in sorted(kw.items()):
-        if value is None:
-            continue
-        if name.endswith('_'):
-            name = name[:-1]
-        parts.append('%s="%s"' % (html_quote(name), html_quote(value)))
-    return html(' '.join(parts))
-
-
-class HTMLTemplate(Template):
-
-    default_namespace = Template.default_namespace.copy()
-    default_namespace.update(dict(
-        html=html,
-        attr=attr,
-        url=url,
-        html_quote=html_quote,
-        ))
-
-    def _repr(self, value, pos):
-        if hasattr(value, '__html__'):
-            value = value.__html__()
-            quote = False
-        else:
-            quote = True
-        plain = Template._repr(self, value, pos)
-        if quote:
-            return html_quote(plain)
-        else:
-            return plain
-
-
-def sub_html(content, **kw):
-    name = kw.get('__name')
-    tmpl = HTMLTemplate(content, name=name)
-    return tmpl.substitute(kw)
-
 
 class TemplateDef(object):
     def __init__(self, template, func_name, func_signature,
@@ -1131,11 +1040,6 @@ def fill_command(args=None):
         dest='output',
         metavar="FILENAME",
         help="File to write output to (default stdout)")
-    parser.add_option(
-        '--html',
-        dest='use_html',
-        action='store_true',
-        help="Use HTML style filling (including automatic HTML quoting)")
     parser.add_option(
         '--env',
         dest='use_env',
@@ -1165,11 +1069,7 @@ def fill_command(args=None):
     else:
         with open(template_name, 'rb') as f:
             template_content = f.read()
-    if options.use_html:
-        TemplateClass = HTMLTemplate
-    else:
-        TemplateClass = Template
-    template = TemplateClass(template_content, name=template_name)
+    template = Template(template_content, name=template_name)
     result = template.substitute(vars)
     if options.output:
         with open(options.output, 'wb') as f:


Test patch: